流媒体实战之推送MP4文件
前言
在我们日常工作中会遇到流媒体相关的业务,特别是音视频领域的小伙伴们,那么什么是流媒体呢?
流媒体协议说白了:就是一种通过Web来传递多媒体的一种协议,当我们在网络上观看视频或者直播时,背后避免不了会与流媒体协议有牵扯。
而流媒体协议有很多种,其主要也分为三大类:
- 传统视频流媒体协议:
- 基于HTTP的流媒体协议:
- Apple HLS
- Low-Latency HLS
- MPEG-DASH
- Adobe HDS
- 新流媒体协议:
本篇文章主要基于 RTMP
进行讲解。
工作原理
RTMP
主要的工作流程如下:
![未命名文件](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
如上图所示:首先是由用户A来将一个媒体流推送至流媒体服务器,流媒体服务器接收到流之后再做一系列的处理。待处理完成之后,此时用户B如果来拉取媒体流,流媒体服务器则会将该流分发给用户B。
前置准备
在我们编写我们的代码之前,我们要先去做一些准备工作:
- 服务器/虚拟机
- SRS
- ffmpeg
在我们编写代码之前,我们首先要将流媒体服务器准备好,这里可以用我们自己的服务器,也可以用虚拟机或者本机。
手动安装
安装ffmpeg
首先我们可以去 http://www.ffmpeg.org/releases 中找到我们所需的版本下载下来,如下:
1 2 3 4 5 6 7 8 9 10 11
| # 下载ffmpeg wget http://www.ffmpeg.org/releases/ffmpeg-6.1.tar.gz
# 解压 tar -zxvf ffmpeg-6.1.tar.gz /opt/ffmpeg-6.1
cd /opt/ffmpeg-6.1
# 编译安装 ./configure --prefix=/usr/local/ffmpeg make && make install
|
当我们安装完成后还要去配置环境变量,如下:
1 2 3 4 5 6 7
| vim /etc/profile
# 在文件最后一行添加环境变量 export PATH=$PATH:/usr/local/ffmpeg/bin
# :wq退出后刷新资源以生效配置 source /etc/profile
|
此时我们就已经安装完成了,我们也可以通过 ffmpeg -version
命令来验证安装。
安装SRS
首先我们要来安装我们的SRS,首先我们要先在服务器上将srs源码给克隆下来
1
| git clone -b 5.0release https://gitee.com/ossrs/srs.git /opt/srs
|
当我们将源码克隆下来之后就可以进行编译安装了
1 2 3 4 5
| # 切换目录 cd /opt/srs/trunk
# 编译安装 ./configure --prefix=/usr/local/srs && make
|
在安装完成之后,我们也可以去 /usr/local/srs/conf/srs/conf
下来更改相应配置
Docker安装
在命令行中执行如下代码:
1
| docker run -p 1935:1935 -p 1985:1985 -p 8080:8080 ossrs/srs:3
|
代码实现
在编写代码之前,我们要先知晓推流的整体流程来辅助我们编码,如下图所示:
![未命名文件](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
我们知道了其工作原理后,就可以来编写我们的代码了。我们的目的是将一个MP4文件推送至流媒体服务器,再通过 VLC
来拉取我们的流。代码如下:
Controller
我们通过 GET 请求方式,且参数为 sourceFilePath
与 pushAddress
,他们分别是推送MP4文件的本地路径和推送的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import com.bummon.service.PushService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequiredArgsConstructor public class PushController {
private final PushService pushService;
@GetMapping("/pushFlv") public void pushFlv(String sourceFilePath, String pushAddress) throws Exception { pushService.pushFlv(sourceFilePath, pushAddress); }
}
|
Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
public interface PushService {
void pushFlv(String sourceFilePath, String pushAddress) throws Exception;
}
|
ServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
| import com.bummon.service.PushService; import lombok.extern.slf4j.Slf4j; import org.bytedeco.ffmpeg.avcodec.AVCodecParameters; import org.bytedeco.ffmpeg.avformat.AVFormatContext; import org.bytedeco.ffmpeg.avformat.AVStream; import org.bytedeco.ffmpeg.global.avcodec; import org.bytedeco.ffmpeg.global.avutil; import org.bytedeco.javacv.FFmpegFrameGrabber; import org.bytedeco.javacv.FFmpegFrameRecorder; import org.bytedeco.javacv.FFmpegLogCallback; import org.bytedeco.javacv.Frame; import org.springframework.stereotype.Service;
@Slf4j @Service public class PushServiceImpl implements PushService { @Override public void pushFlv(String sourceFilePath, String pushAddress) throws Exception { FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(sourceFilePath); long startTime = System.currentTimeMillis();
grabber.start(true);
log.info("帧抓取器初始化完毕,耗时[{}]毫秒", System.currentTimeMillis() - startTime);
AVFormatContext avFormatContext = grabber.getFormatContext();
int streamNum = avFormatContext.nb_streams();
if (streamNum < 1) { log.error("文件中不存在媒体流"); return; }
int videoFrameRate = (int) grabber.getVideoFrameRate(); log.info("检测到视频帧率:[{}],视频时长:[{}]秒,媒体流数量:[{}]个", videoFrameRate, avFormatContext.duration() / 1000000, streamNum);
for (int i = 0; i < streamNum; i++) { AVStream avStream = avFormatContext.streams(i); AVCodecParameters avCodecParameters = avStream.codecpar(); log.info("检测到流索引:[{}],编码器类型:[{}],编码器ID:[{}]", i, avCodecParameters.codec_type(), avCodecParameters.codec_id()); }
int width = grabber.getImageWidth(); int height = grabber.getImageHeight(); int audioChannels = grabber.getAudioChannels();
log.info("检测到视频宽度:[{}],视频高度:[{}],音频通道数:[{}]", width, height, audioChannels);
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(pushAddress, width, height, audioChannels);
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setFormat("flv");
recorder.setFrameRate(videoFrameRate);
recorder.setAudioChannels(2);
startTime = System.currentTimeMillis();
recorder.start();
log.info("帧录制器初始化完毕,耗时:[{}]毫秒", System.currentTimeMillis() - startTime);
Frame frame;
startTime = System.currentTimeMillis();
log.info("开始推流");
long videoTS = 0;
int videoFrameNum = 0; int audioFrameNum = 0; int dataFrameNum = 0;
int interVal = 1000 / videoFrameRate;
interVal /= 8;
while (null != (frame = grabber.grab())) { videoTS = 1000 * (System.currentTimeMillis() - startTime);
recorder.setTimestamp(videoTS);
if (null != frame.image) { videoFrameNum++; }
if (null != frame.samples) { audioFrameNum++; }
if (null != frame.data) { dataFrameNum++; }
recorder.record(frame);
Thread.sleep(interVal); } log.info("推送完成,视频帧:[{}],音频帧:[{}],数据帧:[{}],耗时:[{}]毫秒", videoFrameNum, audioFrameNum, dataFrameNum, System.currentTimeMillis() - startTime);
recorder.close();
grabber.close(); } }
|
测试
我们使用postman请求如下:
![image-20231227181257363](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
然后我们再通过 VLC 来拉取我们的流
![image-20231227181216886](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
可以发现我们是可以正确拉取到对应的流的,也说明我们的服务没有问题。
本文代码
https://github.com/Bummon/media-in-action/tree/main/push-mp4
至此,本篇文章结束,感谢观看。