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 里提取。

注意:许可证服务器返回的响应是加密的,MediaDrm 内部会自己解密。你千万别自作主张去解密它,否则会得到一个「无效响应」的错误。

15.3 解密流程:从加密到播放

解密流程,我画了一张图,方便你理解:

Widevine 解密流程 加密媒体 MP4 / HLS / DASH 解析 PSSH 提取 initData MediaDrm 会话 openSession() 许可证 服务器 解密 provideKeyResponse() 解码 MediaCodec 渲染 Surface 安全媒体路径(Secure Path) 解密 → 解码 → 渲染 全程在 TEE 或安全硬件中完成 App 进程无法直接访问解密后的数据

解密流程其实不复杂。核心就是: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 根本跑不起来。

经验之谈:做 DRM 播放,老老实实用 SurfaceView。TextureView 虽然灵活,但在安全媒体路径上是个大坑。我见过有人花了两周时间想用 TextureView 跑 L1,最后放弃了。

15.5 避坑指南

做 Widevine 集成,有几个坑我踩过,分享给你:

  1. 许可证过期:许可证有有效期,过期了就得重新请求。我建议在播放前检查一下 getKeyStatus(),别等到黑屏了才去排查。
  2. 多会话管理:如果你同时播放多个 DRM 内容(比如画中画),每个内容需要独立的 session。别复用同一个 session,否则解密会乱掉。
  3. 离线播放:Widevine 支持离线许可证,但需要把许可证持久化。记得用 KEY_TYPE_OFFLINE 请求,然后把响应存到本地数据库。
  4. 设备兼容性:不是所有设备都支持 L1。我建议在 App 启动时检测一下安全等级:
int securityLevel = mediaDrm.getPropertyByteArray("securityLevel");
// 返回 "L1"、"L2" 或 "L3"
// 根据等级决定是否允许播放 4K 内容
警告:不要在非安全线程里调用 MediaDrm 的方法。MediaDrm 不是线程安全的,多线程调用会导致崩溃。我建议把所有 DRM 操作放在一个 HandlerThread 里。

最后说一句:Widevine 集成说难不难,说简单也不简单。关键是理解「安全媒体路径」这个核心概念。只要搞懂了数据流怎么走、在哪里解密、在哪里解码,剩下的就是调 API 了。

我曾经在一个项目里,因为没搞懂安全路径,把解密后的数据用 ByteBuffer 读出来,结果被安全团队骂了一顿。嗯,从那以后我再也不敢碰解密后的数据了。


公众号:蓝海资料掘金营,微信deep3321