☰
Wireshark实战:从RTP包中无损提取H.264视频流的完整指南
2026/10/21 14:56:07 网站建设 项目流程

做网络排障这些年,我印象最深的不是某套协议有多复杂,而是一次次“包抓到了,画面却死活看不出来”的憋屈感。后来我才彻底想明白: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 number,16 位循环序号。它负责包的顺序和丢包检测。提取时如果发现这个序号有断层,说明中间丢包了,哪怕后面看起来拼上了,画面也可能在对应位置花掉。

第三个是 timestamp,32 位时间戳。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 indicator,NAL 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.264,payload 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.10,payload 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 OK,RTSP 会话的 DESCRIBE 响应里,都有类似下面的 fmtp 参数:

a=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z0IAKpY1QK/AfAI=,aMqMhA==; profile-level-id=42

sprop-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.h264

ffplay 能直接播 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 != prev+1会导致回绕误判,需要用取模或者补码比较。

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 注入、信封过滤、乱序排序,慢慢迭代成自己的工具链。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询