12、屏幕共享+音频:同时捕获系统音频、audio约束设置、回声消除处理

屏幕共享这件事,做到一半你会发现——光有画面不行。用户一边共享PPT,一边还要说话讲解。这时候问题就来了:系统声音要不要录?麦克风声音和系统声音混在一起怎么处理?回声怎么消除?

嗯,这一章我们就来啃这块硬骨头。我会把我在实际项目中踩过的坑、试过的方案,都摊开来讲。

12.1 同时捕获系统音频:getDisplayMedia 的 audio 参数

先看最基础的问题:怎么在共享屏幕的同时,把系统播放的声音也抓进来?

其实 getDisplayMedia 本身就支持音频捕获。你只需要在约束里把 audio 设为 true

const stream = await navigator.mediaDevices.getDisplayMedia({
  video: true,
  audio: true
});

就这么简单?嗯,理论上是的。但实际用起来,坑不少。

注意: 不是所有浏览器都支持 audio: true。Chrome 桌面端支持,但移动端 Chrome 和 Safari 目前还不支持。Firefox 部分版本也有问题。我在项目里就遇到过用户用 Safari 共享,结果音频死活没声音——排查了半天才发现是浏览器不支持。

另外,audio: true 捕获的是「系统输出音频」,也就是你电脑扬声器里正在播放的声音。不是麦克风的声音。这两个概念千万别搞混。

12.2 audio 约束的精细控制

光设个 true 太粗糙了。实际场景里,你可能想控制采样率、声道数、回声消除等参数。这时候就要用更精细的约束写法:

const stream = await navigator.mediaDevices.getDisplayMedia({
  video: true,
  audio: {
    sampleRate: 48000,
    channelCount: 2,
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true
  }
});

这里每个参数都有讲究:

参数 说明 我的建议值
sampleRate 采样率,单位 Hz 48000(音质好,兼容性高)
channelCount 声道数,1 为单声道,2 为立体声 2(屏幕共享通常有立体声需求)
echoCancellation 回声消除 true(后面细讲)
noiseSuppression 噪声抑制 true
autoGainControl 自动增益控制 true
小技巧: 我个人习惯在设置约束前,先用 getSupportedConstraints() 检查浏览器支持哪些参数。不同浏览器支持的约束项不一样,提前检查可以避免运行时出错。

12.3 回声消除:为什么需要它?

你想想看,屏幕共享的场景下,用户通常开着扬声器。系统音频从扬声器放出来,又被麦克风收进去——这就形成了回声。如果不处理,远端的人会听到自己声音的回声,体验极差。

回声消除(AEC,Acoustic Echo Cancellation)就是干这个的。它通过算法把麦克风采集到的回声信号抵消掉。

在 WebRTC 里,回声消除通常由浏览器底层实现。你只需要在约束里设置 echoCancellation: true 就行。但这里有个关键点:

核心原则: 回声消除必须在「捕获端」开启,而不是在播放端。也就是说,你要在 getDisplayMediagetUserMedia 的约束里设置,而不是在 audio 标签或 RTCPeerConnection 里设置。

我曾经在一个项目里,把回声消除加在了远端音频播放的 audio 标签上——结果完全没用。后来才意识到,AEC 必须在采集阶段就介入。

12.4 屏幕共享 + 麦克风:双音频流的合并

现在问题来了:屏幕共享捕获的是系统音频,但用户还要说话。这两个音频流怎么合并?

最直接的做法:用 MediaStreamaddTrack 方法,把两个流的音频轨道合并到一起。

// 获取屏幕共享流(含系统音频)
const displayStream = await navigator.mediaDevices.getDisplayMedia({
  video: true,
  audio: {
    echoCancellation: true,
    noiseSuppression: true
  }
});

// 获取麦克风流
const micStream = await navigator.mediaDevices.getUserMedia({
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true
  }
});

// 合并两个流
const combinedStream = new MediaStream();

