> 技术文档 > b 站 drm 视频流程简析_b站不支持drm播放的原因

b 站 drm 视频流程简析_b站不支持drm播放的原因


bilibili DRM 视频简析

TL;DR

本文仅讲述 bilibili DRM 视频解密密钥的获取与整体流程简介,不包含任何实现与算法分析

DRM 在浏览器上的实现(该部分内容均为 AI 编写)

浏览器角色: 现代浏览器 (如 Chrome, Firefox, Edge) 都内置了“加密媒体扩展” (Encrypted Media Extensions, EME)。EME 是一个 W3C 标准的 API,它允许网页通过 JavaScript 与浏览器的内容解密模块 (Content Decryption Module, CDM) 进行通信,而无需了解解密过程本身。
CDM (内容解密模块): 这是浏览器中真正负责解密的部分。每个浏览器或操作系统都有自己的 CDM,用于处理特定的 DRM 系统。例如:
Google Widevine: 通常用于 Chrome 浏览器和 Android 设备。
Apple FairPlay: 用于 Safari 浏览器和 iOS/macOS 设备。
Microsoft PlayReady: 用于 Edge 浏览器和 Windows 设备。

这里我们只需要了解 DRM 用到了 EME 和 CDM 模块即可

原生播放的两个核心步骤

对于 DASH 这种自适应流媒体,你不能像播放普通 MP4 一样直接设置 video.src = \"video.mp4\"。你必须:

  1. 手动加载和解析 MPD 文件:用 fetch 获取 .mpd 清单文件,然后用 JavaScript 解析这个 XML 文件,找出视频的编码信息、码率、初始化片段和媒体片段的 URL。
  2. 通过 MSE 动态提供媒体数据
    • 创建一个 MediaSource 对象。
    • 将这个对象的 URL 赋给  元素的 src。
    • 在 MediaSource 对象上创建一个 SourceBuffer
    • 手动下载视频的初始化片段和后续的媒体片段,然后按顺序将它们 appendBuffer() 到 SourceBuffer 中。
  3. 通过 EME 处理加密:在向 SourceBuffer 推送加密片段时,浏览器会触发事件,此时你需要启动 EME 流程来获取解密密钥。

EME 手动解密流程详解

