这段时间处理了一个视频点播平台的多DRM接入需求,正好把Shaka Packager从加密到联调整套流程完整跑了一遍。之前也遇到过几次类似的场景,但每次都要翻文档查参数,网上的资料又特别零散,尤其是Widevine、PlayReady、FairPlay三套系统混在一起的时候,参数对不上、播放器黑屏、许可证请求失败这些问题一个接一个。索性这次把整个过程记录下来,从最底层的DRM生态逻辑,到Packager的安装、密钥体系设计、三套DRM的命令差异,再到许可证服务器联调和常见报错排查,一条线捋清楚,给后面要接多DRM的同行做个参考。
1. 多DRM并存的底层逻辑:为什么Widevine、PlayReady、FairPlay一个都不能少
1.1 各平台DRM支持分裂的现实
很多人一开始想不通,为什么不能只做一套DRM,用一个加密方案把所有平台全部覆盖。理论上当然可以,但你在实际部署两周之后就会放弃这个想法。原因很简单:不同的浏览器、操作系统、智能电视、机顶盒,它们各自支持的DRM体系根本不是同一个。浏览器里Chrome和Firefox基本都集成了Widevine,Android设备大多数走Widevine,Windows生态和不少智能电视对PlayReady更友好,而iOS/macOS生态只认FairPlay。这是一个由商业生态和硬件支持共同决定的局面,不是任何一家单靠技术就能拉平的。
这种分裂带来的直接后果是:只加密Widevine,那么iPhone、iPad上的Safari播放器拿不到许可证;只做PlayReady,Chrome和Android上又不行;只做FairPlay,那Windows端直接躺平。所以要覆盖全平台,就是典型的三套都要做。你可能会问,那如果一个播放器环境同时支持两套DRM怎么办,比如某些智能电视的浏览器同时支持Widevine和PlayReady?实际上播放器会根据你的Manifest文件里PSSH盒子提供的信息,从中选一个它能解密的DRM来请求许可证,所以多DRM的本质就是给同一个媒体资产塞进多套DRM的元数据,让不同环境各取所需。
1.2 从视频文件到加密分片的完整链路
为了不把后面的配置搞晕,先理清一条内容从原始文件到用户播放的链路。原始视频经过转码,变成H.264或H.265的MP4,音频转成AAC或AC-3。然后Shaka Packager登场,它负责两件事:一是把MP4切成适合流媒体播放的分片,封装成DASH或者HLS两类格式;二是对分片做DRM加密,把密钥和key_id以PSSH盒子的形式写进MP4里,或者在HLS的m3u8里生成EXT-X-KEY标签。
加密之后的媒体文件,播放器拿到手先读取里面的DRM信息,提取key_id,然后向指定的许可证服务发起请求。许可证服务验证完设备、业务规则之后,把密钥返回给播放器,播放器用密钥完成解密播放。整套体系里,Shaka Packager只负责加密这一层,不做授权,不验证设备,也不参与密钥的发放。这一点非常关键,很多人排查问题的时候,一会儿改打包参数,一会儿又怀疑是许可证服务的问题,就是没把这条分界线理清楚。
2. 开工前的三件事:安装打包器、准备输入源、规划密钥体系
2.1 安装Shaka Packager的几种方式
Shaka Packager的安装不复杂,官方提供了各平台的二进制包。Linux服务器上最省事的方式是直接把release页面里的可执行文件下载下来,放到/usr/local/bin下,加个执行权限。macOS上用Homebrew也方便,一条命令就装好。Windows用户也有很多在用,下载exe之后在命令行里直接调用就行,只是路径要记得加入PATH环境变量,不然每次都要写完整路径。
如果项目里有Docker环境,也可以直接拉镜像用,好处是版本可控、部署干净,适合在CI流水线里跑。我建议后面如果要写自动化脚本,尽量统一版本,不要今天在这台机器上是2.x,明天那台变成3.x,因为Shaka Packager在命令行参数上有过一些不兼容调整,版本锁住能省很多排查麻烦。装完之后可以先跑一下packager --version确认环境正常,再继续往下走。
2.2 输入文件的前置要求
输入文件的质量直接影响加密后的播放体验。编码层面建议视频至少是H.264 High Profile或H.265,音频用AAC-LC,分辨率按播放端需求出多个码率档位,这跟你是否做DRM没关系。真正跟DRM打包相关的,是GOP结构和关键帧间隔,一般建议关键帧间隔控制在2到4秒,这样切片的时候更干净,加密分片的边界不容易出现画面撕裂。如果原始文件关键帧间隔太大,后面切出来的TS或MP4分片会偏大,播放器在拖动进度条时加载也更吃力。
我自己处理过一个极端案例:源文件可能是无人值守转码出来的,GOP跑到十几秒,打包之后每个分片大小差异特别大,HLS的带宽估算完全不准,播放器频繁切换码率,体验很难看。如果你发现加密后的流播放不顺畅,先别急着怀疑DRM,回头查一下源文件的关键帧均匀度,这个坑比DRM本身的概率高得多。
音频轨道的处理也需要注意。如果原始MP4里音频是多轨、多语言的,建议在打包前就分好轨,Packager支持一次输入多个音频,每个音频单独指定stream=audio和标签,但多语言时建议每个语言单独一条输出,不要让一个输出里混着两种语言,给播放器切换字幕和音轨带来麻烦。
2.3 密钥体系的规划:key_id、key、content_id先理清
很多新手在这里被绕晕。DRM加密相关的几个ID容易混淆:key_id是密钥的唯一标识,播放器通过它向许可证服务请求对应的密钥;key是真正的16字节对称密钥,用来执行AES加密和解密;content_id是业务层面的内容标识,在部分DRM体系如Widevine中,这个ID会写进PSSH盒子里,许可证服务可以据此做业务判断。三者的关系可以简化成:内容标识关联到密钥标识,密钥标识再关联到密钥本身。
规划时建议定一个统一规则。我的习惯是用16字节的随机数生成key和key_id,全部以十六进制字符串表达,key_id和key一一对应,每对密钥只服务一条内容资产。如果同一个内容需要多档位视频流和多语言音频,我倾向于所有音视频轨用同一对密钥,这样明显降低许可证服务的复杂度。你不需要为每个码率单独生成密钥,那样只会让自己在联调时多查一倍的日志。
实际生成密钥,Linux下直接openssl rand -hex 16两次就能得到key_id和key,这个步骤别偷懒用固定字符串,产品上线前一定要随机化。密钥生成之后需要安全存储,数据库或密钥管理服务都行,但许可证服务查询密钥的效率要提前压测,否则并发一上来就超时。
3. 核心加密参数逐项拆解:先跑通一个Widevine实例
3.1 命令格式骨架与输入输出流
Shaka Packager的命令行设计思路是,输入和输出在同一个参数里配对,用逗号分隔属性。基本骨架如下:
packager \ input=source.mp4,stream=video,output=encrypted_video.mp4 \ input=source_audio.mp4,stream=audio,output=encrypted_audio.mp4 \ --enable_raw_key_encryption \ --keys label=video:key_id=xxx:key=xxx,label=audio:key_id=xxx:key=xxx \ --protection_systems widevine \ --mp4_output_flags hls \ --hls_master_playlist_output master.m3u8这个例子里有两个输入流,一个是视频轨,一个是音频轨。每个输入都通过stream=video或stream=audio告诉Packager这一路是什么类型,然后output=指定加密后的输出路径。label=video和label=audio是给密钥打标签,将多路流绑定到具体的密钥上,这一步不能省,否则Packager不知道哪把密钥用于哪条流。
输出的封装方式由--mp4_output_flags hls控制,这个参数决定了产出的是普通MP4还是适合HLS流媒体的分片,同时配合--hls_master_playlist_output生成主播放列表。如果你要生成DASH,则换成--generate_dash_mpd相关参数。加密的开关是--enable_raw_key_encryption,配合--keys传入明文密钥串,这个模式适合开发调试和自建密钥服务。生产环境如果对接商业DRM服务,Packager也支持通过服务端签名方式密钥,但自建系统最常用的还是raw_key。
3.2 protection_systems到底在指定什么
--protection_systems是选择这套打包生成哪家DRM的PSSH盒子。PSSH是一段专门给播放器读取的DRM元数据,里面携带了DRM系统ID、key_id等信息。Widevine、PlayReady、FairPlay各自的系统ID在行业标准里有固定定义,Packager会根据这里的参数生成对应的PSSH。
如果你只指定widevine,那输出的MP4里只会有Widevine的PSSH。PlayReady设备拿到这个文件,会发现自己支持的DRM系统不在里面,直接拒绝播放。这也回答了前面说的多DRM问题:要让一个文件同时应付多家DRM,这里就要列出多个系统,用逗号分隔。
测试时我建议先只做Widevine,原因很简单:桌面Chrome里直接支持,不需要额外装任何播放器插件,验证成本最低。先把Widevine完整跑通一遍,包括加密、生成Manifest、构建播放器、请求许可证,确认全链路没毛病,再引入PlayReady和FairPlay。一次只引入一个变量,排查起来会轻松很多。
3.3 加密封装背后的CENC与CBCS选择
Packager在加密MP4时遵循通用加密标准,也就是Common Encryption,CENC和CBCS都属于这个框架下的具体加密方案。CENC用AES-CTR模式,通常配合DASH使用,兼容性好;CBCS则是一种部分加密的CBC方案,每个加密块内只加密一部分数据,硬件开销小,FairPlay Streaming推荐用这个模式。
这里有个常见的坑:不是所有播放器都同时支持CENC和CBCS。你把一个文件加密成CENC,HLS在某些iOS设备上可能不支持,或者反过来,你用CBCS加密后放进DASH里,老一点的Android设备直接报错。所以实际项目中,我的习惯是DASH的视频用CENC,HLS的FairPlay用CBCS,这是目前兼容性最好的组合。Packager默认如果没有指定--protection_scheme,它会根据封装格式和DRM类型自动选,但自动选的不一定符合你的播放器策略,显式指定更稳。
4. 三套DRM的配置差异对照:参数表与FairPlay特殊项
4.1 三套DRM的参数对照速查
把Widevine跑通之后,下一步就是给同一份内容补上PlayReady和FairPlay。先看一张参数对照表,方便在配置时快速定位差异:
| 对比项 | Widevine | PlayReady | FairPlay |
|---|---|---|---|
| 常用终端 | Chrome/Android | Windows/智能电视 | iOS/macOS |
| 常见封装 | DASH | DASH | HLS |
| 常用加密方案 | CENC | CENC | CBCS |
| key_id长度 | 16字节 | 16字节 | 16字节 |
| 密钥长度 | 16字节 | 16字节 | 16字节 |
| PSSH/Header | 自动生成Widevine PSSH | 可自动生成PlayReady Header | 需要兼容SKD URI |
| 许可证地址注入位置 | 播放器配置/自定义PSSH | 播放器配置/Header | m3u8的EXT-X-KEY URI |
| 是否需要证书 | 不需要 | 通常不需要 | 可能需要,用于许可证服务签发 |
这张表的核心结论是:密钥本身不区分DRM体系,同一对key和key_id理论上可以直接用于三套DRM的加密。区别在于三套体系在元数据、许可证请求和封装形式上各有各的规矩。认清这一点,你就不会觉得多DRM是一套完全独立的体系,它更像是一个加密动作配三套不同的外壳。
4.2 PlayReady配置中容易忽略的Header与la_url
PlayReady在Packager里的配置比Widevine稍多一点。加密时指定--protection_systems playready之后,Packager会自动生成一个PlayReady对象放进文件,但有些播放器或者许可证服务对这个Header的内容有额外要求,比如需要在Header里包含PlayReady的许可证获取地址la_url,如果你发现播放器在需要做PlayReady许可证请求时,请求的地址不对或者没有地址可访问,大概率就是Header信息不全。
遇到这种情况,Packager提供了自定义PSSH的能力,可以通过--pssh_boxes手动传入完整的PlayReady PSSH盒子内容。这样你就能把许可证服务地址、设备兼容规则等额外信息都塞进去。日常调试阶段,如果许可证服务是私有的,我反而建议早点用自定义PSSH,别等到播放器真机上暴露问题再回头改命令,那会儿排查链路已经很复杂了。
4.3 FairPlay的三个特殊配置项
FairPlay是三套里最需要小心对待的。第一,封装形式基本锁定HLS,虽然FairPlay不理论上禁止配DASH,但iOS生态里的HLS播放器对FairPlay的支持最成熟,所以实践中不要硬用DASH封装FairPlay。第二,密钥格式需要额外指定,Packager要求使用--protection_systems fairplay的同时配置合适的key_format,因为FairPlay在HLS的EXT-X-KEY标签里需要带KEYFORMAT="com.apple.streamingkeydelivery",少了这个标识播放器认不出这是FairPlay,就不会发起许可证请求。
第三,FairPlay的许可证请求URI是以skd://为前缀的,播放器从m3u8中读到这个URI之后,会截取里面的内容ID去请求许可证。打包时如果漏配,输出的URI是普通的https或缺失,播放器连请求都直接失败。这三个点同时踩中的概率不低,尤其在把DASH项目扩到HLS时容易忽略。我的建议是单独搭一个HLS打包含FairPlay的环境,跑通一个最小可播放的Demo,再去动其他加密参数,这样定位问题会清晰很多。
5. 多DRM同时打包的完整命令:两种方案与典型报错处理
5.1 方案一:一个文件里同时塞入Widevine和PlayReady
如果你的目标平台是DASH为主,可以走合并打包方案,对同一个MP4加同时生成Widevine和PlayReady的PSSH。核心命令如下:
packager \ input=source_video.mp4,stream=video,output=enc_video.mp4 \ input=source_audio.mp4,stream=audio,output=enc_audio.mp4 \ --enable_raw_key_encryption \ --keys label=video:key_id=...:key=...,label=audio:key_id=...:key=... \ --protection_systems widevine,playready \ --generate_dash_mpd \ --mpd_output output.mpd这一段命令最关键的就是--protection_systems widevine,playready,让Packager在一个文件里同时生成两套DRM的PSSH盒子。播放器拿到Manifest后,会遍历文件里的PSSH,找自己认识的那一套去走许可证流程。这个方案的优势是媒体资产只需存一份加密结果,存储成本低;缺点是PSSH顺序如果不符合播放器的偏好,部分播放器可能选中一套兼容性差的DRM,导致播放失败。我遇到过的智能电视上,播放器只认第一个PSSH的情况比较多,所以如果你们平台有明确的播放器偏好,建议优先把首选DRM放在PSSH列表前面。
5.2 方案二:拆分HLS和DASH,FairPlay单独打包
另一套场景是同一个内容既要在DASH上提供Widevine和PlayReady,又要在HLS上提供FairPlay。这时候我倾向于分开跑两次打包命令:一次生成DASH版本,--protection_systems widevine,playready;另一次生成HLS版本,--protection_systems fairplay。虽然会多占一份存储空间,但换来的是两种封装各自的稳定性,不必在一个文件里赌播放器的逻辑。
FairPlay打包的示例命令类似:
packager \ input=source_video.mp4,stream=video,output=enc_hls_video.mp4 \ input=source_audio.mp4,stream=audio,output=enc_hls_audio.mp4 \ --enable_raw_key_encryption \ --keys label=video:key_id=...:key=...,label=audio:key_id=...:key=... \ --protection_systems fairplay \ --protection_scheme cbcs \ --clear_lead 0 \ --hls_master_playlist_output fairplay_master.m3u8单独打包FairPlay时,--protection_scheme cbcs和--clear_lead 0是我配置里的固定选项。--clear_lead 0表示开头不保留未加密的清晰分片,如果你把它设成默认值不为零,有些播放器会在前几秒播放不加密的内容,之后切到加密段时出现许可证还未获取完成导致的卡顿。第一次做的时候,这个坑让我排查了很久,最后定位到就是这个参数。
5.3 典型报错:PSSH缺失与protection_systems未指定
加密时最容易撞上的报错是Packager提示生成PSSH失败,或者播放器解析Manifest时找不到认证信息。这种问题十有八九是因为--protection_systems没有正确指定。有些版本在没有指定protection_systems且没有手动传PSSH时,会直接报错,另一些版本则会在输出文件里不写入任何DRM元数据。加密完成后看着文件生成正常,但一上播放器就黑屏,排查时用工具拉一下MP4里的PSSH,发现根本没有内容。
解决办法很简单,打包前后各做一次检查:打包前确认命令里写清了--protection_systems;打包后用播放器测试之前,先看看生成的MP4里有没有对应的DRM Box。推荐基础工具用ffprobe或者MP4Box看一下结构,几十秒就能确认。别一上来就怀疑播放器代码,先确认封装里带没带元数据,很多时候问题就出在这一步。
5.4 典型报错:加密后花屏、卡顿或无法拖动进度条
加密本身不会导致花屏,但如果加密方案和封装不匹配,播放器在解密时就会出问题。常见场景是用CENC加密了HLS分片,但播放器预期的是CBCS模式。表现就是前几秒能播,后面花屏或者播放器直接放弃。这种报错在日志里往往查不到明确的“加密格式不支持”之类信息,只在播放器控制台留下解密错误。因此我一般会强调,DASH配CENC、HLS配CBCS,尽量遵循这个组合,减少不可预期的问题。
还有一类卡顿与加密参数无关。文件切片不均匀、切片过小或过大,都会导致播放器频繁缓冲。加密和切片是同时发生的,不要忽略切片参数本身的影响。如果排查到最后发现把密钥全部注释掉、不加密直接播放时也卡,那就回头查源文件关键帧和分片时长,不用继续在DRM参数里耗时间。
6. 许可证服务联调与故障排查实录:从离线测试到设备真机
6.1 许可证服务地址到底放在哪里
许可证地址的配置位置需要根据DRM体系区分,这是一个非常细节、也经常让人迷惑的点。Widevine的许可证地址通常放在播放器的请求配置里,播放器在拿到PSSH后,由前端逻辑主动向指定URL发起许可证请求;PlayReady的许可证地址可以放在播放器配置里,也可以放在PlayReady Header的la_url字段里;FairPlay则是写在HLS m3u8的EXT-X-KEY标签的URI中,播放器看到以skd://开头的URI后会自行发起许可证请求。
所以联调时,不要一边在播放器里配置了Widevine的许可证地址,一边又指望去处理PlayReady,却不给PlayReady配地址。我习惯把每个DRM的许可证URL做成配置文件,播放器加载时按DRM类型取对应地址,在开发阶段把地址打印出来确认。这样能第一时间发现是地址没配上,还是许可证请求失败,排查范围缩小一半。
6.2 先离线测试许可证接口,再串起来跑全链路
许可证服务联调阶段,我强烈建议不要一开始就指着播放器播画面来测。正确顺序是先拿一个明文请求模板,直接向许可证服务发请求,确认服务端能正确返回许可证或明确的错误码。然后再用打包出来的加密文件接播放器,走完整流程。
离线测许可证时,带上固定的key_id,确认服务端返回的许可证里的key和打包时用的key一致。这里有个永远值得怀疑的点:key和key_id是否匹配。打包时用的是key串,许可证服务返回的也是key串,两边只要有一处复制错了,播放器解密必败。我会把打包用的keys参数和许可证数据库里的记录放在同一个文档里反复对照,检查时不要靠肉眼扫,直接脚本比对字符串最省心。
6.3 设备时间与时钟校验的坑
DRM系统普遍有时间校验逻辑,播放器请求许可证时,如果设备当前时间和许可证服务期望的许可窗口偏离太多,可能直接拒绝发放许可证。这个在开发阶段容易被忽略,因为你在电脑上时间准,联调很顺利,一到安装到某些机顶盒上,整机时间可能落后或超前几分钟甚至几年,问题就冒出来了。
排查方法很简单,拿到一台播放失败的设备,先看系统时间是不是对的。如果是,再看许可证服务日志里有没有时间相关的报错。这个坑一旦遇到,修改设备时间后再播放往往立刻恢复,曾经遇到过一台投影仪时间设置错误,播放一直失败,校正后恢复正常。所以在多DRM联调的真机环节,设备时间校验要列入固定检查项。
6.4 排查思路速查表
| 表现 | 优先检查项 | 常见原因 |
|---|---|---|
| 播放器黑屏无报错 | 加密文件PSSH、m3u8的EXT-X-KEY | protection_systems没写,FairPlay URI不是skd:// |
| 许可证请求发送失败 | 播放器配置的许可证URL | 各DRM地址填错或未填 |
| 许可证请求返回但解密失败 | 打包key_id和许可证库中的key | key_id/key不匹配 |
| 部分设备可以播放、部分不行 | 加密方案与封装格式 | 比如PlayReady设备读了CBCS加密的DASH文件 |
| 能播放但前几秒卡顿 | clear_lead、分片时长 | 开头存在未加密段或切片不均匀 |
| 真机设备时间异常导致请求失败 | 设备系统时间 | DRM时钟校验不通过 |
这张表是我在处理过的若干DRM接入项目里沉淀下来的,不一定覆盖所有奇怪场景,但覆盖了绝大多数频率最高的坑。遇到新问题不要慌,按链路从打包端到播放端逐步缩小范围,通常十几分钟内能定位。
7. 实操总结与个人体会
这次把Shaka Packager多DRM全流程跑完,我最想强调的一点是:多DRM不是一个加密问题,而是一个工程化治理问题。密钥体系怎么规划、许可证服务怎么设计、播放器端怎么区分不同DRM的许可证流程,这些工作占比远超Packager那几行加密命令。加密本身只要参数正确,几分钟就能完成,但整个链条的稳定性和可维护性,取决于打包之前你花了多少时间设计密钥结构。
另外,真正的好习惯是把所有打包参数配置化。我后来把每一个内容的打包参数写成了配置文件,包括输入路径、输出路径、key_id、key、DRM类型、封装格式、加密方案,然后通过一个脚本批量调用Packager。这样不管是一次打包一个文件,还是某个深夜要重新生成一批测试资产,都不需要临时拼接命令,也最大程度避免复制密钥时手抖出错。
如果你现在刚准备接多DRM,我建议从Widevine单跑通开始,找一个播放器能播放的最小样例,然后逐步加入PlayReady和FairPlay。不要一次想着覆盖所有平台,也不要一开始就追求所有播放器都完美。先把一条链路走通,再横向扩展。这个思路听起来慢,实际是绕开大量无效排查的最快路径。