1. 抓包抓出的疑问:音频负载为什么老是320字节
第一次认真盯这个数字,是在排查一路对讲回声问题的时候。Wireshark 里过滤出 RTP 流按长度排序,视频包大小从三百多字节到一千四百字节来回跳,唯独音频那一路,负载长度几乎是复制粘贴出来的——稳定在 320 字节,偶尔有 316、318 这种带差异的值,但绝大多数就是 320。当时第一反应是抓包工具做了截断,反复核对frame.len、ip.len、udp.length和 RTP 头之后排除了这个可能,剩下的解释只有一个:设备端就是这么打的包。
GB28181 这套视频监控联网协议在安防行业用了很多年,信令部分走 SIP,媒体部分走 RTP 承载 PS 流,这是大家的共识。但真正落到“为什么音频包是 320 字节”这个问题上,能一口气讲清楚的人并不多。它看起来是个很小的细节,实际上同时牵扯到音频编码格式、采样参数、打包时长、PS 封装结构、RTP 时间戳步进五件事,任何一个环节理解偏了,后面做音频转发、转码、音画同步都会跟着出错。
这篇内容适合三类人看:一是刚接触 GB28181 码流、被 PS 和 PES 结构绕晕的后端开发;二是要做协议对接、被不同厂商的音频包长搞得头疼的集成工程师;三是做流媒体转发、录像回放、跨协议转换,需要精确核算带宽和时间戳的运维与架构同学。下面我按“现象—结构—算式—取舍—验证—踩坑”这条线把它彻底拆开,看完你再看抓包文件,应该能一眼判断出对面设备用的是什么音频编码、什么打包策略。
1.1 先给结论:320 不是协议规定的,是算出来的
必须先把一个误解掐掉:GB28181 标准里没有任何一条写着“音频包必须是 320 字节”。标准规定的是封装方式、信令流程、媒体传输的框架,具体每个 RTP 包带多少音频数据,是由编码格式和打包时长共同决定的,属于实现层面的选择。
320 这个数字最常见的来源是这道算式:
8000 (采样率 Hz) × 1 (每样本字节数) × 0.04 (打包时长 秒) = 320 字节翻译成人话:G.711 编码,8kHz 采样,每个采样点用 1 个字节表示,每 40 毫秒打一个包。8000 乘以 0.04 等于 320 个采样点,乘以 1 字节就是 320 字节。码率正好是 64kbps,这是 G.711 的标准码率,也是历史上电话网络沿用下来的数字。
但这道算式不是唯一解。16 位线性 PCM 在 8kHz 采样、20 毫秒打包时,算出来也是 320 字节:
8000 × 2 × 0.02 = 320 字节两个完全不同的编码格式、完全不同的打包时长,凑出了同一个包长。这就解释了为什么光看包大小判断编码格式是会翻车的,必须结合 SDP 协商结果和 RTP 的 PT 字段一起看。后面我会专门给一套三分钟鉴别的方法。
1.2 这个数字的稳定性本身就是一条线索
视频包长度忽大忽小,音频包长度几乎不动,这个反差不是偶然。视频编码为了在画质和码率之间找平衡,会做变码率(VBR)处理,简单画面一帧可能只有几百字节,复杂场景一帧能顶到几十 KB,再被 MTU 切成一堆 RTP 分片,长度自然乱七八糟。
音频不一样。G.711 是定码率(CBR)编码,每个采样点固定 1 字节,不随内容变化。你说话也好,环境安静也好,40 毫秒就是 320 字节,一个字节都不多不少。这种“包长高度稳定”的特征,本身就是一个非常好用的排查信号——如果你抓到的音频包长度跳动得很厉害,那大概率不是 G.711 定长打包,要么是压缩编码(AAC、AMR 这类),要么是设备把多个音频帧做了聚合,要么是 PS 头长度在变。
我在实际项目里养成了一个习惯:拿到一份陌生的 GB28181 抓包,先不看内容,先按 RTP 负载长度做个直方图。如果音频那一路 95% 以上集中在同一个长度值上,基本可以判定是定长编码 + 固定打包时长,后面所有的时间戳、码率、缓存计算都可以按这个固定值推。如果长度分布很散,那就得老老实实解析 SDP 和 PES 头了。
2. GB28181 媒体流的封装链路拆解
要搞清楚 320 字节在整条链路的什么位置,得先把封装层次捋清楚。很多人卡在这一步,是因为把“RTP 负载长度”和“音频数据长度”混为一谈了。这两个概念差着好几层头部开销,混着用会在算带宽的时候出现明显偏差。
2.1 从信令面到媒体面,一层一层往下剥
一次完整的 GB28181 实时点播,信令面走的是 SIP:INVITE 发起、200 OK 应答、ACK 确认,中间通过 SDP 交换媒体能力。SDP 里最关键的是这几行:
m=video 30000 RTP/AVP 96 a=rtpmap:96 PS/90000 a=recvonly m=audio 30002 RTP/AVP 8 a=rtpmap:8 PCMA/8000 a=recvonly这里有两种常见的音频协商方式,区别非常大:
第一种,a=rtpmap:8 PCMA/8000,PT 值是 8 或 0,时钟频率 8000。这种情况下音频是裸 RTP 承载,没有 PS 封装,RTP 负载直接就是 G.711 数据。320 字节负载就是纯音频,时间戳每次步进 320。
第二种,a=rtpmap:96 PS/90000,PT 值是 96,时钟频率 90000。这种情况下音频和视频都被封进 PS 流,再用 RTP 承载。RTP 负载里首先是 PS 的 pack header,然后是 PES 包。时间戳的步进要按 90000 的时钟来算,40 毫秒对应 3600。
这两种方式在实际设备里都能见到,新一点的设备、走 PS 复用的居多,因为音视频要在同一个 PS 流里做同步,方便录像和转发。老设备或者纯音频对讲场景,裸 RTP 也不少。你在抓包里看到音频包的 RTP 时间戳每次加 320 还是加 3600,就能反推出对面用的是哪种协商——这个技巧比翻 SDP 快得多。
2.2 为什么非要在 RTP 里再套一层 PS
这是个被问过无数次的问题。RTP 本来就是为实时传输设计的,为什么还要在里面塞 PS?直接 RTP 承载编码帧不行吗?
行是行,但会丢东西。PS(Program Stream,节目流)是从 MPEG-2 体系里继承过来的容器格式,它解决的是“多路基本流复用 + 时间基准对齐”的问题。在监控场景里,PS 至少带来三个实打实的好处。
第一是音视频复用。一路摄像头出来的视频 ES 和音频 ES 需要被放进同一个逻辑流里,带上统一的系统时钟参考(SCR)和各自的显示时间戳(PTS),接收端才能做同步。RTP 本身没有复用能力,一个 RTP 会话只能承载一种负载类型,音视频要复用得靠多个端口或多个 SSRC,管理起来更麻烦。
第二是帧边界清晰。PS 用起始码00 00 01加上流 ID 来标识每一段数据的开始,解析器不需要理解编码内容就能切分数据包。这对中间服务器特别友好——转发服务器不需要解码,按起始码切片就行,CPU 开销极低。安防场景动辄几百上千路并发,这个特性是刚需。
第三是录像文件直接落地。PS 流本身就是可以存成文件的格式,录像服务器收到的 RTP 负载剥掉 RTP 头,攒起来就能写成.ps文件,回放的时候再封回 RTP 发出去,中间的转封装成本几乎为零。
代价也很明确:头部开销变大,解析逻辑变复杂。一个 PS 包至少要 14 字节的 pack header,一个 PES 包至少要 6 字节的起始码和长度字段,再加上可选的 PTS 字段。这些开销在 320 字节的音频负载面前占比不算小,后面算带宽的时候必须算进去。
2.3 音频 PES 包的字节级结构
既然音频要被封进 PS,那它首先得是一个 PES 包。我把一个典型的、承载 320 字节 G.711 数据的音频 PES 结构拆开给你看:
| 字段 | 长度 | 典型值 | 说明 |
|---|---|---|---|
| packet_start_code_prefix | 3 字节 | 00 00 01 | PS/PES 通用起始码 |
| stream_id | 1 字节 | C0或BD | C0 为音频流,BD 为私有流 |
| PES_packet_length | 2 字节 | 随负载变化 | 该字段之后所有字节的总数 |
| marker bits + flags | 2 字节 | 80 80 | 标识 PTS 是否存在 |
| PES_header_data_length | 1 字节 | 05 | 后面 PTS 字段的长度 |
| PTS | 5 字节 | 递增 | 显示时间戳,33 位 |
| payload | 320 字节 | G.711 数据 | 真正的音频采样 |
把头部加起来:4(起始码 + 流 ID)+ 2(长度)+ 3(标志位)+ 5(PTS)= 14 字节。所以一个承载 320 字节音频的 PES 包,整体长度是 334 字节,其中PES_packet_length字段填的值是 328(从长度字段之后开始算,包含标志位 3 字节 + PTS 5 字节 + 负载 320 字节)。
外面还要再套 PS 的 pack header。一个标准的 pack header 是 14 字节,包含00 00 01 BA起始码、SCR 系统时钟参考、复用率等信息。有些实现每个 RTP 包都带一个 pack header,有些实现是多个 PES 共享一个 pack header,还有些实现在流开始处发一次 program stream map(00 00 01 BC)之后就省略了。
这样算下来,一个 RTP 包的总负载可能在 334 到 348 字节之间浮动,具体取决于厂商的封装习惯。这就是为什么你抓包看到的音频 RTP 负载不总是正好 320——320 是里面那层纯音频数据的长度,外面还裹着十几到二十几字节的“包装盒”。
3. 320 字节的四种可能来源,逐个验证
前面说了 320 不是一个唯一解。实际排查中我至少见过四种能凑出这个数字的组合,每一种对应的后续处理方式都不一样。这一节我把判断方法给全,你拿去对照抓包就行。
3.1 来源一:G.711 的 8bit 采样配 40ms 打包
这是最常见的一种,也是开头那道算式的来源。G.711 分 A 律和 μ 律两个变种,A 律主要在欧洲和国内用,μ 律在北美和日本用,两者码率都是 64kbps,每个采样点都是 1 字节。
打包时长选 40 毫秒,意味着每 25 个包正好一秒。这个数字在实践中出现频率极高,原因我后面单独讲。这里先记住特征:
- RTP 负载(不含 PS 头)320 字节
- 每秒 25 个音频包
- 码率恒定为 64kbps
- 包长几乎不波动
如果你在 Wireshark 里看到音频包的Time delta稳定在 0.040 秒左右,负载长度稳定,那基本可以锁定了。
3.2 来源二:16 位线性 PCM 配 20ms 打包
这个组合算出来也是 320 字节,但含义完全不同。16 位 PCM 每个采样点占 2 个字节,8kHz 采样下每秒产生 16000 字节,20 毫秒就是 320 字节。码率是 128kbps,比 G.711 高一倍。
这种格式在国标里不是主流,但在一些做音频对讲、广播的设备上能见到,尤其是那些把音频当“高质量语音”处理的场景。特征也很明显:
- 每秒 50 个音频包,包间隔 20 毫秒
- 码率 128kbps
- 单声道情况下负载 320 字节
- 如果是双声道,负载会变成 640 字节
区分它和 G.711 最简单的办法是算总码率。抓 10 秒钟的包,统计音频 RTP 的字节总数,除以 10,如果接近 8000 字节/秒就是 G.711,接近 16000 字节/秒就是 16 位 PCM。这个判断不需要解析任何头部,统计一下就行。
3.3 来源三:压缩编码的帧长恰好撞上
AAC、AMR 这类压缩编码的帧长是固定的采样点数,不是固定字节数。AAC-LC 一帧 1024 个采样点,在 8kHz 采样下就是 128 毫秒一帧,压缩后的字节数随内容变化,通常在几十到两百字节之间浮动,很难稳定在 320。
但有一种情况能撞上:16kHz 采样的 AAC-LC,一帧 1024 个采样点是 64 毫秒,高码率下压缩后可能落在 300 到 400 字节区间。如果你看到包长在 320 附近小幅波动(比如 310 到 330 之间跳),而不是死死钉在 320,那就要怀疑是压缩编码了。
AMR-NB 一帧 20 毫秒,最高码率 12.2kbps,一帧大概 31 字节,离 320 差得远。G.726 的 32kbps 档,40 毫秒是 160 字节,也对不上。
判断压缩编码最可靠的办法是看 SDP 里的a=rtpmap。别看包长猜,猜错的概率不低。
3.4 来源四:PS 头开销堆出来的假象
还有一种容易误导人的情况:音频数据本身不是 320 字节,但加上 PS 和 PES 头之后,RTP 负载总长看着像 320。
比如 G.711 打 32 毫秒的包,音频数据是 256 字节,加上 14 字节 PES 头和若干 PS 头,凑到 280 多。这种情况一般不会精确落在 320 上,但如果厂商做了对齐填充(padding),就可能出现人为凑整。
对齐填充在安防设备里不算罕见,有些芯片方案为了内存对齐或者 DMA 传输效率,会把包长补齐到 2 的幂或者 8 的倍数。看到包长是 320、384、512 这种“整齐”的数字,而 SDP 又对不上,就要考虑填充的可能。
3.5 三分钟鉴别法:三个字段定乾坤
综合上面四种情况,我给一套快速的判断流程,抓包在手三分钟能出结论:
第一步,看 SDP。a=rtpmap里写的是什么编码、什么时钟频率,这是最权威的信息。写着 PCMA/8000 就是 G.711 A 律,时钟 8000。
第二步,看 RTP 头里的 PT 字段和 timestamp 步进。PT 为 8 或 0,时间戳每次加 320,说明是按 8000 时钟走的 40 毫秒。PT 为 96,时间戳每次加 3600,说明是 PS 封装按 90000 时钟走的 40 毫秒。
第三步,算实际音频码率。把 PS 头、PES 头剥掉,只统计有效音频字节,除以时间窗口。64kbps 对应 G.711,128kbps 对应 16 位 PCM。
这三步做完,编码格式、打包时长、封装方式全部确定,不会再有歧义。我见过不少团队在这一步省事,直接按 320 字节当常数写进代码,结果遇到用 20 毫秒打包的设备就算错缓冲区,音频一顿一顿的。
4. 为什么打包时长偏偏选 40 毫秒
这个问题比“320 怎么来的”更有意思。既然 320 = 8000 × 0.04,那为什么是 0.04 而不是 0.02 或者 0.08?这不是随手拍的,背后是几组相互冲突的约束在拉扯。
4.1 头部开销与载荷效率的账
先把账算清楚。假设音频是 G.711,我们比较三种打包时长下的实际网络开销。
每增加一层封装,头部就多一份固定成本。IP 头 20 字节,UDP 头 8 字节,RTP 头 12 字节,这三层是雷打不动的 40 字节。PS 和 PES 头按 20 字节估(pack header 14 + PES 头摊薄后约 6),总共 60 字节固定开销。
| 打包时长 | 音频负载 | 每包总长 | 每秒包数 | 每秒总字节 | 等效带宽 |
|---|---|---|---|---|---|
| 20ms | 160 字节 | 220 字节 | 50 | 11000 | 88 kbps |
| 40ms | 320 字节 | 380 字节 | 25 | 9500 | 76 kbps |
| 80ms | 640 字节 | 700 字节 | 12.5 | 8750 | 70 kbps |
数据很清楚:从 20 毫秒拉到 40 毫秒,带宽从 88kbps 降到 76kbps,省了将近 14%。从 40 毫秒再拉到 80 毫秒,只再省 8%,边际收益明显衰减。
在几百上千路并发的监控系统里,这 14% 不是小数目。一千路音频,20 毫秒打包要多花 12Mbps 带宽,一年下来是实打实的成本。所以从纯带宽角度,打包越长越划算。
4.2 端到端延时的硬约束
但打包不能无限拉长,因为每攒一个包就多一份延时。20 毫秒打包,最坏情况要等 20 毫秒才能发出;80 毫秒打包,最坏要等 80 毫秒。
单看这几十毫秒好像无所谓,问题是它会跟后面的环节叠加。接收端要有抖动缓冲,通常缓冲 2 到 4 个包;解码器有自身延迟;网络传输有往返时间。一圈加起来,80 毫秒打包在双向对讲场景里能明显感觉到“抢话”——你说完一句话,对方要等小半秒才听到。
40 毫秒是个甜点。打包延时 40 毫秒,抖动缓冲按 3 个包算是 120 毫秒,加上网络和解码,总延时能控制在 300 毫秒以内,这个水平在语音对讲里是可以接受的,不会有明显的对话割裂感。
4.3 抖动缓冲与丢包恢复的权衡
抖动缓冲的大小和打包时长直接挂钩。缓冲 N 个包,能吸收的网络抖动就是 N × 打包时长。40 毫秒打包、缓冲 3 个包,能扛住 120 毫秒的抖动;如果打包是 20 毫秒,同样缓冲 3 个包只能扛 60 毫秒,在跨运营商或者无线的链路上就不够用了。
丢包恢复也是同理。G.711 本身没有纠错能力,丢一个包就是 40 毫秒的声音空白(或者按丢包隐藏算法补一段)。包越小,丢一个的影响越小,但包的数量越多,丢包概率也越高。40 毫秒在这两者之间取得了一个相对平衡的位置:丢一个包的影响是 40 毫秒,人耳基本感知不到明显断裂。
4.4 芯片方案与历史习惯的惯性
技术账算完,还有一层现实因素:很多安防 SoC 的音频采集模块,DMA 缓冲区天然就是按 40 毫秒或者 80 毫秒设计的。音频 ADC 采集中断触发一次,SDK 回调里拿到的就是 320 个采样点,开发者顺手就打成 320 字节的包,几乎没有动机去拆成两个 160 字节。
这种“硬件决定了软件”的路径依赖在安防行业特别明显。同一家芯片方案商的参考设计被几十家设备厂商采用,40 毫秒就成了事实标准。你去看不同厂商的设备,只要用的是同一代芯片方案,音频包长往往一模一样。
5. 参数核算与时间戳对齐
理解了 320 的来历,接下来要把它落到实际的代码和配置里。这一节讲的是最容易出错的两个地方:带宽核算和时间戳计算。
5.1 带宽核算不能只算音频
做容量规划的时候,只盯着音频的 64kbps 会严重低估。一路 GB28181 点播的真实带宽,得把音频、视频、PS 头、RTP/UDP/IP 头、以及信令和心跳全部算进去。
音频部分的完整开销(40 毫秒打包、PS 封装):每秒 25 个包,每包 320 字节负载 + 20 字节 PS/PES 头 + 40 字节网络层头 = 380 字节,合计 9500 字节/秒,约 76kbps。
视频部分按 2Mbps 码率算,加上 RTP 和 PS 头,实际网络流量通常在 2.1 到 2.2Mbps。分片带来的额外开销取决于 MTU,按 1400 字节分片、每片 40 字节头算,开销大约 3%。
再加上 SIP 心跳(默认 60 秒一次)和 RTCP 反馈,一路点播的总带宽按 2.3Mbps 估算比较稳妥。做 500 路并发的转发服务器,网卡至少要考虑 1.2Gbps 的实际吞吐,按 2 倍余量配就是 2.5Gbps,万兆网卡起步。
这里有个坑要提醒:很多团队做带宽估算时用码率乘路数,忽略了包头发开销和 TCP/UDP 的重传,上线之后发现带宽打满。音频虽然只占 76kbps,但在路数上千的时候,这部分固定开销加起来也有几十兆。
5.2 两种时间戳步进,写错了就音画不同步
这是我认为最容易踩、也最难查的一个坑。
情况一,裸 RTP 承载 G.711。SDP 里a=rtpmap:8 PCMA/8000,时钟频率 8000。每 40 毫秒一个包,时间戳递增 8000 × 0.04 = 320。注意,这里的步进值 320 和负载长度 320 数值一样,纯属巧合,不要因为看到两个 320 就以为有因果关系。
情况二,RTP 承载 PS 流。SDP 里a=rtpmap:96 PS/90000,时钟频率 90000。每 40 毫秒一个包,时间戳递增 90000 × 0.04 = 3600。这时候如果代码里还按 320 递增,或者按负载字节数递增,接收端算出来的播放时间就完全错乱,表现出来就是音频越来越快或者越来越慢,几分钟后音画彻底对不上。
更麻烦的是,PS 流里每个 PES 包自己还带一个 PTS,单位是 90kHz。所以你在一个 PS 封装的音频流里,会同时看到两套时间:RTP 时间戳(90kHz 步进 3600)和 PES 的 PTS(同样是 90kHz,但基准点不同)。做转发的时候,如果只改 RTP 时间戳不改 PTS,或者反过来,某些播放器会直接花屏或者静音。
我的做法是:转发时 RTP 时间戳按到达间隔重新生成,保证连续性;PS 内部的 PTS 原样透传,不做裁剪。这样既避免了上游时间戳跳变导致下游卡顿,又保住了音视频之间的相对关系。
5.3 音视频同步的锚点在哪
PS 流做同步靠的是 SCR(系统时钟参考)和 PTS。SCR 在 pack header 里,标识整个流的系统时钟;PTS 在 PES 头里,标识这一帧应该在什么时刻显示。
理想情况下,音频 PES 和视频 PES 的 PTS 都是由同一个系统时钟推导出来的,接收端只要按 PTS 排序播放就行。但实际设备里经常出问题:有的厂商音频和视频用两个独立的时钟源,PTS 基准对不上;有的厂商音频 PTS 干脆填 0 或者填固定值。
排查这类问题,我的经验是先看音频 PES 的 PTS 是不是随时间递增。递增就说明设备在认真做时间戳,同步逻辑可以依赖它;如果是固定值,那就只能靠 RTP 时间戳做同步,PTS 完全不能用。
还有一个细节:音频 PES 里的 PTS 一般只在每帧的第一个 PES 包上出现,如果一帧被拆成多个 PES,后续包的 PTS 标志位要清零。有些设备的实现不规范,每个 PES 都带 PTS,解析器处理不好就会出现时间戳重复,播放时表现为声音卡在一个片段反复。
6. 动手复现:抓包、解析、造流
光看不练记不住。这一节给一套能直接跑的操作流程,你本地有台设备或者一段抓包文件就能验证上面所有结论。
6.1 Wireshark 里的关键配置
抓 GB28181 的 RTP 流,第一步是把解码关掉。Wireshark 默认会对 96 号 PT 做启发式解码,有时候会把 PS 流误判成别的格式,显示出来一堆红色错误。解决办法是在Analyze > Enabled Protocols里把不相关的 RTP payload 解码器关掉,只保留原始字节视图。
第二步,加一个自定义列显示 RTP 负载长度。右键 RTP 包的Payload字段,选择Apply as Column,然后按这一列排序,音频包的长度分布一目了然。
第三步,用过滤器把音频单独筛出来。如果音频走独立端口,直接udp.port == 音频端口;如果在同一个 PS 流里,用rtp.stream == N先定位流,再按 PT 值筛选。
顺手记一条:tshark做批量统计比图形界面快得多,命令行跑一遍就能出直方图。
tshark -r capture.pcap -Y "rtp" -T fields \ -e frame.number -e rtp.timestamp -e rtp.p_type \ -e rtp.len -e frame.time_delta_displayed \ -E separator=, > rtp_stat.csv导出的 CSV 丢进表格里,按rtp.len做个计数,按frame.time_delta_displayed求个平均,两个数字一出来,打包时长和是不是定长编码立刻清楚。
6.2 一段解析 PS 流的 Python 脚本
要把 PS 流里的音频 PES 挖出来,得按起始码扫描。下面这段脚本处理单个 RTP 负载,找出所有 PES 包的起点和音频负载长度:
import sys START_CODE = b"\x00\x00\x01" AUDIO_STREAM_IDS = (0xC0, 0xC1, 0xBD) def parse_ps_payload(payload): results = [] idx = 0 while True: pos = payload.find(START_CODE, idx) if pos < 0 or pos + 3 >= len(payload): break stream_id = payload[pos + 3] if stream_id in AUDIO_STREAM_IDS: if pos + 9 > len(payload): break pes_len = (payload[pos + 4] << 8) | payload[pos + 5] flags = payload[pos + 7] header_len = payload[pos + 8] # PES header 总长 = 3(flags+len) + header_len payload_start = pos + 9 + header_len payload_end = payload_start + pes_len - 3 - header_len audio_len = max(0, payload_end - payload_start) results.append((pos, stream_id, pes_len, audio_len, flags)) idx = pos + 3 return results if __name__ == "__main__": with open(sys.argv[1], "rb") as f: data = f.read() for r in parse_ps_payload(data): print(f"offset={r[0]} sid=0x{r[1]:02X} pes_len={r[2]} audio_len={r[3]} flags=0x{r[4]:02X}")脚本的逻辑很直接:从头扫描00 00 01起始码,读流 ID,判断是不是音频,然后按 PES 头里的长度字段切出负载。跑一遍你就能看到每个音频 PES 里到底有多少字节,是 320 还是别的值,一清二楚。
这段代码有几个地方要注意。pes_len字段统计的是该字段之后的所有字节数,包含标志位、可选头和负载,别直接当负载长度用。header_len是可选头的长度,PTS 存在时是 5,PTS 和 DTS 都有时是 10。如果pes_len填 0(不定长模式),这段逻辑需要改成扫描到下一个起始码为止。
6.3 自己造一路 320 字节的音频流做对照
想真正理解这个数字,最好的办法是自己造一路出来,再用抓包工具看。造流不需要真实设备,用一段静音数据或者正弦波就行。
思路是这样:生成 8kHz 采样的正弦波,按 40 毫秒切片,每片 320 个采样点,G.711 A 律编码成 320 字节,然后手工拼 PS 头和 PES 头,交给 RTP 发送。
编码部分可以直接调用现成的库,重点在 PES 头的构造。PES_packet_length要填3 + 5 + 320 = 328(假设带 PTS),PTS 每次递增 3600(如果 RTP 时钟是 90000)。发出去之后本地抓包,看到的负载长度应该是 334 到 348 之间,跟你分析真实设备的结果对得上,就说明理解到位了。
这个练习看着麻烦,做一遍下来,PS、PES、RTP 三层的关系会变得非常具体,之后看任何抓包都不虚。
7. 实战场上的坑与排查速查表
理论和复现都过了,最后讲讲真实项目里那些文档不会写的问题。这些都是我在对接不同厂商设备时踩出来的。
7.1 音频断续、爆音与回声
音频断续最常见的原因是抖动缓冲设置和打包时长不匹配。如果设备用 40 毫秒打包,你的缓冲只留了 60 毫秒,网络稍微抖一下就开始丢包。经验值是按打包时长的 3 到 4 倍设缓冲,40 毫秒打包就设 120 到 160 毫秒。
爆音一般是时间戳或采样对齐出了问题。G.711 解码时如果拿到的数据长度不是 320 的整数倍,最后那几个采样点会被截断或者补零,听感上就是“啪”的一声。做转码时务必保证每次送进解码器的数据是完整的帧。
回声问题跟 320 字节没直接关系,但对讲场景绕不开。根因通常是设备端的回声消除(AEC)没开或者参数不对,跟打包长度无关。判断方法:单向听(只从设备听到服务器)没有回声,双向通才有,那就是 AEC 的问题;单向就有回声,那是链路里某处做了环回。
7.2 音画不同步的三条排查路径
音画不同步是 GB28181 对接里的高频问题,我总结了三步排查:
第一,确认时间戳基准。抓包看音频和视频的 RTP 时间戳是不是同一个钟。如果一个是 8000 步进、一个是 90000 步进,说明走了两条不同的 RTP 会话,同步只能靠外部时钟,误差会累积。
第二,检查 PTS 是否递增。如果音频 PTS 是固定值,同步逻辑只能依赖 RTP 到达时间,网络一抖就漂。
第三,看帧率稳定性。视频帧率不稳会导致时间戳跳变,音频跟着受牵连。这种情况要先解决视频端的问题。
7.3 常见问题速查表
| 现象 | 高概率原因 | 排查动作 | 处理方式 |
|---|---|---|---|
| 音频包长在 320 上下抖动 | 压缩编码或变长 PS 头 | 查 SDP 的 rtpmap,算有效音频码率 | 按实际编码解析,不要假设定长 |
| 每秒包数不是 25 | 打包时长不是 40ms | 看frame.time_delta平均值 | 按实际时长重算缓冲和带宽 |
| 音频快放或慢放 | RTP 时间戳步进算错 | 对比时钟频率和步进值 | 8000 时钟步进 320,90000 时钟步进 3600 |
| 播放几分钟后音画分离 | PTS 基准不一致或未递增 | 提取 PES 的 PTS 序列看趋势 | 以 RTP 时间戳为准重建同步 |
| 声音有明显金属感 | 采样率不匹配(8k 当 16k 播) | 核对 SDP 采样率和解码器配置 | 统一按 8000 采样处理 |
| 丢一个包就有明显断裂 | 打包时长过长 | 看单包对应的毫秒数 | 改成 20ms 打包或启用丢包隐藏 |
| 音频包比预期大 12 到 20 字节 | PS/PES 头未剥离 | 按起始码扫描定位负载边界 | 解析时正确计算头部长度 |
8. 这个数字往下延伸,会影响到哪些设计决策
搞清楚 320 字节的来源,不只是满足好奇心。它往下延伸会影响到系统设计的好几个环节,这些影响在项目初期容易被忽略,等到上线才暴露就很被动。
8.1 对转发服务器的内存与并发模型
转发服务器通常为每一路流维护一个接收缓冲区。如果按 320 字节 × 缓冲包数分配,20 毫秒打包的设备需要的包数量是 40 毫秒的两倍,缓冲区大小算错就会丢包。
更关键的是包的处理频率。40 毫秒打包意味着每路每秒 25 个音频包,1000 路并发就是每秒 25000 个包需要转发。如果打包是 20 毫秒,这个数字翻倍到 50000。单核处理能力是有上限的,包越小,CPU 花在头部解析和系统调用上的时间越多。这也是为什么很多转发服务会选择按包聚合再转发,而不是来一个转一个。
8.2 对录像存储的空间与索引
录像场景下,40 毫秒一包意味着每 25 个包是一秒的音频数据。写文件时如果按包写,索引会非常碎;按秒聚合,需要缓存 25 个包。
有些实现会把 PS 流原样落盘,文件里保留 PES 头和 PTS,回放时直接透传给播放器。这种做法的好处是零转封装成本,坏处是文件比裸音频大 5% 到 8%。按 1000 路、每天 24 小时算,这个比例对应的存储量不小,做容量规划时要算进去。
8.3 对跨协议转换的额外工作
如果要把 GB28181 的流转到 WebRTC,音频这块的工作量比想象中大。WebRTC 主推 Opus,采样率 48kHz,帧长 20 毫秒,跟 G.711 的 8kHz、40 毫秒完全不搭。
转换链路要走:G.711 解码成 PCM 8kHz,重采样到 48kHz,再用 Opus 编码。重采样这一步如果做得粗糙,会有明显的量化噪声。另外时间戳要重建,WebRTC 的 RTP 时钟是 48000,每 20 毫秒步进 960,跟原来的 3600 对不上,必须重新计算。
这套转换的 CPU 开销不小,100 路并发的转码在普通服务器上就能吃满好几个核。如果业务上不需要双向音频,只做单向监听,倒是可以省掉大部分工作。
8.4 对弱网环境的适配策略
弱网下 40 毫秒打包的劣势会放大。丢包率 5% 的情况下,每秒 25 个包平均丢 1.25 个,声音会有 1 到 2 处 40 毫秒的断裂。如果换成 20 毫秒打包,每秒 50 个包丢 2.5 个,断裂次数更多但每次更短,听感上反而可能更好一些。
实际项目里的做法通常是动态调整:网络好的时候用 40 毫秒省带宽,检测到丢包率上升就切到 20 毫秒,牺牲一点带宽换连续性。这个切换需要设备端支持,不是所有厂商都给这个接口,做方案时要提前确认。
我自己在这个问题上走过弯路。早期做一个无线回传的项目,默认按 40 毫秒配置,结果现场无线链路抖动严重,音频断断续续。改成 20 毫秒之后好转明显,但带宽涨了 15%,又得重新算容量。这件事让我养成了一个习惯:音频打包时长不要写死在代码里,做成可配置项,现场调优的时候能救命。
最后分享一个我在实际排查里用得最多的方法:遇到任何和音频相关的异常,先把抓包里的音频 RTP 包按长度和到达间隔做成两张分布图,长度图看编码和封装,间隔图看打包时长和网络抖动。这两张图一出来,八成的问题当场就能定位,比一层层翻日志快得多。至于 320 这个数字,它只是这套体系里的一个坐标点,真正值钱的是你知道了它是怎么被算出来的、什么时候会变、变了之后要改哪些地方。