RTP转H264文件全攻略:UDP拆包、FFmpeg命令与自写程序避坑指南
发布时间:2026/10/7 2:00:06 作者:尧图编辑部 阅读量:1,286

简介一套面向摄像头实时取流场景的C工程实现提供基于UDP的RTP流抽取与H264落盘方案适合网络视频传输、协议分析及流媒体开发人员参考。工程围绕RTP解封装这一核心任务实现了RTP包头解析、H264 NAL单元边界识别、分片NAL重组、起始码修复以及Annex B格式文件写入等关键流程并提供了服务端接收模块与通用工具代码帮助开发者快速搭建从网络流到本地H264文件的处理链路。资源压缩包仅9KB共14个文件以6个.h头文件和4个.cpp源文件为主另含vcxproj工程配置和ReadMe说明文档整体结构简短清晰便于逐模块阅读。目前已有1031人学习阅览代码中涉及的UDP通信、RTP时间戳与序列号处理、NAL分片重组等细节对理解在线视频传输底层原理和自行扩展H264存储功能具有直接参考价值适合具备C和网络编程基础的开发者学习实践。1. 把网络里的 RTP 流落成 .h264 文件这件事比想象中多一层最近在调一套视频采集链路摄像头通过 RTSP 推流我需要在另一台机器上把 UDP 里的 RTP 数据保存成 h264 文件留着离线做码流分析。起初我以为不就是“抓包存文件”结果用 Wireshark 导出原始数据之后VLC 一个都打不开。检查才发现UDP 数据里装的是 RTP 包RTP 载荷里才是 H264 码流而 H264 在 RTP 里还会被分片、聚合、加头一层没对上就全白搭。这篇文章就沿着“rtp 转 h264 文件”这条链路把方案讲透先确认 RTP 怎么装 H264再给出 ffmpeg 最快落地路径最后给一份自写保存程序的核心代码和避坑清单。适合正在做 UDP 网络调试、视频采集或码流分析的人参考。2. RTP 是怎么把 H264 塞进 UDP 的先认清三种打包方式2.1 为什么 RTP 跑在 UDP 上TCP 的重传机制对实时流是负资产很多第一次接触 RTP 的开发者会问为什么要用 UDP 而不是 TCP毕竟 TCP 可靠、有序、有重传。但实时视频恰恰不能等重传——一帧数据如果因为网络抖动重传了 200 毫秒播放端早就该显示下一帧了结果就是画面卡住、延迟滚雪球。RTP 的设计哲学是“宁可丢一帧也不要让整条流卡死”所以它的事实标准承载层就是 UDP。这也是 UDP 和 TCP 协议的区别里最核心的一条TCP 保证可靠但引入队头阻塞UDP 不保证可靠但延迟可控RTP 的序号和时间戳机制让接收端自己能发现丢包并决定怎么处理。在动手保存 h264 文件之前脑子里要有这条链路的概念摄像头编码出 H264 裸流打包成 RTP 包再装进 UDP 数据报经网络到达你的机器。反过来你要做的是把 UDP 载荷里的 RTP 头剥掉把 H264 载荷重新拼成带起始码的 Annex B 码流写进文件。这里有两个关键点一是 RTP 头占 12 字节二是 H264 载荷不总是“一个包就是完整一帧”它可能被分片或聚合。理解这两点后面的代码才不会写歪。2.2 RTP 头结构前 12 字节决定你怎么拆包RTP 头最小 12 字节固定部分如下表。抓包时看到的每个 RTP 包前 12 字节都是这个结构之后的字节才是真正的 H264 载荷。字段位宽说明Version2 bitRTP 版本固定为 2Padding1 bit是否带填充字节Extension1 bit是否有扩展头CSRC Count4 bit贡献源个数单播流通常为 0Marker1 bit帧边界标记H264 场景常在帧尾置 1Payload Type7 bit载荷类型H264 用动态值如 96、97Sequence Number16 bit包序号网络序丢包检测靠它Timestamp32 bitRTP 时间戳H264 按 90000 Hz 计数SSRC32 bit流标识同一条流内不变解析时只需要读三个地方pkt[0]的高 2 位确认版本pkt[1]的低 7 位拿载荷类型pkt[2]和pkt[3]拼出 16 位序号。代码写起来很简单uint8_t version (pkt[0] 6) 0x03; uint8_t pt pkt[1] 0x7F; uint16_t seq (pkt[2] 8) | pkt[3];注意pkt[2] 8 | pkt[3]就是网络序转主机序的等价写法因为 RTP 头里所有多字节字段都是大端。很多新手在这里直接按小端读导致序号错乱重组出来的画面全是花的。2.3 三种打包方式单一 NALU、STAP-A、FU-AH264 的 NALUNetwork Abstraction Layer Unit是基本单位SPS、PPS、IDR 帧、普通帧都是不同类型的 NALU。RFC 6184 定义了 RTP 封装 H264 的三种方式看载荷的第一个字节低 5 位就能区分1 到 23单一 NALU 包载荷就是一个完整 NALU。这是最常见的情况小尺寸的 SPS、PPS、SEI 和大部分视频帧都走这条路。24STAP-A 聚合包多个小 NALU 被拼进一个 RTP 包。典型场景是 SPS PPS SEI 打包在一起跟随关键帧一起发布。28FU-A 分片包一个大的 NALU 被切成多个 RTP 包传输。当一帧数据超过 MTU典型 1500 字节左右时就会触发H264 的 IDR 帧往往很大几乎必然走 FU-A。判断代码只需要一行uint8_t nal_type rtp_payload[0] 0x1F;拿到 24 就进入聚合包解析拿到 28 就进入分片重组其他值直接当作完整 NALU 写入文件。后面第 4 章的自写程序就是围绕这个分发逻辑展开的。2.4 用 Wireshark 先验证打包方式再决定方案动手写代码前我习惯先抓一段包用 Wireshark 确认流的真实形态。过滤表达式这样写rtp rtp.ssrc 0x12345678如果不知道 SSRC可以先过滤rtp udp.port 5000再在 RTP 详情里找到 SSRC 值替换进去。打开一个分片包的详情你会看到 RTP payload 的第一个字节是0x7C或0xFC这类值低 5 位是 28这就是 FU-A如果看到聚合包第一个字节低 5 位是 24。这一步的价值在于它直接决定了接收端处理逻辑的复杂度。如果抓包显示你的流全是单一 NALU处理程序可以砍掉大半逻辑如果全是 FU-A那么重组逻辑是核心不能省。3. 用 ffmpeg 把 RTP/UDP 存成 h264最快落地的命令路径3.1 源是 RTSPffmpeg 一行命令落盘大部分网络摄像头的推流是 RTSP底层传输可以选择 UDP 或 TCP。保存文件时我一般强制走 UDP避免 TCP 叠加缓冲带来额外延迟ffmpeg -rtsp_transport udp -i rtsp://192.168.1.10/live/ch0 -t 60 -c copy out.h264参数拆开讲-rtsp_transport udp让 RTSP 的 RTP 走 UDP 传输-i指定 RTSP 地址-t 60表示只录制前 60 秒适合测试时先用短时长验证-c copy是关键直接复制编码流不做转码所以速度快、CPU 占用低而且输出就是 Annex B 格式的 .h264 文件。这里不要加-bsf:v h264_mp4toannexb那是从 MP4 抽取时才需要的 filterRTSP 来的 RTP 流本来就是 Annex B。这个命令适合“源端已经提供 RTSP”的情况。很多监控项目到这一步就结束了但如果你面对的是裸 RTP 推流——没有 RTSP 会话、没有 SDP 文件只有一台设备往固定端口发 UDP 包——上面的命令就失效了需要进入下一节。3.2 源是裸 RTP/UDP先写一个 SDP 文件再喂给 ffmpegffmpeg 解析 RTP 流时必须知道载荷类型和编码名这些信息在 RTSP 场景由 SIP/RTSP 协议协商好在裸流场景就得靠手动写 SDP。只要发流端告诉你“往 5000 端口推 H264PT 是 96”就可以建一个live.sdpv0 mvideo 5000 RTP/AVP 96 cIN IP4 0.0.0.0 artpmap:96 H264/90000然后执行ffmpeg -protocol_whitelist file,udp,rtp -i live.sdp -c copy out.h264-protocol_whitelist是 ffmpeg 安全机制要求的显式允许读取本地文件和 UDP/RTP 协议否则新版 ffmpeg 会拒绝打开非白名单协议。SDP 里的90000是 H264 RTP 时间戳的标准采样频率必须写对这个值否则 ffmpeg 解时间戳会乱。这里有个容易踩的坑SDP 里的端口必须和发流端完全一致PT 值也必须一致。如果发流端用的是 PT 100你写成 96ffmpeg 会丢弃所有包日志里只有一条“timestamp discontinuity”之类的信息文件输出为空。先确认这些参数再启动命令能省很多无意义的排查。3.3 源是抓包文件ffmpeg 能抽但不能迷信如果你手里只有一个 pcap 抓包文件没有实时流也可以尝试让 ffmpeg 直接从 pcap 里抽 H264ffmpeg -f pcap -i dump.pcap -map 0:v:0 -c copy extracted.h264这个命令在流单一、RTP 包干净的情况下能跑通但我不建议把它当主力方案。ffmpeg 的 pcap demuxer 实现很“浅”它假设 pcap 里只有一条 RTP 流且 RTCP 交错不严重一旦抓包里混着 RTCP 包、重传包或 RTSP-over-TCP 的封装它就会串包或者直接丢掉大量数据导出的文件要么花屏要么时长不对。遇到这种情况改用 tshark 或者第 4 章的自写程序按 SSRC 精确抽取更可靠。3.4 GStreamer 备选方案适合要顺手处理流的场景如果不想写 C 代码又觉得 ffmpeg 的 SDP 方式不够灵活GStreamer 的命令行也能完成同样的事gst-launch-1.0 udpsrc port5000 capsapplication/x-rtp, media(string)video, encoding-name(string)H264, payload(int)96 ! rtph264depay ! h264parse ! filesink locationout.h264这条管线的思路很直白udpsrc接收 UDP 包rtph264depay剥掉 RTP 头并重组 FU-Ah264parse修正码流并补起始码filesink写文件。它的优势是每个环节都可以替换比如把filesink换成autovideosink就能实时预览。劣势是 GStreamer 的 Caps 协商严格PT、编码名、时钟频率任何一个不匹配都会直接报not negotiated错误排错难度比 ffmpeg 高。4. 自己写一个 RTP 转 H264 保存程序核心是 FU-A 重组4.1 主循环骨架socket 接收、RTP 头解析、载荷分类ffmpeg 和 GStreamer 能处理大多数情况但如果你需要对丢包做统计、对时间戳做日志、或者加私有协议头就得自己写。下面这份 C 代码是一个能跑通核心逻辑的最小实现只依赖标准 socket 接口。#include stdio.h #include stdint.h #include stdlib.h #include string.h #include unistd.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #define RTP_PORT 5000 #define MAX_PKT 65536 #define RTP_PT_H264 96 int main(void) { int fd socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in addr {0}; addr.sin_family AF_INET; addr.sin_addr.s_addr htonl(INADDR_ANY); addr.sin_port htons(RTP_PORT); if (bind(fd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(bind); return 1; } /* 调大接收缓冲区抗短暂突发丢包 */ int rcvbuf 4 * 1024 * 1024; setsockopt(fd, SOL_SOCKET, SO_RCVBUF, rcvbuf, sizeof(rcvbuf)); FILE *fp fopen(out.h264, wb); if (!fp) { perror(fopen); return 1; } uint8_t pkt[MAX_PKT]; while (1) { ssize_t n recv(fd, pkt, sizeof(pkt), 0); if (n 12) continue; /* 小于RTP头丢弃 */ uint8_t pt pkt[1] 0x7F; if (pt ! RTP_PT_H264) continue; /* 只处理H264载荷 */ uint16_t seq (pkt[2] 8) | pkt[3]; uint8_t payload_type pkt[12] 0x1F; /* 载荷第一个字节低5位 */ /* 三种分发单一NALU / STAP-A / FU-A */ /* 下一小节填充具体处理 */ } return 0; }这段代码里有两个容易被忽略的细节。一是SO_RCVBUF要主动调大Linux 默认的 UDP 接收缓冲区只有几百 KB码率稍微一高就丢包后面第 5 章还会展开讲。二是recv返回n必须和 RTP 头长度做比较解析前不检查长度会导致越界读轻则花屏重则段错误。实际工程里还应该在解包时记录总包数、丢包数、SSRC 变化次数方便事后判断文件质量。4.2 三类载荷的分流处理完整代码在上面的主循环里补上三种处理分支。单一 NALU 直接加起始码写文件STAP-A 拆出多个 NALU 分别写FU-A 按起始分片、中间分片、结束分片重组。/* 起始码Annex B格式 */ static const uint8_t sc[4] {0x00, 0x00, 0x00, 0x01}; /* FU-A 重组状态 */ static uint8_t fua_buf[1500 * 8]; static size_t fua_len 0; static uint16_t fua_expected_seq 0; /* 下一个期望的RTP序号 */ uint16_t seq; /* 分支一单一NALUtype 1~23 */ if (payload_type 23) { fwrite(sc, 1, 4, fp); fwrite(pkt 12, 1, n - 12, fp); continue; } /* 分支二STAP-Atype 24 */ if (payload_type 24) { size_t off 13; /* RTP头12字节 STAP-A类型1字节 */ while (off 2 n) { uint16_t nal_len (pkt[off] 8) | pkt[off 1]; off 2; if (off nal_len n) break; /* 防止越界 */ fwrite(sc, 1, 4, fp); fwrite(pkt off, 1, nal_len, fp); off nal_len; } continue; } /* 分支三FU-Atype 28 */ if (payload_type 28) { uint8_t fu_header pkt[13]; int start (fu_header 7) 1; int end (fu_header 6) 1; if (start) { /* 新NALU还原NALU头 FU indicator的NRI FU header的type */ fua_buf[0] (pkt[12] 0xE0) | (fu_header 0x1F); fua_len 1; memcpy(fua_buf 1, pkt 14, n - 14); fua_len n - 14; fua_expected_seq seq 1; } else if (fua_len 0 seq fua_expected_seq) { /* 中间或结束分片按顺序拼接 */ memcpy(fua_buf fua_len, pkt 14, n - 14); fua_len n - 14; fua_expected_seq; } if (end fua_len 1) { fwrite(sc, 1, 4, fp); fwrite(fua_buf, 1, fua_len, fp); fua_len 0; /* 复位等待下一个NALU */ } continue; }这段代码里最关键的是 FU-A 重组逻辑。start位为 1 表示这是 NALU 的第一片此时要从FU indicator的高 3 位取 NRI从FU header的低 5 位取原始 NALU type拼出真正的 NALU 头。中间分片必须严格按 RTP 序号递增拼接我用fua_expected_seq记录下一个期望序号收到分片时先判断序号是否连续不连续的说明中间有丢包这个 NALU 已经残缺不应写入文件。4.3 写文件策略SPS/PPS 前置 去重文件体积能小三分之一处理完 NALU 后写文件不是无脑 fwrite 就行。H264 文件首帧之前必须要有 SPS 和 PPS否则播放器根本解不出分辨率。很多发流端每个关键帧都会带一次 SPS/PPS如果全写进去文件能播放但冗余很大体积可能膨胀 30%。我一般这样处理static uint8_t sps_cache[64]; static size_t sps_len 0; static uint8_t pps_cache[64]; static size_t pps_len 0; /* 在单一NALU分支中单独截获SPS和PPS */ if (payload_type 7 n - 12 sizeof(sps_cache)) { memcpy(sps_cache, pkt 12, n - 12); sps_len n - 12; } if (payload_type 8 n - 12 sizeof(pps_cache)) { memcpy(pps_cache, pkt 12, n - 12); pps_len n - 12; }收到首帧 IDRpayload_type 为 5之前先把缓存的 SPS/PPS 写入文件再写 IDR。后续如果同一份 SPS/PPS 再次出现判断sps_len 0且内容相同时跳过不写。这样文件开头干净播放器兼容性最好文件体积也更合理。不过这个策略有一个前提抓流必须从流中间开始也没关系只要首个完整 IDR 之前出现过 SPS/PPS。如果网络丢包把前几秒的 SPS 丢了文件头就缺参数集此时播放器会直接报 “无法解码”。更严格的方案是在内存里缓冲到首个 IDR 出现再决定怎么开头但绝大多数场景下上面的缓存策略配合丢包统计已经够用。5. 避坑RTP 转 H264 保存常见的 5 个翻车现场5.1 播放器打不开文件提示缺少 SPS/PPS文件头参数集没写对现象生成的文件用 ffprobe 能看到流信息但 VLC 打开黑屏或者开头几秒花屏后恢复正常。原因文件头没有 SPS/PPS或 SPS/PPS 写在第一个 IDR 之后。解决确保在写入第一帧 IDR 前先把 SPS(NALU type 7) 和 PPS(type 8) 按“起始码 NALU”的格式写入文件。用第 4 章的缓存逻辑即可。还要检查发流端是否在关键帧前携带参数集如果流本身不带你需要从 SDP 或 RTSP 描述里解析 sprop-parameter-sets 字段把 base64 解码后写入文件头部。5.2 花屏和绿块FU-A 分片丢了中间的包现象文件能播放但画面频繁出现花屏、绿块、撕裂感。原因FU-A 分片在网络上丢了中间包重组出来的 NALU 不完整解码器拿到残缺数据无法恢复。解决两条路。治标——在代码里检测序号不连续后直接丢弃整个 NALU不写入文件让播放器显示上一帧而不是碎块治本——检查网络丢包率确认 UDP 缓冲区是否够大。用netstat -su查看RcvbufErrors计数如果持续增长说明内核缓冲区不足。5.3 UDP 接收缓冲区太小不是程序 bug 而是协议栈参数现象通过 pcap 抓包看数据是通的但程序写出来的文件时长明显比实际短日志里recv频繁返回 EAGAIN。原因Linux 的 UDP 接收缓冲区默认值通常几百 KB跟不上码流到达速率内核直接丢包。解决程序内setsockopt(SO_RCVBUF)调大到 2 到 8 MB同时把系统级参数也调大sysctl -w net.core.rmem_max8388608 sysctl -w net.core.rmem_default8388608注意SO_RCVBUF设多大内核还会翻倍所以 4 MB 的设置实际约 8 MB 的接收能力。另外如果是抓包分析时发现重传或乱序不要把锅都推给协议栈先确认网络环境和交换机的 buffer 配置。5.4 用 nc / socat 收流到文件再转得到的却是“乱码”现象有人习惯先nc -u -l 5000 raw.bin把 UDP 流落盘再拿 ffmpeg 转成 h264结果 ffmpeg 不认。原因raw.bin里存的是完整的 UDP 载荷也就是 RTP 包而 ffmpeg 的-f h264demuxer 期望的是没有 RTP 头的裸码流。解决不要在收流环节直接落盘 RTP要么用第 3 章的 SDP ffmpeg 直转方式要么用第 4 章的自写程序边解析边写。用 nc 做原始数据捕获只适合调试不适合作为生产录制链路。5.5 ffmpeg 读 pcap 抽 H264RTCP 包把 demuxer 搞晕现象用第 3.3 节的命令抽 pcap 里的 H264输出文件时长只有几秒或报Packet corrupt后停止。原因ffmpeg 的 pcap demuxer 对 RTCP 包和不同 SSRC 的 RTP 流处理不健壮一旦 RTP 与 RTCP 交错UDP 端口相邻或者抓包文件里有多条视频流它就可能串流。解决不要依赖这个命令做正式工具。先 Wireshark 里过滤rtp.ssrc明确目标流再用 tshark 提取载荷tshark -r dump.pcap -Y rtp rtp.ssrc 0x12345678 udp.port 5000 -T fields -e rtp.payload payloads.txt然后按 RTP 头去掉每行前几个字节再用脚本拼出 NALU。这个路径能绕过 ffmpeg demuxer 的各种幺蛾子代价是脚本要自己写但可控性强得多。6. 进阶找回时间戳并验证输出文件确实可播放6.1 裸 H264 文件丢失了时间信息三种应对思路RTP 头里 32 位的时间戳是 90000 Hz 计数但你把 NALU 写进 .h264 裸流时Annex B 格式不存在时间戳字段播放器只能按固定帧率猜测显示。文件如果只是给人看影响不大但如果后面要做剪辑、同步音轨或分析帧间隔这个损失就不可接受了。应对思路有三条方案做法适用场景裸流 固定帧率不记录时间戳播放器按帧率推算监控回放、快速预览RTP 时间戳落侧车文件自写程序对每个 NALU 记录 timestamp输出timestamp.txt需要精确帧间隔分析直接封装 MP4转存时按 RTP 时间戳换算生成 PTS封装进容器需要音视频同步或剪辑如果你确定需要精确时间建议在自写程序里顺手把每个 NALU 的 RTP timestamp 和写入偏移记录成两列文本再按需合成每秒帧数统计。对于相对简单的需求直接用固定帧率封装 MP4 也能接受ffmpeg -f h264 -framerate 25 -i out.h264 -c copy out.mp4注意-framerate要按实际帧率填填错会导致播放速度不对。如果你知道 RTP 流是 30 fps就填 30。6.2 用 ffprobe 做出口检查确认你的文件是合格的 H264保存完成后不要只拿 VLC 看一眼就结束。我用 ffprobe 检查输出已经是习惯因为能一次性看出分辨率、帧率、帧数、编码 profile 是否异常ffprobe -v error -select_streams v:0 -show_entries streamcodec_name,profile,width,height,pix_fmt,r_frame_rate,nb_frames -of defaultnoprint_wrappers1 out.h264输出示例codec_nameh264 profileHigh width1920 height1080 pix_fmtyuv420p r_frame_rate25/1 nb_frames1500我一般逐一核对codec_name是 h264profile是 High 还是 Main和源流一致width/height能对上才算文件头 SPS/PPS 正确nb_frames要和你期望的时长基本吻合如果明显偏少说明丢包导致大量 NALU 被丢弃。这个检查做完才算真正完成了一次“rtp 转 h264 文件”的落地。写文件这件事我吃了不少亏有时候是 SPS 没放对位置有时候是时间戳算错导致封装进度不准后来规矩定下来每次都按“先抓包看打包方式再选工具最后 ffprobe 验证”的流程走翻车率就低了很多。希望这套流程也能帮到你。本文还有配套的精品资源点击获取