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 渲染与同步

视频渲染通常用 SurfaceViewTextureView。我个人更推荐 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 音视频通话核心流程 发送端 音视频采集 编码 (VP8/Opus) RTP 打包 网络传输 GCC 带宽估计 NACK / FEC 接收端 JitterBuffer 解码 渲染 (SurfaceView) 质量优化:码率调整 + 丢包恢复 + 音视频同步

嗯,到这里 WebRTC 音视频通话的核心环节就讲完了。从采集编码到网络传输,再到接收渲染,每一步都有坑,但也都有对应的优化手段。记住一句话:没有完美的网络,只有不断优化的系统


公众号:蓝海资料掘金营,微信deep3321