7、媒体流控制:音频轨道的启用/禁用、视频轨道的切换、分辨率与码率动态调整
说到媒体流控制,这其实是WebRTC开发中最常打交道的一块。你想想看,用户可能随时想关掉麦克风、切换摄像头,或者网络一卡就需要降低画质——这些操作,本质上都是在操作媒体轨道。
我个人习惯把媒体流控制分成三个层次:轨道级别的开关、设备级别的切换、编码参数级别的调整。咱们一个一个来聊。
7.1 音频轨道的启用与禁用
先说最简单的。音频轨道的启用和禁用,说白了就是控制麦克风的开关。在WebRTC里,每个轨道都有一个 enabled 属性。设为 false 时,远端会收到静音帧,但连接本身不会断开。
// 获取本地音频轨道
const audioTrack = localStream.getAudioTracks()[0];
// 禁用音频(静音)
audioTrack.enabled = false;
// 启用音频(取消静音)
audioTrack.enabled = true;
我在项目中遇到过一个问题:用户点了静音按钮,但远端还是能听到声音。排查了半天,发现是代码里只改了 audio 元素的 muted 属性,根本没动轨道。嗯,这里要注意——muted 只是本地播放静音,远端该收啥还收啥。
track.enabled 的变化即可。但注意,这个属性没有事件通知,需要自己用 setter 包装一下。
7.2 视频轨道的切换
视频轨道切换比音频复杂一些。因为切换摄像头(比如从前置切到后置)时,你需要重新获取一个新的 MediaStreamTrack,然后替换掉 PeerConnection 里的旧轨道。
具体怎么做?用 RTCRtpSender.replaceTrack()。这个方法的好处是:不需要重新协商(renegotiation),切换过程非常平滑。
async function switchCamera() {
// 获取新的视频流(切换摄像头设备)
const newStream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: newDeviceId } }
});
const newVideoTrack = newStream.getVideoTracks()[0];
// 获取发送器
const sender = pc.getSenders().find(s => s.track.kind === 'video');
// 替换轨道
await sender.replaceTrack(newVideoTrack);
// 别忘了停止旧轨道,释放资源
oldVideoTrack.stop();
}
还有一种场景是「关闭摄像头」:直接把 videoTrack.enabled = false 就行。远端会收到最后一帧画面,然后冻结。如果你想显示一个「摄像头已关闭」的占位图,可以在本地用 canvas 生成一帧替换上去。
7.3 分辨率与码率动态调整
这是媒体流控制里最有技术含量的部分。说白了,就是根据网络状况动态调整视频质量。WebRTC 本身有拥塞控制机制,但很多时候我们需要手动干预,尤其是做直播或会议类应用时。
分辨率调整有两种思路:
- 重新获取流:调用 getUserMedia 时指定新的分辨率,然后 replaceTrack。
- 编码器参数调整:通过 RTCRtpSender 的
setParameters方法动态修改编码参数。
我个人更推荐第二种,因为不需要重新打开摄像头,切换更流畅。
async function adjustVideoQuality(width, height, bitrate) {
const sender = pc.getSenders().find(s => s.track.kind === 'video');
const parameters = sender.getParameters();
// 修改编码器参数
if (parameters.encodings) {
parameters.encodings[0].maxBitrate = bitrate; // 单位 bps
parameters.encodings[0].scaleResolutionDownBy =
Math.max(1, 1280 / width); // 缩放比例
}
await sender.setParameters(parameters);
}
码率调整我一般配合带宽估计来做。WebRTC 的 RTCPeerConnection 会触发 onicecandidate 和 onconnectionstatechange,但更直接的是监听 RTCStatsReport 里的带宽信息。
// 定期获取统计信息
setInterval(async () => {
const stats = await pc.getStats();
stats.forEach(report => {
if (report.type === 'candidate-pair' && report.state === 'succeeded') {
const availableBandwidth = report.availableOutgoingBitrate;
if (availableBandwidth < 500000) { // 带宽低于 500kbps
adjustVideoQuality(640, 360, 400000);
}
}
});
}, 2000);
7.4 知识体系总览
下面这张图把媒体流控制的核心逻辑串起来了。你可以看到,从轨道操作到编码调整,最终目标都是保证用户体验。
7.5 综合示例:自适应码率切换
最后,我给你一个完整的自适应码率切换示例。这个代码我在一个视频会议项目里用过,效果还不错。
class AdaptiveBitrateController {
constructor(pc) {
this.pc = pc;
this.lastAdjustTime = 0;
this.minBitrate = 200000; // 200kbps
this.maxBitrate = 2000000; // 2Mbps
}
async start() {
setInterval(() => this.checkAndAdjust(), 3000);
}
async checkAndAdjust() {
const stats = await this.pc.getStats();
let bandwidth = this.maxBitrate;
stats.forEach(report => {
if (report.type === 'candidate-pair' && report.state === 'succeeded') {
bandwidth = report.availableOutgoingBitrate || bandwidth;
}
});
// 根据带宽动态调整
let targetBitrate = Math.min(bandwidth * 0.8, this.maxBitrate);
targetBitrate = Math.max(targetBitrate, this.minBitrate);
const sender = this.pc.getSenders().find(s => s.track?.kind === 'video');
if (!sender) return;
const params = sender.getParameters();
if (params.encodings) {
params.encodings[0].maxBitrate = targetBitrate;
// 带宽低于 500kbps 时降低分辨率
if (bandwidth < 500000) {
params.encodings[0].scaleResolutionDownBy = 2;
} else {
params.encodings[0].scaleResolutionDownBy = 1;
}
await sender.setParameters(params).catch(e => {
console.warn('调整参数失败,降级处理', e);
});
}
}
}
// 使用
const controller = new AdaptiveBitrateController(pc);
controller.start();
好了,媒体流控制这块就聊到这儿。说白了就是三件事:开关轨道、切换设备、动态调参。你在实际项目中遇到最多的问题是什么?欢迎交流。