☰
RTSP转MP4/AVI/FLV不转码方案:FFmpeg转封装源码详解
2026/10/14 20:06:15 网站建设 项目流程

简介:FFmpeg的RTSP流转封装常见于网络摄像头、视频监控与直播场景,面向具备C++编程和FFmpeg API基础的中高级开发者,这份源码包提供了一套将RTSP流转换为MP4、AVI、FLV三种容器格式的可运行实现。包内共2个文件,包括实现核心逻辑的C++源文件和声明接口的头文件,压缩后仅2KB,结构精简,便于直接集成到工程中或对照源码逐行学习。代码围绕FFmpeg的libavcodec、libavformat等组件展开,完整演示了初始化封装上下文、打开RTSP输入流、创建输出流、逐帧读取与写入数据、关闭资源等关键流程;同时支持在封装过程中自行扩展编码参数、过滤器和错误处理机制,以满足自定义码率与兼容性等特定需求。目前已有7177人学习下载,对于希望深入理解实时流封装原理并以源码方式替代命令行转换的开发者而言,是一份相当实用的参考。

1. 一套源码吃下 RTSP 转 mp4/avi/flv:不转码,CPU 几乎不涨

RTSP 是监控摄像头和视频服务器最常见的取流协议,而 mp4、avi、flv 又是三种最常被要求落盘的容器格式。很多人以为把一路 RTSP 流“封装”成这三种文件必须重新编码,其实只要源流是 H.264/H.265 + AAC,用 FFmpeg 的 libavformat 库做转封装就够了:不碰解码器,CPU 占用几乎可以忽略。下面这套源码,核心逻辑只有四步:打开 RTSP 输入、按文件名后缀选定输出封装器、循环读包写包、最后写 trailer。适合做监控录像、视频采集网关、摄像头测试工具的人,熟手可以直接拿它当骨架扩展。

2. 为什么必须走转封装:RTSP 与三种容器的差异决定实现方式

2.1 转封装不是转码:容器与编码流要分开看

RTSP 拉流时,FFmpeg 的 rtsp demuxer 会把 RTP 包还原成一组 AVPacket,每个 Packet 里是完整的编码帧数据,以及对应的 PTS/DTS。你要做的“封装成文件”,本质是把这一组 Packet 按目标容器的规则重新写一遍。如果源编码和目标容器兼容,就不需要解码再编码,这就是命令行里-c copy的库函数版本。

不转码的好处非常直接:CPU 占用低,画质没有二次损失,单路 1080p 的源在普通 x86 上转封装只占个位数的 CPU。缺点也一样明显:不能改分辨率、不能加文字水印、不能把 G.711 音频变成 AAC。源码里没有创建AVCodecContext,也没有调用avcodec_send_packet/avcodec_receive_frame,全程只做“搬家”。所以当你的需求是“原样录下来”时,这条路是最省事的。

什么情况不能转封装?如果摄像头输出的是 MJPEG,虽然 MP4 容器能写,但很多播放器支持得很差;如果音频是 G.711,而目标平台明确要求 AAC,就必须转码。这个判断要在项目启动前做,不要等录完一批文件再返工——我见过不少团队因为没查摄像头音频编码,最后整批 MP4 在客户端没声音,返工成本非常高。

2.2 MP4、AVI、FLV 三种容器对流的约束各不相同

容器索引方式对时间戳的要求中断风险典型场景
MP4moov atom 在文件头或文件尾需要稳定的 DTS 顺序未写 trailer 时文件几乎报废点播、上传、回看
AVI文件尾部索引块需要流序号和块对齐索引缺失后播放器靠猜本地临时、老设备兼容
FLV无索引,按 tag 链顺序需要连续时间戳,关键帧前必须有序列头可播放到断点直播、边录边传

这里有一个容易误判的点:AVI 是“老格式”,但不代表 AVI 能装任意编码。RTSP 摄像头常见的 H.264 放进 AVI 容器,FFmpeg 能写,但很多播放器的 AVI 解析器只认 MJPEG/Xvid,H.264 的 AVI 会出现有声音没画面,或者直接提示格式不支持。如果甲方只给了“AVI 文件”这个要求,你最好先确认播放环境,再决定是转封装还是转码。

FLV 的时间戳单位是毫秒,MP4 内部可以用 1/90000、1/48000 这样的高精度 time_base。所以源码里必须在写包前做av_rescale_q_rnd,这是转封装真正的“黑匣子”——不换算,轻则音画不同步,重则 muxer 直接拒收。后面第 3.3 节会专门说明这段代码。

2.3 为什么要写源码,而不是继续用 ffmpeg 命令行

如果你只是临时把一段录像导出来,ffmpeg -i rtsp://... -c copy out.mp4就够了,没有必要写 C。但监控类业务通常有几个硬需求:断线自动重连、录像按小时切片、收到停止信号后优雅封口、多路摄像头并发取流。这些用 system 调用也能做,但意味着每个摄像头起一个子进程,状态同步、资源回收、错误排查都很麻烦。

在进程内调用 libavformat,你可以把拉流和写文件放进同一个线程循环里,任何一步出错都能拿到具体错误码。如果想并发处理多路,只需要多开几个线程,每个线程维护自己的AVFormatContext对。很多视频网关的常见架构就是一个 RTSP poller 线程配一路输出上下文。下面拆开的源码,拼起来就是一份可直接编译的 C 文件。

3. 核心源码:RTSP 转 mp4/avi/flv 的 C 实现与关键参数

下面四段代码是同一份 main 函数,按顺序拼接即可。代码里没有解码器,只有 demux 到 mux 的透传,注释里已经标清楚每一步在做什么。

3.1 源码骨架:头文件、main 与 RTSP 输入打开

#include <stdio.h> #include <string.h> #include <unistd.h> #include <libavformat/avformat.h> #include <libavutil/opt.h> #include <libavutil/error.h> #include <libavutil/mathematics.h> static int fatal(const char *msg, int ret) { char err[AV_ERROR_MAX_STRING_SIZE] = {0}; av_strerror(ret, err, sizeof(err)); fprintf(stderr, "[%s] %s\n", msg, err); return -1; } int main(int argc, char **argv) { if (argc < 3) { fprintf(stderr, "usage: %s <rtsp_url> <out.mp4|out.avi|out.flv> [tcp|udp]\n", argv[0]); return 1; } avformat_network_init(); const char *transport = (argc >= 4) ? argv[3] : "tcp"; AVDictionary *opts = NULL; av_dict_set(&opts, "rtsp_transport", transport, 0); av_dict_set(&opts, "stimeout", "5000000", 0); av_dict_set(&opts, "max_delay", "500000", 0); AVFormatContext *ifmt_ctx = NULL; int ret = avformat_open_input(&ifmt_ctx, argv[1], NULL, &opts); av_dict_free(&opts); if (ret < 0) { return fatal("open rtsp input failed", ret); } ret = avformat_find_stream_info(ifmt_ctx, NULL); if (ret < 0) { return fatal("find stream info failed", ret); }

这段代码先处理 RTSP 拉流参数。rtsp_transport=tcp是最重要的一步:UDP 拉流在跨网段或者无线环境下非常容易丢包,丢包后的直接表现是花屏、卡顿,甚至几分钟后av_read_frame超时。默认走 TCP 可以换来稳定,代价是延迟略高,但做录像回传完全够用。

stimeout单位是微秒,5000000表示 5 秒。如果摄像头网络抖动,这个值可以调到 10 秒。max_delay控制 RTSP demuxer 内部缓冲,单位也是微秒,500ms 是一个兼顾延迟和乱序重排的值。摄像头本身推流不稳定时,先调大stimeout,不要一上来就动max_delay,否则延迟会肉眼可见地变大。

3.2 输出上下文与流映射

AVFormatContext *ofmt_ctx = NULL; ret = avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, argv[2]); if (ret < 0) { return fatal("alloc output muxer failed", ret); } // mp4/mov 把 moov 放到文件头,方便文件没写完也能预览 if (ofmt_ctx->oformat->name && (strstr(ofmt_ctx->oformat->name, "mp4") || strstr(ofmt_ctx->oformat->name, "mov"))) { av_opt_set(ofmt_ctx->priv_data, "movflags", "faststart", 0); } // 输入有多少路流,就建多少路输出流,只复制编码参数 for (unsigned i = 0; i < ifmt_ctx->nb_streams; i++) { AVStream *in_stream = ifmt_ctx->streams[i]; AVStream *out_stream = avformat_new_stream(ofmt_ctx, NULL); if (!out_stream) { return -1; } ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar); if (ret < 0) { return fatal("copy codec params failed", ret); } out_stream->codecpar->codec_tag = 0; out_stream->time_base = in_stream->time_base; } ret = avio_open(&ofmt_ctx->pb, argv[2], AVIO_FLAG_WRITE); if (ret < 0) { return fatal("open output file failed", ret); } ret = avformat_write_header(ofmt_ctx, NULL); if (ret < 0) { return fatal("write header failed", ret); }

avformat_alloc_output_context2的第三个参数传 NULL,第四个参数传文件名,FFmpeg 会根据扩展名自动推断容器。所以输出文件名必须是out.mp4、out.avi、out.flv这种严格后缀,否则选错封装器。

av_opt_set只对 MP4/MOV 设置movflags=faststart。这个选项的作用是把 moov 原子写到文件头,让录制还没结束时就有部分播放器能预览已写入的数据。AVI 和 FLV 没有这个选项,跳过就好。

out_stream->codecpar->codec_tag = 0是容易被忽略的一行。RTSP 源流里的 codec_tag 可能取自 RTP 的 payload 类型,直接复制到 MP4/FLV 里可能不匹配。置 0 后,muxer 会在write_header阶段按目标容器重新分配 tag。time_base先复制输入值,是为了让后面的时间戳换算有统一基线。

3.3 转封装主循环与时间戳重算

AVPacket *pkt = av_packet_alloc(); if (!pkt) { return -1; } while (av_read_frame(ifmt_ctx, pkt) >= 0) { AVStream *in_stream = ifmt_ctx->streams[pkt->stream_index]; AVStream *out_stream = ofmt_ctx->streams[pkt->stream_index]; // 把输入时间基下的信息,换算到输出时间基 if (pkt->pts != AV_NOPTS_VALUE) { pkt->pts = av_rescale_q_rnd(pkt->pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); } if (pkt->dts != AV_NOPTS_VALUE) { pkt->dts = av_rescale_q_rnd(pkt->dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); } if (pkt->duration > 0) { pkt->duration = av_rescale_q(pkt->duration, in_stream->time_base, out_stream->time_base); } pkt->pos = -1; ret = av_interleaved_write_frame(ofmt_ctx, pkt); av_packet_unref(pkt); if (ret < 0) { fprintf(stderr, "write frame failed: %d\n", ret); break; } }

av_read_frame返回 0 表示拿到一个包,返回负数表示读到文件尾或拉流错误。RTSP 流没有真正的“文件尾”,正常断开时返回的是错误码,所以这个循环天然适合断线检测。

时间戳换算是这段代码里最核心的体力活。视频流的 time_base 通常是 1/90000,音频流可能是 1/48000,而 FLV 要求毫秒,MP4 内部又可能是 1/12800。直接原样写包,播放器会把 90 kHz 的时间戳当成普通秒数来算,结果就是视频快进、音频对不上。av_rescale_q_rnd的作用就是把输入流的时间值按两个 time_base 的比例转成输出流时间值,AV_ROUND_NEAR_INF是四舍五入,AV_ROUND_PASS_MINMAX保证边界值不会被丢。

pkt->pos = -1不是玄学,而是必须。pos 记录的是源文件里该包的位置,RTSP 拉流时这个值没有意义,如果不重置,某些 muxer 会把错误的位置写进容器索引。av_interleaved_write_frame内部会缓存包并按 DTS 排序,所以写完一个包必须立刻av_packet_unref释放引用,否则长时间运行内存会持续上涨。

3.4 收尾与资源释放

av_write_trailer(ofmt_ctx); av_packet_free(&pkt); if (ofmt_ctx && !(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) { avio_closep(&ofmt_ctx->pb); } avformat_close_input(&ifmt_ctx); avformat_free_context(ofmt_ctx); printf("done: %s\n", argv[2]); return 0; }

av_write_trailer不是可选的装饰步骤,它负责把 MP4 的 moov 索引、AVI 的索引块、FLV 的时长信息补全。如果程序在这个函数之前被 kill,MP4 文件基本打不开,AVI 可能只能靠播放器猜索引,FLV 还能播到断点。

AVFMT_NOFILE标志用于区分输出是否是普通文件。如果以后把输出协议改成rtmp或者hls,就不应该手动关闭pb,而要让协议层自己处理。示例代码只处理本地文件,所以这个判断是安全的。

把 3.1 到 3.4 的代码按顺序拼到同一个 main 函数里,中间不要漏变量,就是一份完整可编译的程序。fatal 函数只在错误时返回,完整项目里建议用 goto cleanup 统一释放资源,这里为了突出主线逻辑做了简化。

4. 编译运行与三种容器的落地差异:参数怎么调、文件怎么验

4.1 编译命令与最小运行命令

# 用 pkg-config 自动带出依赖 gcc -O2 -o rtsp_mux rtsp_mux.c \ $(pkg-config --cflags --libs libavformat libavcodec libavutil)

如果你的环境没有 pkg-config,也可以直接写-lavformat -lavcodec -lavutil -lm,再按实际发行版补上-lz、-lssl、-lpthread等间接依赖。编译后运行:

./rtsp_mux rtsp://192.0.2.10/live/ch1 out.mp4 tcp

第三个参数tcp可以省略,因为源码默认就是 TCP。如果你的摄像头在另一个网段,必须走 UDP 才通,就手动传udp。

录完以后,用 FFmpeg 自带的 ffprobe 验证是最直接的:

ffprobe -show_format -show_streams out.mp4

重点看duration、nb_frames、codec_name是否和预期一致。如果 duration 显示 0,多半是时间戳重算或 trailer 写入出了问题。

4.2 MP4、AVI、FLV 三种容器的落地差异

容器录制中断后果播放器兼容性推荐场景需要额外注意的参数
MP4没写 trailer 基本报废好点播、上传movflags=faststart
AVI索引缺失后播放器靠猜H.264 兼容性一般本地临时无特殊,但别迷信 AVI 万能
FLV可播放到断点网页播放器普遍支持直播、边录边传时长字段固定,别依赖 duration

MP4 最怕异常退出。源码里加了faststart,只能让 moov 在文件头,但文件名能不能收尾,仍然取决于有没有执行到av_write_trailer。监控项目里我一般建议按小时切片,一个文件最多损失一小时,而不是录一整天然后一次崩溃全废。

AVI 的坑在于“能写不代表能播”。RTSP 里的 H.264 丢进 AVI,FFmpeg 不会报错,但很多播放器打开后要么黑屏要么只有声音。如果客户明确要 AVI,先拿目标播放器试播一段,再决定要不要走转码。

FLV 没有索引,所以边录边传非常合适,中断时播放器可以一直播到最后写入的 tag。但 FLV 的 duration 字段在很多实现里固定为 0,播放器会显示“时长未知”,这不是 bug,你也没必要花力气去修。

4.3 时间戳不重算会怎样

转封装里 90% 的疑难杂症都出在时间戳。RTP 视频时间戳一般是 90 kHz,音频 48 kHz,而 FLV 要求毫秒,MP4 要求相对采样率。如果直接把pkt.pts原样丢给av_interleaved_write_frame,轻则音频视频错位,重则 muxer 认为 DTS 回退,返回 Invalid data。

源码里的重算逻辑看起来简单,但顺序不能乱:先重算pts,再重算dts,最后重算duration。duration如果算错,MP4 的播放进度条会跳变,AVI 会出现音画不同步。不要在一个依赖pkt.duration的公式里反过来用已经修改的pkt.duration,各字段应该独立换算。

5. 转封装翻车现场:五个常见问题与排查顺序

5.1 UDP 拉流花屏、断流,几分钟后 av_read_frame 超时

现象:画面出现马赛克,频繁卡顿,日志里看到Connection timed out或End of file。

原因:UDP 传输不重传,RTP 包在跨交换机或无线环境丢失后,解码端没有关键帧就持续花屏。FFmpeg 的 rtsp demuxer 在 UDP 下超时后会直接返回错误,不会自动恢复。

解决:源码默认rtsp_transport=tcp。如果网络策略要求 UDP,把stimeout从 5 秒调到 10 秒,同时在业务层加断线重连。TCP 请求会带来少量延迟和带宽开销,但录像场景基本可以忽略。

5.2 程序被 kill 后 MP4 文件打不开

现象:文件大小正常,ffprobe 报moov atom not found,播放器提示文件损坏。

原因:进程被kill -9,或者断电,av_write_trailer没执行,MP4 的 moov 索引没有落盘。

解决:注册 SIGINT/SIGTERM 信号处理函数,收到信号后把g_stop置位,主循环检测到后退出并调用 trailer。如果物理断电无法避免,就做按时间切片。我个人的习惯是:任何监控录像系统都必须支持优雅停止,否则上线后被运维 kill 一次就等着返工。

5.3 FLV 里的 H.264 在网页播放器里绿屏或黑屏

现象:ffplay 能正常播放,但网页播放器打开只有绿屏或者黑屏,只有音频正常。

原因:FLV 要求 H.264 的 extradata 是AVCDecoderConfigurationRecord,也就是带 lengthSize 和 SPS/PPS 的 AVCC 结构。RTSP 拉流时如果只用 SDP 里的 sprop 参数,不经过 FFmpeg 的 demuxer 处理好 extradata,写进 FLV 的序列头就会缺东西。

解决:源码里用avcodec_parameters_copy完整复制 codecpar,FFmpeg 会保证 extradata 以目标容器需要的格式写入。不要自己去拼 SPS/PPS,尤其不要直接把 RTP payload 里的 start code 写进 FLV,那是典型的自找麻烦。

5.4 av_interleaved_write_frame 返回 Invalid data 或 Packet corrupt

现象:录制跑到中途突然退出,stderr 打印write frame failed: -1094995529之类的错误码。

原因:最常见三个。一是pkt->pos没有置 -1;二是在循环里用pkt->stream_index取输出流时,输入输出流数量不对应;三是时间戳换算后 DTS 仍然非单调,MP4 muxer 拒绝写入。

解决:先检查pkt->pos = -1是否在写入前执行。再确认 for 循环里创建输出流时,索引顺序和输入流一致。如果中间有跳过音频流,必须维护一张in_index -> out_index的映射表,不能直接用pkt->stream_index索引输出流。

5.5 音频进 MP4 失败或无声

现象:视频正常,音频轨缺失,或者封装时报codec not currently supported in container。

原因:RTSP 摄像头常见的音频编码是 PCMU/PCMA,也就是 G.711。MP4 容器虽然可以写 G.711,但很多播放器根本不认这种组合。还有一些私有摄像头输出非标准音频格式,muxer 直接拒绝。

解决:纯转封装里最简单的方法是只保留视频流,在创建输出流的循环里跳过音频流。如果业务上必须保留音频,就需要把 G.711 转成 AAC,这一步已经不属于转封装,得加解码器和编码器,并且 CPU 占用会明显上升。

6. 进阶改动:断线自动重连与按时间切片

6.1 断线重连:不补时间戳偏移就是给自己挖坑

把主循环套一层重连逻辑,是监控录像最刚需的改造。思路是av_read_frame返回负值时,先avformat_close_input,等 3 秒重新avformat_open_input,输出文件不关,继续往下写。但有一个坑:RTSP 重连后新的流时间戳往往从 0 开始,如果不做偏移,MP4 里时间轴会倒退,轻则进度条抖动,重则 muxer 直接报错。

我常用的办法是记录重连前的last_pts,重连后给这一轮所有包统一加一个偏移量。偏移量等于last_pts + 1 - 新流的起始 pts,具体数值可以在拿到第一个关键帧后计算一次,后续包都套用。这个偏移要在时间戳换算之前作用到pkt.pts和pkt.dts上,否则换算结果也会偏移错位。

6.2 按时间切片:在关键帧上切,避免下一个文件无法播放

如果录像要按小时或按 10 分钟分文件,可以在循环里记录写入时长,到达阈值后先av_write_trailer,再重新创建输出上下文,生成新的文件名。但切片点不能卡在任意帧上,否则下一个文件开头不是关键帧,播放器打开后先黑屏两秒,甚至直接卡住。

更稳的做法是在切片条件判断里加上pkt->flags & AV_PKT_FLAG_KEY:只有当前包是关键帧且已超过切片时长,才执行切文件。这样每个分段文件都能从 I 帧开始,首帧可解码。注意切片动作要放在av_interleaved_write_frame之后,确认当前包已经写进旧文件,再切换输出上下文。

我现在写这类录像工具,一定会把信号停止、断线偏移、关键帧切片三件事放进骨架里,等现场跑飞了再补就是一段血泪史。你拿到这套源码后,先跑通,再改重连和切片,改完记得用断网、断电两招分别测一遍,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询