
简介这是一份面向网络视频传输开发者的RTP转H264实战代码资源聚焦将UDP接收到的RTP数据包解封装并还原为可保存的H264文件适用于监控摄像头数据读取、在线会议与远程教育等实时流场景适合具备一定网络编程与视频编码基础的中级开发者参考。压缩包共14个文件约9KB以6个h头文件与4个cpp源文件为核心辅以vcxproj工程文件、filters过滤器配置、user用户设置及ReadMe说明整体结构紧凑便于直接导入Visual Studio编译调试。资源围绕rtpserver服务端与CommonCode通用代码库展开涵盖RTP头部解析、NAL单元边界识别与重组、Annex B起始码处理及H264数据写文件等关键环节并借助MyUdp完成UDP实时接收帮助读者理解从网络包到视频文件的完整链路。目前已有1032人学习下载可作为搭建RTP收流存盘工具与排查丢包、乱序问题的实用起点。1. 从一堆 UDP 包里捞出能播的 H264RTP 转存文件的真实门槛抓包抓到几万个 UDP 包Wireshark 里能看到 RTP 流但导出成.h264用播放器一打开就花屏、绿屏、甚至直接报错——这是做流媒体抓包转存时最常翻的车。问题不在抓包工具而在于 RTP 只是传输层封装它把一帧 H264 数据切成若干分片还夹着时间戳、序列号、标记位直接把这些包体拼起来得到的不是合法码流。真正要做的是按 RTP 载荷结构还原出 Annex-B 格式的 NALU补上起始码处理分片重组和 SPS/PPS 注入最后按顺序写进文件。这套流程在流媒体分析、视频质量排查、离线转码预处理里都用得上适合做音视频开发、协议分析、安防流对接的从业者。下面按“先看懂包结构再动手写转换器最后避坑”的顺序拆一遍。2. RTP 载荷里的 H264 到底长什么样NALU、分片与起始码2.1 为什么不能直接把 RTP 包体拼成文件RTP 头固定 12 字节后面跟的载荷才是 H264 数据。但一个 RTP 包不一定装得下一整帧尤其是 I 帧动辄几十 KB会被拆成多个 RTP 包发送。同时H264 在 RTP 里的封装遵循 RFC 6184定义了三种载荷模式单一 NALU 模式、STAP-A 聚合模式、FU-A 分片模式。直接拼接包体会把 RTP 头、FU 指示字节、分片头都当成码流数据写进去播放器解析到这些非法字节自然花屏。另一个关键点是起始码。H264 的 Annex-B 格式要求每个 NALU 前有00 00 00 01或00 00 01起始码而 RTP 载荷里是没有起始码的必须自己补。少了这一步文件里的 NALU 连在一起解码器根本分不清边界。2.2 三种载荷模式的识别方法判断一个 RTP 包属于哪种模式看载荷第一个字节首字节值模式含义1~23单一 NALU整个载荷就是一个完整 NALU24STAP-A聚合多个 NALU后面跟长度数据28FU-A一个 NALU 被分片首字节是分片指示29FU-B带 DON 的分片较少见FU-A 模式下载荷前两个字节是分片头和分片头扩展。第一个字节高 3 位是28低 5 位是原始 NALU 类型第二个字节的 bit7 是起始位 Sbit6 是结束位 Ebit5 是保留位低 5 位是分片类型。重组时要把第一个分片的低 5 位和后续分片的载荷拼起来还原出原始 NALU 头。2.3 时间戳与序列号在转存中的作用RTP 头里的序列号2 字节用来检测丢包和乱序时间戳4 字节用来判断帧边界。转存文件时序列号不是必须写进 H264但它是排查丢包的唯一线索。时间戳相同的一组 RTP 包属于同一帧时间戳变化意味着新帧开始。实际写文件时通常按序列号排序后顺序写入遇到时间戳跳变就认为一帧结束。如果丢包严重序列号不连续重组出来的 NALU 会缺数据这时候要么丢弃该帧要么在文件里留空——前者更安全。提示抓包时尽量在发送端或中间交换机做端口镜像避免在接收端抓包导致丢包丢包后的重组结果基本不可用。3. 手写一个 RTP 转 H264 的转换器从 pcap 到可播放文件3.1 整体流程与工具选型常见做法是用 Python 配合scapy或dpkt解析 pcap提取 UDP 载荷再按 RFC 6184 重组 NALU最后写 Annex-B 文件。如果只是偶尔转几个文件Wireshark 的“导出 H264 流”功能也能用但它对丢包和乱序的处理比较粗暴遇到 FU-A 分片不完整时容易生成坏文件。自己写脚本的好处是能控制丢包策略、能过滤特定 SSRC、能处理多路流。下面给一个可运行的 Python 脚本框架依赖dpkt和pcap读取。安装命令pip install dpkt3.2 解析 pcap 并提取 RTP 载荷import dpkt import struct def parse_pcap(pcap_path, target_port5004): 从 pcap 中提取指定端口的 RTP 包返回 (seq, ts, payload) 列表 packets [] with open(pcap_path, rb) as f: pcap dpkt.pcap.Reader(f) for ts, buf in pcap: try: eth dpkt.ethernet.Ethernet(buf) ip eth.data if not isinstance(ip, dpkt.ip.IP): continue udp ip.data if not isinstance(udp, dpkt.udp.UDP): continue if udp.dport ! target_port and udp.sport ! target_port: continue # RTP 头至少 12 字节 if len(udp.data) 12: continue rtp udp.data seq struct.unpack(!H, rtp[2:4])[0] timestamp struct.unpack(!I, rtp[4:8])[0] payload rtp[12:] packets.append((seq, timestamp, payload)) except Exception: continue return packets这段代码遍历 pcap 每个包逐层解析以太网、IP、UDP只保留目标端口的 RTP 包。seq和timestamp从 RTP 头固定偏移取payload是去掉 12 字节 RTP 头后的数据。注意这里没有处理 CSRC 扩展头如果 RTP 头带扩展CC 字段非 0偏移要动态计算。常见做法是先读rtp[0]的低 4 位得到 CSRC 数量再跳过对应字节。3.3 重组 FU-A 分片并写入 Annex-Bdef write_h264(packets, out_path): 按序列号排序后重组 NALU写入 Annex-B 文件 packets.sort(keylambda x: x[0]) out open(out_path, wb) fu_buffer b fu_nal_type 0 last_ts None for seq, ts, payload in packets: if len(payload) 0: continue nal_type payload[0] 0x1F if nal_type 28: # FU-A if len(payload) 2: continue fu_ind payload[0] fu_hdr payload[1] start_bit (fu_hdr 7) 1 end_bit (fu_hdr 6) 1 orig_type fu_ind 0x1F if start_bit: # 新分片开始重建 NALU 头 fu_buffer bytes([ (fu_ind 0xE0) | orig_type ]) payload[2:] fu_nal_type orig_type else: fu_buffer payload[2:] if end_bit: # 分片结束补起始码写入 out.write(b\x00\x00\x00\x01 fu_buffer) fu_buffer b elif nal_type in (1, 5, 7, 8): # 单一 NALU只保留常见类型 out.write(b\x00\x00\x00\x01 payload) # 其他类型如 STAP-A 可自行扩展 out.close()核心逻辑先按序列号排序保证顺序正确。遇到 FU-A 时起始分片重建 NALU 头保留 F 和 NRI 位替换类型位中间分片直接追加载荷结束分片补起始码写入。单一 NALU 直接补起始码写入。这里只保留了类型 1、5、7、8分别是非 IDR 帧、IDR 帧、SPS、PPS。实际使用中如果发现文件开头没有 SPS/PPS播放器可能无法解码需要从流里单独提取或手动注入。3.4 参数说明与调用方式target_port默认 5004是 RTP 常见端口实际抓包可能是 5005、8000 等按需改。packets.sort按序列号排序能解决轻微乱序但如果序列号回绕超过 65535 归零排序会出错需要先做回绕处理。fu_buffer在丢包时可能残留不完整数据建议在时间戳变化时清空。调用方式pkts parse_pcap(capture.pcap, target_port5004) write_h264(pkts, output.h264)生成的文件用ffplay output.h264或 VLC 打开验证。如果花屏优先检查 SPS/PPS 是否存在、FU-A 重组是否正确。4. 转存过程中最容易翻车的几个点丢包、SPS/PPS 与时间戳4.1 丢包导致 NALU 残缺现象文件能播放但画面有马赛克、绿块或者播放到某处卡住。原因RTP 基于 UDP丢包后 FU-A 分片不完整重组出的 NALU 数据缺失。解决在重组时检查序列号连续性发现跳变就丢弃当前分片缓冲区不要写入残缺 NALU。如果丢的是 I 帧后续 P 帧也会连锁损坏只能从下一个 I 帧开始恢复。可以在脚本里加一个last_seq变量不连续时清空fu_buffer。4.2 SPS/PPS 缺失导致无法解码现象文件用播放器打开黑屏或者提示“找不到解码器”。原因H264 解码依赖 SPS序列参数集和 PPS图像参数集它们通常在流开始时发送一次如果抓包时错过了开头文件里就没有这两个 NALU。解决从流里其他位置找 SPS/PPS或者用ffmpeg从同源流里提取后手动拼到文件开头。常见做法是抓包时先启动抓包再启动推流确保 SPS/PPS 被捕获。4.3 时间戳回绕与序列号回绕现象排序后文件顺序错乱播放时画面跳来跳去。原因RTP 序列号是 16 位超过 65535 后归零时间戳是 32 位也会回绕。直接按数值排序会把回绕后的包排到前面。解决排序前先做回绕检测把序列号映射到连续空间。简单方法是记录第一个序列号后续序列号如果比前一个小很多就加上 65536 再排序。4.4 多路流混在一起现象转出的文件里混入了其他视频流的数据播放异常。原因同一端口可能承载多路 RTP 流靠 SSRC 区分。抓包时如果没过滤 SSRC会把不同流的数据混在一起。解决解析 RTP 头时读取 SSRC偏移 8~12 字节只保留目标 SSRC 的包。可以在parse_pcap里加一个target_ssrc参数做过滤。4.5 扩展头导致偏移错误现象提取的载荷开头几个字节是乱码重组失败。原因RTP 头有扩展时固定 12 字节之后还有扩展头直接取rtp[12:]会把扩展头当成载荷。解决先读rtp[0]的低 4 位得到 CSRC 数量再读扩展标志位如果有扩展则跳过 4 字节扩展头和扩展数据。常见做法是写一个get_rtp_header_len函数动态计算。注意如果抓包文件本身丢包严重任何重组脚本都救不回来。转存前先用 Wireshark 的“专家信息”看丢包率超过 5% 基本没有修复价值。5. 进阶用 ffmpeg 验证与批量转存的小技巧5.1 用 ffmpeg 快速验证转存结果生成.h264后不要急着用播放器打开先用ffprobe看码流信息ffprobe -v error -show_frames -select_streams v:0 -count_frames output.h264如果输出里能看到帧数和分辨率说明码流基本合法。再用ffmpeg转成 mp4 做兼容性验证ffmpeg -fflags genpts -r 25 -i output.h264 -c:v copy output.mp4-fflags genpts让 ffmpeg 自动生成时间戳-r 25指定帧率按实际流改-c:v copy不重新编码。如果这一步报错说明 NALU 结构有问题回到重组脚本排查。5.2 批量处理多个 pcap 的脚本骨架实际项目里往往有几十个抓包文件要转写个循环for f in *.pcap; do python rtp2h264.py $f ${f%.pcap}.h264 done配合前面的 Python 脚本把parse_pcap和write_h264封装成命令行入口即可。如果文件多建议加个日志记录每个文件的包数、丢包数和输出大小方便回溯。5.3 一个我踩过的坑不要用文本模式写文件早期写脚本时用open(out_path, w)而不是wb结果 Windows 下换行符被转换写出的 H264 文件字节错乱播放器直接崩溃。从那以后我每次写二进制文件都强制走wb并且在写完后用xxd output.h264 | head看一眼开头是不是00 00 00 01。这个习惯帮我省了很多排查时间。希望帮到你。本文还有配套的精品资源点击获取