M3U8 能播放却不能下载?一次 AES-128 加密误判的排查与修复
开发 M3U8 转 MP4 和 M3U8 查看解析 时,我们遇到了一个看似矛盾的反馈:同一个地址在播放器里能正常播放,解析页面显示“未加密”,下载 MP4 却提示加密内容不受支持。真正的问题有两层:解析器把主列表的局部信息当成了整个视频的结论,转换器当时又没有实现 AES-128 解密。
这篇文章记录问题如何定位、修复为什么不能只改一句提示,以及遇到类似现象时应该检查什么。示例地址均为说明结构的虚构地址,不包含用户的媒体地址或密钥。
核验范围: 本文依据本项目实现和 2026 年 10 月 1 日的测试记录整理。我们使用受控样例验证了不同 IV、密钥轮换、明密文切换和加密初始化段,也在 Chromium 中完成了一份用户提供样例的转换。结果不代表所有来源、所有设备或所有加密方式都兼容。
主列表没有密钥标签,为什么仍然可能加密?
最初的判断逻辑很直接:当前文件没有 EXT-X-KEY,就显示未加密。这个判断对主播放列表不成立,因为主列表通常只列出不同清晰度的子列表地址,并不包含视频分片。
例如,入口文件只有下面这些内容:
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=2091000,RESOLUTION=1920x1080
1080p/index.m3u8
进入 1080p/index.m3u8 后,才看到分片和密钥声明:
#EXTM3U
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:100
#EXT-X-KEY:METHOD=AES-128,URI="key.key",IV=0x00000000000000000000000000000000
#EXTINF:4.0,
segment-100.ts
#EXT-X-ENDLIST
因此,入口列表中找不到密钥声明,只能说明当前这一层没有提供相关信息。我们把这类主列表的结果改为“尚未确认,需解析子播放列表”。用户可以继续打开清晰度或音频列表检查。解析工具展示当前文件的结果,不会默默下载并检查所有分支;如果有独立音轨,也需要分别查看。
这种区别来自 HLS 的主列表与媒体列表结构,见 RFC 8216 的协议概述与播放列表定义。排查时先确认“我正在看哪一层”,比直接搜索一个标签更可靠。
播放成功说明播放器完成了解密
播放器可以逐段请求媒体、取得所需密钥、解密后送入播放管线。这个过程通常不会向用户单独显示“正在解密”。能够看到画面,不能推导出分片原本就是明文。
我们的转换工具是另一条处理路径:先获取完整点播内容,再在浏览器里封装成 MP4。早期实现明确拒绝加密流,避免把密文直接交给媒体引擎。播放器支持某种流,并不意味着下载工具已经具备相同能力。
这次修复同时处理了两件事:让解析页面保留未知状态,避免给出错误结论;让转换流程在识别到支持的 AES-128 条件时,先解密分片再封装。单纯删掉“加密不支持”的拦截,会让后面的媒体处理收到无效数据,并不能产生正确的视频。
AES-128 的计算不复杂,状态处理更容易出错
我们使用浏览器原生 Web Crypto 接口执行 AES-CBC 解密,没有自行编写密码算法。接口的使用方式和安全上下文要求可查阅 MDN 的 SubtleCrypto.decrypt 文档。真正需要仔细实现的是每个分片对应的密钥和 IV。
一个密钥标签会影响后续分片,直到新标签改变这个状态。不能只把最后找到的密钥存到整个列表上,否则密钥轮换时,前面的分片可能被错误地使用后面的密钥解密。我们的解析器为分片保存当时的加密信息,遇到 METHOD=NONE 则停止对后续分片应用原有加密状态。
IV 也不能固定写成全零。列表明确给出 IV 时,使用该值;没有给出时,按媒体序列号生成对应的 16 字节值。假设 EXT-X-MEDIA-SEQUENCE 是 100,第一个分片就使用序列号 100,第二个使用 101,而不是按页面表格中的行号从零开始。序列号的字节排列规则见 RFC 8216 第 5.2 节。
密钥文件应是符合该模式的二进制数据。本工具检查取得的密钥是否正好为 16 字节,而不会把登录页、错误 JSON 或一段十六进制文本当成密钥继续处理。初始化段还有单独的约束:如果 EXT-X-MAP 指向的初始化段使用 AES-128,加密声明需要明确 IV;缺失时我们报告问题,不擅自猜测。
为什么下载密钥失败,视频仍可能在别处播放?
转换需要浏览器能够读取主列表、子列表、分片、初始化段和密钥。任何一个请求被跨域策略阻止,都可能使转换失败。另一个网站的播放器可能使用不同域名、授权方式或请求环境,所以“在原站播放成功”并不能保证我们的页面也能读取这些资源。
尤其要检查密钥请求。HTTP 返回 200 也不一定代表成功:它可能返回了登录页面。应查看实际响应类型与长度,但不要把密钥内容或带凭证的链接贴到公开讨论中。本工具只在当前转换任务里缓存密钥,不把它们写入持久存储或日志;相同密钥地址可以复用一次导入结果,任务结束后不继续保留给其他转换使用。
遇到错误时,可以按下面的顺序缩小范围:
- 在解析页面确定入口是主列表还是媒体列表,继续查看实际选中的视频和音频分支。
- 找到
METHOD、密钥 URI 和 IV,确认是不是工具支持的普通 AES-128。 - 在浏览器网络面板检查密钥请求是否成功,是否被跨域限制,返回内容是否为正确长度的二进制数据。
- 如果密钥可读取但解密失败,检查 IV、媒体序列号、密钥轮换位置以及分片是否完整。
- 解密成功后若 MP4 处理失败,再检查音视频编码、缺失分片或流参数变化,不要继续把所有错误归因于加密。
解密会不会消耗很多资源?
对本工具而言,AES 解密是一段原生计算,通常不需要重新编码视频。转换的总体耗时还包含网络下载、写入媒体引擎的内存文件系统,以及输出 MP4。没有一个适用于所有手机、电脑和网络的固定耗时比例。
更值得注意的是内存。浏览器可能同时持有下载数据、解密后的分片、媒体引擎数据和输出文件。本工具目前将下载数据限制为 256 MiB,但这不是“最多使用 256 MiB 内存”的承诺。较长视频仍可能给低内存设备带来压力。具体取舍见 浏览器转换与上线限制的开发记录。
当前支持的范围是可取得密钥的普通 AES-128,使用支持的 identity 密钥格式。SAMPLE-AES、DRM 及其他密钥格式仍不在转换支持范围内。工具也不能恢复缺失的密钥或绕过来源的授权条件。
修复后怎样确认不是“只让报错消失”?
我们用独立生成的加密样例检查解密结果是否与原始字节一致,再验证隐式 IV、显式 IV、密钥轮换和 METHOD=NONE。浏览器测试覆盖加密 TS,以及带加密初始化段的 fMP4;错误密钥、无效密文和取消操作也进入了检查范围。
用户提供的完整样例包含 412 个分片。转换后的 MP4 约为 210.78 MiB,浏览器读取到 1920×1080 的尺寸和约 823.59 秒时长,并能跳转接近结尾的位置。这说明该样例完成了下载、解密和封装;它不是对任意 M3U8 的通用兼容保证。
这次故障最有价值的经验是:解析结果应说明证据覆盖到了哪一层,转换能力应明确支持什么处理步骤。遇到“能播不能下载”,先定位差异发生在列表选择、请求、解密还是封装阶段,再选择对应的修复办法。若问题与文件地址或分片组织有关,可以继续阅读 相对地址、字节范围与初始化段的处理记录。