23、语音通话:纯音频模式、低带宽适配、VoIP优化
语音通话,说白了就是只传声音,不传视频。你可能会想:「这有什么难的?把视频关掉不就行了?」嗯,事情没这么简单。
我刚开始做WebRTC语音通话时,也这么想。结果发现,纯音频模式下,很多默认行为其实是给视频准备的。你不主动调整,系统还在偷偷摸摸做视频编码的准备工作,白白浪费CPU和带宽。
这一章,我们就来聊聊纯音频模式怎么开、低带宽下怎么活、以及VoIP场景下怎么优化到极致。
23.1 纯音频模式的正确打开方式
WebRTC默认是音视频都启用的。要切到纯音频,你得在创建RTCPeerConnection时,主动告诉它:「我不要视频。」
具体做法有两种:
- 方法一:不添加视频轨道
- 方法二:在SDP协商时禁用视频
我个人习惯用方法一,干净利落。
// 只获取音频流
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: false
});
// 只添加音频轨道
stream.getAudioTracks().forEach(track => {
pc.addTrack(track, stream);
});
这样,远端收到的SDP里就不会有m=video这一行。浏览器自然就不会做视频编码了。
还有一种情况:你已经在通话中了,突然想关掉视频。这时候不要重新创建连接,直接调用sender.replaceTrack(null)把视频轨道替换成空即可。
// 关闭视频但不重建连接
const videoSender = pc.getSenders().find(s => s.track?.kind === 'video');
if (videoSender) {
await videoSender.replaceTrack(null);
// 重新协商
await pc.createOffer();
}
嗯,这里要注意:replaceTrack(null)之后,远端会收到一个空的视频轨道,显示为黑屏。如果你希望远端彻底看不到视频,更好的做法是直接removeTrack。
23.2 低带宽适配:让语音在弱网下也能听清
低带宽场景,是移动端语音通话的噩梦。地铁里、电梯中、偏远地区,网络随时可能掉到几十kbps。
WebRTC默认的音频编码器是Opus。Opus本身很优秀,但默认参数是为宽带设计的。在低带宽下,你得手动调参。
我遇到过最极端的情况:用户在农村,上行带宽只有20kbps。默认Opus根本跑不动,声音断断续续。后来我把码率降到8kbps,居然还能听清内容。
23.2.1 调整Opus编码参数
核心参数就几个:
| 参数 | 默认值 | 低带宽推荐值 | 说明 |
|---|---|---|---|
| maxaveragebitrate | 32000 | 8000-16000 | 最大平均码率(bps) |
| maxplaybackrate | 48000 | 16000 | 最大播放采样率(Hz) |
| usedtx | 0 | 1 | 启用不连续传输(静音时省带宽) |
| useinbandfec | 0 | 1 | 启用带内前向纠错 |
设置方式是通过SDP的fmtp行:
// 在createOffer/createAnswer之后,修改SDP
sdp = sdp.replace(
/(a=fmtp:111 .*)/g,
'$1;maxaveragebitrate=8000;maxplaybackrate=16000;usedtx=1;useinbandfec=1'
);
maxaveragebitrate,Firefox用maxbitrate。建议两端都做兼容处理。
23.2.2 自适应码率调整
手动调参只能解决静态场景。真实网络是动态变化的,你需要根据实时带宽动态调整码率。
WebRTC内置了拥塞控制机制,但默认行为偏保守。我建议你主动监听RTCPeerConnection的stats,获取当前可用带宽,然后动态调整Opus码率。
// 定期获取带宽估计
setInterval(async () => {
const stats = await pc.getStats();
stats.forEach(report => {
if (report.type === 'candidate-pair' && report.state === 'succeeded') {
const bw = report.availableOutgoingBitrate;
if (bw < 20000) {
// 带宽低于20kbps,降到最低码率
setOpusBitrate(8000);
} else if (bw < 50000) {
setOpusBitrate(16000);
} else {
setOpusBitrate(32000);
}
}
});
}, 2000);
你想想看,这样做的好处是什么?带宽好的时候用高码率保证音质,带宽差的时候自动降级保证连通性。用户不会感觉到断流,只会觉得「声音稍微有点模糊」,但能听清。
23.3 VoIP优化:让语音通话像打电话一样自然
VoIP优化,核心目标就三个字:低延迟、高清晰、不卡顿。
WebRTC默认的音频处理链路是这样的:
23.3.1 回声消除(AEC)
回声是VoIP最烦人的问题之一。你说话,对方听到自己的回声,体验极差。
WebRTC内置了AEC模块,但默认参数不一定适合所有设备。我建议你主动开启echoCancellation:
const constraints = {
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
};
我曾经遇到一个案例:某款Android手机,回声消除开了跟没开一样。后来发现是手机底层的音频路由有问题——扬声器和麦克风物理距离太近,AEC算法无法收敛。解决方案是强制使用听筒模式,降低扬声器音量。
23.3.2 抖动缓冲(Jitter Buffer)
网络抖动是语音卡顿的元凶。WebRTC的抖动缓冲默认是自适应的,但你可以通过SDP参数调整它的行为。
对于语音通话,我建议设置较小的抖动缓冲,因为语音对实时性要求高,对丢包容忍度相对较高。
// 在SDP中设置抖动缓冲参数
sdp = sdp.replace(
/(a=rtpmap:111 opus)/,
'$1\r\na=fmtp:111 minptime=10; maxptime=40'
);
minptime=10表示最小打包时长10ms,maxptime=40表示最大40ms。值越小,延迟越低,但网络开销越大。值越大,抗抖动能力越强,但延迟越高。
我个人习惯在WiFi下用10ms,4G下用20ms,弱网下用40ms。动态切换效果最好。
23.3.3 丢包补偿(PLC)
语音丢包了怎么办?重传?来不及。VoIP要求实时性,重传会导致延迟飙升。
WebRTC的Opus编码器内置了丢包补偿(Packet Loss Concealment)功能。当丢包发生时,解码器会根据前后帧的信息,自动「脑补」出丢失的音频片段。
你不需要做任何额外配置,Opus默认就支持。但有一个前提:你必须启用useinbandfec,并且设置合理的maxaveragebitrate,让Opus有足够的冗余信息来恢复丢包。
// 启用带内FEC,丢包率15%以下都能较好恢复
sdp = sdp.replace(
/(a=fmtp:111 .*)/g,
'$1;useinbandfec=1;maxaveragebitrate=20000'
);
23.4 实战:一个完整的低带宽语音通话配置
说了这么多,我们来一个完整的配置示例。这是我个人在多个项目中验证过的「保底方案」:
// 1. 获取音频流(低质量模式)
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
sampleRate: 16000, // 16kHz,够用
channelCount: 1 // 单声道
}
});
// 2. 创建连接
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
stream.getAudioTracks().forEach(track => pc.addTrack(track, stream));
// 3. 修改SDP,应用低带宽参数
pc.createOffer().then(offer => {
let sdp = offer.sdp;
sdp = sdp.replace(
/(a=fmtp:111 .*)/g,
'$1;maxaveragebitrate=12000;maxplaybackrate=16000;usedtx=1;useinbandfec=1;minptime=20;maxptime=40'
);
// 移除所有视频相关的行(如果有)
sdp = sdp.replace(/m=video.*\r\n/g, '');
sdp = sdp.replace(/a=rtpmap:\d+ (H264|VP8|VP9).*\r\n/g, '');
pc.setLocalDescription({ type: 'offer', sdp });
});
这套配置在20kbps的带宽下,可以保持基本清晰的语音通话。延迟控制在200ms以内,丢包率15%以下不影响理解。
23.5 避坑指南
最后,分享几个我踩过的坑:
- 不要同时开太多音频处理:回声消除、降噪、自动增益,这三个默认都开。但如果你的设备性能差,建议只开回声消除,其他两个关掉。否则CPU吃不住,反而导致卡顿。
- 注意音频路由:Android上,扬声器模式和听筒模式的音频延迟差异很大。语音通话建议强制使用听筒模式,延迟更低,回声更小。
- 不要忽略静音检测:启用
usedtx后,Opus在静音时只发送极小的数据包。这能节省大量带宽。但要注意,有些网关设备会过滤掉这些「静音包」,导致对方听到断断续续的噪音。遇到这种情况,只能关闭DTX。 - 测试要覆盖真实网络:模拟器上的语音测试都是骗人的。一定要在真实的地铁、电梯、地下室场景中测试。我曾经在模拟器上跑得好好的,一上地铁就崩。
语音通话优化,说白了就是一场「带宽 vs 音质 vs 延迟」的三角博弈。没有银弹,只有根据场景不断调参。你做得越多,经验越丰富,就越知道什么时候该牺牲什么。
好了,这一章的内容就到这里。希望这些实战经验能帮你少走弯路。
公众号:蓝海资料掘金营,微信deep3321