15、DRM 与媒体加密:Widevine 集成、解密流程、安全媒体路径
DRM,说白了就是数字内容的「防盗门」。
我做 Android 多媒体开发这么多年,最头疼的其实不是编解码,而是 DRM。为什么?因为编解码搞不定顶多卡顿、花屏,DRM 搞不定,内容压根播不了。尤其是 Widevine,它几乎是 Android 生态里绕不开的 DRM 方案。
今天我们就来聊聊 Widevine 的集成、解密流程,以及那个让很多人头疼的「安全媒体路径」。
15.1 Widevine 是什么?
Widevine 是 Google 收购来的 DRM 技术,现在已经是 Android 官方推荐的 DRM 方案。它分三个安全等级:
| 等级 | 安全级别 | 解密位置 | 典型场景 |
|---|---|---|---|
| L1 | 最高 | 硬件安全区域(TEE) | 4K 超高清、付费电影 |
| L2 | 中等 | 软件解密 + 硬件渲染 | 部分高清内容 |
| L3 | 最低 | 纯软件解密 | 标清、预览 |
我遇到过不少厂商,明明芯片支持 L1,但系统没配好,结果只能跑 L3。用户看 4K 视频糊成一团,还以为是 App 的问题。嗯,这里要注意:L1 需要芯片厂商、OEM、Google 三方配合,缺一不可。
15.2 Widevine 集成流程
集成 Widevine,说白了就是三步:初始化、请求许可证、播放。
先看初始化代码:
// 创建 MediaDrm 实例
MediaDrm mediaDrm = new MediaDrm(UUID.fromString("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"));
// 这个 UUID 是 Widevine 的唯一标识,别写错了
// 获取设备唯一 ID
byte[] deviceUniqueId = mediaDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
// 创建会话
byte[] sessionId = mediaDrm.openSession();
我个人习惯把 MediaDrm 的生命周期和播放器绑定在一起。播放器销毁时,记得调用 mediaDrm.closeSession() 和 mediaDrm.release()。不然 session 泄漏,内存蹭蹭往上涨。
接下来是许可证请求:
// 生成许可证请求
MediaDrm.KeyRequest keyRequest = mediaDrm.getKeyRequest(
sessionId,
null, // initData,从媒体文件里解析
null, // mimeType
MediaDrm.KEY_TYPE_STREAMING, // 流媒体用 STREAMING,离线用 OFFLINE
null
);
// 把请求发给你的许可证服务器
byte[] licenseResponse = sendToLicenseServer(keyRequest.getData());
// 处理响应
mediaDrm.provideKeyResponse(sessionId, licenseResponse);
这里有个坑:initData 是从媒体文件的 pssh box 里解析出来的。如果你用 ExoPlayer,它会帮你自动解析。但如果你自己写播放器,就得手动从 MP4 或 HLS 的 manifest 里提取。
15.3 解密流程:从加密到播放
解密流程,我画了一张图,方便你理解:
解密流程其实不复杂。核心就是:MediaDrm 负责解密,MediaCodec 负责解码,Surface 负责渲染。但 DRM 的关键在于——解密后的数据不能暴露给 App 进程。
你想想看,如果解密后的数据能被 App 直接拿到,那 DRM 还有什么意义?所以就有了「安全媒体路径」这个概念。
15.4 安全媒体路径
安全媒体路径,说白了就是一条「加密数据 → 解密 → 解码 → 渲染」的管道。这条管道全程在安全硬件里跑,App 根本碰不到原始数据。
具体来说:
- L1 安全路径:解密在 TEE(Trusted Execution Environment)里完成,解码后的数据直接送到显示硬件。App 进程连 YUV 数据都拿不到。
- L3 安全路径:解密在 AP 侧(应用处理器)完成,但数据仍然通过加密通道传给解码器。虽然安全性低一些,但至少不会明文暴露。
我在项目中遇到过一个问题:某款手机在 L1 模式下播放 4K 视频,画面黑屏但声音正常。查了半天,发现是 Surface 没有设置 SECURE 标志。代码里少了一行:
Surface surface = mediaCodec.createInputSurface();
// 关键:设置安全标志
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 通过 MediaCodec 的 Surface 配置
// 实际上,MediaCodec 在创建 Surface 时会自动处理
// 但如果你自己创建 Surface,需要设置 SECURE 标志
}
嗯,这里要注意:SECURE 标志意味着这个 Surface 的内容不能被截屏、录屏。如果你用 SurfaceView,系统会自动处理。但如果你用 TextureView,那就麻烦了——TextureView 不支持安全 Surface,L1 根本跑不起来。
15.5 避坑指南
做 Widevine 集成,有几个坑我踩过,分享给你:
- 许可证过期:许可证有有效期,过期了就得重新请求。我建议在播放前检查一下
getKeyStatus(),别等到黑屏了才去排查。 - 多会话管理:如果你同时播放多个 DRM 内容(比如画中画),每个内容需要独立的 session。别复用同一个 session,否则解密会乱掉。
- 离线播放:Widevine 支持离线许可证,但需要把许可证持久化。记得用
KEY_TYPE_OFFLINE请求,然后把响应存到本地数据库。 - 设备兼容性:不是所有设备都支持 L1。我建议在 App 启动时检测一下安全等级:
int securityLevel = mediaDrm.getPropertyByteArray("securityLevel");
// 返回 "L1"、"L2" 或 "L3"
// 根据等级决定是否允许播放 4K 内容
最后说一句:Widevine 集成说难不难,说简单也不简单。关键是理解「安全媒体路径」这个核心概念。只要搞懂了数据流怎么走、在哪里解密、在哪里解码,剩下的就是调 API 了。
我曾经在一个项目里,因为没搞懂安全路径,把解密后的数据用 ByteBuffer 读出来,结果被安全团队骂了一顿。嗯,从那以后我再也不敢碰解密后的数据了。