ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FFmpeg源码实现RTSP转封装:拉流录制MP4/AVI/FLV

2026/9/8 3:44:06 拓冰建站 浏览量
FFmpeg源码实现RTSP转封装:拉流录制MP4/AVI/FLV 简介实现FFmpeg将RTSP流封装为MP4、AVI、FLV的源码示例面向有一定C基础、需要定制流媒体封装逻辑的开发者。压缩包内共2个文件含1个cpp和1个h包体仅2KB其中h负责接口声明cpp实现具体转换流程集中展示了初始化FFmpeg上下文、设置输入输出参数、读写数据流及关闭资源的关键步骤便于结合项目进行二次开发。该示例覆盖RTSP拉流与MP4、AVI、FLV三种容器格式输出的接口调用三种封装在复用同一套拉流逻辑基础上只需调整输出封装参数即可适配不同场景并可通过修改代码扩展自定义编码设置、过滤器或错误处理策略比单纯使用命令行转换更灵活。代码量小但流程完整适合作为学习和改造模板。已有7173人学习适合正在研究FFmpeg API、需要快速接入多格式封装功能或希望理解RTSP实时流写入容器的工程人员参考。 在安防、音视频、流媒体这类项目里写一个能把 RTSP 流保存成文件的小工具基本是跑不掉的硬需求。尤其是对接网络摄像头、视频采集盒或者做边缘端录像功能时用户的诉求往往就一句话把那个rtsp://地址的实时画面存成 mp4最好同时还能选 avi、flv。这个任务看起来简单真要写得可靠、稳定、不三天两头崩还是有不少细节值得掰扯。1. 先说清楚这不叫转码这叫转封装1.1 RTSP 拉流与容器格式的本质很多刚接触这块的同事会把“把 RTSP 流存成 mp4”理解成“转码”。其实完全不是一回事。RTSP 是实时流传输协议它负责的是“怎么把码流从源头搬到客户端”本身不关心视频编码是什么。而 mp4、avi、flv 是容器格式容器只负责“怎么把已经编码好的 H.264/H.265/AAC 这些数据组织起来存进文件”。换句话说我们从 RTSP 拉回来的码流本身就已经是 H.264 AAC 之类的编码数据了。我们要做的不是重新压缩而是把裸流数据按目标容器的规则重新打包。这个过程专业术语叫 Remux也就是转封装。好处是几乎不消耗 CPU不损失画质处理速度极快十几路并发对现在的硬件来说都是小意思。这才是选择这条路线的根本原因——我能跑得动 8 路 1080p 实时录制靠的就是完全避开转码。1.2 为什么可以用 ffmpeg 源码实现ffmpeg 的libavformat库本质上就是一个极其强大的“容器读写器”。它把 RTSP 的拉流逻辑封装成了输入层把 mp4/avi/flv 的写入逻辑封装成了输出层。我们要做的就是把两者接起来。而且 ffmpeg 的转封装链路本身就是现成的——av_read_frame读出一个 AVPacket经过时间基换算再喂给av_interleaved_write_frame写出去。这正是我推荐直接基于源码而不是靠system(ffmpeg -i rtsp://... out.mp4)去实现的原因。命令行方案虽然省事但没法精细控制错误恢复、没法做成库集成到自己的程序里也没法做中断回调等深度定制。自己调 API初期代码量稍大但换来的是一套完全可控的录制管道。下面这份源码框架是我在实际项目中跑过的按“开输入 - 建输出 - 循环搬包 - 收尾写索引”四步走的经典结构来搭。2. 核心代码框架拉流 转封装的完整实现2.1 API 调用链总览整个转封装程序本质上就是下面这条调用链在执行avformat_network_init初始化网络库新版 ffmpeg 可不显式调用但保留无妨avformat_open_input打开 RTSP 地址这里需要设置rtsp_transporttcp参数avformat_find_stream_info探测流信息拿到视频流、音频流的编码参数avformat_alloc_output_context2根据输出文件名创建输出上下文遍历输入流为每个流创建对应的输出流并拷贝codecpar参数avio_open打开输出文件avformat_write_header写入文件头循环调用av_read_frame读取 AVPacket换算时间基后交给av_interleaved_write_frameavformat_write_trailer写入文件尾索引并清理资源第 5 步是个很容易踩坑的地方。转封装场景下不要把in_stream-codecpar当做旧版codec去拷贝新版已经废弃了后者的直接访问。统一用avcodec_parameters_copy把编码参数从输入流复制给输出流并手动把codec_tag清零避免容器格式对 tag 的校验产生干扰。2.2 完整源码实现下面就是我实际项目里用的一段核心代码C 语言。为方便阅读我把错误处理和资源回收都做了精简但主干逻辑完整保留。#include stdio.h #include libavformat/avformat.h static int open_input(const char* url, AVFormatContext** fmt_ctx) { AVDictionary* opts NULL; av_dict_set(opts, rtsp_transport, tcp, 0); // 强制 TCP避免 UDP 丢包花屏 av_dict_set(opts, stimeout, 3000000, 0); // 3 秒超时防止卡死 int ret avformat_open_input(fmt_ctx, url, NULL, opts); av_dict_free(opts); if (ret 0) { av_log(NULL, AV_LOG_ERROR, open input failed: %s\n, av_err2str(ret)); return ret; } ret avformat_find_stream_info(*fmt_ctx, NULL); if (ret 0) { av_log(NULL, AV_LOG_ERROR, find stream info failed\n); return ret; } av_dump_format(*fmt_ctx, 0, url, 0); return 0; } static int open_output(const char* filename, AVFormatContext* in_ctx, AVFormatContext** out_ctx) { const AVOutputFormat* fmt av_guess_format(NULL, filename, NULL); if (!fmt) { av_log(NULL, AV_LOG_ERROR, cannot guess output format by extension\n); return -1; } int ret avformat_alloc_output_context2(out_ctx, fmt, NULL, filename); if (ret 0) return ret; for (unsigned int i 0; i in_ctx-nb_streams; i) { AVStream* in_stream in_ctx-streams[i]; AVStream* out_stream avformat_new_stream(*out_ctx, NULL); if (!out_stream) return AVERROR(ENOMEM); ret avcodec_parameters_copy(out_stream-codecpar, in_stream-codecpar); if (ret 0) return ret; out_stream-codecpar-codec_tag 0; // 关键清 tag防止格式校验报错 out_stream-time_base in_stream-time_base; } ret avio_open((*out_ctx)-pb, filename, AVIO_FLAG_WRITE); if (ret 0) { av_log(NULL, AV_LOG_ERROR, open output file failed: %s\n, av_err2str(ret)); return ret; } ret avformat_write_header(*out_ctx, NULL); if (ret 0) { av_log(NULL, AV_LOG_ERROR, write header failed: %s\n, av_err2str(ret)); return ret; } return 0; } static void write_packets(AVFormatContext* in_ctx, AVFormatContext* out_ctx) { AVPacket pkt; av_init_packet(pkt); pkt.data NULL; pkt.size 0; while (1) { int ret av_read_frame(in_ctx, pkt); if (ret 0) { if (ret AVERROR_EOF || avio_feof(in_ctx-pb)) av_log(NULL, AV_LOG_INFO, reach EOF, recording done\n); else av_log(NULL, AV_LOG_ERROR, read frame failed: %s\n, av_err2str(ret)); break; } AVStream* in_stream in_ctx-streams[pkt.stream_index]; AVStream* out_stream out_ctx-streams[pkt.stream_index]; // 时间基换算把输入流的时间戳换算到输出流的时间基 av_packet_rescale_ts(pkt, in_stream-time_base, out_stream-time_base); pkt.pos -1; ret av_interleaved_write_frame(out_ctx, pkt); av_packet_unref(pkt); if (ret 0) { av_log(NULL, AV_LOG_ERROR, write frame failed: %s\n, av_err2str(ret)); break; } } } int main(int argc, char* argv[]) { if (argc 3) { fprintf(stderr, Usage: %s rtsp_url output_file.mp4|avi|flv\n, argv[0]); return -1; } avformat_network_init(); AVFormatContext* in_ctx NULL; AVFormatContext* out_ctx NULL; if (open_input(argv[1], in_ctx) 0) goto fail; if (open_output(argv[2], in_ctx, out_ctx) 0) goto fail; write_packets(in_ctx, out_ctx); av_write_trailer(out_ctx); avformat_close_input(in_ctx); if (out_ctx out_ctx-pb) avio_closep(out_ctx-pb); avformat_free_context(out_ctx); avformat_network_deinit(); return 0; fail: avformat_close_input(in_ctx); if (out_ctx out_ctx-pb) avio_closep(out_ctx-pb); avformat_free_context(out_ctx); return -1; }2.3 关键细节时间戳换算与包写入顺序这段代码里最核心、也最容易出问题的就是av_packet_rescale_ts这一步。RTSP 拉流出来的包时间戳是基于 RTP 时间戳机制生成的和容器内部的时间基不是一回事。如果你不做换算直接写进去轻则播放时快进/慢放重则 mp4 的 duration 变成天文数字拖动进度条直接崩。另一个细节是我选了av_interleaved_write_frame而不是av_write_frame。前者会内部缓存并排序保证写入文件的包顺序和时间戳是单调递增的。RTSP 流在网络传输中常见的包乱序问题靠它就能解决大半。代价是内存开销略高一点但对录像这种持续写入场景来说完全值得。注意av_interleaved_write_frame在写入成功后内部会对 pkt 做 unref。所以我在调用前就 unref 了旧包这是标准姿势避免自己重复回收导致 double free。3. 不同封装格式的差异化处理3.1 mp4moov 位置与碎片化mp4 是默认首选因为兼容性最好。但 mp4 有个经典问题它的核心索引moov元数据可以放在文件头部或尾部。默认情况下 ffmpeg 会在write_trailer时才写 moov这意味着如果程序中途断电前面录的所有数据全部白费——播放器打开文件找不到 moov 直接报错。解决思路有两个常用手段。一是开启faststart让写完文件后自动把 moov 挪到文件头部AVDictionary* opt NULL; av_dict_set(opt, movflags, faststart, 0); avformat_write_header(out_ctx, opt); av_dict_free(opt);二是直接走碎片化 MP4也就是把movflags设置成frag_keyframeempty_moov。这种模式下文件会按关键帧间隔切成一个个碎片段每个片段的索引独立存在即使程序中途崩溃已经写好的片段也能正常播放。对于长时间不间断录像我强烈建议用碎片化方案。3.2 avi索引结构与时间基avi 格式的特点是索引结构直接写入逻辑最简单。但它的坑在于对时间基的要求比较特殊而且 avi 默认不支持某些编码。我实测的情况是 H.264 AVI 在某些播放器里会出现声画不同步根源是 avi 的时间基粒度和 RTSP 的 RTP 时间戳不匹配加上音频帧间隔不规则导致。如果业务上必须用 avi我会建议在创建输出流时把视频流 time_base 固定设置为{1, 90000}音频流根据采样率设置例如 44100 Hz 就是{1, 44100}。不要直接用输入流的时间基。3.3 flv流式友好与编码限制flv 是直播领域的老将依靠 HTTP-FLV 协议在网页播放里应用极广。把 RTSP 转封装成 flv 文件的代码逻辑和 mp4 几乎一样但要注意flv 对编码格式限制很死视频基本就是 H.264音频最稳妥的是 AAC。如果你拉到的 RTSP 流里音频是 G.711 或 Opus 这种编码写入 flv 大概率会报错或无法播放。提示写完 flv 之后可以把文件直接丢到 flv.js 播放器里做本地测试比用 VLC 更能暴露容器层问题。4. 实际项目中的坑与排查速查表4.1 高频故障与定位思路下面这个表是我这几年做录像服务时攒下来的高频问题排查清单按出现的频率排序。现象直接原因排查思路与解决录制完的 mp4 打不开moov 元数据未写入写入中断异常或未调用 write_trailer改用 faststart / 碎片化 mp4画面花屏或绿屏UDP 拉流丢包导致关键帧损坏设置rtsp_transporttcp强制走 TCP 拉流音画不同步时间基换算错误检查是否调用了 av_packet_rescale_ts以及输出流 time_base 是否合理录像文件时长不对最后一段数据未刷新确认 write_trailer 一定被调用同时检查包写入是否为 interleaved 模式长时间录制后内存暴涨AVPacket 未释放每轮循环后调用 av_packet_unref检查每条 break 路径上是否漏释程序卡在打开流阶段RTSP 地址无响应默认超时过长设置stimeout参数单位微秒根据网络情况给 3~5 秒文件能播但无法拖动进度条关键帧间隔过大或缺失RTSP 流本身的关键帧间隔由推流端决定录像端可通过-g思路请求更多关键帧有一个屡试不爽的排查技巧先用命令行 ffmpeg 拉同一个 RTSP 地址、存成同样的格式看是否复现。如果命令行也失败那问题大概率出在流本身或网络层面而不是你的代码和 API 用法。4.2 断流重连和中断回调的工程化处理前面给的源码框架应对的是“输入流稳定”的理想情况。但真实环境里摄像头重启、网络抖动、路由器囤积延迟都是家常便饭。裸版本代码一旦av_read_frame返回超时或断流错误整个程序就退出了。工程上至少要加两层防护第一层是给AVFormatContext注册中断回调。这样av_read_frame阻塞时能按时返回不会永久卡死。核心是定义一个回调函数通过标志位或者超时计数来判断是否中断static volatile int interrupt_flag 0; static int interrupt_cb(void* ctx) { return interrupt_flag; } // 在 open_input 里设置回调 in_ctx-interrupt_callback.callback interrupt_cb; in_ctx-interrupt_callback.opaque NULL;第二层是断线后的自动重连。当读包返回错误时先avformat_close_input释放旧上下文然后重新走一遍avformat_open_input流程并做重试退避比如 1 秒、2 秒、4 秒的指数退避最多 5 次。重连成功后需要先判断时间戳是否发生了跳变如果跳变值超过阈值最稳妥的策略是关闭当前输出文件、开一个新文件录否则录出来的文件时间轴会看起来很怪异。对于“按时间段切片”的需求可以在每写一个包时检查累计时长到了设定的录像时长就主动av_write_trailer、关文件、重新走 open_output 流程。这样每个文件时长可控后续做检索和回放都方便。这个逻辑我在多个摄像头并发录制的项目里验证过稳定跑过一个月没崩。个人习惯是录制服务里永远不依赖默认的读写超时宁可把stimeout设大一点也要配合 interrupt_callback 来兜底。因为stimeout只在连接阶段有效一旦进入持续读流阶段网络闪断导致的阻塞问题是stimeout管不住的。只有把中断回调做扎实整个录像服务才真正具备长期运行的可靠性。这套转封装框架不复杂真正考验人的地方全在边界处理上。把时间戳、关键帧、断线重连这几个点想清楚一个轻量级录像服务的外壳就算立住了。接下来可以再往切片策略、磁盘空间预警、录像索引数据库这些方向继续加码那就是一个完整的商用录像系统了。本文还有配套的精品资源点击获取