FFmpeg - 将网络流保存到文件
1. 开发环境
● FFmpeg版本:7.1
● 开发环境:Ubuntu20.04
2. 基本流程
网络包
->解复用/解封装
->PES裸流
->再复用/封装
->保存到文件/转为网络流
3. 命令行实现网络流保存
-
ZL_MediaKit
流媒体服务器地址:192.168.16.230
-
启动一个
FFmpeg
进程,向流媒体服务器推RTSP
流# TCP方式推RTSP流 $ ffmpeg -re -stream_loop -1 -i ./clock.mp4 -vcodec h264 -acodec aac -f rtsp -rtsp_transport tcp rtsp://192.168.16.230/live/test # UDP方式推RTSP流,这也是RTSP的默认方式 $ ffmpeg -re -stream_loop -1 -i ./clock.mp4 -vcodec h264 -acodec aac -f rtsp rtsp://192.168.16.230/live/test # -re: 实时速率读取输入,按照输入文件的原始帧率进行输出 # -stream_loop -1: -1 表示无限循环,如果你想循环特定次数,可以用正整数替换 -1 # -i ./clock.mp4: 指定输入文件为一个本地文件clock.mp4 # -vcodec h264: 设置视频编码器为H.264,如果输入文件已经是H.264编码了,并不会重新编码 # -acodec aac: 设置音频编码器为 AAC,同样,如果输入文件已经是 AAC 编码,不会重新编码。 # -f rtsp: 指定输出格式为 RTSP 协议 # -rtsp_transport tcp: 指定 RTSP 传输使用 TCP 协议,而不是默认的 UDP。TCP 通常更可靠,特别是在网络不稳定的情况下 # rtsp://192.168.16.230/live/test: 这是输出的 RTSP 流地址。在这个例子中,流将被推送到 IP 地址为 192.168.16.230 的流媒体服务器,路径为 /live/test
-
另启一个
FFmpeg
进程,从流媒体服务器拉取RTSP
流,并保存为MP4
格式$ ffmpeg -i rtsp://192.168.16.230/live/test -c copy output.mp4
-
如下图:
-
ffplay
播放保存的output.mp4
$ ffplay output.mp4
4. FFmpeg
代码
4.1 包含FFmpeg头文件
- 建议不要一次引入FFmpeg所有的头文件,用到哪个模块就包含响应的头文件;这样一来可以帮助我们了解ffmpeg中各个模块功能划分,二来编译出的文件也没有那么大
- 本例程中包含两个头文件即可
extern "C"{ #include "libavutil/mem.h" #include "libavformat/avformat.h" }
4.2 初始化FFmpeg
- 本例中需要拉取网络流,因此需要初始化FFmpeg的网络组件
- 设置FFmpeg内部日志等级
// 初始化ffmpeg int Test::ffmpeg_init(void) { // 初始化FFmpeg的网络组件 avformat_network_init(); // 设置FFmpeg内部日志等级 av_log_set_level(AV_LOG_INFO); return 0; }
4.3 打开输入源
- 本例程中的输入源是一个网络流,可以是RTSP/RTMP...
int Test::open_input(std::string iurl) { // 错误日志 char errMsg[1024] = {0}; // 输入格式上下文 av_inputCtx = avformat_alloc_context(); // 打开输入源 int ret = avformat_open_input(&av_inputCtx, iurl.c_str(), nullptr, nullptr); if(ret < 0) { av_strerror(ret, errMsg, sizeof(errMsg)); fprintf(stderr, "%s errMsg:%s\n", __func__, errMsg); return -1; } // 从输入源中查找流 ret = avformat_find_stream_info(av_inputCtx, nullptr); if(ret < 0) { // 关闭输入源 avformat_close_input(&av_inputCtx); // 释放输入上下文 avformat_free_context(av_inputCtx); av_strerror(ret, errMsg, sizeof(errMsg)); fprintf(stderr, "%s errMsg:%s\n", __func__, errMsg); return -2; } fprintf(stdout, "%s open %s success!", __func__, iurl); return 0; }
4.4 创建输出流
- 本例程中的将输出流保存到文件中,输出流就是一个文件
// 创建输出流 int Test::open_output(std::string ourl) { // 错误日志 char errMsg[1024] = {0}; // 输出上下文, ts流 int ret = avformat_alloc_output_context2(&av_outputCtx, nullptr, "mpegts", ourl.c_str()); if(ret < 0){ av_strerror(ret, errMsg, sizeof(errMsg)); fprintf(stderr, "%s avformat_alloc_output_context2 failed! errMsg:%s\n", __func__, errMsg); return -1; } // 创建输出流,写入文件调用avio_open2 ret = avio_open2(&av_outputCtx->pb, ourl.c_str(), AVIO_FLAG_READ_WRITE, nullptr, nullptr); if(ret < 0){ av_strerror(ret, errMsg, sizeof(errMsg)); fprintf(stderr, "%s avio_open2 failed! errMsg:%s\n", __func__, errMsg); goto ERR1; } // 遍历输入流,拷贝输入流的编码参数到输出流 for(int i = 0; i < av_inputCtx->nb_streams; i++){ // 创建输出流 AVStream *out_stream = avformat_new_stream(av_outputCtx, nullptr); // 拷贝输入流的编码参数到输出流 ret = avcodec_parameters_copy(out_stream->codecpar, av_inputCtx->streams[i]->codecpar); if(ret < 0){ av_strerror(ret, errMsg, sizeof(errMsg)); fprintf(stderr, "%s avcodec_parameters_copy failed! errMsg:%s\n", __func__, errMsg); goto ERR2; } } // 初始化媒体文件的头部信息 ret = avformat_write_header(av_outputCtx, nullptr); if(ret < 0){ av_strerror(ret, errMsg, sizeof(errMsg)); fprintf(stderr, "%s avformat_write_header failed! errMsg:%s\n", __func__, errMsg); goto ERR2; } fprintf(stdout, "%s open %s success!", __func__, ourl.c_str()); return 0; ERR2: if(av_outputCtx){ avio_closep(&av_outputCtx->pb); } ERR1: avformat_free_context(av_outputCtx); return ret; }
4.5 从输入流中读取一个包
- 从输入流中读取一个包
std::shared_ptr<AVPacket> Test::read_packet_from_source() { // av packet, std::shared_ptr std::shared_ptr<AVPacket> packet(av_packet_alloc(), [](AVPacket *p){av_packet_free(&p);}); if(!packet){ fprintf(stderr, "%s av_packet_alloc failed!", __func__); return nullptr; } // 从输入流中获取一个编码后的包 PES包 int ret = av_read_frame(av_inputCtx, packet.get()); if(ret < 0){ av_strerror(ret, errMsg, sizeof(errMsg)); fprintf(stderr, "%s avformat_write_header failed! errMsg:%s\n", __func__, errMsg); return nullptr; } return packet; }
4.6 向输出流中写入一个包
- 向输出流中写入一个包
// 向输出流中写包 int Test::write_packet_to_target(std::shared_ptr<AVPacket> packet) { auto inputStream = av_inputCtx->streams[packet->stream_index]; auto outputStream = av_outputCtx->streams[packet->stream_index]; // 时间基转换 av_packet_rescale_ts(packet.get(), inputStream->time_base, outputStream->time_base); return av_interleaved_write_frame(av_outputCtx, packet.get()); }
4.7 拉取RTSP流,并保存到文件
- 拉取RTSP流,并保存到文件
// 获取RTSP网络流,保存到文件;也可以获取其它协议的网络流,如RTMP,SRT
int Test::rtsp_save_to_file(void)
{
// 初始化
ffmpeg_init();
// 打开输入流
int ret = open_input(std::string("rtsp://192.168.16.230/live/test"));
if(ret < 0){
return -1;
}
// 打开输出流
ret = open_output(std::string("/tmp/test.ts"));
// 循环从输入流中读包,写入到输出流中
while(true){
auto packet = read_packet_from_source();
if(packet){
write_packet_to_target(packet);
fprintf(stdout, "%s writePacket success", __func__);
}else{
fprintf(stderr, "%s writePacket failed!\n", __func__);
return -1;
}
}
return 0;
}
4.8 释放资源
- 释放资源
// 释放资源 void Test::release(void) { // 关闭输入源 avformat_close_input(&av_inputCtx); // 释放输入上下文 if(av_inputCtx) avformat_free_context(av_inputCtx); // 关闭输出源 if(av_outputCtx->pb) avio_close(av_outputCtx->pb); // 释放输出格式上下文 if(av_outputCtx) avformat_free_context(av_outputCtx); // 释放ffmpeg网络资源 avformat_network_deinit(); }