资讯动态

Wireshark实战:从RTP包中无损提取H.264视频流的完整指南

发布时间:2026/10/22 17:29:47 来源:尧图企业网站定制
做网络排障这些年我印象最深的不是某套协议有多复杂而是一次次“包抓到了画面却死活看不出来”的憋屈感。后来我才彻底想明白Wireshark 的价值不只是帮你看包它完全可以配合一些脚本从 RTP 数据包里把 H.264 视频流像考古一样一点点抠出来还原成能播、能查、能当证据的视频文件。这个活听起来是纯技术活但真做起来每一步都有细节每一处细节背后都藏着坑。先说清楚一点这里说的“Wireshark 插件实战”其实并不一定需要装第三方插件。Wireshark 自带 RTP 分析能力配合 tshark 命令行和自写脚本就能形成一套足够通用的提取流程。我见过很多人以为要用什么神秘扩展组件其实不是。真正需要你花心思的是理解 RTP 对 H.264 的封装规则以及怎么把 Wireshark 导出的 payload 整理成 H.264 标准的 Annex-B 裸流。这篇文章适合三类人一类是做音视频协议开发的想快速验证自己的流封得对不对一类是做网络运维或安防的经常要抓摄像头、话机、对讲的流来排查卡顿或黑屏还有一类是刚开始学抓包想找个有意思的项目练手的人。我会从封装原理讲起再给完整提取路径最后把踩过的坑按频率排个序尽量让你别重复劳动。1. 先搞懂 RTP 封装 H.264 的三件套很多教程一上来就教你点菜单、敲命令但遇到一点异常就懵了。原因很简单看不懂包里的数据是怎么组织的。所以我建议先把 RTP 封装 H.264 的基本规则过一遍这花不了多长时间却能让后面的每个操作都对着答案做。1.1 RTP 头上那些不能忽略的字段RTP 头部默认是 12 字节后面跟着负载。想无损提取 H.264至少要懂四个字段。第一个是 payload type也就是负载类型。H.264 在 RTP 里没有固定值绝大多数情况通过 SIP 或 RTSP 信令动态协商常见取值在 96 到 127 之间但有些老设备会直接用 96也有摄像头用 97、98、108不能一概而论。定位流的时候不要凭经验猜要看抓包里的实际值。第二个是 sequence number16 位循环序号。它负责包的顺序和丢包检测。提取时如果发现这个序号有断层说明中间丢包了哪怕后面看起来拼上了画面也可能在对应位置花掉。第三个是 timestamp32 位时间戳。H.264 里同一帧的所有 RTP 包时间戳通常相同。时间戳变了意味着新的一帧开始。这个字段非常关键它可以用来判断帧边界但不能直接当播放时间轴用单位既不是毫秒也不是微秒而是和采样率挂钩。第四个是 marker bit也就是 M 位。在 H.264 视频里惯例是设置在一帧最后一个 RTP 包上方便接收端知道帧边界。但实际厂家实现并不完全守规矩我遇到过好几家设备只在某个 slice 结束置位甚至完全不置位。所以做帧边界判断时要优先依赖时间戳marker 只能当辅助信号。CSRC 计数也可能影响负载偏移。正常情况下 RTP 头是 12 字节如果 CSRC 数量不为 0头部还会多出 4 乘以 CSRC 数量的字节。Wireshark 的 rtp.payload 字段已经帮你去掉了头部直接就是负载内容这个细节通常不用你操心。1.2 NAL 单元的三种常见打包方式H.264 视频流的基本单位是 NAL 单元。SPS、PPS、IDR 帧、普通 P 帧本质上都是不同 NAL 类型的单元。RTP 把它们装进包里有三种常见方式。第一种是单一 NAL 单元模式。一个 RTP 包正好装一个 NAL负载的第一个字节就是 NAL header。这种情况最简单提取时直接把每个包的负载加上起始码连起来就行。第二种是 STAP-A聚合模式。一个 RTP 包塞了多个小型 NAL。负载第一个字节是 STAP-A 的 NAL type 24后面跟着若干个由两字节长度字段加上 NAL 数据组成的小块。碰到这种包要按长度字段一个个拆开再分别添加起始码而不是整个负载只加一个起始码。第三种是 FU-A分片模式。常用于 MTU 比较小的网络一个很大的 IDR 帧被拆成多个 RTP 包。负载第一个字节是 FU indicatorNAL type 固定为 28第二个字节是 FU header。重组时要把第一个分片里丢失的那个 NAL header 拼回去具体做法是用 FU indicator 的高三位代表 NAL 类型的高位部分和 FU header 的低五位拼出真正的 NAL header。这个点在后面脚本里会体现。STAP-A 和 FU-A 是新手最容易踩坑的区域。STAP-A 拆长度字段拆错FU-A 忘了恢复 NAL header都会导致解码器报语法错。1.3 为什么“直接拼 payload”一定不行如果直接把 RTP Streams 对话框里保存的 payload 拼接成文件绝大多数情况是播不了的。原因有三个。第一H.264 裸流要求每个 NAL 前必须有起始码也就是00 00 00 01或00 00 01而 RTP 负载里没有。第二FU-A 分片没有拼回去。你拿到的是一个大 NAL 的若干碎片中间还夹着 RTP 头部解码器根本没有能力自己还原。第三SPS/PPS 参数集可能缺失。没有 SPS 和 PPS解码器连分辨率、帧率、参考帧结构都不知道自然连流都初始化不了。明白这三点你就知道“无损提取”的真正工作量在哪里了。不是按下导出按钮而是把 RTP 的封装结构翻译成解码器认识的裸流格式。2. 抓包姿势与目标流定位提取的效果有一大半在抓包阶段就决定了。包没抓对后面的脚本再牛也白搭。2.1 抓包点与 snaplen 的取舍抓包点尽量靠近视频流的接收端或发送端。如果是在交换机上做端口镜像要注意镜像口带宽和实际流量是否匹配很多时候镜像环拥塞丢包抓下来的文件看起来完整实际上少了中间的 RTP 包。snaplen 这个参数容易被忽略。默认抓包长度一般够用但如果设置在几百字节UDP 大包很可能会被截断。RTP 包通常在 200 到 1400 字节之间截断后 payload 不完整提取出来的视频后半段全是花屏。建议把 snaplen 设到 262144文件再大也不至于截断。抓包时长也要控制。抓几十 MB 到几百 MB 的 RTP 数据足够了。想测试 SPS/PPS 恢复尽量抓到包含视频开头和中间关键帧切换的时间段别只抓十秒钟。2.2 三步锁定 H.264 视频流第一步打开抓包文件确认信令。如果是 SIP 通话或 RTSP 会话可以先找信令里的 SDP看到rtpmap:96 H264/90000这类描述就能确定哪条流是 H.264payload type 是多少。第二步用 Telephony 菜单下的 RTP 子菜单找到 RTP Streams。这里会列出所有 RTP 流包含源 IP、目的 IP、SSRC、payload type、包数、丢包率。语言进到这个列表按 SSRC 找目标流。第三步选中目标流后点击 Analyze进入 RTP Stream Analysis 对话框。这里可以看到更详细的抖动、丢包统计、序列号跳变情况。确认 payload 里解析出来的编解码是 H.264再去做提取。如果手头没有信令文件也可以在 Wireshark 主界面的显示过滤器里直接输入rtp.payload_type 96或者rtp.payload_type 99之类碰碰运气。但更靠谱的办法是先看 RTP Streams 列表靠 IP 和端口区分。2.3 抓包质量判断丢包严重就别提无损所谓无损提取前提是抓包里没有丢包。如果 RTP Streams 对话框里的丢包率是红色的或者序列号跳变次数很多就要评估一下还有没有必要继续。一两帧位的小丢包处理得好也许画面只闪一下还能凑合看。关键 IDR 帧丢了没有参考帧依赖的后续 P 帧全都会变成马赛克。最坏的结果是整个视频起播失败因为解码器找不到一个完整关键帧。我处理这种情况时会先看丢包分布如果只是偶尔丢几个小包先提取然后转成 MP4 验证大概率能出画面如果丢包集中在开头的 IDR 帧附近就别指望这段抓包能还原完整视频了回去补抓更现实。网络分析不是玄学问题出在抓包链条的哪个环节要先定位清楚。3. 无损提取做实战三种可靠路径接下来是正文重头戏。我给出三种从 RTP 数据里提取 H.264 的路径覆盖不同场景和不同基础的人。3.1 Wireshark GUI 直接导出 payload最直观的方法是用 Wireshark 的 RTP Stream Analysis 对话框。选中 RTP 流进入分析页找到 Save Payload 按钮选择保存格式就能把该流内的所有 RTP 负载导出为一个原始二进制文件。这个操作只有两步但它存下来的东西仍然只是“按顺序拼接的 RTP 负载”通常还没有处理 FU-A也没有补起始码。所以严格来说这只是拿到了半成品。如果你的视频恰好是单一 NAL 模式而且抓的是完整流直接把这个文件加上起始码就能播。但现实里 FU-A 太常见了比如 MTU 限定在 1200 字节的无线网络中一帧 IDR 基本必然分片。我建议把 GUI 导出当成快速查看的工具正规操作还是结合脚本走一遍更保险。3.2 tshark 命令行提取最通用也最可控tshark 是 Wireshark 自带的命令行工具在脚本化和批量处理上无可替代。假设抓包文件是 video.pcapng目标摄像头 IP 是 192.0.2.10payload type 是 96用下面这条命令导出 RTP 包的序列号和负载tshark -r video.pcapng -Y rtp ip.src 192.0.2.10 rtp.payload_type 96 -T fields -e rtp.seq -e rtp.payload payload.txt先解释一下各个参数。-r指定输入文件-Y后面是显示过滤器-T fields表示输出字段而不是完整解析结果-e指定输出哪些字段这里的 rtp.seq 是序号rtp.payload 是负载的十六进制文本字段之间默认用制表符分隔每包一行。不同 Wireshark 版本的字段名可能有差异保险起见可以先跑一下tshark -G fields | grep rtp.seq确认字段名再执行。如果输出的是带冒号的十六进制比如00:11:22:aa下一步要么用 xxd 转要么交给脚本解析。之所以建议连序列号一起导出是为了在重组时判断丢包区间这一步的价值后面会看到。3.3 给 tshark 配一个重组脚本附代码拿到 payload.txt 之后需要把十六进制文本还原成二进制并且处理 FU-A、STAP-A、补充起始码。手写解析最灵活我直接用 Python 写了一个足够通用的重组脚本逻辑并不复杂。#!/usr/bin/env python3 import sys def parse_hex_line(s): # 去掉冒号和空白字符转化为 bytes return bytes.fromhex(s.replace(:, ).strip()) def write_nalu(out, nalu): # H.264 Annex-B 起始码每个 NAL 前都必须有 if not nalu: return out.write(b\x00\x00\x00\x01) out.write(nalu) def main(): if len(sys.argv) 3: print(usage: reassemble_h264.py payload.txt out.h264) sys.exit(1) input_file sys.argv[1] output_file sys.argv[2] cur_fu b prev_seq None with open(input_file, r) as f, open(output_file, wb) as out: for line in f: parts line.strip().split(\t) if len(parts) 1: seq None payload_hex parts[0] else: seq int(parts[0]) payload_hex parts[1] # 检查序列号是否连续注意 RTP 序列号是 16 位回绕 if seq is not None and prev_seq is not None: if seq ! (prev_seq 1) 0xFFFF: # 出现丢包丢弃当前未完成的 FU-A 分片 cur_fu b prev_seq seq data parse_hex_line(payload_hex) if not data: continue header data[0] nal_type header 0x1F if nal_type 24: # STAP-A按 2 字节长度字段拆分为多个 NAL pos 1 while pos 2 len(data): nalu_len (data[pos] 8) | data[pos 1] pos 2 if pos nalu_len len(data): write_nalu(out, data[pos:pos nalu_len]) pos nalu_len else: break elif nal_type 28: # FU-A分片模式 fu_header data[1] start_bit fu_header 0x80 end_bit fu_header 0x40 # 关键一步恢复真正的 NAL header real_header bytes([(header 0xE0) | (fu_header 0x1F)]) if start_bit: cur_fu real_header data[2:] elif cur_fu: cur_fu data[2:] if end_bit and cur_fu: write_nalu(out, cur_fu) cur_fu b else: # 单一 NAL 模式直接写入 write_nalu(out, data) if __name__ __main__: main()脚本的核心就两件事。第一是遇到 FU-A 分片时把真正的 NAL header 恢复出来再拼接这解决了新手最常犯的错误。第二是遇到 STAP-A 时循环拆解不会把一个聚合包错误当成一个 NAL。运行方式很简单python3 reassemble_h264.py payload.txt out.h264生成的 out.h264 就是 Annex-B 裸流。推荐你用脚本而非纯手动拼接是因为人能拼几十个包拼不了几万个包而且脚本可以重复利用。实测处理一个包含几万 RTP 包的文件脚本运行时间不到一秒。如果发现 RTP 包乱序严重建议在重组前先按序列号对 payload.txt 排序。最朴素的方式是用 Python 读出来排序再写回去不要直接依赖 sort 命令对十六位循环序号排序容易出现序号回绕排序错误。4. SPS/PPS 缺失自救与 ffmpeg 封装裸流拿到手并不代表能播。最常见的情况是播放器或者 ffmpeg 报错提示找不到 SPS/PPS。这是因为抓包头几分钟可能已经过去了而 SPS/PPS 只在会话开始时通过 RTP 传过一次或者厂家实现了周期性发送但你抓的区间恰好没有。4.1 从 SDP 反向找回 SPS/PPS找回 SPS/PPS 最可靠的方式是从信令的 SDP 描述里拿。SIP 会话的 INVITE/200 OKRTSP 会话的 DESCRIBE 响应里都有类似下面的 fmtp 参数afmtp:96 packetization-mode1; sprop-parameter-setsZ0IAKpY1QK/AfAI,aMqMhA; profile-level-id42sprop-parameter-sets 里逗号前面的是 base64 编码的 SPS逗号后面的是 PPS。把这两段取出来解码就能得到二进制 NAL 单元手动加到裸流前面即可。解码命令可以用echo Z0IAKpY1QK/AfAI | base64 -d | xxd拿到二进制内容直接插入到 out.h264 的头部。注意顺序SPS 在前PPS 在后然后接 IDR 帧。如果手动插入记得 SPS、PPS 的前面也要有00 00 00 01起始码。还有一种情况抓包文件里本身就带有 SPS/PPS。在 Wireshark 里可以用rtp_h264.nal_unit_type 7这类过滤条件搜索或者在后面提到的 ffprobe 验证时判断一下裸流里有没有参数集。我个人的习惯是无论抓包里有没有都从 SDP 里把 SPS/PPS 解出来统一注入到输出文件里。这样即使后续要转封装参数集也一定是完整的。4.2 ffmpeg 把裸流封成 MP4 的标准操作有了正确的 Annex-B 裸流封装 MP4 就是一行命令ffmpeg -f h264 -i out.h264 -c copy out.mp4-f h264是必须的告诉 ffmpeg 输入是裸流格式不写的话它对文件扩展名和内容会产生误判可能会把二进制当作未知格式处理。如果这一步报 “Could not find codec parameters” 或者 no decodable frames基本可以断定裸流本身有问题回头看三处起始码是否齐全、SPS/PPS 是否存在、FU-A 是否拼错。如果只是想快速预览内容可以用ffplay out.h264ffplay 能直接播 Annex-B 裸流。要是加装 SPS/PPS 之前 ffplay 黑屏加了之后正常说明问题就在参数集。把裸流录制成可分享的 MP4 时一般用-c copy保持原质量。如果你发现视频时间戳乱了可以考虑加上-fflags genpts重新生成时间戳不一定能根治但偶尔有效。5. 避坑指南不提这些坑会白忙一晚上前面每一步都可能有不顺利的地方。我把常见的坑集中整理出来按来源分类方便排查。5.1 抓包侧的高频问题抓包点不对是最“冤”的一个坑。比如在接收端旁边抓包端口镜像却是 SPAN 到另一个口中间有段时间没有镜像流量提取出来的流就缺了一块。抓包之前先确认链路是否双向可见。snaplen 截断也很有迷惑性。Wireshark GUI 默认的 snaplen 不会太大如果你在 tcpdump 里手动指定过较短的长度RTP 负载会缺尾巴。抓完包后可以随机挑一两个 RTP 包查看负载长度是否完整再开始后续操作。还有一个坑是抓包文件过大。好几百 MB 的 pcap 在 RTP Streams 对话框里会十分卡顿。建议抓包时用环形缓冲比如每个文件 100MB保存 10 个文件够用就行。5.2 协议重组侧的高频问题常见问题是把 marker bit 当成可靠的帧边界。我前面说过不少设备并不严格遵守如果重组脚本完全信任 marker会把一个完整帧拦腰截断解码器报帧不完整。另一个高频问题是 FU-A 重组时没有恢复 NAL header。很多教程只给了简单拼接法不看 FU indicator 和 FU header 的高位结果第一个分片就少了三位的核心信息解码器无法识别 IDR 类型。代码里那句real_header bytes([(header 0xE0) | (fu_header 0x1F)])就是干这个的。STAP-A 的拆包同样容易错。两字节的长度字段必须按大端字节序解析不要按小端。还有聚合包里的每个 NAL 都要单独补起始码不能在整个聚合包前后各补一个。最多人忽略的反而是 UDP 层乱序。视频传输延时抖动大的时候RTP 包到达顺序和发送顺序不一致。tshark 导出的顺序是抓包时的到达顺序如果乱序发生重组后的 NAL 内数据顺序就错了。处理办法是脚本导入时先按 seq 排序排序时注意 16 位回绕直接seq ! prev1会导致回绕误判需要用取模或者补码比较。5.3 工具使用侧的高频问题tshark 的-e rtp.seq字段名在不同版本里可能不一样。不要慌先用tshark -G fields | grep -i rtp查字段名。此外如果你过滤的时候用了rtp udp.dstport 5004注意 RTCP 通常跑在相邻端口比如 5005如果你把 5004 和 5005 都滤进来tshark 输出里会混入非 RTP 数据重组脚本会解析出异常。最稳妥的过滤条件还是rtp.payload_type 目标值。还有payload.txt 中如果包含了大写冒号分隔的十六进制用 Python 的 bytes.fromhex 处理没问题。但如果你先把文本里的换行去掉了再交给 xxd -r -p容易把每包的边界模糊掉建议先确认源格式。最后ffmpeg 封装失败时很多人会怀疑 ffmpeg 配置有问题。其实先检查ffprobe out.h264看一眼有没有识别出 SPS/PPS就能定位差不多八成问题。6. 验证与效果评估怎样算真正“无损”做完提取其实还差一步验证。你不验证就不知道这段流能不能看也不知道是不是漏了几帧。6.1 三步验证法第一步做格式检查ffprobe out.h264如果输出里能看到 Stream #0 有 H.264 的 profile、level、分辨率等说明参数集没问题。要是提示 bitstream filtering 相关错误多半是 start code 前有残留的 RTP header。第二步做画面抽查ffmpeg -i out.h264 -vframes 1 -f image2 preview.jpg生成第一帧预览图。如果图片正常说明关键帧和解码链路基本通了。如果画面花但有轮廓说明 FU-A 重组逻辑可能漏了个包或者丢包区间没有处理干净。第三步做连续播放测试ffplay out.h264快速拖动播放或者至少播放十几秒重点观察画面有没有出现大块花屏。花屏如果集中在某一个时间段回看对应时间戳附近的序列号是否连续。这里的逻辑是花屏意味着那段 NAL 不完整可能是 RTP 抓包范围本身丢包了。6.2 适合场景与局限这套方案适合什么场景适合抓包之后做问题复现、证据留存、画面验证适合在没有任何视频回放条件时把网络层数据还原成视频适合作为自动化测试脚本的一部分定期抓一段流提取并喂给解码器做质量分析。不太适合什么场景不太适合长期实时监控也不适合流量极大、丢包严重到根本凑不出连续帧的网络。理论上丢包严重时如果没有后续 IDR 帧丢失数据的画面永远找不回来。所以无损提取的真正含义是“把抓包里存在的完整数据按正确顺序还原”不要指望它能制造出网络里不存在的数据。影响范围再放大一点讲这个方法能用在很多地方智能摄像头对接、视频话机通话质量分析、无人机图传链路验证、车载 DVR 数据审查。甚至在做流媒体服务排障时给主管和客户看一段从 RTP 包里还原出来的“现场”比几十个截图都管用。最后再分享一个个人操作体会我会把 tshark 导出和 Python 重组脚本放进一个 Shell 脚本改三处参数就能复用。每次遇到新的摄像头协议先抓包跑脚本再 ffprobe基本十分钟内就能判断问题是出在摄像头端、网络端还是解码器端。这套流程救过我很多次至少让我少走很多重复路。你如果要做类似的事情也可以从最简路径开始跑通之后再逐步加上 SPS/PPS 注入、信封过滤、乱序排序慢慢迭代成自己的工具链。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