chromium中 getDisplayMedia() 无法采集8声道音频设备
文章由ai生成,但内容和问题真实,希望可以帮到你
一个自建的屏幕共享应用,getDisplayMedia() 的调用方式是这样的:
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { frameRate: { ideal: 15, max: 30 } },
audio: true,
})
在一台 Windows 11 笔记本上,行为是这样分裂的:
| 操作 | 结果 |
|---|---|
| 共享标签页,勾选音频 | ✅ 正常 |
| 共享屏幕,勾选音频 | ❌ NotReadableError,页面提示「系统拒绝了这次采集」 |
| 共享屏幕,不勾音频 | ✅ 正常 |
| 共享屏幕,把默认播放设备换成板载声卡 | ✅ 正常 |
| 再换回耳机 | ❌ 又失败 |
关键线索是第二行和第三行:失败的是音频,但赔上的是画面。
涉及的头戴设备是 Sony INZONE H9,通过 USB dongle 暴露了两个渲染端点:INZONE H9/H7 - Game 和 INZONE H9/H7 - Chat。
结论先行
环回采集(WASAPI loopback)有一个硬性要求:流的声道数和采样率必须与端点的 mix format 一致(位深和 float/PCM 可以不同)。
于是:
- 耳机把
Game端点声明成 7.1(8 声道) 设备——那是它的 360 虚拟环绕。它的 mix format 是 8 声道 / 48kHz。 - Chromium 的系统音频采集按立体声去开环回流。
- 8 ≠ 2,WASAPI 返回
AUDCLNT_E_UNSUPPORTED_FORMAT。 - Chromium 把音频初始化失败当成致命错误,把整个
getDisplayMedia请求一起拒掉——画面跟着声音一起没了。
Chat 端点和板载声卡都是 2 声道,请求格式刚好等于 mix format,所以能过。这就是「选标签页可以、选屏幕不行」的全部原因。
排查:三个被推翻的假设
假设一:驱动或声卡坏了
最直接的做法是绕开 Chromium,直接调 WASAPI 跑一遍环回序列:
// 简化后的核心序列
IAudioClient client = Activate(device, IID_IAudioClient);
IntPtr mixFormat;
client.GetMixFormat(out mixFormat);
client.Initialize(
AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
1000000, 0, mixFormat, IntPtr.Zero);
client.SetEventHandle(eventHandle);
client.Start();
在这台机器的每一个活动渲染端点上跑,包括默认的那个——全部成功。
所以设备没问题,驱动没问题,WASAPI 也没问题。假设推翻。
假设二:有别的程序占着采集通道
关掉远程控制、语音、录屏类程序,结果不变。而且这个假设本身就不成立:真被独占的话,标签页采集也会一起挂。推翻。
假设三:在自动化里复现,然后二分
这是最像样的思路,结果我被自己的测试工具骗了两次。
第一次:用 Puppeteer 驱动浏览器时,我加了 --auto-select-desktop-capture-source=Entire screen 来跳过系统的选择器。这个开关确实跳过了选择器——但也就意味着我从来没有走过用户实际走的那条路径。所有「自动化里能成功」的结论,对真实场景零信息量。
第二次,也是更隐蔽的一次:我加了 --use-fake-ui-for-media-stream 来自动同意权限。它能用,音轨也拿到了,我一度以为音频路径是好的。直到我把音轨的设置打出来:
{
"audioTracks": 1,
"label": "Fake audio",
"audioSettings": {
"deviceId": "default",
"channelCount": 1,
"echoCancellation": true,
"autoGainControl": true,
"noiseSuppression": true,
"sampleRate": 48000
}
}
label 是 "Fake audio",而且 echoCancellation / autoGainControl / noiseSuppression 全是 true、单声道——这是一个麦克风桩,不是系统环回。这个开关会把桌面采集的音频整个替换掉,所以我此前所有「带音频也成功」的测试,测的都是一个假设备。
教训比结论更值钱:**测试用的便利开关,可能恰好把你唯一想测的那条路径替换掉。**加任何「让测试跑起来更容易」的 flag 之前,先确认它没有改变被测对象。
根因:三步证据
第一步:两个端点唯一的差别是声道数
读端点自己的默认格式(注册表里的 PKEY_AudioEngine_DeviceFormat,就在 HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Render\{guid}\Properties):
INZONE ... - Game tag=0xFFFE ch=8 rate=48000 bits=16 channelMask=0x3F
INZONE ... - Chat tag=0xFFFE ch=2 rate=48000 bits=16 channelMask=0x03
Realtek tag=0xFFFE ch=2 rate=48000 bits=32 channelMask=0x03
channelMask=0x3F 是 FL|FR|FC|LFE|BL|BR|SL|SR,标准 7.1 布局。这个 8 声道是设备级声明,不是系统上混出来的——dongle 把自己注册成了一个 7.1 声卡,HRTF 下混在固件里做。
其余全部相同:同样的 WAVE_FORMAT_EXTENSIBLE、同样的 48kHz、同样的 10ms/3ms 设备周期、都不支持硬件卸载、独占模式返回同样的错误码。唯一的变量就是声道数。
第二步:把环回请求格式测全
对每个端点开环回流,只改请求的格式:
Game 端点 (mix = 8ch / 48k) Chat 端点 (mix = 2ch / 48k)
8ch 48k 32f ok 2ch 48k 32f ok
8ch 48k 16bit ok <- 位深可以不同 2ch 48k 16bit ok
8ch 44.1k UNSUPPORTED_FORMAT 2ch 44.1k UNSUPPORTED_FORMAT
2ch 48k UNSUPPORTED_FORMAT 8ch 48k UNSUPPORTED_FORMAT
6ch 48k UNSUPPORTED_FORMAT 1ch 48k UNSUPPORTED_FORMAT
1ch 48k UNSUPPORTED_FORMAT 6ch 48k UNSUPPORTED_FORMAT
以上每一个失败的,加上 AUTOCONVERTPCM 之后全部变成 ok
规则很干净:声道数和采样率必须匹配 GetMixFormat() 的返回,采样格式(位深、float/PCM)可以不同。
所以在 Game 端点上,一条环回流想成功,只有「8 声道 + 48kHz」这一条路。
第三步:结论
- 实测:Chromium 在
Game上失败;在Chat和板载上成功。 - 实测:在
Game上,只有 8 声道 / 48k 的环回能成功。 - 推论:Chromium 请求的不是 8 声道。
(这一步是推论不是实测——Chromium 具体要几声道只有它自己知道。想确认的话,在能成功的端点上开着共享,打开 chrome://media-internals,看那条 audio capture stream 的 channels 值。)
一个反直觉的 API 细节
排查过程中最容易走错的一步在这里:
// 在 8 声道的 Game 端点上问:支持 2 声道吗?
client.IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, stereoFormat, out closest);
// => S_OK
它说支持。 然后你真的去开:
client.Initialize(AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_LOOPBACK,
..., stereoFormat, ...);
// => 0x88890008 AUDCLNT_E_UNSUPPORTED_FORMAT
它拒绝。
原因是 IsFormatSupported 在 shared 模式下回答的是「引擎可以做格式转换」,而环回流在没有显式带上 AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM 时根本不走转换路径。
先答应、后拒绝——这就是为什么这个故障看起来像设备坏了,而不是像格式不匹配。任何只靠 IsFormatSupported 做能力判断的代码,都会在这里翻车。
为什么「标签页可以、屏幕不行」
因为这是两条完全不同的音频路径:
| 采集来源 | 音频从哪来 | 碰声卡吗 |
|---|---|---|
| 标签页 | 标签页自己的音频流 | 不碰 |
| 屏幕 / 窗口 | 默认输出设备的 WASAPI 环回 | 碰 |
这个不对称本身就是最好的诊断入口。当你看到「某个来源可以、另一个来源不行」时,去找两者在系统层面的差异,而不是在应用代码里找。
顺带一提,为什么音频失败会连画面一起赔进去:getDisplayMedia 里视频和音频是一次请求,音频初始化失败会以 NotReadableError 拒绝整个 Promise,视频轨道也就拿不到。对使用者来说,他看到的是「屏幕共享用不了」,而不是「声音用不了」。
责任归属
把这条链理清楚,就知道该去哪里提 issue:
- 不是设备:同一个端点,请求 8 声道就能采。
- 不是 WASAPI:加上
AUTOCONVERTPCM | SRC_DEFAULT_QUALITY,2 声道的请求也能在 8 声道端点上成功。 - 是 Chromium:它既不退回端点的 mix format 再做下混,也不带那两个转换标志。这两件事它做任何一件,这个端点今天就能用。
所以准确的说法不是「Chromium 在 8 声道设备上采不到声音」这种普遍结论,而是:**当前版本的 Chromium 请求固定声道数,遇到声道数不匹配的端点会硬失败。**规则层面的东西(环回必须匹配声道数和采样率)是 WASAPI 的既定行为,Chromium 改了请求方式就会好。
怎么办
在没有改 Chromium 的前提下,实际可用的路子:
一、用一个 2 声道的端点。 同一个 dongle 上的 Chat 端点就是 2 声道 / 48kHz 的完整渲染端点——同一个耳机、同样的音质,代价只是这个端点没有 360 虚拟环绕。这是成本最低的解法。
二、把默认播放设备设成板载声卡。 有效,但你就听不到耳机了(除非用下面第三条)。
三、在中间插一层虚拟声卡。 Voicemeeter / VB-Cable 这类工具可以让 Chromium 环回一个 2 声道的虚拟设备,再把它转发到耳机:
所有程序 -> CABLE Input(虚拟,设为默认)
|
v
Chromium 环回这里 <- 普通 2 声道端点,能采
|
v
CABLE Output -> 转发 -> INZONE 耳机 <- 你听这里
代价是多一个虚拟设备、多一点点延迟;收益是「戴着耳机听 7.1」和「共享系统声音」可以同时成立。
四、如果代码是你自己的,兜一下。 既然音频失败会毁掉整个请求,那就别让它毁掉:
- 捕获失败且错误是
NotReadableError时,自动关掉「共享系统声音」,并在界面上说明; - 把「重试」按钮的名字改准确(「重试(只共享画面)」);
- 把这个选择记住,别让用户每次先失败一遍;
- 共享中若确实没有音轨,状态行要明确写出来——否则是等到观众问「怎么没声音」才发现。
对一个屏幕共享工具来说,「有画面但没声音」几乎总是比「什么都没有」好。
30 秒检查你自己的机器
如果哪天遇到「共享屏幕没声音、共享标签页正常」,先看默认播放设备有几个声道:
图形界面:设置 → 系统 → 声音 → 选中当前默认输出设备 → 属性 → 高级 → 看「默认格式」是不是 7.1 / 5.1 环绕。是的话,换一个 2 声道的格式或换一个端点。
命令行(读端点自己声明的格式):
Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Render' |
ForEach-Object {
$state = (Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue).DeviceState
if ($state -band 1) { # 1 = ACTIVE
$p = Get-ItemProperty (Join-Path $_.PSPath 'Properties') -ErrorAction SilentlyContinue
# 名字(扬声器/耳机)几乎都一样,能区分设备的是描述
$name = $p.'{a45c254e-df1c-4efd-8020-67d146a850e0},2'
$desc = $p.'{b3f8fa53-0004-438e-9003-51a46e139bfc},6'
$blob = $p.'{f19f064d-082c-4e27-bc73-6882a1bb8e4c},0' # PKEY_AudioEngine_DeviceFormat
if ($blob -and $blob.Length -ge 26) {
# 前 8 字节是属性框架,WAVEFORMATEX 从偏移 8 开始
$ch = [BitConverter]::ToUInt16($blob, 10)
$rate = [BitConverter]::ToUInt32($blob, 12)
$mark = if ($ch -gt 2) { ' <-- 嫌疑' } else { '' }
Write-Host ($name + ' [' + $desc + '] -> ' + $ch + ' ch @ ' + $rate + ' Hz' + $mark)
}
}
}
声道数大于 2 的那个,就是嫌疑对象。
带走的东西
getDisplayMedia里音频失败会杀死画面。 排查时不要把「没画面」当成画面问题——反过来,也不要把「没声音」当成小事。- WASAPI 环回必须匹配端点的声道数和采样率。 位深和 float/PCM 可以不同,声道数和采样率不行。
IsFormatSupported返回S_OK不代表Initialize会成功。 在 shared 模式下它回答的是「引擎能不能转换」,而环回流默认不走转换路径。- 标签页音频和屏幕音频是两条路径。 「一个来源行、另一个不行」是系统层差异的信号,不是应用层 bug 的信号。
- 测试里的便利开关会骗你。
--auto-select-desktop-capture-source让我绕过了真实的交互路径,--use-fake-ui-for-media-stream把一个假麦克风塞进了我想测的位置。在自动化里「跑通了」之前,先确认你测的是你想测的东西——把返回值打出来看,而不是看它没报错。
当然这仅仅是chromium的问题,因为safari和firefox根本就没有支持getdisplaymedia的音频采集,自然不会遇上这个问题
评论
评论加载中…