ARTICLE DETAIL

资讯详情

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

从pcap抓包提取国标PS流:RTP载荷重组与GB/T 28181流分析实践

从pcap抓包提取国标PS流:RTP载荷重组与GB/T 28181流分析实践 简介一份面向网络管理员、安全分析人员及协议开发者的实用工具包用于从tcpdump或Wireshark生成的pcap抓包文件中提取符合国标的网络数据流。资源内置pcap2ps-main的C语言源码pcap2ps.c与说明文档README.md完整呈现了读取pcap、解析协议字段、按国标规则筛选数据的实现思路。压缩包仅2个文件体积约5KB代码精简适合直接阅读或二次改造。目前已有221人学习下载对需要掌握抓包文件解析、国标流提取及网络故障排查技能的读者具有参考价值。通过学习该源码可理解pcap文件格式、关键解析流程以及如何在混合流量中精准过滤目标数据流为后续网络性能分析、安全检测或格式转换提供基础手段。1. 从抓包文件里提取国标流这到底是解决什么问题的工具做国标设备联调的人大概率都有过这种经历平台显示通道在线却拉不出画面抓包一看UDP 端口上密密麻麻全是 RTP 包wireshark 能告诉你这是媒体流却没法直接告诉你里面到底是 H.264 还是 H.265、帧有没有破、SPS/PPS 在不在。pcap2ps 这类工具就是干这个的把 tcpdump 或 wireshark 抓到的 pcap 文件按会话重组 RTP 载荷剥掉传输层头把国标 GB/T 28181 承载的 PS 流原样提取出来落成一个能丢给 ffprobe、VLC、ffmpeg 的 .ps 文件。适合正在做 28181 网关接入、摄像头推流调试或者被「设备在线但无视频」逼到要抓包的人。不需要解码器也能确认设备到底有没有在推流这就是它的价值。2. 国标流为什么装进 PS 壳抓包前先看懂 RTP 里的载荷国标 GB/T 28181 在传输视频时走的是 SIP 信令加 RTP 媒体。SIP 负责协商端口、编码格式和 RTP payload type媒体面把视频切成一包一包的 RTP 数据。这里有个容易让新手懵的点RTP 的 payload 里装的不是裸 H.264而是 PS 流Program Stream。也就是说抓包文件里看到的每个 RTP 包其实是 PS 流的一个切片PS 流本身又被摄像头编码器按 GOP 切开装进多个 RTP 包。搞不清这层封装关系后面提取出来的文件长什么样、能不能播全凭运气。2.1 PS、TS、ES 三种封装国标选 PS 是沿袭安防行业习惯视频编码后的裸数据叫 ESElementary Stream也就是一长串 H.264/H.265 NAL 单元。但裸 ES 没有时间信息、对丢包零容错所以很少直接塞进 RTP。TSTransport Stream固定 188 字节一个包带 PID 和 PCR设计目标是广播和易错信道适合卫星、有线电视那种链路。PSProgram Stream则是不定长打包适合可靠信道、本地存储和内网传输安防行业从早期的 MPEG-2 时代就一直用 PS 做封装国标 28181 沿用了这条老路SDP 里通常这样描述媒体mvideo 10000 RTP/AVP 96 artpmap:96 PS/90000所以国标流PS 流切片RTP 只是搬运工。PS 流里有几个关键的起始码提取时要靠它们找边界列成一张表最直观起始码Hex名称在国标 PS 里的作用00 00 01 BAPS pack header每个 PS 包的起点提取时从这里对齐00 00 01 BBsystem header描述流属性包括视频编码类型00 00 01 E0PES video header视频 PES 包的起点真正的 H.264/H.265 数据在这里面00 00 01 BDPES private私有流部分摄像头用它推音频或扩展数据记住这张表就够了。BA 告诉你「一个 PS 包从哪开始」E0 告诉你「视频数据在哪」BB 告诉你「编码器声明自己是什么格式」。提取国标流的本质工作就是把 RTP 载荷拼回去之后靠这些起始码重新找到边界。2.2 抓包前置准备镜像口、过滤条件与抓多长的判断抓包位置很讲究。如果问题出在摄像头侧直接在摄像头出口网卡抓如果问题在平台侧需要在交换机上做端口镜像把平台下行口的数据复制一份出来。镜像口配置不在本文范围但记住一个原则宁可在平台侧抓不要在设备侧抓完再拿两台机器对时间否则信令和媒体的对应关系要靠猜。抓包命令用 tcpdump 最省事职业做法是直接抓全部 UDP 而不预先过滤端口因为国标媒体端口是动态协商的你事先不知道它落在哪个端口tcpdump -i eth0 -s 0 -w gb28181.pcap udp参数说明-s 0表示整包捕获不截断。默认 snaplen 是 96 字节只够抓 IP/UDP 头RTP payload 全丢pcap2ps 提取出来就是一堆空壳。udp过滤掉了 TCP 干扰。如果你已经通过 SIP 抓包知道了媒体端口可以收紧成udp port 10000但大多数情况下我没这么做——多抓一点事后在脚本里过滤更方便。抓多久合适我的经验是复现问题前后各抓 10 到 30 秒就够不要抓几小时。pcap 文件一旦超过几个 GB解析脚本跑得慢不说wireshark 打开都卡。而且国标流分析只需要看「有没有视频、有没有花屏、有没有丢包」几十秒的数据足够判断故障模式。注意wireshark 默认保存的是 pcapng 格式不是 classic pcap。pcap2ps 这类按 pcap 结构写的解析脚本读到 pcapng 会直接报错先用 tshark 转一下格式再喂给脚本。tshark -r capture.pcapng -F pcap -w capture.pcap-F pcap指定输出格式为老式 pcap。这一步建议养成习惯抓完包先看一眼后缀.pcapng就直接转别等脚本跑挂了才想起来。另外说一句wireshark 自带的 Telephony - RTP - Stream Analysis 也能导出 RTP payload但它的导出是为音频分析设计的会做重排序和 jitter 统计对 PS 流这种面向解码器的数据来说导出结果经常带额外处理痕迹不如自己按原始顺序拼一遍干净。真到了要精确提取的时候还是自己写脚本可控。3. pcap2ps 第一步解析 pcap 并按会话分出干净的 RTP 载荷理解了 PS 封装结构现在动手写一个最小可用的 pcap2ps。我的习惯是只用 Python 标准库不用 scapy 这类第三方包。原因很现实现场机器可能没有外网pip install 都可能绕半天struct 解析是零依赖方案代码也就一百来行。这一章先搞定 pcap 解析和 RTP 载荷提取下一章再做 PS 组包。3.1 读 pcap 文件magic、字节序与每包记录结构classic pcap 文件结构很简单24 字节全局头后面跟着一帧一帧的数据。全局头里最重要的就是前 4 字节的 magic它决定了整个文件的字节序。小端机器上写入的 magic 是d4 c3 b2 a1大端则是a1 b2 c3 d4。这段判断写错后面解析出来的包长全乱这是 pcap 解析里最容易翻车的点。import os, struct def read_pcap(path): 读取 classic pcap返回 (linktype, frame_bytes) 列表 frames [] with open(path, rb) as f: g f.read(24) if len(g) 24: raise ValueError(文件太小不是有效 pcap) magic g[:4] if magic b\xd4\xc3\xb2\xa1: le # 小端 elif magic b\xa1\xb2\xc3\xd4: le # 大端 else: raise ValueError(不是 classic pcap可能是 pcapng先用 tshark 转换) # linktype 在全局头偏移 20 处4 字节 linktype struct.unpack(le I, g[20:24])[0] while True: rh f.read(16) if len(rh) 16: break ts_sec, ts_frac, incl_len, orig_len struct.unpack(le IIII, rh) data f.read(incl_len) if len(data) incl_len: break frames.append((linktype, bytes(data))) return frames参数说明le是文件字节序只用于解析 pcap 头里的长度字段。incl_len是实际写入文件的帧长度orig_len是原始帧长度正常情况下两者相等不等说明抓包时做了截断这种情况下面解析 UDP 头时要小心。linktype是链路层类型以太网是 1Linux cooked 是 113。如果你在抓包机上用了tcpdump -i any这种抓所有接口的写法得到的就是 113不是以太网。我一般建议直接指定物理网卡避免 cooked 头带来的额外解析工作。3.2 剥到 UDP 载荷跳过以太网头、IP 头拿到链路层帧后要一路剥到 UDP。以太网头 14 字节其中frame[12:14]是 EtherType0x0800才是 IPv4。IPv4 头里版本和 IHL 共用一个字节ip[0] 4是版本ip[0] 0x0F是头部长度单位 4 字节。协议字段在ip[9]17 是 UDP。def udp_payloads(frames): 从帧列表里提取 UDP 载荷返回 (sport, dport, payload) 生成器 for linktype, data in frames: if linktype ! 1 or len(data) 14: continue # 非以太网直接跳过 if data[12:14] ! b\x08\x00: continue # 只要 IPv4 ip data[14:] if len(ip) 20 or (ip[0] 4) ! 4: continue ihl (ip[0] 0x0F) * 4 if ihl 20: continue if ip[9] ! 17: continue # 17 UDP udp ip[ihl:] if len(udp) 8: continue sport, dport struct.unpack(!HH, udp[0:4]) # 取出 UDP payloadRTP 从这开始 yield sport, dport, bytes(udp[8:])这里有个细节UDP 头里有个 length 字段按规范它等于 UDP 头加 payload 的总长度。但实际帧可能被截断所以直接按udp[8:]取更稳妥。另外 RTP/UDP 头里的字段一律是大端序和 pcap 文件的字节序是两码事都用!前缀解。这个混淆过不少人文件头用le由 magic 决定IP/UDP/RTP 头用!网络序别混。3.3 按 SSRC 分组并剥离 RTP 头滤掉 RTCP处理扩展头RTP 头至少 12 字节版本2 bit、填充位 P、扩展位 X、CSRC 计数 CC4 bit、payload type7 bit、序列号、时间戳、SSRC。其中最容易忽略的是扩展头——X 位置 1 时固定头后面还跟着 4 字节扩展头描述和若干扩展数据不跳过它拼出来的 PS 流会花成败絮。def split_sessions(udp_packets): 按 SSRC 把 RTP 载荷归组返回 {ssrc: bytearray} 和会话元信息 sessions {} meta {} for sport, dport, pkt in udp_packets: if len(pkt) 12: continue if (pkt[0] 6) ! 2: # RTP/RTCP 版本都是 2 continue pt pkt[1] 0x7F if 192 pt 223: # RTCP 的 PT 范围必须滤掉 continue ssrc struct.unpack(!I, pkt[8:12])[0] cc pkt[0] 0x0F # CSRC 数量 x (pkt[0] 4) 0x01 # 扩展头标志 hlen 12 cc * 4 # 固定头 CSRC if x: if len(pkt) hlen 4: continue ext_len struct.unpack(!H, pkt[hlen:hlen 2])[0] hlen 4 ext_len * 4 # 扩展头描述 4 字节 扩展数据 if ssrc not in sessions: sessions[ssrc] bytearray() meta[ssrc] {sport: sport, dport: dport, count: 0} sessions[ssrc].extend(pkt[hlen:]) meta[ssrc][count] 1 return sessions, meta逻辑说明pkt[0] 6 2校验版本pkt[1] 0x7F取 payload type 时要清掉最高位的 marker 位。RTCP 的 PT 分布在 192 到 223最常遇到的是 200SR和 201RR直接用这个范围滤。网上流传的「RTP 是偶数、RTCP 是奇数」是按端口老约定总结的不是 PT 规律不要套用。分组为什么用 SSRC 而不是 IP 加端口因为一个抓包里经常同时存在多路摄像头四元组会变摄像头 NAT 出去后源端口可能变化但 RTP 层的 SSRC 是会话级唯一标识一路流从头到尾不会变。这就是后续多会话批处理的基础。跑完这一步sessions字典里的每个 key 就是一路媒体流对应的 bytearray 是所有 RTP 载荷按抓包顺序拼接的原始数据。但这个数据还不能直接播——开头可能不是 PS 包起始处中间可能有丢包留下的空洞。下一章处理这两个问题。4. pcap2ps 第二步PS 起始码对齐把载荷落成可播放文件上一章得到的拼接数据是「RTP 载荷直接拼接」存在两个天然缺陷抓包开始的时刻大概率落在某个 PS 包中间文件开头是半个包丢包会在中间留下损坏的拼接点。PS 流自带起始码解码器遇到损坏处会找下一个 BA 重新同步所以我们的任务很明确把数据对齐到第一个 BA然后原样落盘剩下的交给播放器容错。4.1 用 BA 起始码对齐为什么不是从 E0 开始对齐的逻辑就是找00 00 01 ba这个四字节序列。找到后把之前的内容全部丢弃从 BA 处开始写文件。如果整个载荷里一个 BA 都找不到说明这路流根本不是国标 PS 流可能是裸 H.264 RTP 或者其它私有封装直接跳过。def align_ps(data: bytes): 把拼接数据对齐到第一个 PS pack header找不到返回 None start data.find(b\x00\x00\x01\xba) if start 0: return None return bytes(data[start:])为什么不从 E0 对齐E0 只是 PES 包的起点不是 PS 包的起点。从 E0 开始丢掉了前面的 pack header 和 system header虽然解码器也能解出视频但部分播放器会因为没有 system header 而无法确定流的时长和编码参数显示成未知时长。从 BA 开始是唯一正确的对齐方式这也解释了为什么抓包时不用刻意卡在某个起始码位置——对齐是提取阶段的事抓包阶段只要保证包完整就行。顺手写一个统计函数提取完输出几个关键指标用来判断「提取是否成功」def analyze_ps(data: bytes): 统计 PS 流的关键起始码数量用于判断流是否完整 return { ps_packets: data.count(b\x00\x00\x01\xba), system_headers: data.count(b\x00\x00\x01\xbb), pes_video: data.count(b\x00\x00\x01\xe0), }参数说明ps_packets是 PS 包总数正常 10 秒的视频大概有 100 到 300 个取决于关键帧间隔和码率。system_headers理论上至少有 1 个为 0 说明抓包起点在流中间或者流本身不标准。pes_video是要重点关注的值可以理解为视频帧切片数如果为 0 说明这路流里没有视频只有音频或者私有数据。4.2 完整的 pcap2ps 主流程遍历会话、命名、落盘把前两章的代码串起来一个最小工具就成型了def pcap2ps(pcap_path: str, out_dirout): 从 pcap 中提取所有国标 PS 流每个会话输出一个 .ps 文件 frames read_pcap(pcap_path) sessions, meta split_sessions(udp_payloads(frames)) os.makedirs(out_dir, exist_okTrue) for ssrc, payload in sessions.items(): aligned align_ps(bytes(payload)) if aligned is None: print(fssrc{ssrc:08x}: 未找到 PS 起始码跳过) continue stat analyze_ps(aligned) name f{ssrc:08x}_{stat[ps_packets]}pkt.ps with open(os.path.join(out_dir, name), wb) as f: f.write(aligned) print(f{name} system{stat[system_headers]} fvideo{stat[pes_video]} packets{meta[ssrc][count]})参数说明文件名用 SSRC 的十六进制加 PS 包数这样能直接和 wireshark 的 RTP stream 对应上排查时不用猜哪个文件是哪路流。ps_packets明显偏少时说明这路流丢包严重后续播放画面大概率花屏。输出目录建议单独建一次抓包可能提取出多路流混在一起很难看。4.3 时间戳和丢包哪些要处理哪些不要碰这里有个关键认知要建立起来裸 PS 文件不要求 RTP 时间戳连续。播放器播放 PS 流时依靠的是 PES 头里的 PTS/DTS33 bit90kHz不是 RTP 时间戳。国标摄像头在推流时PES 头里基本都带 PTS所以拼接后的文件直接可播。RTP 时间戳的作用是会话层的关联和丢包检测和 PS 内部的呈现时间不是一回事不要混着看。那丢包要不要处理我的建议是不要手工补包。PS 流有起始码做同步解码器遇到损坏的 PES 会丢弃数据直到下一个 BA 重新同步表现是花屏一闪然后恢复这正是播放器的正常容错行为。你手工补一个假包进去反而可能让解码器把错位数据当真渲染出更奇怪的东西。唯一值得做的处理是记录 RTP 序列号跳变的位置用来精确定位丢包时间点这个后面会讲。提示输出文件扩展名写.ps而不是.mpgffprobe 和 ffmpeg 会按 MPEG-PS demuxer 自动识别。如果 VLC 打不开试试ffplay -i output.ps或者把文件改名.mpg再拖进 VLC这是播放器识别策略的问题不是文件坏了。跑完这步你已经能从任意 pcap 里提出可播放的国标流了。下一章讲这过程中最容易翻车的地方。5. 提取国标流的四个必踩坑现象、原因与排查路径下面的坑都是真机抓包时踩过的按概率排序。每一个都是「现象 - 原因 - 解决」三段式值不值得照着做看第一条就够说服你。5.1 pcapng 当成 pcap 读脚本直接崩现象脚本跑起来第一行就报ValueError: not classic pcap而同一个文件在 wireshark 里打开完全正常。原因wireshark 从 1.12 版本开始默认保存 pcapng这是一种 block 结构的格式文件头是0A 0D 0D 0A和 classic pcap 的d4 c3 b2 a1完全不同。很多教程里的解析脚本只认 classic pcap没做 pcapng 适配。解决不改脚本用 tshark 转格式这是最省事的路tshark -r input.pcapng -F pcap -w output.pcap如果你不想依赖 tshark也可以给脚本加一个分支读到0A 0D 0D 0A时按 pcapng 的 Section Header Block 和 Interface Description Block 解析。但 pcapng 的 option 字段是 TLV 结构写起来至少多几十行前面那句「先转格式」其实是最快路径。5.2 RTP 扩展头没剥离视频花成一片现象提取出的 PS 文件用 ffprobe 能识别出 H.264但画面大面积花屏、时间轴乱跳甚至丢帧严重。原因部分国标摄像头在 RTP 头里开了扩展X 位置 1扩展头里有 Profile 定义、长度字段和一些厂商私有数据。如果只按固定 12 字节剥离 RTP 头扩展数据会被当成 PS 载荷拼进去起始码错位解码器从错位处开始解析直到碰上 BA 才重新同步于是表现为周期性花屏。解决在剥离 RTP 头时检查 X 位按第 3 章的hlen计算公式跳过扩展。排查时可以在 wireshark 里点开一个 RTP 包看 Header 里 Extension 字段是否为 1一眼就能确认。这个坑最阴险的地方在于 ffprobe 依然能识别出编码格式让人误以为文件是好的。5.3 RTCP 滤错按「奇数 PT」判断是老黄历现象提取的文件里 PS 包数量正常但文件里混着大量不可解析的垃圾数据用 ffprobe 看时长忽长忽短。原因网上大量教程写「RTP 偶数端口、RTCP 奇数端口」于是按源端口奇偶性滤 RTCP。但这条规律源于早期 RTP 端口分配的约定不是协议层规则。国标平台在协商媒体端口时完全不遵守奇偶而 RTCP 的 payload type 其实是 200 以上的数字和端口奇偶没有对应关系。按端口过滤要么放过 RTCP要么误伤 RTP。解决按 payload type 范围过滤RTCP 的 PT 分布在 192 到 223SR 是 200RR 是 201代码里if 192 pt 223: continue一行就解决。排查时在 wireshark 里看rtcp过滤后的包数占总量比例如果超过 1%说明你的过滤逻辑有问题。5.4 丢包之后拼接点损坏别手工补包交给解码器容错现象播放提取出的 PS 文件时前半段正常某时间点之后连续花屏几秒然后恢复。看 PS 包数量和正常值差别不大。原因RTP 丢包造成 PS 流断续。摄像头把一个大 PS 包拆成多个 RTP 发送中间丢一个拼接出来的 PS 包就少一块PES 头里的长度字段和实际内容对不上。解码器按错误的 PES 长度解析直到遇到下一个 BA 才恢复同步。解决不要在提取阶段补假数据。正确的做法是保持原样落盘播放时用ffmpeg -err_detect ignore_err降低容错带来的连锁问题分析时只看丢包点之前的帧。如果想精确定位丢包位置在拼接阶段检查 RTP 序列号是否连续把跳变点输出成 CSV。这个比修文件有用得多——它告诉你的是网络质量而网络质量才是问题根源。这四个坑有一个共性都是「看似能跑、实则输出不达标」的隐性错误。养成提取后先看统计指标、再 ffprobe 验证的习惯能省掉大量排查时间。6. 验证与进阶用 ffprobe 验收 PS 流再解出 H.264 裸流提取完不等于提取对先花十秒验证ffprobe -v error -show_entries streamcodec_name,width,height,r_frame_rate \ -show_entries formatduration -of default output.ps期望看到codec_nameh264或h265width和height非零。如果duration为 0 或 N/A说明 PES 时间戳有问题多半是抓包起点不在 PS 包边界或丢包严重。更直观的验证是用ffplay -i output.ps直接播放能看到画面就说明流可解。两个都不行的时候回到第 5 章的统计指标逐项查。验证通过后进阶需求通常是解出裸 H.264。这一步不需要解码ffmpeg 用 copy 模式把 PS 容器脱掉就行ffmpeg -i output.ps -map 0:v -c:v copy output.h264-c:v copy是关键参数只做解封装不解码速度极快一个百兆的 PS 文件几秒就能完成。得到的裸流可以直接喂给帧级分析工具或者和其它模块做对接。批处理大量 pcap 时一个 for 循环就够了for f in *.pcap; do python pcap2ps.py $f out_${f%.pcap} done我现在的习惯是接到国标视频问题先抓 20 秒包跑一遍 pcap2ps确认 PS 流里有东西、起始码数量正常再回头看信令和平台日志。这一步能过滤掉一大半「设备根本没推流」「推了但格式不对」的伪故障少走很多弯路。希望帮到你。本文还有配套的精品资源点击获取
返回列表