资讯动态

FFmpeg RTP发送H264低延迟传输与排错实战

发布时间:2026/10/27 7:03:40 来源:尧图企业网站定制
做视频传输的同行大概都遇到过一个场景手头有一段视频或者一路摄像头画面需要把它推到局域网里另一台机器上延迟要低最好是毫秒级中间不想架任何服务器也不想走那些需要握手、需要信令的协议栈。这时候最直接的办法就是把画面编码成 H264然后让 FFmpeg 发送 RTP 封包接收端拿到 SDP 就能播放。FFmpeg 发送 RTP 封包的 H264 这件事表面上看只是一条命令实际里面藏着 RTP 头部字段、H264 NAL 单元切分、FU-A 分片、90kHz 时钟换算、MTU 与 IP 分片、SPS/PPS 内联等一堆细节任何一个没对齐画面就是花屏、绿屏、卡顿或者干脆黑屏。我把它定位成一个“最小可用的低延迟视频传输骨架”适合做内网监看、实验台画面回传、设备屏幕投送、嵌入式板子出图、以及各种需要自己掌控码流的自研系统。刚入门的人可以照着命令跑通有经验的同行可以顺着协议细节去调优和排障。下面我按“链路怎么设计—协议怎么对齐—命令怎么写—怎么验证—怎么排错”这条线把我这几年的实操记录整理一遍。1. 先把链路拆开RTP 裸流方案到底解决了什么问题1.1 一条最朴素的传输链路长什么样很多人一上来就敲命令结果参数一多就懵。我习惯先把整条链路画在纸上从左到右五个环节采集 → 编码 → 打包 → 传输 → 接收解码。采集环节负责拿到原始帧可能是摄像头、屏幕抓取、文件解码也可能是某个 SDK 回调出来的 YUV 数据编码环节把 YUV 压成 H264 码流通常是 libx264 或者硬件编码器打包环节把 H264 的 NAL 单元按照 RTP 规范切成一个个不超过 MTU 的包并加上 RTP 头传输环节就是 UDP 发送组播还是单播取决于你的网络结构接收端反过来解包、重组成完整帧、送解码器、渲染。FFmpeg 在这条链路里其实一口气吃掉了后面四个环节libx264负责编码rtpmuxer 负责打包UDP socket 负责传输而 SDP 文件则是给接收端的一份“说明书”。理解这一点很重要因为后面所有排查你都要先判断问题出在哪一段。比如画面能出但偶尔一块块发绿八成是打包或传输段丢包画面完全黑可能是 SDP 和实际码流对不上画面动但音画不同步那是时间戳段的问题。把链路分段之后排错就不至于抓瞎。我见过不少人把 RTP 当成“另一个 RTMP”以为推过去就能自动被播放器识别。不是的。RTP 本身只负责承载它不知道里面装的是 H264 还是 AAC也不知道采样率、分辨率、profile 这些信息这些全靠 SDP 来描述。所以“FFmpeg 发送 RTP 封包的 H264”这个动作严格来说是两件事发流以及把描述文件交给接收端。少做第二件事接收端就不知道该怎么解。1.2 RTP 裸流、RTMP、TS 三种方式的取舍选型这块我踩过不少弯路值得单独说说。RTMP 的好处是自带握手和元数据交换播放器拿到地址就能播坏处是它基于 TCP一旦网络抖动就会在缓冲区里堆积延迟会越跑越大而且它更适合公网推流的场景在内网低延迟回传里反而拖后腿。TS over UDP 的好处是容错性强一个包丢了不影响后续解析坏处是封装开销大每个包 188 字节里有不少是头部和填充而且它对低延迟的支持不如裸 RTP 直接。RTP 裸流的优势就很明确了头部只有 12 字节封装开销极小基于 UDP没有重传带来的延迟累积时间戳用 90kHz 时钟精确描述每一帧的采样时刻接收端可以据此做抖动缓冲和同步。代价也很明显——没有信令没有重传丢包就是丢了SDP 得自己管SSRC、payload type 这些字段必须两端对齐任何一处不一致就是黑屏。我的经验是局域网、可控网络、延迟敏感、接收端能自己管 SDP就用 RTP 裸流需要跨公网、需要穿透各种中间设备、接收端是完全不受控的第三方播放器那就老老实实上 RTMP 或者别的带信令的方案。别为了“看起来更底层”硬上 RTP最后花在排错上的时间远超收益。1.3 FFmpeg 在链路里到底承担了什么角色有人会问既然 FFmpeg 一条命令就搞定了那还需要自己写代码吗取决于你的场景。如果只是把一段文件推出去做演示命令行足够如果是嵌入到自己的系统里通常会用libavformat的AVFormatContext手动打开rtp输出用av_interleaved_write_frame送包。这条路径和命令行走的是同一套代码所以命令行上调通的参数在代码里一一对应-pkt_size对应av_dict_set(opts, pkt_size, 1200, 0)-payload_type对应payload_type-ssrc对应ssrc。这里有个容易忽略的点RTP muxer 期望的输入是Annex-B 格式的 H264也就是每个 NAL 前面带00 00 00 01起始码。如果你喂进去的是 AVCC 格式长度前缀muxer 会解析失败或者输出异常。FFmpeg 在从 mp4 之类的容器读入时会自动插入h264_mp4toannexb这个 bitstream filter 做转换所以命令行层面通常感觉不到但自己写代码时必须显式加这个 bsf否则推出去的流接收端根本解不出来。这个坑我在早期项目里踩过一次现象是 Wireshark 里能看到 RTP 包但负载内容完全不符合 NAL 结构接收端一直报“no frame”。2. RTP 承载 H264 的协议细节字段对不上就全白干2.1 RTP 头里那几个必须盯住的字段RTP 头固定 12 字节字段不多但每一个都可能成为黑屏的原因。版本号固定为 2padding一般 0extension一般 0CSRC 计数一般 0marker 位非常关键它标记“这是当前帧的最后一个包”接收端靠它判断一帧什么时候收完如果 marker 设置错误接收端会一直等表现为画面卡在某一帧不动payload type必须和 SDP 里的artpmap编号一致常见取值 96 到 127 属于动态范围FFmpeg 默认 96如果你在 SDP 里写成 97接收端就会认为这是未知负载直接丢弃序列号每发一个包加一接收端用它检测丢包和乱序时间戳在同一帧的所有分片里必须完全相同跨帧才递增SSRC标识一路流多个流混在一个端口上时靠它区分。我印象最深的一次故障是 marker 位。当时用自己写的发送端图省事在每帧第一个包上设了 marker结果接收端永远只收到半帧画面撕裂成上半部分清晰下半部分灰。抓包一看marker 出现在帧首而不是帧尾接收端立刻把半帧送去解码了。改成帧尾置位之后一次通过。这件事让我养成一个习惯任何 RTP 相关问题第一件事是抓包看头部字段而不是改编码参数。编码参数改一百遍也治不了头部字段的错。SSRC 这个字段也值得说一句。默认情况下 FFmpeg 会随机生成一个 SSRC如果接收端按 SSRC 做流识别或者做多路复用重启发送端之后 SSRC 变了接收端可能就认不出来了。用-ssrc固定下来稳定性会好很多。另外同一个端口上如果跑了两路流SSRC 撞了就会互相干扰表现是画面在两个源之间跳这种问题极其难查固定 SSRC 并且做好规划是最省事的做法。2.2 单个 NAL、STAP-A 与 FU-A 三种打包方式H264 的 NAL 单元大小差异很大SPS/PPS 可能只有十几字节一个 IDR 帧可能几百 KB。RTP 的负载上限受 MTU 限制所以必须按大小分成三种打包方式。单 NAL 单元模式用于小于 MTU 的 NAL直接把整个 NAL 塞进负载负载第一个字节就是 NAL 头。STAP-A 聚合模式把多个小 NAL典型就是 SPS 和 PPS拼进一个 RTP 包减少包数量SDP 里packetization-mode1才支持。FU-A 分片模式用于超大 NAL把一个 NAL 切成多片每片前面加两个字节FU indicator 和 FU header。FU-A 的细节必须讲清楚因为花屏问题大多出在这里。FU indicator 的 type 字段固定为 28表示这是分片FU header 的高位是 S 位Start标记这是分片的第一片中间是 E 位End标记最后一片低 5 位是原始 NAL 的类型。接收端把第一片的 FU header 里的类型取出来作为重建后的 NAL 头中间的片只做拼接。如果 S 位或 E 位设置错误或者分片顺序乱了重组出来的 NAL 就是垃圾数据解码器要么报错要么出绿块。还有一个隐蔽的坑分片必须按 NAL 边界切不能把一个 NAL 的分片和另一个 NAL 的分片混在一个 RTP 包里。有些自己实现的打包器为了凑满 MTU把上一帧最后一个 NAL 的尾部和下一帧第一个 NAL 的头部拼在同一个包里这在 FU-A 模式下是非法的标准接收端会直接丢包。标准做法是一个 RTP 包只承载一个 NAL 的片段哪怕因此浪费几十字节。2.3 90kHz 时钟与时间戳的换算RTP 视频的时间戳时钟频率固定是 90000Hz这一点在 SDP 的artpmap:96 H264/90000里声明。时间戳的增量等于“这一帧和上一帧的显示时间差乘以 90000”。比如 25fps帧间隔 40ms增量就是 360030fps 是 33.33ms增量 3000。听起来简单但实际踩坑很多。第一个坑是可变帧率输入。如果源是摄像头帧率并不稳定每帧的实际间隔不同这时候时间戳必须按实际 PTS 换算而不是固定加 3000。FFmpeg 从文件读入时用的是文件里的 PTS一般没问题但如果用-use_wallclock_as_timestamps或者自己构造帧就容易出错。时间戳不单调递增接收端的抖动缓冲会紊乱表现为画面一顿一顿或者播放器直接报错。第二个坑是B 帧带来的 DTS/PTS 差异。有 B 帧时解码顺序和显示顺序不一致RTP 时间戳应该用 PTS 而不是 DTS。如果发送端错误地用 DTS接收端会看到时间戳乱序甚至回退缓冲逻辑直接崩掉。规避方式很简单低延迟场景本来就不该开 B 帧-bf 0或者直接用 baseline profile从根上消除这个问题。第三个坑是时间戳基准。RTP 时间戳的起始值应该是随机的这不是为了安全而是规范建议避免多路流之间产生相关性。FFmpeg 默认会做随机化如果自己实现别老老实实从 0 开始虽然大多数接收端不在乎但严格的实现会检查。这个细节在实际项目里影响不大知道就行。2.4 SPS 与 PPS 为什么会丢丢了会怎样SPS 和 PPS 是解码器初始化必须的参数集里面包含分辨率、profile、level、帧率提示、熵编码方式等信息。没有它们解码器连“这是多大分辨率的画面、该用哪种解码工具”都不知道。RTP 流里 SPS/PPS 有两种传递方式一是放在 SDP 的sprop-parameter-sets字段里Base64 编码二是在码流里内联发送通常在每个 IDR 帧前重复一次。只靠 SDP 传递的问题在于接收端如果是中途加入的或者解码器状态被重置了它手里有参数集但流里的第一个帧可能不是 IDR依然解不出来。只靠内联传递的问题在于如果发送端编码器不重复发送参数集x264 默认只在流开头发一次接收端晚加入就永远拿不到。稳妥做法是两边都做SDP 里带上sprop-parameter-sets同时让编码器在每个 IDR 前重复输出 SPS/PPS。x264 的对应参数是repeat-headers1通过-x264-params repeat-headers1传入。实测这个参数在“接收端中途接入”的场景里是决定性的加上之后接收端随便什么时候启动都能在下一个 GOP 边界拿到完整参数集出画面不加的话经常要等很久才出图或者一直黑屏。GOP 长度也要配合-g设得太大会导致中途加入的等待时间过长我一般设成帧率的一到两倍25fps 就设 25 到 50。3. 发送端实操命令、参数与 SDP 的完整落地3.1 一条能跑通的完整命令与逐参数解释先给一条我日常用的基线命令把一段本地视频按 25fps、1.5Mbps 推到组播地址ffmpeg -re -stream_loop -1 -i input.mp4 \ -an \ -c:v libx264 -preset ultrafast -tune zerolatency \ -profile:v baseline -level 3.1 \ -bf 0 -g 50 -keyint_min 50 -sc_threshold 0 \ -b:v 1500k -maxrate 1500k -bufsize 1500k \ -x264-params repeat-headers1 \ -f rtp -payload_type 96 -ssrc 1234 \ -pkt_size 1200 \ -sdp_file stream.sdp \ rtp://239.255.10.10:5004?ttl1逐条说。-re让 FFmpeg 按输入的原速读取不加这个参数它会以最快速度把整个文件推完接收端缓冲区瞬间被打满表现为卡死或者延迟几十秒。-stream_loop -1是循环播放做长时间测试时用。-an去掉音频先把视频这条链路调通音频后面再叠加。-preset ultrafast牺牲压缩率换编码速度低延迟场景基本是必须的用medium会引入几十毫秒的编码缓冲。-tune zerolatency是 x264 专门为实时场景准备的预设它会关掉 lookahead、关掉 B 帧、减小 VBV 缓冲这一条对延迟的影响比任何其他参数都大。-profile:v baseline是为了兼容性baseline 不支持 B 帧和 CABAC很多嵌入式解码器和老播放器只认这个。-level 3.1限制码率和分辨率的上限某些硬件解码器对 level 很敏感超了直接拒解。-g 50 -keyint_min 50 -sc_threshold 0三个一起用强制每 50 帧一个 IDR 且不允许场景切换插关键帧目的是让 GOP 结构完全可预测方便接收端做缓冲和错误恢复。-b:v和-maxrate设成一样配合-bufsize等于码率是 CBR 近似网络带宽有限时能避免瞬时码率冲高把链路打爆。-f rtp指定 muxer-payload_type 96要和 SDP 对齐-ssrc 1234固定流标识-pkt_size 1200控制单个 RTP 负载大小下一小节细说-sdp_file stream.sdp让 FFmpeg 把自动生成的 SDP 写到文件里这一条极其省事很多人不知道。最后的 URL 里ttl1表示组播只在本网段传播单播地址就不需要这个参数。3.2 MTU 与 pkt_size 的计算过程-pkt_size是这条命令里最容易被忽略又最容易出问题的参数。它的含义是单个 RTP 包的最大负载字节数。以太网的标准 MTU 是 1500 字节减去 IP 头 20 字节、UDP 头 8 字节、RTP 头 12 字节留给负载的是 1460 字节。如果用 FU-A 分片还要再减 2 字节的 FU 头理论上限是 1458。那为什么我建议设 1200 而不是 1458原因有三。第一很多链路上存在额外封装开销比如 VLAN 标签占 4 字节、某些链路层封装会再吃掉十几到几十字节一旦实际包超过 MTUIP 层就会做分片而 IP 分片对视频是灾难——丢一个分片整个 IP 包全废接收端连 RTP 头都拿不到序列号断档检测也就失效了。第二中间设备尤其是软件转发和虚拟网桥对超大 UDP 包的处理不一定规范。第三1200 这个值在很多实时传输实践里被验证过余量足够。FFmpeg 的 rtp muxer 默认pkt_size是 1472这个值放在纯以太网环境勉强能用但只要链路有一点点额外封装就会踩雷。我遇到过最典型的一次同一套配置在有线直连的机器上跑得好好的换到一台经过虚拟网桥的机器上就频繁花屏抓包看到大量 IP 分片重组失败把pkt_size从 1472 改成 1200 立刻恢复。这条经验值记住默认值不可信1200 起步。顺带算一下包速率方便评估网络压力。1.5Mbps 的码流每包负载 1200 字节即 9600 bit加上各种头约 40 字节即 320 bit单包总开销约 9920 bit包速率大概 150 包/秒。这个量级对普通交换机和网卡完全无压力但如果你的码率上到 20Mbps包速率就接近 2000 包/秒这时候网卡的中断合并、socket 缓冲区大小就开始变得重要了需要关注-buffer_size和接收端的fifo_size。3.3 SDP 的自动生成与手写模板-sdp_file这个选项会让 FFmpeg 在启动时把 SDP 写出来内容大致是这样v0 o- 0 0 IN IP4 127.0.0.1 sNo Name cIN IP4 239.255.10.10 t0 0 atool:libavformat mvideo 5004 RTP/AVP 96 artpmap:96 H264/90000 afmtp:96 packetization-mode1; sprop-parameter-setsZ2QAM6wVFKCgL/lQ,aO48gA几个字段必须看懂。mvideo 5004 RTP/AVP 96里的端口和 payload type 要和实际发送一致artpmap:96 H264/90000声明编码和时钟频率afmtp里的packetization-mode1表示支持 FU-A 分片和 STAP-A 聚合如果接收端只支持 mode 0你就不能发分片包大帧会被直接丢弃sprop-parameter-sets就是 Base64 编码的 SPS 和 PPS逗号分隔。自动生成的 SDP 有个小问题c行里的连接地址有时是127.0.0.1或者发送端网卡地址而不是你实际推的组播地址接收端拿去用可能连不上。所以我的习惯是自动生成一份当模板然后手工改。改的时候重点核对三处c行的地址、m行的端口、artpmap的 payload type。这三处对了剩下基本不会错。如果接收端是组播c行必须是组播地址如果是单播就填发送端的地址或者接收端自己的地址取决于接收端实现有些播放器要求填发送端地址。这一点挺坑的不同播放器行为不一致实测下来 VLC 对单播 SDP 里的地址比较宽容而 FFmpeg 的ffplay更严格填错就直接收不到包。3.4 编码器参数怎么配才不卡编码参数对延迟的影响比传输参数大得多。我做过一组对比测试同样 720p25 的输入-preset medium加默认 tune端到端延迟大概 300 到 500ms换成-preset ultrafast -tune zerolatency之后降到 60 到 100ms差距是数量级的。原因在于 x264 默认会缓存若干帧做多帧预测缓冲区不清空就不输出这个缓冲就是延迟的来源。-tune zerolatency具体做了几件事把rc-lookahead设为 0把sync-lookahead设为 0关闭 B 帧把sliced-threads打开把 VBV 缓冲设得很小。这些都是为实时传输服务的。如果你想更细粒度地控制可以不用 tune 而直接写-x264-params-x264-params rc-lookahead0:sync-lookahead0:bframes0:sliced-threads1:repeat-headers1:keyint50:min-keyint50:scenecut0sliced-threads1这个参数值得单独说。默认的多线程编码是“帧级并行”靠缓存多帧来提高吞吐这会带来延迟。切片级并行是把一帧切成若干片同时编码不增加帧缓冲延迟更低代价是压缩率略降。低延迟场景我一般都会打开它。还有一个常见误区是码率设得越高越好。实际上码率超过链路承载能力UDP 丢包会急剧上升画面反而更差。我的经验是千兆局域网内单路 1080p 给 4 到 8Mbps 足够百兆网络要控制在 3Mbps 以内无线链路波动大宁可给 2Mbps 配合小 GOP靠频繁 IDR 快速恢复。4. 接收端验证与抓包用证据说话4.1 ffplay 与常见播放器的加载方式接收端最简单的方式是ffplay加载 SDP 文件播放ffplay -protocol_whitelist file,udp,rtp -i stream.sdp-protocol_whitelist这个参数必须加FFmpeg 出于安全考虑默认不允许 SDP 里引用网络协议不加就会报“Protocol not on whitelist”。这个报错我见过太多人卡住其实就是一个参数的事。加上之后就能正常接收组播或单播流。如果想直接把流存下来做分析ffmpeg -protocol_whitelist file,udp,rtp -i stream.sdp -c copy -f mp4 recv.mp4注意-c copy是直接转封装不重编码适合验证“收到的码流本身是否完整”。如果存下来的文件播放正常说明传输链路没问题问题在播放器的渲染或缓冲如果存下来的文件花屏那就是传输或打包的问题往下查抓包。VLC 也能播打开“网络串流”填 SDP 文件路径即可。不同播放器对 SDP 的容错度差别很大我的建议是先用 ffplay 验证再用目标播放器验证。如果 ffplay 能播而目标播放器不能多半是兼容性问题payload type 不在它支持的动态范围、packetization-mode不支持分片、或者它只认 baseline profile。这种时候改发送端比改播放器现实。还有一种情况是接收端根本不支持 SDP直接给rtp://地址。这时候 FFmpeg 无法自动推断编码格式必须手动指定ffplay -f h264 -i rtp://239.255.10.10:5004?overrun_nonfatal1fifo_size1000000overrun_nonfatal1让缓冲区溢出时不致命退出fifo_size加大接收缓冲这两个参数在网络抖动时能救命但也会增加延迟属于权衡项。4.2 抓包看 FU-A 分片与头部字段排查 RTP 问题Wireshark 是必备工具。抓包命令用tsharktshark -i eth0 -f udp port 5004 -d udp.port5004,rtp -w rtp.pcap-d参数告诉 tshark 把 5004 端口按 RTP 解析不然它只当普通 UDP 显示。抓下来之后在 Wireshark 里打开重点看三样东西。第一看序列号连续性。选择任意一个 RTP 包展开头部看 Sequence Number 是否连续递增。如果有跳号说明中间丢了包如果乱序说明网络路径有多条或者有重排序。丢包率在千分之一以内对画质影响不大超过百分之一就会明显看到马赛克。第二看时间戳增量。同一帧的所有分片时间戳应该完全一样相邻帧的差值应该等于 90000 除以帧率。如果出现时间戳不变但内容在变说明发送端时间戳算错了如果差值忽大忽小说明输入帧率不稳。第三看FU-A 的 S/E 位。展开 H264 负载看 FU header 的 Start 和 End 标志。一帧的第一个包 Start 应为 1最后一个包 End 应为 1 且同时 RTP 头的 marker 位为 1。中间的分片这两个位都是 0。如果发现 marker 出现在 Start 上那就是我前面说的那个坑。Wireshark 还有一个很实用的功能Telephony → RTP → Show all streams可以看到每路流的包数、丢包数、抖动统计、码率曲线。做长时间稳定性测试时我通常会挂一个几小时的抓包然后用这个面板看抖动是否随时间恶化。4.3 用 ffprobe 和统计信息确认码流健康抓包只能看传输层码流本身是否健康要靠ffprobe。把收到的流存成文件后ffprobe -v error -show_streams -show_format recv.mp4重点看codec_name是不是 h264width/height是否符合预期r_frame_rate是否稳定nb_frames和duration是否和发送端一致。如果nb_frames明显少于预期说明丢帧严重如果r_frame_rate显示异常值说明时间戳有问题。更细一点可以用ffprobe -show_frames输出每一帧的信息看pict_type的分布。正常情况下应该是 I 帧之后跟一串 P 帧按固定间隔重复。如果 I 帧间隔忽长忽短说明 GOP 参数没生效或者场景切换插了关键帧。如果出现大量 I 帧码率会飙升网络压力变大。还有一个技巧是用 FFmpeg 的解码错误日志来判断。播放时加-loglevel verbose如果频繁出现error while decoding MB、concealing errors之类的提示说明码流有损坏问题在传输段。如果日志干净但画面还是不对那就要怀疑 SDP 参数或者解码器兼容性了。5. 排错实录我踩过的那些坑和对应解法5.1 首帧花屏、绿屏、黑屏的三类成因花屏最典型的表现是画面第一次出现时有半屏或整屏的马赛克持续一两秒后恢复正常。这几乎百分之百是 SPS/PPS 没到位或者第一个收到的帧不是 IDR。解法就是前面说的两件事一起做SDP 带sprop-parameter-sets编码器开repeat-headers1。如果做了还是花检查一下 GOP 是不是太长接收端等了太久才等到 IDR。绿屏通常是解码器拿到了数据但参数不对比如分辨率不匹配、profile 不支持。有一种很隐蔽的情况是SDP 里的sprop-parameter-sets是从上一次运行抄过来的而这次发送端的分辨率改了参数集没更新解码器按旧参数解新码流画面就是一片绿。自动生成 SDP 能避免这个问题手写模板的一定要每次核对。黑屏分两种。一种是完全收不到包ffplay窗口一直不出这时候先看网络tcpdump -i any udp port 5004有没有包进来没有就查防火墙、组播加入、地址端口。另一种是收到包但解不出来ffplay日志里会有解析错误这时候多半是 payload type 不匹配或者码流格式不对比如喂了 AVCC 格式的 H264。提示排查顺序永远是“先确认有包再确认包对最后确认能解”。跳过前两步直接怀疑编码参数是最浪费时间的一条路。5.2 卡顿、延迟累积与马赛克的定位路径延迟累积是 RTMP 和 TCP 方案的典型症状RTP over UDP 一般不会但如果你在接收端开了过大的fifo_size或者播放器做了很长的抖动缓冲也会出现。判断方法很简单拿一个秒表放在镜头前看屏幕上的时间差。差值稳定在某个值说明是固定缓冲可以调小差值随时间越来越大说明是发送端速度快于接收端处理速度要去查-re有没有加。周期性卡顿比如每隔一两秒顿一下八成是 GOP 边界的问题。每个 IDR 帧的码率远高于 P 帧如果-maxrate设得太紧IDR 帧会被强行压小画质断崖式下跌如果链路带宽不够IDR 帧就是丢包重灾区每次到 IDR 都花一下。解法是给-bufsize留余量或者降低 IDR 频率。随机马赛克是丢包。先看是不是pkt_size太大导致 IP 分片改到 1200 试试再看发送端 socket 缓冲够不够-buffer_size可以加大最后看网络本身ping -s 1400测一下大包是否丢能快速判断链路的 MTU 是不是小于 1500。有些虚拟网络和隧道链路的实际 MTU 只有 1400 甚至更低这种情况下pkt_size得跟着调到 1100 左右。5.3 收不到包时的网络层检查清单这一类问题跟 FFmpeg 没关系全是网络配置。我整理成一张表按顺序排查效率最高现象可能原因验证方式处理完全无包地址或端口写错tcpdump看有无入包核对 SDP 与 URL本机能收别的机器不能组播未加入或 TTL 太小换单播测试加?ttl或改单播单播收不到防火墙拦 UDP临时关防火墙测试放行对应端口能收但很快断端口冲突或 SSRC 冲突换端口测试规划端口与 SSRC时断时续无线链路质量差看丢包率与重传换有线或降码率播放器报未知负载payload type 不一致对照 SDP 与抓包统一为 96端口规划上有个细节RTP 规范里 RTCP 使用 RTP 端口加 1所以 RTP 一般用偶数端口。FFmpeg 发裸 RTP 不涉及 RTCP用奇数也能跑但为了和别的接收端兼容建议还是用偶数比如 5004、5006、5008 这样排下去。多路流的时候一个端口一路不要复用。组播地址选择也有讲究239.0.0.0/8 是本地管理范围适合内网使用。TTL 设 1 表示不出本网段设大了可能被路由器转发到不该去的地方造成不必要的流量。如果是跨网段的组播还得确认中间设备是否开启了组播路由这一块经常是“本网段能收、跨网段收不到”的根因。5.4 参数配置速查与调优优先级调优不要一次改一堆参数否则出了问题不知道是哪个引起的。我的习惯是按影响从大到小逐项调每改一项验证一次优先级参数作用建议值1-tune zerolatency消除编码缓冲必开2-preset ultrafast提升编码速度低延迟必开3-pkt_size避免 IP 分片12004-g控制 GOP 与恢复速度帧率的 1 到 2 倍5repeat-headers1中途接入不花屏必开6-bf 0消除 DTS/PTS 差异必开7-ssrc固定流标识按规划8-profile:v baseline兼容老解码器有兼容需求时这套顺序背后是一个判断延迟的大头在编码器缓冲画质的大头在码率与 GOP稳定的大头在打包与网络。先解决延迟再解决画质最后处理偶发问题效率最高。反过来先纠结码率往往发现延迟还是那么高白折腾。还有一个我个人的小习惯任何新环境第一次部署都会先用一段 10 秒的测试片跑通观察三件事——接收端多久出画面、画面是否完整、用ffprobe数出来的帧数是否对得上。这三项过关了再上真实业务流。真实业务流往往帧率不稳、分辨率会变暴露出的问题比测试片多得多。6. 一些只有做过才知道的细节中途加入的接收端还有一个隐藏问题即使它拿到了 SPS/PPS也可能因为第一个 IDR 帧的分片丢了中间某一片而解不出来。UDP 没有重传这种丢失只能靠下一个 IDR 恢复。所以 GOP 长度其实是“最大黑屏等待时间”的上限-g 50在 25fps 下意味着最坏情况要等 2 秒。如果业务不能接受 2 秒黑屏就得把 GOP 缩短代价是码率上升。这个权衡没有标准答案得按场景定。多路并发的时候我倾向把编码和发送放在同一个进程里用不同的AVFormatContext对应不同端口而不是起多个 FFmpeg 进程。原因是多进程会各自维护一份编码器上下文和 socketCPU 缓存局部性差而且不方便统一控制。当然如果只是临时验证多个命令行进程最简单用完就杀。最后提一个容易忽略的资源问题长时间推流时接收端的 socket 接收缓冲区是高负载下最先撑爆的地方。默认值往往只有几百 KB码率高的时候几毫秒就填满填满之后内核直接丢包。发送端可以通过-buffer_size调整接收端在 FFmpeg 里用fifo_size和overrun_nonfatal配合。我一般会把接收缓冲设到 1MB 到 4MB 之间具体看码率和允许的延迟上限这个值每加 1MB 大约带来 5 到 10ms 的额外延迟按 1Mbps 到 8Mbps 估算属于可以接受的代价。如果非要说一条最有价值的经验那就是RTP 这套东西命令行写得再花哨都不如抓一次包。头部字段、分片标志、时间戳增量全都明明白白摆在抓包文件里看一眼顶得上猜半小时。我现在遇到任何“画面不对”的反馈第一句话都是“抓个包发我看看”。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