这是不使用库时最复杂的部分。整个过程是一个精确的“握手”协议:

  1. 选择 DRM 系统:
    • 使用 navigator.requestMediaKeySystemAccess(\'com.widevine.alpha\', ...) 来询问浏览器是否支持 Widevine。
  2. 创建 MediaKeys:
    • 如果支持,就调用 createMediaKeys() 来创建一个 MediaKeys 对象。这个对象代表了浏览器中的 CDM(内容解密模块)。
  3. 关联到 Video 元素:
    • 调用 video.setMediaKeys(mediaKeys),将这个 MediaKeys 对象与你的  元素关联起来。
  4. 监听加密事件:
    • 当你通过 MSE 的 appendBuffer() 向 SourceBuffer 推送了一个加密的媒体片段后,MediaSource 会触发一个 \'encrypted\' 事件。
  5. 生成许可证请求:
    • 在这个事件的处理器中,你会得到 initData(初始化数据,通常来自 MPD 或媒体片段的 \'moov\'/\'moof\' 盒子)。
    • 使用 mediaKeys.createSession() 创建一个许可证会话 (MediaKeySession)。
    • 调用 session.generateRequest(\'cenc\', initData) 来生成一个许可证请求。
  6. 发送请求到许可证服务器:
    • generateRequest 是一个 Promise,完成后,session 对象会触发一个 \'message\' 事件。
    • 这个事件的 message 属性就是需要发送给 Widevine 许可证服务器的请求体 (Challenge)
    • 使用 fetch 将这个请求体(通常是二进制的 ArrayBuffer)POST 到你的许可证服务器 URL。
  7. 提供许可证给 CDM:
    • fetch 请求会收到许可证服务器返回的许可证 (License)
    • 调用 session.update(license),将这个许可证交给 MediaKeySession。
  8. 解密和播放:
    • 一旦 CDM 成功处理了你提供的许可证,它就拥有了解密密钥。
    • 现在,当后续的加密媒体片段被推送到 SourceBuffer 时,CDM 就可以在后台自动解密它们,然后视频就能正常播放了。
接口/方法/事件 作用 navigator.requestMediaKeySystemAccess() 入口函数。询问浏览器是否支持特定的 DRM 系统(如 \'com.widevine.alpha\')和视频格式。这是所有操作的第一步。 MediaKeySystemAccess.createMediaKeys() 如果上一步成功,调用此方法来创建一个 MediaKeys 对象,这个对象就代表了 CDM 的一个实例。 video.setMediaKeys() 将创建好的 MediaKeys 对象与你的  mediaKeys.createSession() 创建一个 MediaKeySession 对象。一个会话代表一次完整的许可证获取流程。 video.addEventListener(\'encrypted\', ...) 关键事件监听器。当 MSE 收到加密数据时, session.generateRequest() 在 \'encrypted\' 事件处理器中调用。它告诉 CDM:“根据我给你的初始化数据 (initData),请生成一个许可证请求(Challenge)。” session.addEventListener(\'message\', ...) 关键事件监听器。当 CDM 成功生成许可证请求后,会触发这个事件。事件的 message 属性就是需要发送给许可证服务器的二进制数据。 fetch() 使用标准的 fetch API,将从 \'message\' 事件中获取的请求体 POST 到你的许可证服务器 URL。 session.update() 关键方法。当你从许可证服务器那里 fetch 到了许可证 (License) 之后,调用这个方法,将许可证交给 CDM。CDM 处理后,就拥有了真正的解密密钥。握手至此成功

请求列表

  • https://api.bilibili.com/pugv/player/web/playurl?avid=…&drm_tech_type=2

    视频元信息

  • https://bvc-drm.bilivideo.com/cer/bilibili_certificate.bin

  • https://bvc-drm.bilivideo.com/bili_widevine

  • https://api.bilibili.com/pugv/player/web/playurl?avid=…

    注意这个和上面那个用途不一样

  • https://s1.hdslb.com/bfs/static/player/main/widgets/npd.drm_sdk.7d8e1e5f.js

    CKC 解密模块

  • https://bvc-drm.bilivideo.com/cer/bilidrm_pub.key

    DRM 公钥文件

  • https://bvc-drm.bilivideo.com/bilidrm

    CKC 数据


playurl 的请求里可以发现多出了一些奇怪的字段 drm_type widevine_pssh

分析

widevine_pssh(playurl&drm_tech_type=2)

加载请求中的 widevine_pssh 数据到每个音频 / 视频流数据上

该数据将作为 KeySystemInitData

其中 KeySystem 可能是其中的一个(根据环境变化)

this.CLEARKEY_KEYSTEM_STRING = \"org.w3.clearkey\",this.WIDEVINE_KEYSTEM_STRING = \"com.widevine.alpha\",this.PLAYREADY_KEYSTEM_STRING = \"com.microsoft.playready\"

通过以下方式解码,注意该处会先调用所有可用的 KeySystem 进行 InitData(测试哪些可用)

这里可以把所有的 KeySystem 都断点

bilibili_certificate.bin

在请求结果处理函数中,我们注意到有一个 setMpdBody 的函数,很明显是用于设定 Mpd 元信息的函数

跟踪进去

attachExternal 调用 attachSourceProxy

attachSourceProxy 内,判断视频流的格式,然后第一次进入该函数会请求 bilibili_certificate.bin 也就是证书文件

获取到证书文件后 base64 作为 serverCertificate

bili_widevine,playurl

然后 POST 请求 bili_widevine 接口得到一个奇怪的字符串(请求数据 TODO )

在请求完成回调处断点,我们会得到这样的东西

接着他会进入 updateKeySession 函数,该函数就是更新 kid 与 key 的地方

只不过传入以上数据它并不是合法的,所以我们会等到第二次调用这个函数,第二次调用才会传入正确值

抛出错误后他会再进行一次请求,不过这次请求的接口是 playurl ,请求函数见上

接着它就能获取到 kid (从 playurl 接口得到),注意对比两次的 playurl 请求会发现第一次请求是没有 bilidrm_uri 而只有 widevine_pssh 的,第二次则相反,因为第二次请求没有加 &drm_tech_type=2 的参数

该值是获取 bilidrm_uri 的最后一个 // 后的文本得出的,而该值从 得出

npd.drm_sdk.7d8e1e5f.js,bilidrm_pub.key,bilidrm

最后获取 key 的函数均在 getKeyDetail 内,比较简单,不做分析

最后我们能够得到

{ \"osStatus\": 0, \"iv\": \"pIEmSahBQLWILQEUa+yFEw==\", \"key\": \"c6xChuWnTweKvL8/j0Cm8A==\", \"protectionData\": { \"org.w3.clearkey\": { \"clearkeys\": { \"2PZrk9soSYS05_xQ1xJ4_w\": \"c6xChuWnTweKvL8_j0Cm8A\" }, \"priority\": 0 } }}

使用 clearkeys 解密即可

测试

使用 bbdown 下载视频流和音频流(注意不要合并)

1

BBDown.exe https://www.bilibili.com/cheese/play/ep1302284 --skip-mux

kid 和 key 解码 base64 (注意是 url safe )

1

d8f66b93db284984b4e7fc50d71278ff:73ac4286e5a74f078abcbf3f8f40a6f0

1

2

mp4decrypt.exe --key d8f66b93db284984b4e7fc50d71278ff:73ac4286e5a74f078abcbf3f8f40a6f0 \"voice.m4a\" \"voice_.m4a\"

mp4decrypt.exe --key d8f66b93db284984b4e7fc50d71278ff:73ac4286e5a74f078abcbf3f8f40a6f0 \"video.mp4\" \"video_.mp4\"

1

ffmpeg.exe -i voice_.m4a -i video_.mp4 -c:v copy -c:a copy output.mp4