上个月在逛论坛时,看到了一个标题:“100u 有偿请前端老哥实现解密播放 m3u8 文件”,看着比较感兴趣,也正对自己专业,本着“当代人应多从助人为乐中体验幸福”的原则,从标题点了进去。
目录
OP 的核心问题
简要概括一下 就是,使用 OpenSSL 命令对影片的明文 **.m3u8**
和 **.ts**
切片文件进行加密(AES-ECB),需要前端对文件进行解密,并把视频播放出来,能实现就给 100 美元的报酬。
评论区
当我往下翻看评论的时候,有相当多的回复,都在表达对 OP 问题的安全性的看法,以及质疑需求是否合理,还有在嫌弃价格低云云;这些并没有直接帮助 OP 解决如何实现前端解密并播放视频的问题,更多的都是在吐槽问题。
关于问题
评论区的讨论当然有存在的价值,而作为一个前端开发人员,我首先会去思考以下几点:
这个需求是为了解决什么问题?
需求是通过 AES-ECB 加密技术保护视频流的 .m3u8
和 .ts
文件,这样可以在网络传输中增加一层保护,但同时要保证视频能在浏览器端解密播放。最终目的或许是为了解决视频版权或数据安全问题,最常见的也就是数字版权管理(DRM)了。
前端能否实现?
前端当然可以实现,正如 op 提到的 crypto-js
可以用来解密,hls.js
用来播放 .m3u8
视频流文件,关键点在于如何解决如何解密和播放的配合。翻看文档,发现 hls.js
的配置项 (HlsConfig type) 中可以自定义 Loader
,这大大降低了解决问题的难度,只需要在 Loader
(Loader interface) 内部去处理解密,解密后的文件返回给播放器播放就好了。

思考这种解决方式有哪些弊端?
密钥的问题,在浏览器端解密始终有密钥暴露的风险,当然通过服务端下放动态密钥可以很大程度上解决这个问题;其次就是每个视频片段的解密都会在用户设备上进行,对于性能差的设备可能会造成卡顿。
代码实现
有了上一步中实现的思路,接下来就可以写代码了。
处理 hls.js
的 loader
配置项
// 创建一个自定义的加载器类 CustomLoader,继承自 Hls.DefaultConfig.loader
class CustomLoader extends Hls.DefaultConfig.loader {
constructor(config) {
// 调用父类构造函数,以确保继承父类的加载逻辑
super(config);
// 绑定当前对象的 load 方法,以便在自定义的 load 函数内调用
const load = this.load.bind(this);
// 重写 load 方法,实现自定义解密逻辑
this.load = async function (context, config, callbacks) {
// 获取成功回调函数,用于在解密后继续后续操作
const onSuccess = callbacks.onSuccess;
// 重写 onSuccess 回调,以便在成功加载资源后对内容进行解密
callbacks.onSuccess = async function (response, stats, context) {
try {
// 调用自定义解密函数 readAndDecryptFile 对加载的文件解密
const decryptedData = await readAndDecryptFile(context.url);
// 将解密后的数据替换原始响应数据
response.data = decryptedData;
// 调用原始的 onSuccess 回调,传递解密后的数据
onSuccess(response, stats, context, null);
} catch (err) {
// 捕获解密过程中的错误并输出到控制台
console.error(err);
}
};
// 调用原始的 load 方法,继续加载过程
load(context, config, callbacks);
};
}
}
// 创建一个 Hls 实例,并使用自定义的 CustomLoader 进行解密处理
const hls = new Hls({ loader: CustomLoader });
实现 readAndDecryptFile
解密函数
// 将十六进制密钥转换为 CryptoJS 可用的格式
const key = CryptoJS.enc.Hex.parse(keyHex);
// 通用 AES 解密函数
function decryptAES(encryptedData, outputType = "utf8") {
// 将二进制数据转换为 WordArray
const wordArray = CryptoJS.lib.WordArray.create(encryptedData);
const decrypted = CryptoJS.AES.decrypt(
wordArray.toString(CryptoJS.enc.Base64),
key,
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
}
);
// 根据输出类型返回相应格式的数据
return outputType === "utf8"
? decrypted.toString(CryptoJS.enc.Utf8)
: new Uint8Array(
decrypted.words
.map(word => [
(word >> 24) & 0xff,
(word >> 16) & 0xff,
(word >> 8) & 0xff,
word & 0xff,
])
.flat()
);
}
// 读取并解密文件,根据文件类型选择解密方式
export async function readAndDecryptFile(file) {
const response = await fetch(file);
const encryptedData = await response.arrayBuffer(); // 以二进制格式读取
// 解密为 UTF-8 字符串(m3u8)或 Uint8Array(二进制 ts 文件)
return file.endsWith(".m3u8")
? decryptAES(encryptedData, "utf8")
: decryptAES(encryptedData, "binary");
}
通过自定义 Loader
和结合 CryptoJS
解密,实现了前端对 m3u8
视频流的解密和播放,至此完成。
源码地址
最后附上完整源码: