C++并发编程-2 FFmpeg推流命令参数
其实没太想好FFmpeg这一篇博客放在哪个分类下面,后续应该不会开一个单独的音视频处理的分类,就接着上次的多路视频流采集继续写一些。
视频的基本概念
诚然,我对视频处理其实并不熟悉,因此需要对各个概念做些明确与区分,比如MP4是什么?H264又是什么?以及推流中的关键帧等概念,需要提前认识一下。
封装格式(快递箱子)
例如:.mp4、.flv、.mkv、.avi。这些文件后缀名,只是一个外壳。它就像一个快递箱,里面装着“视频画面”和“音频声音”两样东西。有些箱子是透明的(支持网页直接播),有些箱子很结实(支持更多功能)。直播推流通常用 FLV 箱子,因为它传输快。
编码格式
例子:H.264、H.265、AAC、MP3,这里面混杂了视频与音频的编码方法。原始的视频画面是非常巨大的(几秒钟就能有几百兆)。为了塞进箱子传输,我们必须把它“压缩”。H.264:这是一种把视频画面“折叠/压缩”的标准(方法)。AAC:这是一种把声音“折叠/压缩”的标准。
注意,H.264、H.265这俩有啥区别?
H.264(也叫AVC) —— 老大哥,最稳,目前世界上兼容性最好的视频编码标准。上到十几年前的诺基亚,下到最新的 iPhone,甚至是网页浏览器,都能直接播放它。因为兼容性好,推流基本上必须用 H.264,否则观众可能看不了。H.265(也叫HEVC) —— 接班人,省流量,画质一样的情况下,它的体积只有 H.264 的一半!这意味着更省带宽。
推流中的关键概念(码率、GOP、Copy)
码率(Bitrate):水管的水流大小
参数:-b:v 2000k:每秒钟传送多少数据。
- 太大:画质清晰,但如果观众网速不好,就会一直转圈缓冲(水管爆了)。
- 太小:流畅,但是画面全是马赛克(水流太细)。
GOP (Group of Pictures) 与关键帧:翻书的完整页
参数:-g 60 视频压缩不是每一帧都存一张完整的照片。
- I帧(关键帧):一张完整的照片。
- P帧/B帧:只记录相对于上一张照片“哪里变了”。
直播痛点:如果你把关键帧间隔设得太长(比如 10秒一个 I帧),新进来的观众必须等到下一个 I帧来了才能看到画面,这导致他进来可能会黑屏好几秒。直播通常要求 2秒一个 I帧。如果帧率是 30,那就设-g 60。
Copy 模式:直接换个箱子
参数:-c copy (或 -c:v copy -c:a copy)
解释:这是 FFmpeg 最神的一个命令。
场景:假设你有一个 .mp4 视频,里面的视频本来就是 H.264,音频本来就是 AAC。你想把它推流到服务器。
做法:你不需要让 libx264 重新把衣服折叠一遍(不需要重新编码)。你只需要把 .mp4 盒子拆开,直接把里面的流拿出来,塞进 .flv 的盒子里发走。
好处:极快!CPU 占用几乎为 0! 画质完全无损。
小疑问
这里其实会有一个神奇的问题,我个人的疑问:是不是固定了FPS、画面宽高(分辨率),是不是就能确定码率?确定了码率,是不是能决定实际观看中的FPS?
对于1秒的直播数据量(RAW),我们大约估算为:分辨率*颜色深度*FPS,这个数据量是比较大的,因此我们采用了压缩算法,那么压缩后的数据量有多少?这个数据的预算为:码率。(注意,我们已经锚定了时间窗:1秒)
理想的状态是什么?码率足够!编解码之后对画面的还原度高,也流畅!
但如果码率设置低会发生什么?全力保障帧率与流畅性,牺牲画面质量,即便是分辨率为1080,我画成简笔画,他用的数据量就会少(时刻注意,我们讨论的画面是压缩后的,并不是原始的图像理论数据量)
老板让我每秒画30张蒙娜丽莎,但是给我的笔只够画成15张,但是数量不能少,所以我只能把每一张都画得很潦草,最后保证交付30张
但有没有一种可能?30张是真画不完?这时候ffmpeg要求30帧,而CPU撑死渲染了20帧,此时不能赖数据量不够大,不能说是画师颜料配发少了,而是画的本来就慢,会发生丢帧。
FFmpeg 推流的基本命令结构
一条标准的推流命令通常包含以下几个部分:
1 | ffmpeg [全局参数] [输入文件参数] -i [输入地址] [视频编码参数] [音频编码参数] [输出封装格式] [推流地址] |
最简单的推流示例(直接复制流,不重新编码):
1 | ffmpeg -re -i local_video.mp4 -c copy -f flv rtmp://server/live/streamName |
核心参数详解
通用与输入参数
-i filename:指定输入文件或设备地址(可以是本地文件、摄像头、RTSP流等)。-re(Read native): 表示按本地帧率读取数据。推本地文件(如 .mp4)时必须加,否则 FFmpeg 会以最快速度读取文件并发送,导致流媒体服务器瞬间由于数据量过大而崩盘。如果是推摄像头或实时流(RTSP),通常不要加这个参数,因为源本身就是实时的。-y:若输出文件已存在,直接覆盖(在推流场景下用的少,录制时用得多)。-stream_loop -1:无限循环输入文件(放在 -i 之前)。
视频编码参数 (Video)
-c:v或-vcodec:指定视频编码器。copy: 最常用。直接复制视频流,不进行重新编码(CPU占用极低,画质无损,速度快)。libx264: 软件编码 H.264(通用性最好,但吃 CPU)。h264_nvenc: NVIDIA 显卡硬件加速编码 H.264。libx265: H.265 编码。
-b:v 2000k: 设置视频平均码率为 2000kbps。控制画质和带宽占用的核心参数。-maxrate 2500k: 设置视频最大码率。-bufsize 5000k: 设置码率控制缓冲区大小。通常设置为 maxrate 的 2 倍,用于平滑码率波动。-r 30: 强制设置帧率为 30 fps。-s 1920x1080: 设置分辨率(Scale)。-g 60(GOP size): 关键参数。设置关键帧间隔(Group of Pictures)。直播通常要求 2秒一个关键帧。如果帧率是 30fps,建议-g 60。如果不设置,进入直播间可能首屏加载很慢。-pix_fmt yuv420p: 设置像素格式。为了兼容各种播放器,建议强制指定为yuv420p。-preset(仅限 libx264): 编码速度预设。- 可选值:
ultrafast,superfast,veryfast,faster,fast,medium (默认),slow…
- 可选值:
-tune zerolatency(仅限 libx264): 优化编码参数以实现零延迟,直播必备。
音频编码参数 (Audio)
-c:a或-acodec: 指定音频编码器。- aac: 最通用的直播音频编码。
- copy: 不转码,直接复制。
-b:a 128k: 音频码率,通常 128k 或 64k 足够人声使用。-ar 44100: 音频采样率,通常为 44100Hz 或 48000Hz。-ac 2: 声道数,2 代表立体声。-an: 去除音频(静音推流)。
输出与封装参数
-f flv: 强制输出格式为 FLV。RTMP 推流必须加。-f mpegts: UDP/RTP 推流常用格式。-f rtsp: 推送 RTSP 流。
实践时的一些问题
最近做yolox的端侧推理的时候,MediaMTX有时会报写队列满的警告,在此处复盘分析一下:
在之前的博客中我曾使用ffmpeg推一个mp4视频,模拟监控设备的RSTP流,bash脚本为:
1 |
|
首先过一遍命令,采用copy的方式,推流到MediaMTX服务器,其中有一个之前没有提到的命令:-rtsp_transport tcp,他保证了mp4文件到MediaMTX的过程是满足TCP的,如果这部分采用了udp,虽然RSTP协议本身甚至默认采用UDP发送数据包,摄像头像个机关枪,向ffmpeg发送,但有的时候会因为网络问题导致丢包,如果丢弃了关键帧,可能要等更长时间,花屏更久。加了TCP,至少不会出现丢包。
但是这个参数与我实际的场景还是稍微有一点点不同的,我们分上下行来说:
上行(推流)
路径:本地文件 -> FFmpeg -> MediaMTX
参数作用:-rtsp_transport tcp FFmpeg 会跟 MediaMTX 握手说:“老兄,我要给你发视频,用 TCP 协议,别丢包。” MediaMTX 同意了。
下行(拉流)
路径:MediaMTX -> C++ 程序
参数作用:
- FFmpeg 脚本里的参数跟这里毫无关系。
- 这一段是用 TCP 还是 UDP,完全取决于你的 C++ 代码是怎么写的。
- 如果在 OpenCV 里没指定,默认通常是 UDP(也就是那个容易花屏的模式)。
- 但是! 既然 MediaMTX 报了
Write Queue Full,这就反向证明了你的 C++ 程序实际上正在使用 TCP 连接。因为只有 TCP 才会让服务器“排队等待”,UDP 服务器早就直接扔掉了,根本不会排队。
原因分析
两段路拼起来,看看“写队列满”是怎么发生的:
- 供货商(FFmpeg):通过 TCP,源源不断、保质保量地把货(视频帧)运到了仓库(MediaMTX)。
- 仓库(MediaMTX):因为是 TCP,供货商发多少,我就得收多少,不能拒收。
- 取货人(C++):也在用 TCP 跟仓库连着。
- 取货人虽然连着线,但是取货动作特别慢(read 慢,解码慢)。
- 取货人通过 TCP 告诉仓库:“等等!我处理不过来了,别发了!”
灾难发生:
- 仓库夹在中间。
- 左边进货停不下来(FFmpeg 还在推)。
- 右边出货被堵住了(C++ 不要)。
- 仓库里的货越堆越高(Write Queue 变大)。
- 最后仓库爆仓了 -> Write Queue Full -> 断开 C++ 的连接。
根本原因:C++ 消费速度 < FFmpeg 生产速度。
再来看看C++的代码:
1 | void InferenceWorker::captureLoop(const std::string& streamId) { |
存在以下性能隐患:
- 全局锁与 复杂度的丢帧逻辑
- frame.clone() 的深拷贝成本
- stream->capture.read(frame) 的隐性阻塞(是同步阻塞的,并且包含软解码。)



