☰
FFmpeg推送H264到RTP服务端:打包、SDP与接收实战
2026/9/30 3:35:15 网站建设 项目流程

简介:这是一份采用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分片与SDP

H264 裸流本质上是一串 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 muxer=rtp

这里的-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 testsrc=size=1280x720:rate=25 \ -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里会包含类似m=video 5004 RTP/AVP 96的行,以及a=fmtp:96 packetization-mode=1之类的描述。这份 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

抓满几秒钟后 Ctrl+C,然后用 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} ' f'payload_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 发送、接收和服务端验证这条路走顺。

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

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

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

立即咨询