简介这份资源面向从事网络视频传输、监控系统或流媒体开发的工程师与学习者聚焦于将RTP包中的H264数据解封装并保存为本地文件同时借助UDP实现摄像头数据的实时读取。包内共14个文件以6个C头文件与4个cpp源文件为核心辅以vcxproj工程文件、filters过滤配置及ReadMe说明整体约9KB结构紧凑便于直接编译调试。内容围绕RTP头部解析、NAL单元重组与H264文件写入展开涉及Annex B与MP4两种常见存储格式的取舍并包含rtpserver服务端与UDP客户端交互的代码框架可帮助读者理解RTP与H264协同工作的完整链路。目前已有1030人学习下载适合希望掌握实时视频流处理、需要可运行参考代码的开发者对照实践。1. RTP 转 H264 文件把 UDP 裸流落成可播放视频的那条链路你抓过包就知道Wireshark 里一堆 UDP 报文端口 5004、8000、甚至 30000 以上Payload 里全是80 60开头的字节看着像视频又播不了。这就是 RTP 承载的 H264 裸流。它和直接存.h264的区别在于RTP 有 12 字节固定头、有序列号、有 Marker 位、有分片规则而 H264 文件需要的是 Annex-B 起始码和完整 NALU。把 UDP 收到的 RTP 包按序还原、去掉 RTP 头、处理 FU-A 分片、补上00 00 00 01才能得到播放器认的 H264 文件。这套链路在国标平台对接、IPC 抓流、udp 网络调试里天天用也是排查start preview failed maybe rtp session false这类问题的基本功。下面按我实际落地的顺序讲清楚。2. 先搞懂 RTP 载荷里到底装了什么H264 的三种打包方式2.1 RTP 固定头与 H264 NALU 的对应关系RTP 头固定 12 字节关键字段是序列号2 字节、时间戳4 字节、Marker 位1 bit、Payload Type。H264 的 NALU 头是 1 字节结构是F(1) NRI(2) Type(5)。Type 决定这个包是单包、分片还是聚合Type 值含义处理方式1-23单 NALU 包直接加起始码写入24STAP-A 聚合包拆出多个 NALU 分别写25-27STAP-B/MTAP少见一般丢弃或特殊处理28FU-A 分片首片带原始 NALU 头后续片拼接29FU-B 分片带 DON兼容性差实际抓 IPC 流90% 以上是 Type 1、28、24 三种。搞不清这个对应关系写出来的文件要么花屏要么根本打不开。2.2 为什么不能直接把 UDP Payload 拼起来存文件有人图省事把 UDP 收到的每个包 Payload 直接 append 到文件结果播放器打开一片绿或者直接报错。原因有三个第一RTP 头 12 字节混在里面H264 解码器看到的是垃圾数据第二FU-A 分片的首片和后续片 Payload 结构不同首片第一个字节是 FU indicator第二个字节才是 FU header后续片没有原始 NALU 头第三UDP 本身不保证顺序序列号乱序时直接拼会错位。所以必须按序列号排序、按 Type 分支处理、重组后再写。2.3 最小可跑的 Python 解析脚本下面这段是我调试时最常用的骨架用 scapy 读 pcap 或者直接接 UDP socket 都行。核心逻辑是维护一个分片缓冲遇到 Marker 位为 1 或下一个包序列号不连续时输出完整 NALU。import struct # RTP 头解析返回 seq, marker, payload def parse_rtp(data): if len(data) 12: return None # 版本号、填充位、扩展位、CSRC 计数 b0 data[0] version b0 6 if version ! 2: return None cc b0 0x0F # 第二个字节的 bit7 是 Marker marker (data[1] 7) 0x01 seq struct.unpack(!H, data[2:4])[0] # 跳过固定头 CSRC offset 12 cc * 4 return seq, marker, data[offset:] # 处理 H264 载荷写入输出文件 def handle_h264_payload(payload, out_file, frag_buf): if not payload: return nalu_type payload[0] 0x1F if nalu_type 28: # FU-A 分片 fu_header payload[1] start (fu_header 7) 0x01 end (fu_header 6) 0x01 # 原始 NALU 头 FU indicator 的 F/NRI FU header 的 Type orig_header (payload[0] 0xE0) | (fu_header 0x1F) if start: frag_buf.clear() frag_buf.append(orig_header) frag_buf.extend(payload[2:]) if end: out_file.write(b\x00\x00\x00\x01 bytes(frag_buf)) frag_buf.clear() elif nalu_type 24: # STAP-A idx 1 while idx 2 len(payload): size struct.unpack(!H, payload[idx:idx2])[0] idx 2 nalu payload[idx:idxsize] out_file.write(b\x00\x00\x00\x01 nalu) idx size elif 1 nalu_type 23: # 单 NALU out_file.write(b\x00\x00\x00\x01 payload)逻辑说明parse_rtp先校验版本号非 2 直接丢handle_h264_payload按 Type 分支FU-A 用frag_buf累积首片重建原始 NALU 头尾片到达时补起始码写出。参数上frag_buf建议用bytearray而不是 list拼接效率更高输出文件用wb模式每写一个 NALU 就 flush 一次防止进程被杀丢数据。3. 从 UDP socket 到 H264 文件完整落地步骤与参数调优3.1 接收端 socket 配置与缓冲区设置直接recvfrom(65535)在流量大时会丢包因为默认 socket 接收缓冲区只有 64KB 左右。我一般这样设import socket sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4 * 1024 * 1024) sock.bind((0.0.0.0, 5004))SO_RCVBUF设 4MB 是经验值1080p 25fps 的流大概 2-4Mbps4MB 能扛住 8 秒左右的突发。注意 Linux 下SO_RCVBUF会被内核限制在net.core.rmem_max用sysctl net.core.rmem_max看一下不够就改。Windows 下这个值上限不同设太大反而报错建议先试 1MB。3.2 序列号排序与乱序处理策略UDP 乱序是常态尤其是跨公网或经过多级交换。我的做法是维护一个滑动窗口窗口大小 64 个包收到包先按 seq 插入有序位置然后从窗口头部连续输出。如果窗口满了还有空洞说明丢包直接跳过空洞继续同时记录丢包数。丢包超过阈值比如 5%就告警因为 H264 丢一个 I 帧分片后面全花。from collections import OrderedDict class ReorderBuffer: def __init__(self, max_size64): self.buf OrderedDict() self.max_size max_size self.next_seq None def push(self, seq, payload): if self.next_seq is None: self.next_seq seq self.buf[seq] payload # 窗口溢出强制推进 while len(self.buf) self.max_size: k next(iter(self.buf)) self.buf.pop(k) self.next_seq (k 1) 0xFFFF # 输出连续部分 out [] while self.next_seq in self.buf: out.append(self.buf.pop(self.next_seq)) self.next_seq (self.next_seq 1) 0xFFFF return out参数说明max_size根据网络抖动调局域网 32 够用公网建议 128。next_seq用 16 位掩码回绕RTP 序列号是 uint16。3.3 写文件时的起始码与 SPS/PPS 处理H264 文件要能播开头必须有 SPS 和 PPS。RTP 流里 SPS/PPS 通常在每个 I 帧前以单 NALU 发送Type 7 和 8。如果抓包从中间开始可能错过 SPS/PPS这时文件播不了。解决办法是缓存最近一组 SPS/PPS遇到 I 帧时先写 SPS/PPS 再写 I 帧。判断 I 帧看 NALU Type 5。sps_pps_cache b def write_nalu(nalu, out_file): global sps_pps_cache nalu_type nalu[0] 0x1F if nalu_type 7 or nalu_type 8: sps_pps_cache b\x00\x00\x00\x01 nalu return if nalu_type 5 and sps_pps_cache: out_file.write(sps_pps_cache) out_file.write(b\x00\x00\x00\x01 nalu)注意sps_pps_cache不要无限增长每次遇到新的 SPS 就替换而不是追加否则文件头会重复写多组。3.4 用 ffmpeg 验证输出文件是否可播写完文件别急着交付先用 ffmpeg 过一遍ffmpeg -v error -i output.h264 -f null -没有报错说明 NALU 结构正确。再用ffprobe看帧信息ffprobe -v error -show_frames -select_streams v -of csv output.h264 | head -20如果报Invalid NAL unit size或no frame回去查 FU-A 重组逻辑大概率是首片 NALU 头重建错了。4. 避坑与排查RTP 转 H264 最常见的 5 个翻车现场4.1 文件能播但花屏每隔几秒绿一次现象播放器打开正常但周期性花屏。原因丢包导致 I 帧分片不完整或者 P 帧参考丢失。解决在重组逻辑里加丢包检测发现序列号空洞且空洞跨越了 FU-A 的 start 和 end直接丢弃这个不完整 NALU不要写半截数据。同时统计丢包率超过 3% 就说明网络需要优化。4.2 文件头有 SPS/PPS 但播放器仍报错现象用十六进制看文件开头确实有00 00 00 01 67但 VLC 打不开。原因SPS/PPS 写在了第一个 I 帧之后或者 SPS 和 PPS 之间插入了其他 NALU。解决确保 SPS、PPS、I 帧三者连续中间不能有 SEI 或其他类型。我一般缓存 SPS/PPS 后遇到 I 帧时按 SPS、PPS、I 的顺序一次性写出。4.3 UDP 收包丢包严重recvfrom 返回 EAGAIN现象recvfrom频繁抛BlockingIOError或EAGAIN。原因socket 设了非阻塞但没数据或者缓冲区太小。解决非阻塞模式下用select或epoll等待可读缓冲区按 3.1 节调大。另外注意 Python GIL 在高包速率下会成为瓶颈超过 2000 包/秒建议用 C 或 Go 重写接收端。4.4 序列号回绕导致排序错乱现象流跑了几小时后突然大量丢包告警。原因RTP 序列号是 uint16从 65535 回绕到 0排序逻辑没处理回绕。解决所有序列号比较都用(a - b) 0xFFFF的方式判断先后不要直接比大小。上面的ReorderBuffer已经用了掩码但自定义逻辑容易漏。4.5 国标平台对接时 start preview failed现象平台下发预览请求后返回start preview failed maybe rtp session false or preview links nun。原因SIP 信令协商的 RTP 端口和实际发流端口不一致或者 SSRC 不匹配。解决抓 SIP 包看 SDP 里的mvideo端口确认发流目标端口一致检查 RTP 头的 SSRC 是否和平台期望的一致。这类问题用 udp 网络调试工具对着端口抓一下就能定位。5. 进阶用 iperf3 打流验证接收端极限与性能调优5.1 用 iperf3 UDP 模式测接收端吞吐上限写完接收程序别只拿真实流测用 iperf3 打 UDP 流能压出极限# 服务端接收端机器 iperf3 -s -u -p 5004 # 客户端发送端机器 iperf3 -c 192.168.1.100 -u -p 5004 -b 50M -l 1400 -t 60-b 50M是目标带宽-l 1400是包长模拟 RTP 包大小。跑完后看服务端的Lost/Total和Jitter。如果 50Mbps 下丢包超过 1%说明接收端处理不过来需要优化把 Python 换成 C 扩展、用recvmmsg批量收包、或者上 DPDK。我实测 Python 单线程在 2.5GHz CPU 上大概能处理 30-40Mbps 的 RTP 流再高就丢。5.2 接收端零拷贝与批量收包技巧Linux 下用recvmmsg一次收多个包减少系统调用#define VLEN 32 struct mmsghdr msgs[VLEN]; struct iovec iovecs[VLEN]; char bufs[VLEN][2048]; for (int i 0; i VLEN; i) { iovecs[i].iov_base bufs[i]; iovecs[i].iov_len sizeof(bufs[i]); msgs[i].msg_hdr.msg_iov iovecs[i]; msgs[i].msg_hdr.msg_iovlen 1; } int n recvmmsg(sockfd, msgs, VLEN, MSG_WAITFORONE, NULL);VLEN设 32 是经验值太大延迟高太小系统调用多。MSG_WAITFORONE表示收到一个就返回适合低延迟场景。这个改动能把吞吐提升 2-3 倍。5.3 验证方法从文件反推 RTP 流质量最后分享一个我常用的验证习惯把生成的 H264 文件用 ffmpeg 转成 mp4然后看帧率和时长是否和预期一致。ffmpeg -fflags genpts -r 25 -i output.h264 -c copy output.mp4 ffprobe -v error -show_entries formatduration -of defaultnw1 output.mp4如果时长明显偏短说明丢帧多如果帧率不对检查时间戳处理。我一般会在接收端同时记录 RTP 时间戳和文件帧数做交叉验证。这套流程跑熟之后拿到任何一路 UDP RTP 流从抓包到出可播文件半小时内能搞定。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?