IPTVnator 离线 DASH ClearKey 测试固件:VP9/Opus 加密资产构造、再生与 e2e 验证全解
【免费下载链接】iptvnator:tv: Cross-platform IPTV player application with multiple features, such as support of m3u and m3u8 playlists, favorites, TV guide, TV archive/catchup and more.项目地址: https://gitcode.com/GitHub_Trending/ip/iptvnator
IPTVnator 是跨平台 IPTV 播放器(支持 m3u/m3u8 播放列表、电视指南、时移回看等)。为了保证 DASH + ClearKey 加密播放链路在 Web 与 Electron 双端的稳定性,仓库在apps/web-e2e/src/fixtures/dash/下维护了一套完全离线、确定性内容的 DASH 固件(fixtures):4 秒时长的 VP9 视频 + Opus 音频,分别打包为 CENC(ClearKey)加密版与明文对照版。本文以 fixtures/dash/README.md 为主线,结合固件生成脚本 generate-fixture.mjs、Web 端 dash-clearkey.e2e.ts 与 Electron 端 dash-clearkey.e2e.ts 的源码实现,讲透这套固件"为什么这样设计、如何被播放、如何重新生成"。
读完本文,你将掌握:IPTVnator 如何用离线资产做加密与非加密 DASH 播放的端到端回归;为什么选 VP9/Opus 与 Shaka Packager(而不是 ffmpeg 加密);固定 ClearKey 凭据如何映射到 m3u 播放列表的#KODIPROP指令;以及如何用一条命令完整再生整套固件。
固件全景:六个文件覆盖"加密 / 明文"两条播放链路
固件目录apps/web-e2e/src/fixtures/dash/下共六个文件,全部为静态点播(on-demand)DASH 资产,服务于 Web 与 Electron 两套 e2e 套件:
| 文件 | 内容 | 用途 |
|---|---|---|
clearkey-video.mp4 | VP9 视频,CENC(cencAES-CTR,subsample)加密 | 加密视频轨道 |
clearkey-audio.mp4 | Opus 音频,CENC 加密 | 加密音频轨道 |
clearkey.mpd | 静态 on-demand MPD,指向加密对 | 加密流清单 |
clear-video.mp4 | VP9 视频,未加密 | 明文回归用例(clear-DASH)视频轨道 |
clear-audio.mp4 | Opus 音频,未加密 | 明文回归用例音频轨道 |
clear.mpd | 静态 on-demand MPD,指向明文对 | 明文流清单 |
两套资产内容同源(同一段测试视频与音频),差异仅在是否经过 Shaka Packager 加密。这样设计让 e2e 可以在同一个测试文件里对比"加密流能否播放"与"明文流能否播放",还能为 DRM 诊断、段加载失败等场景构造可控输入。
固定 ClearKey 测试凭据:KID/KEY 与 m3u 指令的映射
固件使用固定、明显合成(synthetic)的 128 位 ClearKey 测试凭据,可安全提交到仓库:
- KID:
00112233445566778899aabbccddeeff - KEY:
ffeeddccbbaa99887766554433221100
这两个值在 generate-fixture.mjs 中以导出常量定义,并被加密步骤使用;在 e2e 侧,dash-clearkey.e2e.ts 同样硬编码了这对凭据,用来构造CLEARKEY_LICENSEJSON。
在 IPTVnator 的播放列表生态中,DRM 凭据通过 Kodi 风格的#KODIPROP指令注入 m3u。README 明确指出这对凭据对应于:
#KODIPROP:inputstream.adaptive.license_key=KID:KEYWeb e2e 实际构造的播放列表验证了这一点(见 dash-clearkey.e2e.ts):license_type声明为clearkey,license_key则是把 KID/KEY 编码为无填充 Base64 的 JSON(kty: oct、type: temporary)。之所以用 Base64 JSON 而非裸KID:KEY,是因为该用例同时承担了对 issue #1466 的回归验证——PWA 下必须接受无填充(去掉=)的标准 Base64 JSON 格式:
const CLEARKEY_LICENSE = JSON.stringify({ keys: [ { kty: 'oct', kid: Buffer.from(CLEARKEY_KID, 'hex').toString('base64').replace(/=+$/, ''), k: Buffer.from(CLEARKEY_KEY, 'hex').toString('base64').replace(/=+$/, ''), }, ], type: 'temporary', });对应的播放列表片段为:
#EXTINF:-1 tvg-id="ck-dash" group-title="DASH",ClearKey DASH #KODIPROP:inputstream.adaptive.license_type=clearkey #KODIPROP:inputstream.adaptive.license_key=<Base64 JSON> https://dash-fixture.local/clearkey.mpd在播放器侧,DRM 凭据最终落到 Shaka Player 的drm.clearKeys配置。见 shaka-video-session.ts:
if (drm?.clearKeys) { player.configure({ drm: { clearKeys: drm.clearKeys } }); }这里 ClearKey 走的是clearKeys(本地密钥注入,无需 license server);而同一个会话类也会对不支持的 license 类型(如 Widevine)直接发射 DRM 诊断而不再启动引擎(shaka-video-session.ts),这正好被 e2e 的"Widevine DASH 频道显示加密诊断横幅"用例覆盖。
为什么选 VP9 + Opus:一套固件服务两个运行时
README 给出的理由是:Playwright 捆绑的 Chromium 不包含专有编解码器(无 H.264/AAC),而免版税的 VP9/Opus 在 Playwright Chromium 与 Electron 里都能解码——于是一套固件同时喂饱 Web 与 Electron 两套 e2e 套件,无需维护两份资产。
这一选择也体现在生成脚本的参数里(generate-fixture.mjs):视频用testsrc2测试图源 +libvpx-vp9,音频用sine正弦波 +libopus:
ffmpeg -y \ -f lavfi -i "testsrc2=duration=4:size=320x180:rate=24" \ -f lavfi -i "sine=frequency=440:duration=4" \ -c:v libvpx-vp9 -b:v 150k -g 24 \ -c:a libopus -b:a 48k \ content-master.tmp.mp4320×180、24fps、4 秒、150kbps 视频 + 48kbps 音频——资产刻意做得小,让 e2e 在 CI 上保持轻量;testsrc2与sine是 ffmpeg 内置信号源,保证内容确定性可合成(无需任何外部媒体素材)。
实际 MPD 中的编解码器标识也印证了这一点。加密版 clearkey.mpd 的视频 Representation 声明codecs="vp09.00.11.08.01.02.02.02.00"、mimeType="video/mp4",音频声明codecs="opus"、audioSamplingRate="48000"。这个 vp09 codec 字符串在 e2e 里还被断言用于诊断面板的编解码器回显(见 dash-clearkey.e2e.ts)。
为什么用 Shaka Packager 加密:senc与saiz/saio的合规性鸿沟
README 用两个具体失败现象解释了"加密为何必须交给 Shaka Packager,ffmpeg 做不到":
ffmpeg 的 mp4 muxer 只写
sencsample-encryption 元数据。Chromium 的解复用器(demuxer)要求saiz/saiobox,缺失时直接以CHUNK_DEMUXER_ERROR_APPEND_FAILED: Sample encryption info is not available失败。也就是说,ffmpeg 产出的"加密 mp4"在 Chromium 里根本无法进入解码流程。ffmpeg 无法产生 VP9 CENC 绑定(binding)所要求的 subsample 加密。对 VP9 而言,CENC 规范强制使用 subsample 模式(仅加密样本中特定字节范围),整轨全量加密的 VP9 最终会以
MEDIA_ERR_DECODE收场。
Shaka Packager 对两者都能产出规范合规的输出。生成脚本中加密变体的完整调用(generate-fixture.mjs):
packager \ "in=content-master.tmp.mp4,stream=video,output=clearkey-video.mp4,drm_label=CK" \ "in=content-master.tmp.mp4,stream=audio,output=clearkey-audio.mp4,drm_label=CK" \ --enable_raw_key_encryption \ --keys "label=CK:key_id=00112233445566778899aabbccddeeff:key=ffeeddccbbaa99887766554433221100" \ --clear_lead 0 \ --protection_scheme cenc \ --mpd_output clearkey.mpd参数要点:
--enable_raw_key_encryption+--keys label=...:key_id=...:key=...:使用 RAW key 直接指定 KID/KEY(即固件那对固定凭据),无需 license server;--protection_scheme cenc:采用cenc(AES-CTR,subsample)加密方案,这是与 Shaka/Chromium 兼容性最好的方案;--clear_lead 0:没有明文引导段(clear lead),整个轨道从头加密;--mpd_output:由 Packager 顺带生成 MPD,保证sidx/Initialization/indexRange与实际分片严格对齐。
加密版 MPD 中可以看到标准cenc保护声明(clearkey.mpd):
<ContentProtection value="cenc" schemeIdUri="urn:mpeg:dash:mp4protection:2011" cenc:default_KID="00112233-4455-6677-8899-aabbccddeeff"/> <ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b"> <cenc:pssh>AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAEAESIzRFVmd4iZqrvM3e7/AAAAAA==</cenc:pssh> </ContentProtection>urn:mpeg:dash:mp4protection:2011声明了 CENC 方案与 default_KID,1077efec-c0b2-4d02-ace3-3c1e52e2fb4b则是 ClearKey 的 UUID(PlayReady 体系内也是该 UUID),cenc:pssh内嵌了 PSSH box 供 EME 直接消费。
MPD 结构:静态 on-demand profile 与字节范围寻址
两份 MPD 都采用profiles="urn:mpeg:dash:profile:isoff-on-demand:2011"、type="static"、mediaPresentationDuration="PT4S",属于单文件点播型 DASH:不切分 ts/m4s 分片,而是用SegmentBase+ 字节范围(byte range)从单个 mp4 中按需读取初始化段和媒体段。
以加密版为例(clearkey.mpd):
<Representation id="0" bandwidth="149370" codecs="vp09.00.11.08.01.02.02.02.00" mimeType="video/mp4" sar="1:1"> <BaseURL>clearkey-video.mp4</BaseURL> <SegmentBase indexRange="939-982" timescale="12288"> <Initialization range="0-938"/> </SegmentBase> </Representation>这里indexRange="939-982"指向 mp4 中的sidxbox,Initialization range="0-938"指向moov等初始化数据。播放器(Shaka)拿到 MPD 后,会通过HTTP Range 请求精确抓取这些字节区间——这正是 e2e 的"虚拟固件主机"必须实现 Range 响应(206 Partial Content)的根本原因。
e2e 如何消费固件:虚拟主机 + 路由拦截
固件不依赖真实网络:e2e 通过 Playwright 路由拦截把https://dash-fixture.local虚拟主机上的每个请求(含 Range 请求)转发到本地固件文件(dash-clearkey.e2e.ts):
- 无
Range头 → 200 +Accept-Ranges: bytes; - 有
Range头 → 解析bytes=start-end,返回 206 +Content-Range,并用body.subarray(start, end+1)切出真实字节。
同时测试用serviceWorkers: 'block'关闭 Angular Service Worker——因为经过 SW 的请求会绕过 Playwright 路由拦截,虚拟主机将永远无法解析(见 dash-clearkey.e2e.ts 注释)。另外还通过--autoplay-policy=no-user-gesture-required允许无手势自动播放。
Web 套件覆盖了四类场景(dash-clearkey.e2e.ts):
- ClearKey 与明文 DASH 频道都能内联播放:验证
currentTime > 0.5且无诊断横幅; - ClearKey 从"最近播放 / 收藏"集合重开:冷加载后从 recent、favorites、全局 recent、全局 favorites 四个入口点播,验证持久化频道可复播;
- 不支持的 DRM(Widevine)显示加密诊断:断言横幅文案 "This player does not support the stream's DRM configuration."、DRM 声明为 Widevine,且无 MPV/VLC 回退推荐;
- 段加载失败 / 清单 DRM 声明:对
clear-video.mp4返回 403,断言诊断包含 "Media segment loading"、HTTP 403 与 codec 信息,并验证"复制诊断"到剪贴板的 JSON 结构(engine: 'shaka'、stage: 'segment'、httpStatus: 403、videoCodecs: [...]),且不泄露私有标记、虚拟主机地址或sourceUrl。
Web 端播放链路本身由 shaka-video-session.ts 支撑:Shaka Player 模块在首次播放时才懒加载并缓存(loadModule+polyfill.installAll()),start()/stop()为同步入口,内部用generation 计数器 + 操作链保证切台时不会复活过期引擎;加载失败会被分类为诊断问题(classifyShakaPlaybackIssue),player.destroy()会中断在途load()(以LOAD_INTERRUPTED拒绝),避免卡死的清单请求阻塞后续操作。
Electron 端:打包file://渲染器下的真实 ClearKey 证明
固件不止服务 Web 套件。Electron e2e(dash-clearkey.e2e.ts)直接以仓库路径引用同一份apps/web-e2e/src/fixtures/dash目录(DASH_FIXTURE_DIR),用本地 HTTP server 伺服这些文件,在真实 Electron 运行时验证:ClearKey EME 在打包后file://渲染器(安全上下文)下依然可用。这补上了 Playwright 端的一个已知空白——README 与 e2e 代码均注明,ClearKey EME 仅在捆绑的 Chromium 中行为确定(WebKit 缺乏 ClearKey),所以桌面真实运行时由 Electron 套件单独把关。
再生成固件:一条命令与三档 Packager 解析策略
固件可按需重新生成,命令为:
node apps/web-e2e/src/fixtures/dash/generate-fixture.mjs前置依赖:
- ffmpeg(README 注明以 7.x 测试),需编译了
libvpx-vp9与libopus; - Shaka Packager,按以下顺序解析(见 generate-fixture.mjs):
- 环境变量
SHAKA_PACKAGER指向的packager二进制; - 已安装的
shaka-packagernpm 包(通过createRequire(...).resolve('shaka-packager')找到 launcher,以 Node 运行); - 否则脚本通过
npm pack shaka-packager --silent一次性拉取官方 npm 包(内含各平台预编译二进制:packager-linux-x64、packager-osx-arm64、packager-win-x64.exe等)到临时目录解压并chmod 755后调用;若当前平台/架构没有对应二进制,则报错并提示手动设置SHAKA_PACKAGER。
- 环境变量
脚本流程(generate-fixture.mjs)三步走:
- 用 ffmpeg 合成 4 秒清晰母版
content-master.tmp.mp4(VP9+Opus,muxed); - 用 Shaka Packager 对母版做 CENC 加密,产出
clearkey-video.mp4/clearkey-audio.mp4/clearkey.mpd; - 对母版做明文打包,产出
clear-video.mp4/clear-audio.mp4/clear.mpd;
最后finally中清理临时母版文件。
可复现性边界:README 明确声明,不同工具版本间不保证字节级一致(byte-exact)——提交进仓库的固件文件才是 CI 的"事实来源"(source of truth)。因此实际工作中应把再生成视为"更新基线"操作:生成后需人工比对差异并重新提交,而不是在 CI 里每次运行都现生成。
实战要点小结
- 加密 DASH 的 e2e 覆盖需要合规的容器加密:
senc元数据不足,Chromium 需要saiz/saio;VP9 强制 subsample CENC,这两点都指向 Shaka Packager 而非 ffmpeg; - 免版税编解码器(VP9/Opus)是让一套离线固件同时服务 Playwright Chromium 与 Electron 的关键选型;
- 固定、合成、可提交的 ClearKey 凭据让 e2e 无需 license server 即可完成 EME 全链路验证,
license_key既支持裸KID:KEY,也支持无填充 Base64 JSON(PWA 回归用例); - 离线固件通过 Playwright 路由拦截 + 虚拟主机 + 完整 Range 语义模拟真实 DASH 请求,保证测试确定性、零外网依赖;
- 再生成固件是"一次性基线更新"操作:
node apps/web-e2e/src/fixtures/dash/generate-fixture.mjs,依赖 ffmpeg(libvpx-vp9 + libopus)与 Shaka Packager(环境变量 / npm 包 / npm pack 三档解析)。
如需深入,可继续阅读:fixtures/dash/README.md、generate-fixture.mjs、Web e2e 用例、Electron e2e 用例、Shaka 引擎实现。
【免费下载链接】iptvnator:tv: Cross-platform IPTV player application with multiple features, such as support of m3u and m3u8 playlists, favorites, TV guide, TV archive/catchup and more.项目地址: https://gitcode.com/GitHub_Trending/ip/iptvnator
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考