自动化浏览器中的 Widevine:EME 为何失败,如何检测
说明 Playwright 启动的 Chrome 为什么对 requestMediaKeySystemAccess 返回 NotSupportedError、Widevine 许可请求如何工作,并提供检测 DRM 就绪情况的脚本。
BotBrowser Team
BotBrowser 在你的构建具备相应授权和内容解密模块时,可以让 Widevine 能力报告与配置文件保持一致,并按上下文应用许可质询策略。它不能改变许可服务器的决定,也不能单靠自身让受保护内容可以播放。
TL;DR
只有当 Encrypted Media Extensions API 能够访问内容解密模块(CDM)时,浏览器才能播放受 Widevine 保护的视频。在 Playwright 1.58.2 驱动 Google Chrome 155 的一次测量中,默认启动对每一种 com.widevine.alpha 配置都返回 NotSupportedError,无头与有头模式结果相同。去掉一个默认参数 --disable-component-update 之后,同一个 Chrome 就报告支持。在排查许可服务器之前,先用一个简短的探测脚本检查可用性,并把每个结果理解为关于浏览器的事实,而不是关于内容提供方的事实。
内容
CDM 与许可请求做了什么
受保护的流媒体把工作分成三部分。播放器获取加密的媒体分段。许可服务器决定这个用户和这台设备能否解密。运行在浏览器内部的内容解密模块持有密钥并完成解密,页面始终看不到密钥。Encrypted Media Extensions(EME)规范就是页面与该模块之间的接口。页面按名称请求一个密钥系统,例如 com.widevine.alpha,并描述想要播放的媒体。浏览器返回一个 MediaKeySystemAccess 对象,或者拒绝这次请求。
请求成功后,页面创建 MediaKeys,打开一个 MediaKeySession,并把加密流的初始化数据交给模块。模块通过类型为 license-request 的 message 事件作出响应。页面把这条不透明的消息发给自己的许可服务器,收到响应后用 session.update() 交还给模块。如果服务器批准了请求,会话会通过 keystatuseschange 报告可用的密钥,随后开始播放。页面自己从不解读这条消息,它是模块与许可服务器之间的对话,由应用代码中转。
排查时有两点很重要。第一,许可请求只有在 requestMediaKeySystemAccess 成功之后才存在,所以在第一步就被拒绝,意味着许可服务器根本没有被联系。第二,请求绑定在模块和设备上:服务器只能根据模块对自己的描述做决定,因此同一个网站对两个都装有模块的浏览器可能给出不同的回答。
Widevine 还定义了安全级别。级别 3 在软件中完成解密,级别 2 和 1 在不同程度上依赖可信硬件。在 EME 中,这体现为请求能力里的 robustness 字符串,例如 SW_SECURE_CRYPTO、SW_SECURE_DECODE 或 HW_SECURE_ALL。网站会按分辨率选择自己需要的最高级别;只支持软件级别的浏览器可能只能得到较低分辨率,或者被高级内容拒绝。这是内容提供方的产品决定,在下面的探测脚本里,它会以 robustness 结果的形式呈现给你。
robustness 结果描述的是模块在当前环境里愿意协商什么,并不承诺某个具体片源一定能播放。提供方会按片源、地区、账号和设备类别应用自己的规则,探测脚本看不到这些规则。反过来,不支持硬件级别也不是你的环境有缺陷。许多桌面环境本来就没有硬件安全通路,只要提供方在软件级别提供普通内容,就能正常工作。
默认的自动化启动为什么报告不支持
模块并不只是浏览器目录里的一个文件:在这次测量中,它是否可用取决于组件更新器。自动化框架通常会用一组默认参数启动浏览器,目的是让运行安静且可重复。Playwright 为 Chromium 准备的默认参数里包含 --disable-component-update,它让浏览器不要运行组件更新器。对于不希望出现后台网络活动的测试运行器来说,这是合理的默认值。但它有一个容易被忽略的副作用:在这次测量中,用这种方式启动的全新配置目录报告不支持 Widevine。
先检查二进制,再检查参数
只有二进制本身能够携带模块,启动参数才有意义。开源的 Chromium 构建通常不包含 Widevine 模块,Google Chrome 包含。如果测试套件启动的是内置的 Chromium 构建,无论传什么参数都很可能报告不支持(本次测量只覆盖了已安装的 Chrome),此时的解决办法是改为启动已安装的 Chrome。Playwright 通过 channel 选项提供这一能力,其他驱动则接受可执行文件路径或等价设置。请在运行开始时打印产品名称和版本,不要把二进制当作一个没有说明的假设。
临时配置目录还会带来一个因素。它每次运行都是全新的,因此依赖首次运行注册的内容每次都要重复,来自长期使用的桌面配置的结果,并不能说明每个任务都新建目录的 CI 容器会怎样。请用同类对同类:新配置对新配置,持久配置对持久配置。
测量
下面的测量隔离了这个参数。测量于 2026-10-14 在 macOS 上用 Google Chrome 155.0.8059.80 完成,由 Playwright 1.58.2 驱动,加载一个普通的 HTTPS 页面,并以单个 H.264 能力调用 navigator.requestMediaKeySystemAccess。每一行都是一次独立启动,使用新的临时配置目录。
| 启动方式 | 模式 | com.widevine.alpha 的结果 |
|---|---|---|
| Playwright 默认参数 | 无头 | 每种配置都是 NotSupportedError |
| Playwright 默认参数 | 有头 | 每种配置都是 NotSupportedError |
去掉 --disable-component-update | 无头 | 默认、SW_SECURE_CRYPTO、SW_SECURE_DECODE 受支持;HW_SECURE_ALL 不支持 |
去掉 --disable-component-update | 有头 | 与无头相同 |
| 普通 Chrome,全新配置,无框架 | 有头 | 默认、SW_SECURE_CRYPTO、SW_SECURE_DECODE 受支持;HW_SECURE_ALL 不支持 |
由此得到两点观察。在这次测量中,结果与无头或有头无关,因此把 DRM 缺失归咎于无头模式的说法,至少对这个 Chrome 版本是不完整的。另外,即使软件级别可用,这台机器上的硬件级别仍被报告为不支持,这正是没有硬件安全通路的桌面浏览器应有的样子,不要把它当成配置失败。 关于无头与有头启动如何对应同一个 Profile,请参阅 Headless 与 Headed 浏览器 Profile 一致性,其中讲解了如何让两种模式保持一致。
测量展示的是一种效果,不是内部原因。Chrome 的组件系统是 Chrome 自己的,不同版本或不同操作系统的表现可能不同。可以依赖的是方法:只改一个启动参数,运行同一个探测脚本,然后比较。
在自己的测试套件中复现对比
使用 Playwright 时,通过 ignoreDefaultArgs 去掉这个参数。两次运行之间保持其他一切相同,包括页面来源和配置类型,让这个参数成为唯一的差别。
import { chromium } from 'playwright';
const variants = {
playwrightDefaults: {},
componentUpdateEnabled: { ignoreDefaultArgs: ['--disable-component-update'] },
};
for (const [label, options] of Object.entries(variants)) {
const browser = await chromium.launch({ channel: 'chrome', headless: true, ...options });
const page = await browser.newPage();
await page.goto('https://example.com/');
const supported = await page.evaluate(() =>
navigator
.requestMediaKeySystemAccess('com.widevine.alpha', [
{
initDataTypes: ['cenc'],
videoCapabilities: [{ contentType: 'video/mp4; codecs="avc1.42E01E"' }],
},
])
.then(
() => true,
() => false
)
);
console.log(label, supported);
await browser.close();
}
重新启用组件更新器是有代价的。运行期间浏览器可能联系更新服务器,这在离线的 CI 网络里或者会断言出站请求的套件里会造成问题。如果两次执行之间组件版本发生变化,运行结果的可重复性也会下降。请先决定你的套件是否真的需要真实的 DRM 播放。如果只有少数几条用户旅程需要,就只为这些项目启用,其余保持安静的默认值,并在每个结果旁边记录浏览器构建,让两次运行之间的差异有明确的解释。
这次测量的局限
这次测量只覆盖一个浏览器构建、一个操作系统和一个框架版本。它只测试了 com.widevine.alpha 密钥系统和一种 H.264 能力。它没有播放内容,没有联系许可服务器,也没有测试 Puppeteer、Selenium 或其他驱动,这些驱动启动浏览器时的默认值可能不同。它同样没有测试 BotBrowser 本身;本文中关于 BotBrowser 的陈述来自它的公开文档(见文末链接),应当对照你实际使用的构建和授权再次确认。 关于用 Selenium 而不是 Playwright 驱动浏览器 Profile,请参阅 Selenium 浏览器 Profile 集成与会话归属,其中讲解了会话如何归属与启动。
浏览器更新会改变这些行为。Chrome 可能改变模块的注册方式或者哪些启动参数会影响它,Playwright 也可能改变默认参数列表。升级其中任何一个之后都要重新运行探测脚本,并把脚本和输出与测试记录一起保存,这样变化会以差异的形式出现,而不是以意外的形式出现。
可运行的探测脚本与结果解读
下面的脚本会请求默认配置和三个 robustness 级别,以及一种持久许可会话,并输出每一项的结果。它只使用标准的 EME 调用,可以在任何通过 HTTPS 提供的页面上运行,也不会联系许可服务器。在责怪流媒体网站之前,先在你自己的自动化环境里运行它。
const configs = {
default: { robustness: undefined, sessionTypes: ['temporary'] },
SW_SECURE_CRYPTO: { robustness: 'SW_SECURE_CRYPTO', sessionTypes: ['temporary'] },
SW_SECURE_DECODE: { robustness: 'SW_SECURE_DECODE', sessionTypes: ['temporary'] },
HW_SECURE_ALL: { robustness: 'HW_SECURE_ALL', sessionTypes: ['temporary'] },
persistent: { robustness: undefined, sessionTypes: ['persistent-license'] },
};
const results = {};
for (const [name, cfg] of Object.entries(configs)) {
const capability = { contentType: 'video/mp4; codecs="avc1.42E01E"' };
if (cfg.robustness) capability.robustness = cfg.robustness;
try {
const access = await navigator.requestMediaKeySystemAccess('com.widevine.alpha', [
{
initDataTypes: ['cenc'],
sessionTypes: cfg.sessionTypes,
videoCapabilities: [capability],
},
]);
results[name] = `supported (${access.getConfiguration().videoCapabilities?.[0]?.robustness || 'default'})`;
} catch (error) {
results[name] = error.name;
}
}
console.table(results);
在已注册模块的浏览器上,预期 default 行和至少一个软件级别报告受支持。在没有模块的浏览器上,每一行都报告 NotSupportedError。上面的测量没有测试持久许可会话;如果 persistent 行失败而其他行通过,请先与同一构建的普通启动对比,再下结论。请在多次运行之间保持探测输入不变:相同的页面来源、相同的编解码器字符串、相同的 robustness 列表。如果同时改动两件事,不同的结果就有两个可能原因,你无法知道是哪一个起了作用。
可以把结果当作一棵简短的决策树。
- 每一行都是
NotSupportedError。 浏览器没有为这些能力暴露该密钥系统。检查浏览器是如何启动的、传入了哪些参数。与同一构建的普通启动和全新配置目录做对比。如果普通启动可用而自动化启动不可用,差别在启动参数或配置目录,而不在页面。 TypeError或SecurityError。TypeError表示页面不处于安全上下文,因此该方法不存在;SecurityError表示权限策略阻止了该 API。请通过 HTTPS 或localhost提供页面,并检查嵌入框架的allow属性。- 默认行通过,更高的 robustness 行失败。 模块存在,但只提供较低级别。需要更高级别的提供方会拒绝或降级,这是对方的策略决定。请记录你的环境支持的最高级别,并与提供方的文档对照。
- 请求通过,但许可调用失败。 浏览器已经完成了它的部分:
license-request消息已生成,问题出在服务器如何处理它。HTTP 错误、被拒绝的账号、地区限制或过期的授权,都属于提供方一侧的条件。请记录 HTTP 状态和提供方的错误码,而不是那条不透明的许可消息本身,它对人来说没有诊断价值。 - 播放开始后又停止。 查看会话上的
keystatuseschange。密钥可能变成expired、output-restricted或internal-error。尤其是输出限制,涉及的是显示通路,例如虚拟显示器无法满足的受保护输出要求,这同样是关于环境的陈述。
CI 任务检查清单
- 固定浏览器构建和框架版本,并在任务开始时打印两者。
- 先运行探测脚本,并在默认行不受支持时用清晰的信息立即失败,避免把模块缺失误报为流媒体网站的故障。
- 把探测结果表作为任务产物,与浏览器版本放在一起保存。
- 把可用性检查与播放检查分开。可用性检查不需要账号,也不需要访问提供方的网络。播放检查需要有授权的测试账号,应放在单独且有明确标签的任务里。
- 不要把许可消息、令牌和 Cookie 写进日志或产物,改为记录状态码和错误名称。
- 每次升级浏览器或框架之后重新运行探测脚本,把结果表的变化当作需要评审的事项,而不是噪声。
有两个习惯可以让这项工作保持可重复。把浏览器构建和操作系统记录在每个结果旁边,因为行为取决于两者。并且用你有权播放的内容来测试,有测试素材时优先使用提供方自己的测试素材;在正式片源上失败,能说明的问题少于在为诊断设计的片源上失败。
BotBrowser 能做什么与不能做什么
BotBrowser 的文档把 Widevine 支持描述为一项有明确限制的可选能力。Widevine 集成需要 ENT Tier2 授权,文档说明了模块如何随具体构建和授权提供;请对照你所用版本的指南,因为本文并不假定模块如何到达你的构建。当配置文件启用该能力时,BotBrowser 报告的 DRM 能力结果与配置文件的平台身份保持一致:Windows 配置文件报告符合 Windows 的行为,Android 配置文件报告符合 Android 的行为。 关于如何判断 Profile 在不同操作系统上是否表现一致,请参阅 部署前如何评估跨平台浏览器一致性,其中列出了上线前应进行的检查。
对于许可质询,--bot-widevine 选项按上下文选择策略:stub 是由配置文件支撑的默认值,send 在受支持时允许正常的质询,block 抑制许可请求,no-cdm 为该上下文关闭模块暴露。该策略取决于授权和配置文件,并且必须在第一个页面或 Worker 启动之前应用。
这四个取值与测试目标的对应关系如下。
| 目标 | 策略 | 文档对它的说明 |
|---|---|---|
| 让能力报告与配置文件保持一致 | stub | 许可质询的、由配置文件支撑的默认策略 |
| 用有授权的账号走通真实许可流程 | send | 在受支持时允许正常的质询 |
| 确认许可失败时页面能够恢复 | block | 抑制许可请求,让页面的错误路径运行 |
| 确认页面能处理没有 DRM 的浏览器 | no-cdm | 为该上下文关闭模块暴露 |
请按它本来的意思理解这份列表。它让团队可以把 DRM 行为变成测试上下文中一个明确、可重复的属性,而不是启动参数带来的偶然结果。它不会让缺失的模块出现,不会改变许可服务器的决定,也不会让账号获得其无权播放的受保护内容。如果探测脚本报告 NotSupportedError,而配置文件策略是 send,下一个问题仍然是:这个浏览器构建上是否安装并注册了模块。
这里的主题是可用性诊断。它不描述如何提取密钥、截取解密后的流或者破解提供方的保护,BotBrowser 的文档也没有这样的内容。内容保护是权利人与观看者之间的约定;测试环境存在的目的,是确认合法用户的设置能正常工作,而不是去除保护。
结论
自动化浏览器里的 Widevine 播放问题,可能是在第一次 EME 调用就决定的“有或没有”的问题,远在许可服务器介入之前。在这次测量里,一个禁用组件更新的默认参数,就是“不支持”与“软件级别支持”之间的全部差别,无头与有头模式都是如此。运行探测脚本,一次只改一个启动变量,把每个结果理解为关于浏览器的事实,然后再去调试提供方的许可流程。BotBrowser 可以在你的构建和授权包含 Widevine 支持的前提下,让 DRM 能力与许可质询行为成为配置文件中可控的一部分;它不能替代授权,也不能替代许可服务器的决定。
延伸阅读
继续阅读 BotBrowser 的 Widevine DRM 设置了解配置方面,阅读 作为指纹信号的 DRM 能力了解为什么能力报告必须保持一致,阅读 MIME 与编解码器支持信号了解媒体能力检查中的编解码器部分。