TT Lab
开始
学习 学习路径 课程

实时通信 — WebSocket、gRPC 流式调用与 WebRTC

测量实时通道并发送语音帧

在 TT Lab 中继续学习

一句话总结

实时通道要看的不是平均值,而是延迟分布的尾部和重连率;语音帧要遵守 20ms 的节拍发送;并通过播放缓冲区在卡顿与延迟之间做交换。

为什么需要它

实时通道的故障大多是“偶尔”发生的。如果有 1% 的帧晚了 300ms,平均延迟几乎不变,对话却会中断。连接数指标看起来很正常,重连却可能每分钟发生几千次。语音 AI 服务在此之外还多了节拍这一点。麦克风每 20ms 生成一帧,识别引擎和播放器都期待这个节拍。如果违背节拍,接收方的缓冲区就会溢出或被排空,声音就会跳动或中断。

工作原理

帧的算术。语音识别通常接收的格式是 16kHz、16 位、单声道。每秒是 32,000 字节(256kbps),一个 20ms 的帧是 320 个采样点、640 字节。用 Opus 编码后,语音会缩减到几十 kbps。发送帧时,截止时刻要按绝对时刻(起点 + i × 20ms)来设定。如果每次发送都睡 20ms,编码所用的时间就会每次累加,每秒会推迟几百 ms。

延迟的分布。往返延迟的测量方法是:把发送瞬间的单调时钟写入帧头,再与回显到达的瞬间相减。单向延迟需要两台机器的时钟一致,所以困难得多。收集到的样本用 p50、p95、p99 来概括。最近秩方法会选取排序后第 ceil(p/100 × n) 个值,所以只报告实际观测到的值。把多台服务器的 p99 求平均会得到没有意义的数字,所以必须把每台服务器的直方图汇总合并之后,再计算百分位数。

抖动。RFC 3550 第 6.4.1 节用连续两个数据包的传输时间差 D 的绝对值,反复套用 J = J + (|D| − J)/16 来估计到达间隔抖动。1/16 是用来减小噪声的增益。浏览器的 WebRTC 统计(getStats) 会为每个接收的 RTP 流给出 jitter、packetsLost、jitterBufferDelay、concealedSamples 之类的值,还会给出对方报告的 roundTripTime。

播放缓冲区。接收方在第一帧到达之后,等待缓冲区时长,然后每 20ms 播放一帧。如果第 i 帧比它自己的播放时刻来得晚,就等于没有。增大缓冲区,迟到的帧会减少,但所有声音也会相应地推迟。TCP 之上的 WebSocket 虽然没有丢失任何数据,但一旦停顿,这期间的帧就会一次性迟到,所以要保持同样的卡顿比例,就需要与停顿时长相当的缓冲区。不要求顺序和重传的 WebRTC 数据通道或 RTP,则是丢了多少就丢多少。

通道的观测。对实时通道,需要查看的数字有五个。

指标 原因
连接准备时间(握手、ICE、DTLS) 用户第一次等待的时间
消息延迟 p50、p99 藏在平均值背后的尾部
重连率与关闭码分布 在哪里、因为什么而断开
每个订阅者的队列长度、丢弃数 慢消费者
ping 往返时间 网络状况与已死的对端

测量方法的陷阱。如果负载生成器要等收到响应之后才发送下一个请求,那么服务器停顿期间本应发出的请求,就会完全从测量中消失(coordinated omission)。必须用遵守节拍的发送器——前面讲过的无累积误差的发送——来测量,停顿才能被准确地记入尾部。此外,延迟必须用同一台机器的单调时钟来测。挂钟在 NTP 中途校正时有时会倒退,从而得出负的延迟。

要给观测数字加上标签。必须同时留下它属于哪种传输通道(WebSocket、WebRTC)、哪个地区和网络类型(Wi-Fi、LTE),当整体 p99 恶化时,才能分清是哪个群体的尾部。

重连率也用同样的方式来看。即使连接数看起来是恒定的,如果不单独统计每分钟的重连数和关闭码的分布,那么 1% 的用户每 30 秒断开又连上的情况,就会藏在指标背后。

在现场相遇的样子

语音 AI 的体感延迟是整条链的总和:收集 20ms 帧的时间、网络、说话结束检测、识别的部分结果、语言模型的第一个令牌、合成的第一个片段、播放缓冲区。ITU-T G.114 所说的单向 150ms 是人与人通话的标准,而语音 AI 还要在其上叠加处理时间。因此在传输通道上能节省的几十 ms 就很重要,而且必须先有能测量这几十 ms 的工具。本实验中的数字,在语音 AI 课程中把识别和合成放到这条传输通道上时,会成为基线。

下一项实验要做什么

把 16kHz 的 WAV 切成 20ms 的帧,在没有累积误差的情况下遵守节拍发送,并计算百分位数和 RFC 3550 抖动。通过 WebSocket 和 WebRTC 数据通道测量同一帧的往返延迟,用 Opus 媒体轨道发送声音并确认以 48kHz 接收之后,再用到达记录计算播放缓冲区的大小。