ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FFmpeg推送H264到RTP服务端:打包、SDP与接收实战

FFmpeg推送H264到RTP服务端:打包、SDP与接收实战 简介这是一份采用C语言实现的实时传输协议RTP视频流传输工程完整覆盖H264编码视频从发送到接收的全过程适合音视频开发、网络编程学习及流媒体项目起步阶段的工程师作为参考。压缩包约62.95MB共1118个文件主体为C/C源码h、cpp、c、编译中间产物obj、pdb、idb、库文件lib、dll及Visual Studio工程配置同时附有YUV与264原始码流样本目录覆盖从源码到可执行程序的完整开发链便于直接构建调试。工程演示了服务端如何将H264文件分割为RTP数据包并发送客户端如何接收、重组H264的NAL单元并解码输出YUV画面涉及时间戳同步、RTP头解析、分片打包和FFmpeg工具协同等关键知识点。目前已有287人学习下载通过这份资源可系统掌握实时媒体传输链路中的协议封装细节与编解码衔接方法对完成课程设计、毕业设计或预研实际产品均有直接参考价值能显著降低从理论理解到代码落地的难度。1. 把H264文件搬进RTP服务端从FFmpeg命令到自建接收端的最小闭环拿到一个像 Rtp.zip_C 这样的压缩包里面不外乎是一堆示例代码和 ffmpeg 命令行核心要解决的问题就一个本地有一个 H264 裸流文件要按 RTP 打包后发送到服务端或者反过来服务端把收到的 H264 RTP 流重组后落盘。很多开发者一上来就想到 RTSP、RTMP但底层联调时最常用的是裸 RTP不需要信令会话一对 UDP 端口加一份 SDP 描述就能跑通。这篇文章围绕 ffmpeg h264、RTP 服务端这两个关键词把发送端压缩成一条命令接收端用 FFmpeg 或几十行 Python 验证顺带把花屏、丢包、时间戳这类坑挨个说清楚。适合正在做流媒体网关、国标平台接入或者被“RTP 收不到流”折磨的从业者。2. 开工前的选型先搞懂 H264 在 RTP 里怎么拆包再决定服务端怎么写2.1 一段H264裸流怎么变成一个RTP包NAL单元、FU-A分片与SDPH264 裸流本质上是一串 NAL 单元每个 NAL 以起始码00 00 01分隔。SPS、PPS、IDR、非 IDR 切片都是不同类型 NAL 单元。RTP 打包不是把文件按字节切碎塞进 UDP而是把每一个 NAL 当作一个逻辑载荷。发送端要做的第一件事是去掉起始码然后在 NAL 头前加一个真正的 RTP 负载头。这个头只有一字节高五位是 NAL type低两位是 NRI 优先级。如果 NAL 字节数小于 MTU通常按 1200 字节算整个 NAL 可以直接放进 RTP 载荷如果大于 MTU比如一个 IDR 帧的切片非常大就必须拆成多片用 FU-A 分片方式发送。接收端拿到 FU-A 后要按 FU header 里的 S 位和 E 位判断分片边界手动把分片拼回完整 NAL。SPS 和 PPS 是解码的关键信息它们体积小可以用 STAP-A 聚合在一个 RTP 包里带过去也可以放在 SDP 文件里通过带外方式传递。FFmpeg 的 rtp muxer 会自动处理这些但你自己写接收端时就要注意只做 RTP 载荷透传是不够的必须解析 SPS/PPS否则解码器不知道分辨率、帧率、profile花屏就是必然的。2.2 用 FFmpeg 当 RTP 发送端不用重编码数据链路也清晰FFmpeg 在这里承担三个角色解析 H264 裸流、按 RTP 规则打包、把包写到指定 UDP 端口。它不是一个完整的流媒体服务端只负责把媒体载荷“推到”网络里。最常见的命令写法是-c:v copy这意味着不做解码和重编码FFmpeg 直接从输入文件里读 NAL 单元再做 RTP 封装。好处是 CPU 占用极低适合在调试时保持原始画面质量。副作用是如果输入 H264 文件本身没有正确的时间信息RTP 时间戳可能不准这个在避坑章节再展开。动手之前先确认 FFmpeg 编译时带上了 RTP 协议ffmpeg -protocols 2/dev/null | grep rtp输出里能看到rtp和rtp_mpegts之类就算正常。如果没有就得换一个完整版 FFmpeg 编译包。接下来还要看一眼 rtp muxer 支持哪些选项ffmpeg -h muxerrtp这里的-sdp_file、-payload_type、-ssrc是后面必用的几个参数。把这些参数提前背下来比直接抄命令要少走弯路。2.3 服务端方案裸UDP、FFmpeg命令行、自建socket程序的取舍标题里的“服务端”在多数工程语境下指的不是 SRS 或 ZLMediaKit 这类完整流媒体服务而是接收 RTP 包那一侧。它可以是 FFmpeg 命令行也可以是自己写的一个 UDP socket 程序。服务端形式是什么适合场景不适用场景FFmpeg SDP用 ffmpeg/ffplay 读取 SDP 并解码快速验证链路、临时拉流多路并发、长时间无人值守Python/Go/C socket自己绑定 UDP 端口收包协议学习、定制化落盘需要完整解码播放时复杂度高SRS/ZLMediaKit专业流媒体服务生产级推拉流本标题只讲裸 RTP接口方式差异太大我个人建议第一版服务端直接用 FFmpeg 命令行因为它能把 RTP 层、NAL 层、解码层一次全处理完。等你确认链路参数没问题了再决定要不要自建 socket 服务端。先跑通最小闭环永远比一开始就写复杂逻辑要快。3. 用FFmpeg发送H264文件到RTP服务端最小命令与SDP接收全流程3.1 准备一份测试用的H264裸流文件如果你手头没有现成的.h264裸流可以用 FFmpeg 生成一份避免用 MP4 或 MKV 测试时因为容器时间戳干扰 RTP 判断。ffmpeg -f lavfi -i testsrcsize1280x720:rate25 \ -t 10 -c:v libx264 -preset ultrafast -g 50 \ -f h264 test.h264逻辑说明-f lavfi -i testsrc让 FFmpeg 生成测试画面不需要摄像头或采集卡-g 50是每 50 帧插一个关键帧也就是大约 2 秒一个 IDR后面测试花屏时够用-f h264输出 AnnexB 格式的裸流文件。参数说明-preset ultrafast只是让测试文件生成得快一点不影响 RTP 发送测试。如果后面想验证更真实的网络画面可以把testsrc换成smptebars或者一个本地视频文件。3.2 发送端命令用rtp muxer把H264文件推出UDP包这是整个文章里最核心的一条命令ffmpeg -re \ -i test.h264 \ -c:v copy \ -f rtp \ -sdp_file send.sdp \ -payload_type 96 \ -ssrc 10086 \ rtp://127.0.0.1:5004逻辑说明-i test.h264读入裸流-c:v copy告诉 FFmpeg 不要转码直接把 H264 编码数据交给 muxer-f rtp激活 RTP 打包器rtp://127.0.0.1:5004是目标地址和端口。执行后 FFmpeg 会持续向该 UDP 端口发送 RTP 包直到文件发完。参数说明-re表示按文件原始帧率读取默认不加时 FFmpeg 会全速发送一个 10 秒的文件可能 0.5 秒就发完了接收端根本来不及处理-sdp_file send.sdp把媒体描述信息导出到本地文件接收端必须靠它才能知道载荷类型、SPS/PPS、端口等参数-payload_type 96是动态 RTP 载荷类型H264 一般选 96 到 127 之间的值-ssrc 10086是同步源标识如果同一端口有多路流可以靠它区分。执行后send.sdp里会包含类似mvideo 5004 RTP/AVP 96的行以及afmtp:96 packetization-mode1之类的描述。这份 SDP 要原样复制给接收端。3.3 接收端用ffplay预览用ffmpeg落盘在同一台机器或者另一台机器上拿到send.sdp后可以这样预览ffplay -protocol_whitelist file,udp,rtp -i send.sdp逻辑说明-protocol_whitelist是 FFmpeg 4.x 之后的安全限制不加这个参数会报 “Input/output error”因为 SDP 文件里会引用rtp://和文件路径FFmpeg 默认不放行这些协议。如果想直接落盘成 H264 文件用ffmpeg -protocol_whitelist file,udp,rtp \ -i send.sdp \ -c:v copy -y received.h264参数说明-c:v copy仍然不做转码直接把从 RTP 里解出来的 NAL 单元写进文件-y覆盖已有输出。如果这两个命令报端口冲突检查send.sdp里的端口是否和发送端命令一致。3.4 接收端是自建服务端时要注意什么自建服务端通常只绑定了 UDP 端口但 RTP 包里有payload_type有ssrc有seq和timestamp这些都得解析。FFmpeg 接收端会自动处理而自建 socket 程序需要自己做。很多人在这一步直接把收到的 UDP 内容全部写入文件结果得到一个无法解码的.h264。正确做法是先提取 RTP 头再按 NAL 单元重组至少要把单 NALU 和 FU-A 分片两种情况处理掉。如果不想一上来写这么多逻辑就用 5.2 节的最小代码先验证链路通不通再决定要不要实现完整解包。4. 避坑H264 over RTP 的常见问题与排查路径4.1 花屏或绿屏关键帧缺失、SPS/PPS没有及时送到解码器现象接收端画面刚开始是彩色条纹过一会儿又正常或者画面整体糊成一片只有偶尔闪出完整帧。原因RTP 链路里第一个到达的 NAL 单元不是 IDR 帧解码器没有参考帧就无法重建画面另一种常见情况是 SPS/PPS 只放在 SDP 里但自建服务端没有解析 SDP直接裸收 RTP 载荷解码器永远拿不到参数集。解决发送端强制缩短关键帧间隔测试文件用-g 50甚至-g 25保证每 1 秒至少一个 IDR。接收端如果是 FFmpeg确认 SDP 里的fmtp参数包含sprop-parameter-sets如果是自建 socket 程序要么解析 SDP要么在发送端开启带内参数集。FFmpeg 的 rtp muxer 在部分版本里可以用-rtpflags h264_mode强制以兼容模式发送遇到老解码器时可以试一下但首选还是把 SDP 解析干净。4.2 服务端收不到任何包或者只有发送端自己能看到现象发送端 FFmpeg 一直正常输出日志接收端ffplay卡在黑屏自建服务端 recvfrom 一直阻塞。原因最常见的是防火墙拦截 UDP 端口其次是发送端和接收端的rtp://地址写错比如发送给127.0.0.1而接收端在另一台机器上。还有一种隐蔽情况发送端绑定的端口和 SDP 里不一致接收端用 SDP 里的端口去 recv实际包却落在另一个端口。解决先不要怀疑协议直接抓包确认包是否到达。发送端保持运行在接收端执行sudo tcpdump -i any -n udp port 5004如果没有任何输出说明网络层就没过来。再查防火墙和路由如果有输出说明 UDP 已经到达主机只是应用层没读对。这个排查顺序能帮你省下大量时间。4.3 视频速度不对时间戳跳变导致播放器快放或慢放现象没有音频单看 H264 画面播放速度明显比预期快或者越来越慢。发送端按文件顺序发送接收端解码后像抽帧。原因H264 裸流本身没有时间戳FFmpeg 在读取裸流时只能按文件内数据顺序推断。如果输入文件是从 MP4 抽取出来的但抽取时没有保留时间基RTP muxer 就会用默认的 90kHz 时钟去计算 timestamp得到的时间戳不一定匹配真实帧率。解决发送端在-i test.h264前面显式指定帧率ffmpeg -re -framerate 25 -i test.h264 \ -c:v copy -f rtp -sdp_file send.sdp \ rtp://127.0.0.1:5004-framerate 25放在-i前告诉 FFmpeg 裸流每秒钟 25 帧。如果原始文件是 30fps参数要改成 30。这是很多人最容易忽略的一个点也是 RTP 调参里最典型的“玄学”之一。4.4 长时间运行延迟越来越大最终开始丢包现象刚启动时画面正常跑几分钟后延迟越来越高接收端画面明显滞后随后出现花屏或卡顿。原因裸 RTP 是 UDP没有拥塞控制。发送端如果按文件原速推流码率超过网络或服务端处理能力时包会在内核缓冲丢队接收端如果收包逻辑不够快也会来不及从 socket buffer 里读数据。解决自建服务端调大 socket 接收缓冲区把两端放到同一交换机下避免跨公网测试。还有个小技巧在发送端用-max_delay 200000调整 RTP 最大延迟单位是微秒默认值可能太大。不要指望裸 RTP 能像 RTSP 一样对抗弱网你需要的是先证明链路在稳定环境下是通的。注意这里不要一开始就把问题归结为“UDP 协议不行”八成是发送参数或者接收逻辑有问题。先把 pcap 拿到手再下结论。5. 进阶验证与最小服务端代码先抓包子再改参数5.1 用tshark解析RTP头确认序列号和载荷类型发送端跑起来之后抓一份 pcap 文件sudo tcpdump -i any -n udp port 5004 -w rtp.pcap抓满几秒钟后 CtrlC然后用 tshark 提取关键字段tshark -r rtp.pcap -T fields \ -e rtp.seq -e rtp.timestamp -e rtp.payload_type -e rtp.ssrc逻辑说明这一步能直接看到序列号是否连续、时间戳是否跳跃、载荷类型是否和 SDP 里一致。序列号不连续说明丢包payload_type 不是预期值说明发送端参数或 SDP 信息有出入。这个黑匣子打开之后很多问题就不用猜了。5.2 一个足够验证链路通的UDP RTP服务端如果你不想启动完整 FFmpeg 接收端可以用这段 Python 代码先验证服务端能不能收到合法的 RTP 包import socket s socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((0.0.0.0, 5004)) while True: data, addr s.recvfrom(2048) if len(data) 12: continue seq int.from_bytes(data[2:4], big) ts int.from_bytes(data[4:8], big) pt data[1] 0x7f ssrc int.from_bytes(data[8:12], big) print(f{addr[0]}: seq{seq} ts{ts} pt{pt} ssrc{ssrc} fpayload_len{len(data)-12})逻辑说明RTP 头固定 12 字节载荷从第 12 字节开始。这里没有做 NAL 重组只验证“服务端是否有数据到达、头部字段是否合理”。如果打印出来的 seq 持续递增说明发送端和服务端的网络链路是通的如果 seq 跳跃大需要回到发送参数和网络环境排查。如果你需要真正落盘 H264 文件还是要实现 FU-A 重组。没有捷径唯一可靠的是把单个 NAL 和 FU-A 分片两种情况都解析再把相同 timestamp 的 FU-A 分片拼接成一个完整 NAL。5.3 我自己的验证习惯和最后的提醒我一般会在发送端加-sdp_file之后先把send.sdp存好再在接收端用 tshark 对照一次。确认协议层没问题才开始调代码。以前有一次花了一晚上改负载类型参数最后发现只是防火墙忘了放行 UDP 5004 端口。从那以后我养成了这个习惯一切链路异常先抓包再改配置。RTP 是明明白白能抓出来的协议序列号、时间戳、载荷类型就摆在那里按图索骥比拍脑袋改命令靠谱得多。希望这篇笔记能帮你把 RTP/H264 发送、接收和服务端验证这条路走顺。本文还有配套的精品资源点击获取
返回列表