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\"
。你必须:
- 手动加载和解析 MPD 文件:用
fetch
获取.mpd
清单文件,然后用 JavaScript 解析这个 XML 文件,找出视频的编码信息、码率、初始化片段和媒体片段的 URL。 - 通过 MSE 动态提供媒体数据:
- 创建一个
MediaSource
对象。 - 将这个对象的 URL 赋给
元素的 src。
- 在
MediaSource
对象上创建一个SourceBuffer
。 - 手动下载视频的初始化片段和后续的媒体片段,然后按顺序将它们
appendBuffer()
到SourceBuffer
中。
- 创建一个
- 通过 EME 处理加密:在向
SourceBuffer
推送加密片段时,浏览器会触发事件,此时你需要启动 EME 流程来获取解密密钥。
EME 手动解密流程详解
这是不使用库时最复杂的部分。整个过程是一个精确的“握手”协议:
- 选择 DRM 系统:
- 使用
navigator.requestMediaKeySystemAccess(\'com.widevine.alpha\', ...)
来询问浏览器是否支持 Widevine。
- 使用
- 创建 MediaKeys:
- 如果支持,就调用
createMediaKeys()
来创建一个 MediaKeys 对象。这个对象代表了浏览器中的 CDM(内容解密模块)。
- 如果支持,就调用
- 关联到 Video 元素:
- 调用
video.setMediaKeys(mediaKeys)
,将这个 MediaKeys 对象与你的元素关联起来。
- 调用
- 监听加密事件:
- 当你通过 MSE 的
appendBuffer()
向SourceBuffer
推送了一个加密的媒体片段后,MediaSource 会触发一个\'encrypted\'
事件。
- 当你通过 MSE 的
- 生成许可证请求:
- 在这个事件的处理器中,你会得到
initData
(初始化数据,通常来自 MPD 或媒体片段的\'moov\'/\'moof\'
盒子)。 - 使用
mediaKeys.createSession()
创建一个许可证会话 (MediaKeySession)。 - 调用
session.generateRequest(\'cenc\', initData)
来生成一个许可证请求。
- 在这个事件的处理器中,你会得到
- 发送请求到许可证服务器:
generateRequest
是一个Promise
,完成后,session
对象会触发一个\'message\'
事件。- 这个事件的
message
属性就是需要发送给Widevine
许可证服务器的请求体 (Challenge)。 - 使用 fetch 将这个请求体(通常是二进制的 ArrayBuffer)POST 到你的许可证服务器 URL。
- 提供许可证给 CDM:
- fetch 请求会收到许可证服务器返回的许可证 (License)。
- 调用
session.update(license)
,将这个许可证交给 MediaKeySession。
- 解密和播放:
- 一旦 CDM 成功处理了你提供的许可证,它就拥有了解密密钥。
- 现在,当后续的加密媒体片段被推送到 SourceBuffer 时,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 数据到每个音频 / 视频流数据上
该数据将作为 KeySystem
的 InitData
值
其中 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