20、WebRTC音视频通话:音视频采集与发送、音视频接收与渲染、通话质量优化
WebRTC 音视频通话,说白了就是让两个浏览器(或 App)能直接“面对面”说话。这背后涉及三个核心环节:采集与发送、接收与渲染、质量优化。今天我就把这套流程拆开揉碎了讲给你听。
核心要点:WebRTC 通话的本质是“端到端”的实时媒体流处理。采集端负责把摄像头和麦克风的数据变成网络包,接收端负责把网络包还原成画面和声音,而质量优化则是在不稳定的网络里尽量保证流畅。
20.1 音视频采集与发送
先说说采集。我个人习惯把采集比作“水龙头”——你得先把水(音视频数据)打开,才能流出去。
20.1.1 采集设备与约束
在 Android 上,我们通过 MediaDevices.getUserMedia() 来获取摄像头和麦克风。但这里有个坑:不同手机的摄像头参数千差万别。我曾经在适配某款低端机时,发现它只支持 640x480 的分辨率,强行设置 720P 直接黑屏。
// 设置采集约束
MediaConstraints constraints = new MediaConstraints();
// 视频:优先 720P,但允许降级
constraints.mandatory.add(new MediaConstraints.KeyValuePair("maxWidth", "1280"));
constraints.mandatory.add(new MediaConstraints.KeyValuePair("maxHeight", "720"));
constraints.mandatory.add(new MediaConstraints.KeyValuePair("maxFrameRate", "30"));
// 音频:自动增益控制 + 回声消除
constraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl", "true"));
constraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation", "true"));
我的经验:采集分辨率不要一味求高。720P 在大多数场景下已经足够清晰,而且能节省带宽。1080P 在弱网下反而容易卡顿。
20.1.2 编码与打包发送
采集到的原始数据是 YUV 或 PCM 格式,体积巨大。必须经过编码压缩才能发送。WebRTC 默认使用 VP8/VP9 视频编码和 Opus 音频编码。
编码后的数据会被封装成 RTP 包。这里有个关键点:时间戳。每个 RTP 包都带有一个时间戳,接收端靠它来同步音视频。我记得有一次排查音画不同步问题,最后发现是采集端的时钟漂移导致的。
// 发送流程(伪代码)
VideoTrack videoTrack = factory.createVideoTrack("video_track", videoSource);
AudioTrack audioTrack = factory.createAudioTrack("audio_track", audioSource);
// 添加到 PeerConnection
peerConnection.addTrack(videoTrack);
peerConnection.addTrack(audioTrack);
// 内部自动完成:采集 -> 编码 -> RTP打包 -> 网络发送
20.2 音视频接收与渲染
接收端的工作正好反过来:收到 RTP 包 -> 解码 -> 渲染。你想想看,如果发送端是“水龙头”,接收端就是“水杯”——得稳稳地接住每一滴水。
20.2.1 接收与解码
WebRTC 内部维护了一个 JitterBuffer(抖动缓冲)。为什么需要它?因为网络包到达的时间是不均匀的。JitterBuffer 会先把包缓存起来,等攒够一定数量再统一交给解码器。
注意:JitterBuffer 是一把双刃剑。缓冲太大,延迟高;缓冲太小,容易卡顿。WebRTC 会根据网络抖动自动调整缓冲大小,但极端情况下还是需要手动干预。
20.2.2 渲染与同步
视频渲染通常用 SurfaceView 或 TextureView。我个人更推荐 TextureView,因为它支持硬件加速和后期处理(比如美颜)。
音频渲染则通过 AudioTrack 播放。这里有个容易忽略的点:音频采样率。如果接收到的音频是 48kHz,而设备只支持 44.1kHz,就需要重采样。我曾经遇到过某款手机播放音频时“吱吱”响,最后发现是重采样算法没选对。
// 渲染视频
VideoRenderer renderer = new VideoRenderer(new VideoRenderer.Callbacks() {
@Override
public void renderFrame(VideoRenderer.I420Frame frame) {
// 将 YUV 数据渲染到 SurfaceView
surfaceView.drawFrame(frame);
}
});
peerConnection.addRenderer(renderer);
20.3 通话质量优化
质量优化是 WebRTC 通话中最“玄学”的部分。说白了,就是在网络不好的情况下,尽量让用户感觉不到卡顿和模糊。
20.3.1 网络自适应:带宽估计与码率调整
WebRTC 内置了 GCC(Google Congestion Control) 算法。它会实时监测网络延迟和丢包率,然后动态调整编码码率。
举个例子:当网络变差时,GCC 会通知编码器降低码率,视频分辨率可能会从 720P 降到 360P,甚至 180P。虽然画面模糊了,但至少不会卡死。
| 网络状况 | 码率调整 | 用户感知 |
|---|---|---|
| 良好(延迟 < 50ms,丢包 < 1%) | 保持高码率(1.5-2Mbps) | 清晰流畅 |
| 一般(延迟 100-200ms,丢包 2-5%) | 降低码率(500-800Kbps) | 轻微模糊,但流畅 |
| 较差(延迟 > 300ms,丢包 > 10%) | 大幅降码率(200-300Kbps) | 明显模糊,但能通话 |
避坑指南:我曾经在弱网环境下测试,发现视频频繁卡顿。后来抓包分析,发现是 GCC 的反馈周期太长。解决方案是缩短 RTCP 报告间隔,让带宽估计更灵敏。
20.3.2 丢包重传与 FEC
网络丢包是不可避免的。WebRTC 提供了两种应对方式:
- NACK(丢包重传):接收端发现丢包后,请求发送端重传。优点是准确,缺点是会增加延迟。
- FEC(前向纠错):发送端额外发送一些冗余数据,接收端即使丢了一部分包也能恢复。优点是低延迟,缺点是浪费带宽。
实际项目中,我通常建议两者结合使用。对于关键帧(I帧),用 FEC 保护;对于非关键帧(P帧),用 NACK 重传。
20.3.3 音视频同步
音视频不同步是用户最敏感的体验问题。WebRTC 通过 RTP 时间戳 和 NTP 时间 来实现同步。
具体做法是:发送端在 RTP 包中携带采样时间戳,接收端根据时间戳将音频和视频对齐。如果发现音频比视频快,就稍微延迟音频播放;反之则延迟视频渲染。
我的经验:音视频同步的容忍范围是 ±80ms。超过这个范围,用户就能明显感觉到“嘴型对不上”。我曾经在项目中通过日志发现,某款手机的音频播放延迟比视频大 150ms,最后通过调整 AudioTrack 的缓冲大小解决了问题。
20.4 本章知识体系
下面这张图总结了 WebRTC 音视频通话的完整流程。你可以把它当作一个“地图”,随时回来对照。
嗯,到这里 WebRTC 音视频通话的核心环节就讲完了。从采集编码到网络传输,再到接收渲染,每一步都有坑,但也都有对应的优化手段。记住一句话:没有完美的网络,只有不断优化的系统。