FFmpeg发送H264 RTP封包实战:从NAL分片到SDP与排障 1. 需求拆解谁在把 H264 塞进 RTP 里FFmpeg 发送 RTP 封包的 H264 这个组合我第一反应是——命令本身只有一行靠的是对原理的理解硬扛。命令行谁都会敲抄一段现成的就能跑起来但流发出去之后画面花屏、接收端只收到第一个包、时间戳跳得像心电图这些问题能让人在工位前坐到半夜。所以这篇不打算只甩一条命令完事而是把 FFmpeg 发送 RTP 封包的 H264 这条链路从头到尾拆开H264 码流长什么样、RTP 怎么把它切块、FFmpeg 在中间做了哪些自动处理、哪些坑必须自己在参数里堵住。一句话概括这讲的是用 FFmpeg 把编码好的 H264 视频按 RFC 6184 的规则打成 RTP 包用 UDP 点对点发到网络另一端中间不经过任何服务器中转。1.1 这条链路到底在干什么先把整条链路摆清楚。源头可以是本地 mp4、摄像头采集、屏幕抓取甚至是一路已有的裸流。FFmpeg 拿到这些输入之后如果源不是 H264就得先编码用 libx264 或者硬件编码器都行如果源本来就是 H264可以直接转封装不解码。编码器吐出来的是 H264 的字节流按 Annex B 格式带00 00 00 01起始码里面混着 SPS、PPS、IDR 帧、普通 P 帧。接下来 RTP 封装层FFmpeg 里的 rtp muxer负责把这串字节按 NAL 单元切开每个 NAL 判断能不能塞进一个 RTP 包塞不下就切成分片然后给每个 RTP 包补上 12 字节的头版本号、序列号、时间戳、SSRC、payload type最后交给 UDP 协议栈发出去。整条链路里 FFmpeg 帮你自动做掉的事情比想象中多自动提取 SPS/PPS 写进 SDP、自动对超过 MTU 的 NAL 做 FU-A 分片、自动维护序列号递增、自动按 90kHz 时钟换算时间戳。这些默认行为在多数场景下够用但一旦出问题你必须知道它替你做了什么才知道从哪层下手改。我见过不少朋友一上来就怀疑网络抓包一看根本没发出去——是命令里-re忘了加FFmpeg 用了几秒钟把整部片子全推完了接收端缓冲瞬间被打爆看起来就像卡住了。这条链路的典型读者群其实挺明确需要在设备侧把一路视频低延迟地推给另一台设备的工程师。比如一台采集设备要把画面送到另一块屏幕、一个移动平台要把实时画面传回控制端、一套测试工装要把画面送到验证机。这些场景有个共同点——不需要用户管理、不需要拉流分发、不需要 CDN就是两个端点之间的一条管道。RTP 恰好是最贴合这个诉求的协议没有握手、没有会话、没有鉴权一个 UDP 端口发就完事。1.2 为什么不用 RTSP/RTMP偏偏选裸 RTP这个问题值得单独说因为选型错了后面全是无用功。RTSP 是控制协议本身不承载媒体它的价值在于请求-响应式的会话管理播放器发 DESCRIBE、SETUP、PLAY服务器才吐流。RTMP 是老牌直播协议基于 TCP可靠但延迟高握手也啰嗦。两者都需要一个服务端角色哪怕你只是点对点也得有人监听、有人应答。裸 RTP 把这层全砍了。发送方只管往一个 IP 加端口打 UDP 包接收方只管在那个端口上收。没有会话建立、没有能力协商、没有心跳保活。代价也很明确丢包了不会重传、接收方没起来就全丢、没有身份验证。所以这条路适合的是可控的局域网、专线、或者设备直连的场景对端是确定的、网络质量是可控的。如果你要做的是公网直播、多观众分发、跨 NAT 通信那这条路不对别硬上。还有一个现实原因——很多硬件解码器、嵌入式平台、低延迟接收端只认 RTP。它们的固件里就实现了一个极简的 RTP 收包加 H264 解码的循环你给它 RTSP 它反而不认。这种场景下发送端不管用什么协议栈最后落地都得是裸 RTP。FFmpeg 恰好能一步把文件或者采集源直接变成裸 RTP省掉中间搭服务的麻烦。提示RTP 本身不加密也不鉴权用在受控环境没问题一旦跨公网就要慎重考虑链路层的保护手段不要在开放网络上裸奔。1.3 适合谁上手给三类人画个像。第一类是嵌入式和应用侧开发者设备上要跑一个推流模块把 SDK 拿到的 H264 码流打成 RTP这时候用 FFmpeg 命令行做验证确认对端能收能解再把这套参数搬进代码里。第二类是运维和调试人员现场反馈画面卡住了需要快速拉一条 RTP 流复现问题手上有 FFmpeg 就能当场定位是编码问题还是网络问题。第三类是音视频初学者想看明白 H264 和 RTP 之间的关系命令行是最直观的教具——你能从抓包里看到每一帧是怎么被切成几个包的这种体感比读十遍 RFC 都强。三类人需要的深度不一样但底层原理是同一套。原文的语境里通常只给一条命令和一句能用了剩下的全是坑。我按自己的实操顺序往下写原理和命令交替着来你可以按需跳读但建议至少把第二节封装原理过一遍。2. 原理拆透H264 的 NAL 单元与 RTP 封包的配对逻辑这一节是全篇的地基。不理解 NAL 和 RTP 怎么配对后面所有参数都是瞎调。2.1 H264 码流里到底有什么H264 编码器输出的不是一帧一段视频而是一串NAL 单元Network Abstraction Layer Unit。每个 NAL 前面有一个字节的头结构是F(1位) | NRI(2位) | Type(5位)。F 是禁止位NRI 是重要性指示Type 才是关键——它告诉你这个 NAL 是什么东西。常见的 Type 有几个必须记住Type名称作用1非 IDR 切片普通 P 帧/B 帧的主体数据5IDR 切片关键帧的主体数据解码的起点6SEI补充增强信息可有可无7SPS序列参数集告诉解码器分辨率、帧率等全局参数8PPS图像参数集告诉解码器熵编码模式、量化参数等9AUD访问单元分隔符标记一帧的开始SPS 和 PPS 是整条链路的命门。解码器拿到一段 H264 裸流第一件事就是找这两个参数集找不到就直接拒收画面黑屏。所以 RTP 场景里发送端必须保证接收端在解码第一个 IDR 帧之前已经拿到 SPS 和 PPS。FFmpeg 的做法是把它们 Base64 编码后塞进 SDP 文件的sprop-parameter-sets字段里接收端解析 SDP 时就能拿到。如果你手动写 SDP 或者用别的工具收流这一步漏了症状就是流在跑但画面全黑。Annex B 格式的流里NAL 之间用起始码分隔三字节00 00 01或四字节00 00 00 01。编码器通常输出四字节起始码FFmpeg 拆包时会识别并剥离。你在抓包里看到的 RTP payload 是纯 NAL 数据没有起始码——这正是 RTP 封装层做的一件隐形工作。2.2 RTP 打包的三种模式RFC 6184 定义了三种把 NAL 塞进 RTP 的方式SDP 里的packetization-mode字段决定用哪种。FFmpeg 默认用模式 1也就是最通用的非交错模式。单 NAL 模式Single NAL Unit Packet最简单一个 NAL 小到能装进一个 RTP 的 payload就直接放进去RTP payload 的第一个字节就是 NAL 头。这种包在抓包里看payload type 对应 H264payload 开头就是0x65或0x41这类 NAL 头字节。STAP-A 模式聚合包把多个小的 NAL 合到一个 RTP 包里省传输开销。RTP payload 开头是0x18Type24后面跟多个2字节长度 NAL 数据的组合。SPS 和 PPS 通常被 STAP-A 聚合在同一个包里发出去因为它们都很小。FU-A 模式分片包处理大 NAL。一个 IDR 帧可能有几十 KB远超 MTU必须切。切法是把原始 NAL 头拆开RTP payload 的头两个字节变成 FU indicator 和 FU header。FU indicator 是F|NRI|Type28FU header 是S|E|R|TypeS 表示分片开始E 表示结束R 保留Type 是原始 NAL 的真实类型。中间各分片的 S 和 E 都是 0只有首片 S1、末片 E1。举个具体的一个 30KB 的 IDR NALMTU 按 1400 算payload 能装约 1380 字节那就要切成 22 个左右的 FU-A 包。接收端看到 Type28 就开始攒S1 起头E1 收尾拼回完整 NAL 再送解码器。中间丢一个分片整个 NAL 就废了——这就是为什么 UDP 传 H264 时丢包导致的往往不是局部模糊而是整帧垮掉甚至后续几十帧连带花屏。注意分片阈值不是拍脑袋定的。设太小包数暴增UDP 头开销占比大设太大超过 MTU 会导致 IP 层再分片任何一个 IP 分片丢了都让整个 UDP 包报废概率反而更高。2.3 MTU 与分片阈值怎么算标准以太网 MTU 是 1500 字节。一个 RTP 包经过协议栈时的开销是这样叠的IP 头 20 字节 UDP 头 8 字节 RTP 头 12 字节 40 字节。所以留给 H264 payload 的理论上限是 1500 - 40 1460 字节。但这是理想值实际上还要留余量。原因有三。第一链路中间可能有隧道、PPPoE 之类额外封装把 MTU 悄悄压到 1492 甚至 1400你的 1460 直接触发 IP 分片。第二IP 分片后的重组成本高且任意一片丢失整包报废丢包放大的效应很显著。第三部分 WiFi 和移动网络的实际可用 MTU 更低。工程上的经验值局域网内有线连接可以用 1400跨 WiFi 或者不确定链路时降到 1200特殊场景比如有隧道可以压到 1000 以下。FFmpeg 里通过-x264-params slice-max-sizeN控制的是 H264 切片大小而不是直接控制 RTP 包大小——RTP muxer 用的是由-f rtp时内部的默认 MTU 参数一般不用手动改。真正要调的是让编码器别切出超过 MTU 的 NAL这一步靠slice-max-size完成。计算逻辑是这样设slice-max-size为 S那么 H264 编码器保证每个切片不超过 S 字节。RTP 封装时如果 NAL 加头超过 MTU-payload 上限约 1460就自动走 FU-A。所以 S 设成 1200 是最稳的——既不会触发 FU-A省掉分片重组开销也不会触发 IP 分片S 40 1500。代价是切片变多压缩效率略降因为每个切片要重新做熵编码收尾。2.4 时间戳与序列号是怎么生成的RTP 头里两个动态字段最关键sequence number16 位和timestamp32 位。序列号每发一个 RTP 包加 1到 65535 回绕到 0接收端靠它判断丢包和乱序。时间戳反映的是采样时刻H264 的时钟频率是90000 Hz也就是说每帧的增量是 90000 除以帧率。比如 30fps 的视频每帧时间戳增加 90000 / 30 3000。25fps 就是 3600。这个值在抓包里看非常规整你可以用它反推发送端的帧率也可以发现时间戳跳变的问题比如突然多出几万说明有帧被重复或者时间基换算出错。同一帧的所有分片共享同一个时间戳接收端靠时间戳判断哪些包属于同一帧这个规则必须由发送端保证FFmpeg 的 rtp muxer 自动处理。marker bitM 位也值得一提一帧的最后一个 RTP 包要把 M 置 1其余包 M 为 0。接收端优先靠它判断一帧结束比靠时间戳变化更及时。抓包时如果发现一帧里所有包的 M 都是 0说明封装有 bug接收端可能一直等不到帧结束表现为延迟累积。3. FFmpeg 命令行实操从编码到发流一次打通原理摸清了接下来是落地。这一节给可直接抄的命令以及每个参数为什么这么设。3.1 最基本的一条命令长什么样ffmpeg -re -i input.mp4 \ -c:v libx264 -preset ultrafast -tune zerolatency \ -pix_fmt yuv420p \ -f rtp -sdp_file stream.sdp \ rtp://192.168.1.100:5004不讲道理地先看结果这条命令会把input.mp4实时转码成 H264打成 RTP 发到192.168.1.100的 5004 端口同时在当前目录生成一个stream.sdp文件。接收端拿着这个 SDP 用 ffplay 或者 VLC 打开就能看到画面。如果输入本身已经是 H264可以省掉编码环节直接用-c:v copyffmpeg -re -i input.h264 -c:v copy \ -f rtp -sdp_file stream.sdp \ rtp://192.168.1.100:5004转封装比重编码快得多CPU 占用可以忽略不计适合源本来就是 H264 的场景。但注意-c:v copy时你没法再调编码参数GOP 长度、码率、切片大小都取决于源。如果源的关键帧间隔是 250 帧那接收端中途加入就要等很久才有画面。3.2 关键参数逐个掰开-re是第一个必加的。它的作用是让 FFmpeg 按文件本身的帧率读输入而不是能读多快读多快。不加它一个 10 分钟的视频可能在 5 秒内全部变成 RTP 包打出去接收端 UDP 缓冲区瞬间溢出绝大多数包被内核丢掉表现就是发完了但只收到开头几帧。这个参数在实时推流场景几乎是强制的除非你要做的是批量灌包测试。-preset ultrafast -tune zerolatency是编码器的组合拳。ultrafast 让 libx264 用最快的速度编码牺牲压缩率换延迟。zerolatency 关掉 B 帧、关掉前瞻、关掉帧级线程缓冲保证每编码完一帧立刻吐出来不攒批。两者配合端到端延迟能压到几十毫秒级别。如果你不在乎延迟用-preset veryfast就能在质量和速度之间取得更好的平衡。-pix_fmt yuv420p保证输出格式是 4:2:0 采样。这个几乎是硬要求——绝大多数接收端和硬件解码器只认 yuv420p输出 4:2:2 或者 4:4:4 会导致解码失败或者颜色错乱。-f rtp明确指定输出封装格式为 RTP。不加这个FFmpeg 会根据输出 URL 猜格式rtp://前缀通常能猜对但显式指定更稳妥也避免了 URL 解析的意外。-sdp_file stream.sdp是把 SDP 描述落盘。SDP 里面包含了对端接收所需的全部信息payload type 是 96、时钟频率 90000、编码名 H264、packetization-mode1、以及最重要的sprop-parameter-setsSPS/PPS 的 Base64。接收端解析这份 SDP 才知道怎么解包和解码。没有 SDP 文件很多接收端尤其是 VLC 和硬件解码器根本不知道收到的是什么会直接丢弃。3.3 显式控制码率和 GOP上面那条命令的码率是 libx264 自己猜的结果往往偏大或者在复杂场景爆炸。显式控制用这几组参数-b:v 2M -maxrate 2M -bufsize 4M-b:v是目标平均码率-maxrate是瞬时上限-bufsize是码率控制的缓冲窗口。三者的关系是bufsize 越大码率波动越平滑但延迟越高bufsize 越小码率响应越快但可能影响画质稳定性。经验值是bufsize 2 × maxrate在低延迟场景下能兼顾稳定性和响应速度。GOP 控制在 RTP 场景里格外重要因为新加入的接收端必须等到下一个关键帧才能出画面。GOP 太长新接收端要黑屏好几秒GOP 太短码率浪费在重复的关键帧上。常用组合-g 30 -keyint_min 30 -sc_threshold 0这三条告诉 libx264每 30 帧强制一个关键帧、最小间隔也是 30 帧、场景切换不额外插入关键帧。-sc_threshold 0这一条容易被忽略——默认情况下编码器检测到场景切换会强制插关键帧看似是好事但会打乱固定的 GOP 节奏导致码率抖动和接收端缓冲策略失效。做点对点 RTP 传输时固定 GOP 的确定性比场景自适应的画质收益更重要。3.4 切片大小的实战配置前面算过slice-max-size设 1200 是稳的。写法是在-x264-params里传-x264-params slices4:slice-max-size1200slices4是把一帧切成 4 个独立切片并行编码能提升多核利用率同时每个切片本身就是独立的 NAL天然适配 RTP 分片。slice-max-size1200限制每个切片不超过 1200 字节保证不会触发 FU-A 和 IP 分片。完整命令合起来是这样ffmpeg -re -i input.mp4 \ -c:v libx264 -preset ultrafast -tune zerolatency \ -pix_fmt yuv420p \ -b:v 2M -maxrate 2M -bufsize 4M \ -g 30 -keyint_min 30 -sc_threshold 0 \ -x264-params slices4:slice-max-size1200 \ -f rtp -sdp_file stream.sdp \ rtp://192.168.1.100:5004这条命令能覆盖 80% 的常规场景。遇到特殊需求比如跨公网、超低带宽再在上面的基础上微调。3.5 SDP 文件里到底写了什么生成的stream.sdp大概长这样v0 o- 0 0 IN IP4 127.0.0.1 sNo Name cIN IP4 192.168.1.100 t0 0 atool:libavformat 59.27.100 mvideo 5004 RTP/AVP 96 bAS:2048 artpmap:96 H264/90000 afmtp:96 packetization-mode1; sprop-parameter-setsZ00AH5WoFAFuQA,aO48gA逐行解释几个关键字段。mvideo 5004 RTP/AVP 96声明这是一路视频端口 5004传输协议 RTP/AVPpayload type 96。artpmap:96 H264/90000把 payload type 96 映射到 H264时钟 90000。afmtp那一行是核心packetization-mode1表示支持 STAP-A 和 FU-Asprop-parameter-sets后面两个 Base64 串就是 SPS 和 PPS用逗号分隔。接收端必须拿到这份 SDP 才能正确收流。有两种方式传递一是像上面这样存成文件接收端打开文件二是接收端用rtp://直连但那样就拿不到 sprop-parameter-sets只能等发送端周期性重发 SPS/PPS。有些接收端支持带外方式获取参数集有些不支持稳妥的做法始终是用 SDP。提示SDP 里的cIN IP4那一行是发送端的地址接收端不一定用它——它只是描述性的。真正决定往哪发的是命令行里的rtp://ip:port目标。4. 接收端验证怎么确认流真的发出去了发送端跑起来只是第一步能不能收到、收到的对不对需要一套验证手段。4.1 用 ffplay 直接验证最快的验证方式另开一个终端ffplay -protocol_whitelist file,rtp,udp -i stream.sdp-protocol_whitelist是必须加的参数。ffplay 默认出于安全考虑禁止某些协议rtp 和 udp 都在白名单外不加会报Protocol not on whitelist错误。这个参数把 file、rtp、udp 三个协议放行够用了。如果能看到画面在动链路就通了。如果黑屏但 ffplay 不报错多半是 SPS/PPS 没传过去检查 SDP 文件里的sprop-parameter-sets是不是空的。如果 ffplay 报no frame或者超时那是根本没收到包往下看抓包部分。也可以不用 SDP直连ffplay -protocol_whitelist file,rtp,udp -i rtp://0.0.0.0:5004这样会监听本地 5004 端口但因为没有 SDPffplay 不知道 payload type 和 sprop 参数通常会失败。不推荐这种用法除非你在命令里额外指定了-rtp_payload_type之类的参数。4.2 抓包看 RTP 头到底是什么样调试 RTP 最有力的手段是抓包。在发送端或者中间机器上tcpdump -i any -nn udp port 5004 -w rtp.pcap抓一小段比如 5 秒CtrlC 停掉用 Wireshark 打开。Wireshark 通常能自动识别 RTP识别不出的话右键任一 UDP 包 → Decode As → RTP。打开之后你会看到一系列 RTP 包展开一个看头Version 2Padding 0Extension 0CSRC count 0Marker 0 或 1Payload type 96动态类型Sequence number 递增Timestamp 每帧递增 300030fps 时SSRC 一个固定的 32 位标识Payload 部分展开如果第一个字节是0x67那就是 SPS0x68是 PPS0x65是 IDR 切片0x41是普通切片0x7C即 0x60 | 28 0x7C是 FU-A 分片。抓包能回答的问题非常具体包有没有真的发出去发的速率是多少每个包的大小是多少有没有超过 MTU时间戳是不是规整序列号有没有断层我在排查一个接收端花屏的问题时抓包一看序列号从 1200 直接跳到 1350中间丢了 150 个包——问题不在发送端参数而是中间某台交换机的端口限速把突发流量丢了。4.3 验证清单收到流之后按这个清单逐项过检查项正常表现异常现象与原因有 RTP 包到达tcpdump 能看到流量无流量命令没跑、端口不对、防火墙拦截序列号连续每次加 1偶尔回绕断层多网络丢包、缓冲区溢出时间戳规整每帧加 90000/fps乱跳时间基转换错误、输入 VFRMarker 位正确每帧最后一包为 1一直为 0封装 bug接收端无法判断帧结束首包是 SPS/PPS0x67、0x68 或 STAP-A 0x18缺失sprop 没设接收端全程黑屏包大小小于 MTU 减去 40大于 1460会触发 IP 分片风险高这张表我通常会打印出来贴在工位上排查时一条条对比凭直觉猜快得多。5. 问题排查实录花屏、卡顿、断流的典型成因真正消耗时间的不是首次调通而是后续各种花式故障。这一节按症状归类给可直接对号入座的排查路径。5.1 画面花屏、绿块、局部马赛克症状画面能出来但时不时出现绿块、局部马赛克严重时整帧乱掉过几帧自己又恢复。这个症状几乎可以锁定为FU-A 分片丢失。一个 IDR 帧被切成若干个 FU-A 包接收端要攒齐才能组装出完整 NAL。中间丢一个整个 NAL 作废这一帧就花了。如果丢的是 IDR 帧后续依赖它的 P 帧全部连锁花屏要等下一个 IDR 才能恢复。表现就是花一阵、好一阵、又花跟关键帧间隔高度相关。排查步骤抓包看序列号断层。如果有规律地每隔几十个包丢一次基本确认是网络问题或者接收端 UDP 缓冲不足。检查包大小。如果 RTP payload 接近 1400 或者超过说明可能触发了 IP 分片链路中间任何一跳丢一片都会导致整个包报废。检查slice-max-size是否生效。用ffprobe或者抓包看 NAL 实际大小如果单个 NAL 动辄几 KB说明参数没传进去。解决途径减小slice-max-size到 1000 甚至 800让分片更细单次丢包影响范围更小同时在接收端加大 UDP 接收缓冲区SO_RCVBUF避免内核层就丢了。有条件的话在链路上做 QoS把视频流标记为高优先级。还有一种花屏是整个画面偏绿或偏紫这种不一定是丢包可能是像素格式不对。发送端输出 yuv422 或者 yuv444接收端解码器只支持 yuv420p颜色就会错。确认命令里加了-pix_fmt yuv420p。5.2 延迟越积越大症状刚开流时延迟还行跑了几分钟后画面明显落后越拖越严重最后差出几十秒。原因几乎总是-re没加或者接收端缓冲区设置过大。发送端全速推流时接收端处理不过来包堆积在内核缓冲区里越积越多播放出来就是越来越延迟。这不是网络慢是链路节奏不匹配。排查路径发送端看 FFmpeg 输出的实时码率。如果瞬间跑到几百 Mbps 说明-re没生效。注意-re必须放在输入文件之前放错了不生效。接收端调整缓冲。ffplay 有-fflags nobuffer和-flags low_delay参数能减少内部缓冲。检查 GOP 长度。GOP 太长接收端遇到错误时要等很久才能靠关键帧恢复体感就是卡住。根治方法是让发送端和接收端节奏严格对齐。发送端加-re编码器用-tune zerolatency关掉内部缓冲接收端用低延迟播放参数。三层都对上延迟能稳定在百毫秒级别。5.3 接收端只收到几个包就断症状抓包能看到前几十个包然后就没了接收端画面定格在第一帧或者干脆黑屏。这个症状比较隐蔽有几个常见成因SDP 里 sprop-parameter-sets 为空。接收端拿到流但不知道编码参数解码器初始化失败后续包直接丢弃。这种情况抓包看流量是有的但接收端不消费。payload type 不匹配。发送端用 96接收端的 SDP 里写的不是 96包对不上号。手动改过 SDP 或者用模板 SDP 时容易出这个问题。端口冲突。接收端监听的端口被别的进程占用包到了但没人接。用ss -ulpn或者netstat -ulpn查一下。防火墙规则。UDP 端口默认不开放跨机器时经常忘配。逐个排除就行。抓包有流量 接收端没画面 大概率是 SDP 或端口问题抓包流量只持续了几十个包 发送端提前退出了看 FFmpeg 的 stderr 有没有报错。5.4 时间戳和帧率异常症状画面能看但播放速度不对——快了、慢了或者间歇性卡顿。成因一输入是可变帧率VFR比如手机录屏、某些摄像头采集。FFmpeg 按输入时间戳打包每个 RTP 时间戳增量不规整接收端按固定帧率播放就错位。解决方法是强制转成恒定帧率加-r 30参数重新采样。成因二时间基换算问题。FFmpeg 内部用时间基表示时间RTP muxer 要做一次换算到 90kHz。如果输入的时间基特殊比如 1/1000换算时可能丢精度导致时间戳抖动。成因三用了-copyts但 SDP 的起始时间戳不是 0某些接收端处理不好。稳妥做法是让 FFmpeg 自己管时间戳不要手动干预。处理办法一般是加-vsync cfr -r 30强制恒定帧率让 FFmpeg 重新生成时间戳。这样做了之后时间戳会非常规整接收端就没问题了。5.5 常见问题速查表症状最可能的原因快速处理黑屏无任何提示SDP 缺 sprop-parameter-sets检查 SDP 文件重新生成花屏绿块FU-A 丢包 / IP 分片减小 slice-max-size加大接收缓冲延迟越跑越大缺-re或者接收端缓冲大加-re接收端低延迟参数只收到几包就断端口不通 / payload 不匹配查防火墙、核对 SDP播放速度不对VFR 输入加-r强制恒定帧率颜色错乱像素格式不是 yuv420p加-pix_fmt yuv420pCPU 占满用了默认 preset换 ultrafast或用硬件编码器码率远超预期复杂场景触发码率爆炸设 maxrate bufsize这张表覆盖了多数首次调流会遇到的问题。真正奇难杂症往往是网络环境导致的那就要靠抓包 分段测试来定位。6. 参数调优与长期运行经验命令跑通只是起点要让它长时间稳定运行还有一堆细节要处理。6.1 码率控制在 RTP 场景的特殊性RTMP、HLS 这类基于 TCP 的场景码率波动再大都行TCP 有流控和重传慢一点不影响正确性。但 RTP 跑在 UDP 上码率一旦超过链路带宽包就在瓶颈处直接丢了没有任何补救机会。所以RTP 场景下码率预算必须比链路带宽保守得多。经验公式目标码率 链路带宽 × 0.7。比如 10Mbps 的链路视频码率设到 7Mbps 以内留 30% 余量给协议开销和其他流量。如果用-maxrate那个值应该就是目标码率不要再往上浮动。-bufsize设成2 × maxrate是平衡点更小的 bufsize 让码率响应更平滑但延迟更低更大的更平稳但延迟更高。硬件编码器如各平台的硬件加速单元在码率控制上和 libx264 表现不同很多硬件编码器的 CBR 实现并不严格瞬时码率可能大幅超过设定值。如果你的场景对带宽敏感建议先用小流量测试硬件编码器的实际行为必要时用-maxrate配合-minrate强制约束。6.2 GOP 策略与新加入者体验GOP 长度在点对点 RTP 场景影响很大。一个接收端如果在流开始之后的第 N 秒才加入它必须等到下一个 IDR 帧才能解码出画面等待时间平均是 GOP/2 秒。GOP 是 30 帧1 秒平均等 0.5 秒GOP 是 300 帧10 秒平均等 5 秒体验很差。推荐 GOP 在 30 到 60 帧之间也就是 1 到 2 秒一个关键帧。这个间隔既控制住了新接收端的等待时间又没有把码率浪费在过多的关键帧上。如果需要更快的加入速度可以用更短的 GOP比如 15 帧代价是码率上升 10%-20%。还有一个技巧是周期性重发 SPS/PPS。默认 FFmpeg 只把 SPS/PPS 放在 SDP 和每帧 IDR 前面如果接收端中途加入且错过了 IDR要等到下一个 IDR 才能拿到参数集。可以在编码参数里加上-x264-params repeat-headers1让每个关键帧前面都带上 SPS/PPS接收端任意时刻加入都能快速拿到参数集。这个参数和-f rtp组合使用没问题muxer 会正确处理重复的 SPS/PPS。6.3 低延迟组合拳如果你的场景对延迟极度敏感比如远程操控、VR 图传上面那套参数还要再收紧ffmpeg -re -i input.mp4 \ -c:v libx264 -preset ultrafast -tune zerolatency \ -profile:v baseline -level 3.1 \ -pix_fmt yuv420p \ -b:v 4M -maxrate 4M -bufsize 2M \ -g 15 -keyint_min 15 -sc_threshold 0 \ -x264-params slices1:slice-max-size1000:no-scenecut \ -fflags nobuffer -flags low_delay \ -f rtp -sdp_file stream.sdp \ rtp://192.168.1.100:5004几个关键改动-profile:v baseline禁用 B 帧和 CABAC虽然压缩率降低但解码复杂度低硬件解码器友好-bufsize 2M让码率响应更快缓冲更小GOP 缩到 15 帧slice-max-size压到 1000 进一步降低分片丢包影响-fflags nobuffer -flags low_delay减少 FFmpeg 内部缓冲。这套参数实测在局域网内端到端延迟能压到 50-80ms 之间代价是码率效率降低 20%-30%画质在高动态场景下会有可见的块效应。延迟和画质永远是跷跷板调参的过程就是找平衡点。6.4 长期运行要看住的几个指标流跑一天没问题不代表跑一个月没问题。长时间运行要盯这几个指标CPU 占用。libx264 用 ultrafast 也在吃掉不少 CPU。如果目标设备是嵌入式平台优先用硬件编码器h264_nvenc、h264_vaapi、h264_videotoolbox等把 CPU 留给业务逻辑。硬件编码器的命令写法是把-c:v libx264替换成对应编码器其他参数大多通用但-x264-params要换成对应的参数集。内存增长。FFmpeg 长时间运行时如果输入是不断变化的源比如摄像头缓冲可能缓慢积累。监控进程的 RSS发现持续增长就定期重启或者检查输入的处理逻辑。网络抖动。局域网理想环境下丢包率是 0但 WiFi、电力线、移动网络环境下丢包是常态。发送端没法直接感知丢包RTP 没有重传只能靠接收端反馈。如果接收端支持 RTCP可以打开 RTCP 让发送端拿到接收质量报告不过 FFmpeg 的 RTP muxer 对 RTCP 的支持比较基础实际用得不多。更常见的做法是用应用层心跳——接收端定期发一个 UDP 包回来告诉发送端我还活着最近 N 个包丢了 M 个发送端根据反馈动态调整码率。端口资源。RTP 每个流占一个端口多路并发时要规划好端口分配。5004 是标准起始端口多路就用 5004、5006、5008 递增RTP 和 RTCP 通常成对占用 5004/5005、5006/5007。注意端口范围不要和系统服务冲突也不要开在 NAT 环境下无法映射的位置。最后分享一个我踩过的坑。有一次在设备上跑流跑了三天第四天突然黑屏抓包看发送端还在正常发包接收端就是不出画面。查了两小时才发现接收端的解码器有内存泄漏跑了三天把内存占满被系统杀了。这个教训告诉我RTP 这种无会话协议没有断开这个事件发送端永远不知道对方还活着没长期运行必须靠两边各自守好自己的资源不能假设对方一直健康。这个坑写出来希望能帮到后来人少走一点。提示长跑之前至少做一次 8 小时稳定性测试观察 CPU、内存、丢包率三条曲线。曲线平稳才能进入生产环境任何一条出现缓慢恶化趋势都要提前处理。