7、媒体流控制:音频轨道的启用/禁用、视频轨道的切换、分辨率与码率动态调整

说到媒体流控制,这其实是WebRTC开发中最常打交道的一块。你想想看,用户可能随时想关掉麦克风、切换摄像头,或者网络一卡就需要降低画质——这些操作,本质上都是在操作媒体轨道。

我个人习惯把媒体流控制分成三个层次:轨道级别的开关、设备级别的切换、编码参数级别的调整。咱们一个一个来聊。

7.1 音频轨道的启用与禁用

先说最简单的。音频轨道的启用和禁用,说白了就是控制麦克风的开关。在WebRTC里,每个轨道都有一个 enabled 属性。设为 false 时,远端会收到静音帧,但连接本身不会断开。

核心要点:enabled 控制的是「发送端是否发送有效数据」,而不是「接收端是否播放」。所以本地静音和远端静音是两回事。
// 获取本地音频轨道
const audioTrack = localStream.getAudioTracks()[0];

// 禁用音频(静音)
audioTrack.enabled = false;

// 启用音频(取消静音)
audioTrack.enabled = true;

我在项目中遇到过一个问题:用户点了静音按钮,但远端还是能听到声音。排查了半天,发现是代码里只改了 audio 元素的 muted 属性,根本没动轨道。嗯,这里要注意——muted 只是本地播放静音,远端该收啥还收啥。

小技巧:如果你想在UI上显示「麦克风已关闭」的图标,监听 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();
}
注意:replaceTrack 之后,旧的轨道一定要调用 stop() 方法,否则摄像头灯还会亮着。我曾经因为这个被用户投诉「为什么我切了摄像头,原来的灯还亮着?」——其实就是忘了 stop。

还有一种场景是「关闭摄像头」:直接把 videoTrack.enabled = false 就行。远端会收到最后一帧画面,然后冻结。如果你想显示一个「摄像头已关闭」的占位图,可以在本地用 canvas 生成一帧替换上去。

7.3 分辨率与码率动态调整

这是媒体流控制里最有技术含量的部分。说白了,就是根据网络状况动态调整视频质量。WebRTC 本身有拥塞控制机制,但很多时候我们需要手动干预,尤其是做直播或会议类应用时。

分辨率调整有两种思路:

  1. 重新获取流:调用 getUserMedia 时指定新的分辨率,然后 replaceTrack。
  2. 编码器参数调整:通过 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);
}
关键点:scaleResolutionDownBy 是相对于原始分辨率的缩放比例。比如原始是 1280x720,你想降到 640x360,就设为 2。maxBitrate 控制码率上限,单位是 bps。

码率调整我一般配合带宽估计来做。WebRTC 的 RTCPeerConnection 会触发 onicecandidateonconnectionstatechange,但更直接的是监听 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);
踩坑提醒:setParameters 不是所有浏览器都支持完全一致。Chrome 支持得很好,但 Safari 上可能会有兼容性问题。我建议在生产环境中加一个 try-catch,降级到重新获取流的方式。

7.4 知识体系总览

下面这张图把媒体流控制的核心逻辑串起来了。你可以看到,从轨道操作到编码调整,最终目标都是保证用户体验。

媒体流控制知识体系 音频轨道 enabled = true/false 视频轨道 replaceTrack / enabled 编码参数 setParameters 静音 / 取消静音 切换设备 / 关闭摄像头 分辨率 / 码率调整 RTCRtpSender getUserMedia RTCStatsReport 保证实时通信的用户体验

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();
经验之谈:不要频繁调用 setParameters,建议间隔至少 2-3 秒。另外,调整码率时留出 20% 的余量,避免因为网络抖动导致频繁切换。我一般用带宽的 80% 作为目标码率。

好了,媒体流控制这块就聊到这儿。说白了就是三件事:开关轨道、切换设备、动态调参。你在实际项目中遇到最多的问题是什么?欢迎交流。

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