// 添加屏幕共享的视频轨道
displayStream.getVideoTracks().forEach(track => {
  combinedStream.addTrack(track);
});

// 添加屏幕共享的音频轨道(系统音频)
displayStream.getAudioTracks().forEach(track => {
  combinedStream.addTrack(track);
});

// 添加麦克风的音频轨道
micStream.getAudioTracks().forEach(track => {
  combinedStream.addTrack(track);
});

合并之后,combinedStream 就包含了三个轨道:一个视频轨道、两个音频轨道。你可以直接把它传给 RTCPeerConnectionaddStreamaddTrack

注意: 两个音频轨道同时传输,远端会听到两路声音。如果系统音频和麦克风音频有重叠(比如用户说话的声音也被系统音频捕获了),就会产生「双重声音」的问题。解决办法是:在屏幕共享端,让用户戴上耳机,避免系统音频被麦克风二次采集。

12.5 更优雅的方案:使用 AudioContext 混音

如果你想要更精细的控制,可以用 AudioContext 把两路音频混成一路。这样远端只需要处理一个音频轨道,更干净。

const audioContext = new AudioContext();

// 创建两个源节点
const displaySource = audioContext.createMediaStreamSource(displayStream);
const micSource = audioContext.createMediaStreamSource(micStream);

// 创建混音节点
const merger = audioContext.createChannelMerger(2);

// 连接:显示音频 -> 混音器
displaySource.connect(merger, 0, 0);
// 连接:麦克风音频 -> 混音器
micSource.connect(merger, 0, 1);

// 创建目标流
const dest = audioContext.createMediaStreamDestination();
merger.connect(dest);

// 最终流:视频来自 displayStream,音频来自混音器
const finalStream = new MediaStream();
displayStream.getVideoTracks().forEach(track => {
  finalStream.addTrack(track);
});
dest.stream.getAudioTracks().forEach(track => {
  finalStream.addTrack(track);
});

这个方案的好处是:你可以控制每路音频的音量、延迟、甚至加特效。我在做在线教育项目时,就用这个方案实现了「老师声音优先于系统音频」的效果——当老师说话时,自动降低系统音量。

12.6 知识体系总览

下面这张图把本章的核心逻辑串起来了:

屏幕共享 + 音频:知识体系 音频捕获 • getDisplayMedia audio: true • 精细约束设置 • 采样率/声道数控制 • 浏览器兼容性检查 回声消除 (AEC) • 捕获端开启 AEC • echoCancellation: true • 噪声抑制 + 自动增益 • 避免二次采集 音频合并 • addTrack 合并流 • AudioContext 混音 • 音量/延迟控制 • 单轨道输出 最终输出流 1 路视频 + 1~2 路音频 → RTCPeerConnection 关键:AEC 必须在捕获端开启,合并流注意避免双重声音 公众号:蓝海资料掘金营 · 微信 deep3321

12.7 避坑指南

最后,分享几个我实际踩过的坑:

  • 回声消除不是万能的。 如果用户把扬声器音量开到最大,AEC 算法也扛不住。我建议在 UI 上提示用户「建议使用耳机」。
  • 两个音频轨道同时传输时, 远端可能会听到回声。因为系统音频里可能已经包含了用户的声音。解决办法:让用户戴耳机,或者用 AudioContext 混音后只传一路。
  • 采样率不匹配 会导致音频失真。如果麦克风是 44100Hz,系统音频是 48000Hz,合并前最好统一采样率。AudioContext 的 createBuffer 可以帮你做重采样。
  • 我曾经遇到过一个诡异的问题: 在 Windows 上,getDisplayMediaaudio: true 捕获到的系统音频有 2 秒延迟。后来发现是声卡驱动的问题,换了个驱动就好了。所以如果遇到延迟,先检查驱动。

嗯,这一章的内容就到这里。屏幕共享加音频,说白了就是「捕获 → 处理 → 合并 → 传输」这四个步骤。每一步都有细节,但掌握了核心逻辑,你就能应对大部分场景。