首页 / 资讯中心 / 文章详情

G711 over RTP封装实战:载荷、时间戳步进与抖动缓冲排查

G711 over RTP封装实战:载荷、时间戳步进与抖动缓冲排查 ★ FEATURED ARTICLE
简介面向实时音频通信与网络协议练习者的G.711封装RTP传输实践包重点解决G.711编码音频如何按RTP头结构打包、经UDP发送并由VLC接收播放的最小链路问题。包内共4个文件涵盖C语言实现源码、SDP会话描述、G.711音频样本与使用说明压缩包约2.16MB体量轻便便于对照理解序列号、时间戳、负载插入和连接参数等关键环节。已有1486人学习说明该主题在实时音频流实验中具有一定关注度。适用于课程实验、协议分析与音视频传输调试场景也可作为网络电话入门的对照范例。读者可借此搭建可运行的发送端观察RTP包构造与VLC播放效果并结合说明梳理两种律制编码、语音质量、UDP传输和SDP会话协商之间的配合关系为排查收包异常、同步偏差及扩展RTP功能提供可复用参考。1. 从一段发闷的通话说起G711 装进 RTP 传输前必须想清楚的三个值接通了能听见但声音发闷每隔 20 毫秒还咔一下。这是几年前拆一个语音对讲模块时最典型的一幕。问题不在编解码器本身——G711 只有 8 位、8 kHz、恒定 64 kbps 这点本事简单到几乎没有调参空间真正出问题的是 G711 封装成 RTP 传输这一段净荷里到底该放压扩后的字节还是线性 PCM、时间戳按什么步进、序号在 SSRC 不变的前提下怎么不跳变。这一段做错后面接什么播放器都救不回来。这份资源解决的正是这一段把 G711 的 A-law / μ-law 码流按 RTP 载荷格式切开打包、按固定节奏发出去、在收端拆包还原。适合正在做 SIP 对讲、VoIP 终端、录音回放、嵌入式音频网关的人也适合想搞明白 RTP 头里那几个字段到底谁在用的开发者。下面按「码流怎么对应 → 怎么封 → 怎么拆 → 哪儿翻车 → 怎么验」走一遍。2. 码流与载荷的对应关系PCMA/PCMU 的载荷类型、时间戳步进和打包周期2.1 先看清 G711 出手的是什么8 位压扩码不是线性 PCMG711 干的事情只有一件把 13 位A-law或 14 位μ-law的线性采样用折线段量化的方式压成 1 个字节。也就是说编码器输出的一字节就是一个采样点8000 个采样点就是 8000 字节一秒净荷恒定 64 kbps。这个「一样本一字节」的性质决定了后面所有打包逻辑净荷长度永远等于样本数20 毫秒就是 160 个样本、160 字节不需要任何帧头、不需要长度前缀、不需要分隔符。RTP 的包边界本身就是帧边界。A-law 和 μ-law 在 RTP 里有各自的静态载荷类型不需要 SDP 里额外的 rtpmap 协商参数但 SDP 中仍然要把对应的 PT 列出来。项目PCMAG.711 A-lawPCMUG.711 μ-lawRTP 静态载荷类型80采样率8000 Hz8000 Hz每样本位数8 bit8 bit净荷速率64 kbps64 kbps20 ms 净荷长度160 字节160 字节静音空闲码字0xD50xFF常见使用区域欧洲、中国等北美、日本等静音码字这一行值得单独记一下。做静音检测或者补包时如果你随手填 0x00对端解出来不是静音而是一个幅度不小的直流偏置听感上就是持续的「嗡」。填错静音码字属于那种查半天查不出、一听就听出来的问题。A-law 和 μ-law 之间可以互转但那是二次量化转一次掉一点信噪比。整条链路上只要有一处无谓的 A↔μ 转换就等于白扔一档音质。常见做法是SDP 协商阶段两个 PT 都报谁发谁定收端按收到的 PT 解码全程不转。2.2 RTP 头里真正要用到的六个字段12 字节固定头G711 场景下大部分字段是常量真正需要每包算的只有三个。字段位宽G711 场景取值说明V版本22固定P填充10G711 净荷天然字节对齐不需要填充X扩展10无扩展头CCCSRC 数40单声道点对点无混音源M标记1见下会话首包、静音期后首包置 1PT载荷类型70 或 8PCMU / PCMA序号16每包 116 位回绕不跳号时间戳32每包 160时钟频率 8000 HzSSRC32会话内固定随机生成中途不能变时间戳是坑最多的地方。RTP 的时间戳单位不是秒、不是毫秒、不是字节数而是「采样点数」。G711 的时钟频率是 8000所以一个 20 毫秒的包时间戳步进是 8000 × 0.02 160。如果哪天把打包周期改成 30 毫秒步进就要变成 240同时净荷变成 240 字节——这两个数必须同步改只改一个就会出现「音调对但节奏不对」或者「节奏对但音调不对」。序号的 16 位回绕也是同类问题。65535 之后回到 0收端如果直接用seq - last_seq判断连续回绕那一下会算出一个 -65535 的巨大间隙抖动缓冲会误判成大规模丢包然后清空重来。注意M 位不是「每包都置 0」就完事的。会话第一个包、以及静音抑制结束后的第一个包都应该置 1收端靠它判断「这里可以重置播放节奏」。2.3 打包周期怎么定10/20/30/40 毫秒的带宽与时延账G711 净荷速率固定但加上 IP20 字节、UDP8 字节、RTP12 字节共 40 字节的头部之后实际占用的带宽跟打包周期强相关。包打得越密头部占比越高。打包周期每包样本数净荷字节包速率单包总长含头总速率10 ms8080 B100 pps120 B96 kbps20 ms160160 B50 pps200 B80 kbps30 ms240240 B33.3 pps280 B74.7 kbps40 ms320320 B25 pps360 B72 kbps从 10 毫秒到 40 毫秒带宽从 96 kbps 降到 72 kbps省了 25%。但代价是单包时延从 20 毫秒涨到 80 毫秒往返算两次而且丢一个包就是 40 毫秒的声音缺口。我的取值习惯是局域网、对讲机这类对时延敏感的场景用 20 毫秒跨公网、带宽紧张的用 30 毫秒只有在明确的卫星链路或者极窄带信道上才考虑 40 毫秒。10 毫秒一般不划算——多花的 16 kbps 换来的 10 毫秒时延在实际网络抖动面前基本被淹掉。单包总长的上限要盯一眼 MTU。40 毫秒包 360 字节加上以太网头也远低于 1500不会分片。但如果做多帧捆包把两个 20 毫秒帧塞进一个包时间戳步进要改成 320净荷 320 字节一样安全捆到 8 帧就是 1280 字节净荷加 40 字节头在 PPPoE 环境下就危险了。2.4 输入不是 8 kHz 怎么办重采样加压扩的两步换算现实里拿到的音频往往是 16 kHz、16 bit 的 PCM。直接丢进 RTP 净荷是最常见的翻车方式之一对端按 8 bit 解长度翻倍、播放速度减半、内容全是噪声。正确的顺序是先降到 8 kHz再做压扩。降采样必须先低通再抽取否则 4 kHz 以上的成分会混叠回来。import audioop # 注意Python 3.13 起标准库已移除该模块老版本或自备实现可用 # pcm16k: 16 kHz / 16 bit / 单声道 的原始字节流 # 第一步重采样到 8 kHzratecv 内部带简单滤波语音场景够用 pcm8k, _ audioop.ratecv(pcm16k, 2, 1, 16000, 8000, None) # 参数依次为输入数据、样本字节宽度(216bit)、声道数、输入采样率、输出采样率、状态 # 第二步16 bit 线性 - A-law输出一样本一字节 alaw audioop.lin2alaw(pcm8k, 2) assert len(alaw) len(pcm8k) // 2这段代码有两个地方要看清楚。ratecv的第二个参数是样本宽度字节数不是位深16 bit 就填 2填 16 会直接抛异常。lin2alaw的第二个参数同理也是样本宽度输出长度正好是输入的一半这个断言可以当成一道自检如果长度不对说明前面的样本宽度或声道数填错了。对音质有要求的场景ratecv的滤波滚降不够陡4 kHz 附近会有轻微混叠。替代做法是自己写一个 63 阶左右的 FIR 低通截止 3.4 kHz滤波后再每隔一个点取一个。这个在嵌入式上要算一下 MIPS一般 Cortex-M4 级别的片子跑 8 kHz 单声道没问题。3. 封装落地用 12 字节头和 160 字节净荷把 G711 打成 RTP 包3.1 手工构造 12 字节 RTP 头路由器和交换机不关心负载是什么但收端关心头部每一个字节。手写一遍头部打包比调库更容易理解后面出问题该看哪儿。import struct, socket, time, random PT 8 # 8PCMA0PCMU SSRC random.getrandbits(32) # 会话内固定进程重启才变 SEQ0 random.getrandbits(16) # 起始序号随机化避免明文预测 TS0 random.getrandbits(32) # 起始时间戳同样随机化 def rtp_header(pt, seq, ts, ssrc, marker0): 构造 12 字节 RTP 固定头无填充、无扩展、无 CSRC b0 0x80 # V2, P0, X0, CC0 b1 ((marker 0x01) 7) | (pt 0x7F) # M 位 7 位载荷类型 return struct.pack(!BBHII, b0, b1, seq 0xFFFF, ts 0xFFFFFFFF, ssrc 0xFFFFFFFF)struct.pack的格式串!BBHII里!表示网络字节序大端。这一点不能省——RTP 规定所有多字节字段按网络字节序传输在小端机器上不加!打出来的包收端解出的序号和时间戳是高低位颠倒的表现是「能收到包但序号乱跳」。0x80这个常量把版本号 2 写死在最高两位剩下的 P/X/CC 全零。以后如果要加 CSRC 或者扩展头改的就是这个字节和后面的偏移量。SSRC、起始序号、起始时间戳都随机化不是可有可无的讲究。固定起始值在有多个终端同时上线的环境里会造成 SSRC 冲突两路流在收端被当成同一路合并听感上是两个人的声音交替出现。3.2 按 20 毫秒切帧与时间戳步进封装的节奏控制比头部构造更容易出问题因为它的错误是累积的。FRAME_MS 20 CLOCK 8000 # G711 时钟频率固定 8000 SAMPLES CLOCK * FRAME_MS // 1000 # 160 个样本 PAYLOAD SAMPLES # G711 一样本一字节 160 字节 def send_stream(sock, dst, g711_frames, marker_firstTrue): seq, ts SEQ0, TS0 next_t time.monotonic() first marker_first for payload in g711_frames: if len(payload) ! PAYLOAD: # 长度不对宁可跳过也不要发 continue pkt rtp_header(PT, seq, ts, SSRC, marker1 if first else 0) payload sock.sendto(pkt, dst) first False seq (seq 1) 0xFFFF ts (ts SAMPLES) 0xFFFFFFFF next_t FRAME_MS / 1000.0 delay next_t - time.monotonic() if delay 0: time.sleep(delay) else: next_t time.monotonic() # 落后过多就重新对齐不补发关键点有三个。第一ts的步进用的是SAMPLES而不是len(payload)。这两个值在 G711 下恰好相等所以很多人写ts len(payload)也能跑通但一旦换成 G.722 或者加了冗余编码样本数和字节数就不再一一对应这个写法会立刻出错。从一开始就写SAMPLES是给自己留后路。第二 0xFFFF和 0xFFFFFFFF的掩码不能省。Python 的整数是无界的不掩码的话序号会一路涨到几百万struct.pack(H, ...)直接抛struct.error。第三节奏用next_t累加而不是每轮sleep(0.02)。后者每次都会带上调度误差误差单向累积一分钟后时间戳和真实时间就错开一大截收端的抖动缓冲会被迫反复调整。累加式写法把误差限制在单个周期内落后太多时重新对齐而不是疯狂补发——补发会造成瞬间的带宽尖峰比丢几帧更糟。3.3 净荷从哪儿来编码器输出直接对接发送循环拿到的g711_frames必须是已经压扩好的字节流按 160 字节切好。这个切分动作在上游做不要在发送循环里做因为发送循环承担着实时性责任任何多余的内存操作都会让节奏抖动。常见的流水线是采集回调拿到 16 bit PCM → 重采样到 8 kHz → 压扩成 G711 → 塞进一个环形缓冲 → 发送线程按 20 毫秒从环形缓冲里取 160 字节。环形缓冲的容量建议 200 毫秒左右太小会被调度抖动掏空太大则把时延堆起来。如果采集侧的一次回调不是 160 个样本比如声卡给的是 441 个样本一块就必须在环形缓冲这一层做重切分不能把一个回调块直接当成一个 RTP 包发出去。变了长的包会破坏时间戳步进的等距性收端的抖动缓冲会一直处于追赶状态。3.4 多帧捆包与 MTU 边界有些场景为了压头部开销会把多个帧捆进一个 RTP 包。做法很简单净荷拼成 320 字节时间戳步进改成 320序号照样 1。收端不需要知道捆了几帧它只看时间戳差值就能算出一共有多少样本。代价是丢包影响放大一倍。20 毫秒一个包丢一个缺口 20 毫秒40 毫秒一个包丢一个缺口就是 40 毫秒。在有丢包的链路上捆包是拿抗丢包能力换带宽值不值要看实测的丢包率。MTU 边界要算清楚以太网 1500 字节减去 IP 头 20、UDP 头 8、RTP 头 12净荷上限 1460 字节也就是 182 个 8 毫秒样本、9 个 20 毫秒帧。PPPoE 环境还要再减 8 字节。跨越 NAT 或者隧道时实际可用 MTU 可能只有 1400 出头。留一点余量捆包不超过 8 帧比较稳妥。提示判断有没有发生分片看抓包里 IP 层的 Fragment Offset 和 MF 位比看应用层表现快得多。分片的直接后果是丢一片就整包作废而很多实现只统计 RTP 层丢包看起来丢包率不高但声音一直在断。4. 收端还原序号回绕、抖动缓冲与丢包补偿的实现细节4.1 解析头部与序号回绕的正确判断收端第一步是把 12 字节头拆开并按 CC 和 X 位正确跳过 CSRC 列表和扩展头。虽然 G711 点对点场景里这两位通常是 0但一旦中间经过了混音服务器或者网关扩展头就会出现写死data[12:]当净荷就会把扩展头当成音频数据播出去听感是开头一小段刺耳的噪声。def parse_rtp(data): if len(data) 12: return None b0, b1, seq, ts, ssrc struct.unpack(!BBHII, data[:12]) if (b0 6) ! 2: # 版本必须是 2 return None cc b0 0x0F # CSRC 个数 ext (b0 4) 0x01 # 扩展头标志 pad (b0 5) 0x01 # 填充标志 off 12 cc * 4 if ext: if len(data) off 4: return None ext_words struct.unpack(!H, data[off 2:off 4])[0] off 4 ext_words * 4 # 扩展头长度以 4 字节为单位 end len(data) - (data[-1] if pad else 0) # 末尾填充字节数由最后一字节给出 if end off: return None return {pt: b1 0x7F, m: b1 7, seq: seq, ts: ts, ssrc: ssrc, payload: data[off:end]}三个偏移量要盯住cc * 4因为每个 CSRC 是 4 字节ext_words * 4因为扩展头长度字段的单位是 32 位字而不是字节填充长度由最后一个字节给出所以要先把最后一字节读出来再从尾部扣掉。序号回绕的判断标准写法是先算差值再折回有符号区间def seq_delta(seq, base): 返回相对 base 的有符号差值自动处理 16 位回绕 d (seq - base) 0xFFFF return d - 0x10000 if d 0x8000 else d差值大于 0x8000 就认为是「回退」减掉 65536。这样 65535 → 0 算出来是 10 → 65535 算出来是 -1抖动缓冲的排序逻辑就不用为回绕单独写分支了。4.2 抖动缓冲按时间戳排序不按到达顺序网络包的到达顺序不可信。抖动缓冲的核心是攒够一定深度再开始播播放时按序号等价于按时间戳取缺的补、多的丢。class JitterBuffer: def __init__(self, depth3, clock8000, frame_ms20): self.depth depth # 目标缓冲包数3 包 60 ms self.base_seq None self.buf {} # seq - payload self.clock clock self.frame_samples clock * frame_ms // 1000 def push(self, seq, payload): if self.base_seq is None: self.base_seq seq d (seq - self.base_seq) 0xFFFF if d 0x8000: # 比播放进度还早的迟到包丢掉 return if d 2000: # 超出窗口上限视为新会话重新对齐 self.base_seq seq self.buf.clear() return self.buf[seq] payload def pop(self): 返回 (payload 或 None, 是否缺失) if self.base_seq is None or len(self.buf) self.depth: return None, False # 还没攒够不启动播放 payload self.buf.pop(self.base_seq, None) self.base_seq (self.base_seq 1) 0xFFFF return payload, payload is Nonedepth是这份缓冲里唯一需要调的参数它直接等于引入的额外时延。3 包 60 毫秒适合局域网公网建议 56 包。攒够之前pop返回None并且不推进base_seq这一点很重要——如果没攒够就推进会把前几个包全部判成丢失。窗口上限 2000 是一个防呆值。正常情况下迟到的包差值只有几十一旦出现几千的差值说明对端换了 SSRC 或者重启了这时候重新对齐比继续等更合理。4.3 丢包补偿重复上一帧加淡出G711 没有帧间预测丢一帧就是丢 20 毫秒不会像某些带记忆的编码那样错误扩散。所以补偿策略可以很轻重复最后一个好帧每重复一次衰减一点幅度连续丢超过 3 帧就填静音码字。def conceal(last_payload, lost_run, silence_byte0xD5): last_payload: 上一个成功解码的 G711 帧lost_run: 连续丢包计数从1开始 if last_payload is None or lost_run 3: return bytes([silence_byte]) * 160 # 超出补偿能力直接静音 gain 1.0 - 0.25 * lost_run # 每次衰减 25%避免机械重复感 out bytearray(len(last_payload)) for i, b in enumerate(last_payload): if b silence_byte: out[i] b continue # 只对幅度做衰减保持压扩码的段号不变避免解码端重新量化 seg (b ^ 0x55) 0x70 out[i] b if gain 0.5 else (b ^ 0x55) 0x0F | seg | (b 0x80) ^ 0x55 return bytes(out)最后那段按段号操作的取巧写法只是示意实际工程里更省事的做法是先解码成线性 PCM、乘增益、再重新压扩——多一次量化但代码可读性好得多20 毫秒的量级上也听不出差别。关键在于「连续丢 3 帧就停」。超过 60 毫秒的重复听感上会变成一种奇怪的嗡鸣比直接静音难听得多。4.4 解码回 PCM 并落盘验证从压扩码回到线性 PCM标准库audioop在新版本里已经移除自己写查表也就十几行。A-law 的解码逻辑是固定的def alaw2linear(a): a ^ 0x55 # 奇偶位取反标准要求的预处理 t (a 0x0F) 4 # 段内量化值左移 4 位 seg (a 0x70) 4 # 段号 0..7 if seg 0: t 8 elif seg 1: t 0x108 else: t 0x108 t seg - 1 return t if (a 0x80) else -t # 最高位是符号位1 为正前两段的偏移量8和0x108是折线表的固定常数不是可调参数第三段开始按段号左移这是 A-law 折线段斜率的体现。写错这两个常数解码出来的声音会有明显的「阶梯感」安静段落尤其明显。验证方式很朴素把解码后的 16 bit PCM 按 8000 Hz、单声道写成一个 wav用耳朵听。这个动作比看任何统计指标都快。听得出问题再去抓包里找听不出问题说明链路基本可用。5. 避坑与排查G711 over RTP 最常见的五个翻车现场5.1 能通但全是沙沙声现象通话建立成功双方都能收到包但听到的是持续的白噪声偶尔能辨认出人声轮廓。原因净荷里塞的是 16 bit 线性 PCM或者虽然做了压扩但字节序搞反了。G711 一字节一样本对端按 8 bit 解 16 bit 数据等于把每个样本拆成两个、播放速度减半、内容全乱。解决抓一个包看 RTP 净荷长度是不是正好等于时间戳差值20 毫秒包应该是 160。长度是 320 就说明没压扩。另外单独验一次编码器输出把输入的正弦波压扩后再解码波形应该基本还原如果解出来是方波或者全零问题在压扩那一步而不在 RTP 层。5.2 声音节奏对但音调不对现象语速正常人声变得又尖又细或者又低又闷像磁带快放慢放。原因采样率不匹配。发送侧按 16 kHz 采集、按 8 kHz 的标准压扩或者接收侧按 16 kHz 播放 8 kHz 数据。RTP 头里的时间戳是对的但实际音频内容的采样率跟协商的不一致。解决检查三处——SDP 里artpmap:8 PCMA/8000的采样率是不是 8000重采样那一步有没有真的执行打断点看输出长度是不是输入的一半播放端有没有按 8 kHz 打开设备。音调偏差刚好是两倍或一半基本可以锁定在采样率而不是时间戳。5.3 单向无声反向正常现象A 能听到 BB 听不到 A抓包看两个方向都有 RTP 流量。原因载荷类型不对称。一侧发的是 PCMAPT 8另一侧按 PCMUPT 0解。G711 的两种压扩码是完全不同的映射表用错表解出来的不是噪声而是严重失真的、音量极小的声音很容易被误判成「没有声音」。另一种可能是 SSRC 冲突两路流被接收端合并成一路。解决抓包看两个方向的 PT 值是否一致或者接收端是否按收到的 PT 动态选择解码器而不是写死。SSRC 冲突看接收端的统计页面如果收到的包数远大于发送端发出的包数就是两路流混到一起了。5.4 每隔十几秒卡一下随后恢复现象通话大部分时间正常但周期性出现一次明显的停顿或者「噗」的一声。原因发送节奏用固定sleep累积了漂移时间戳逐渐偏离真实时间收端抖动缓冲攒到上限后被迫丢包重对齐。另一种可能是抖动缓冲深度设得太小网络稍有抖动就掏空。解决把发送循环改成next_t累加式见 3.2并把落后时的行为从「补发」改成「重新对齐」。抖动缓冲深度从 3 包提到 5 包试一次。如果周期性卡顿的间隔正好等于缓冲深度乘以帧长那基本可以确认是缓冲被掏空。5.5 按键音对方听不到或者听成别的声音现象语音正常但 DTMF 按键对方识别不出来或者识别成错误的号码。原因把 DTMF 当成普通音频走 G711 带内传输。G711 的压扩对小信号是不均匀的双音多频的两个频率分量经过压扩后谐波结构被改变接收侧的检测器容易判错。同时语音编码器本身的非线性也会破坏 DTMF 的幅度关系。解决DTMF 走带外用 RTP 的 telephone-event 载荷类型通常是动态 PTSDP 里通过artpmap:101 telephone-event/8000协商按键作为独立的事件包发送语音流不受影响。如果链路两端有一端不支持带外那就只能在带内传但要保证这一段不做任何增益调整和降噪处理。6. 抓包验证与落盘试听两招确认封装真的对了封装写完我一般不急着接播放器先做两件事。第一件是把裸码流落盘试听。取一段发送侧的 G711 数据按 8000 Hz、单声道、对应压扩类型写成 wavA-law 的 wav 格式标签是 6μ-law 是 7直接用系统播放器打开。这一步能立刻区分「编码问题」和「传输问题」——如果落盘的声音干净那就是 RTP 层的事如果落盘就已经发闷、有咔哒声跟网络一点关系都没有。第二件是抓包做量化统计。用tshark把 RTP 字段拉成表格重点看序号有没有跳变、时间戳差值是否恒等于 160# 提取 RTP 流的关键字段按时间排序输出 tshark -r call.pcapng -Y rtp \ -T fields \ -e frame.time_relative \ -e ip.src \ -e rtp.seq \ -e rtp.timestamp \ -e rtp.p_type \ -e rtp.marker \ -e rtp.len \ | sort -n-Y rtp是显示过滤器只留 RTP 包-T fields配合-e输出成制表符分隔的纯文本方便后面用脚本扫。rtp.len是净荷长度正常应该恒等于 160。如果这一列出现了 320 或者 0说明打包逻辑有分支没覆盖到。拿到表格后用几行脚本扫一遍异常prev_seq prev_ts None for line in open(rtp.tsv): t, src, seq, ts, pt, m, ln line.rstrip(\n).split(\t) seq, ts, ln int(seq), int(ts), int(ln) if prev_seq is not None: d (seq - prev_seq) 0xFFFF if d ! 1: print(f{t}s 序号跳变: {prev_seq} - {seq} (差 {d})) dt (ts - prev_ts) 0xFFFFFFFF if dt ! 160: print(f{t}s 时间戳异常: 步进 {dt}, 净荷 {ln}) prev_seq, prev_ts seq, ts序号跳变定位丢包时间戳步进异常定位打包逻辑。两者同时出错通常是发送侧的帧切分和节奏控制没对齐。还有一个容易被忽略的检查项抓包文件里同一个 SSRC 是否只对应一条时间戳单调递增的流。如果同一 SSRC 下时间戳出现大幅回退说明有设备在重启后复用了 SSRC收端必须能识别并重新对齐否则抖动缓冲会一直卡在「等一个永远不会来的包」的状态。再补一个实操技巧把tshark的 RTP 流分析打开-z rtp,streams会给出一张按 SSRC 汇总的表包含包数、丢包数、最大抖动、最大偏差。这张表适合快速判断「问题在不在 RTP 层」比逐包看高效得多。但它的丢包统计依赖序号连续性如果发送侧本身就在跳号表里的数字会误导人——所以先跑上面的脚本确认序号是干净的再看这张汇总表。血泪经验是不要一上来就怀疑网络。这类问题里编码器输出格式不对、时间戳步进写错、载荷类型写死这三种情况占了大半而它们全都能靠「落盘听三秒」和「看净荷长度是不是 160」当场定位。从那以后我每接一条语音链路都强制先把裸码流落成 wav 听一遍再去碰 RTP 头。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站