前言
公司业务技术上为实现视频资源的体验优化,服务端团队针对视频点播方面采用了 HLS .m3u8
格式文件技术方案,并且服务端在处理 m3u8 格式文件时进行了加密
。
这要求在 Web 端在进行播放时需要先进行权限认证(解密)
。特此借助此片文章来记录这一过程。
下面,我们先简单了解视频点播新方案的几个概念。
HLS
HTTP Live Streaming
(缩写 HLS)是一个由苹果公司提出的基于 HTTP 的流媒体网络传输协议
。工作原理是把整个流分成一个个小的基于 HTTP 的文件来下载。
HLS 将视频作为一系列小文件发送,通常每段小文件持续时间约为 6 秒,称为媒体段文件。 索引文件或播放列表提供媒体段文件的 URL 的有序列表。 HLS 的索引文件保存为 M3U8 播放列表,客户端访问索引文件的 URL,然后客户端按顺序请求索引文件。
M3U8
M3U8
是苹果公司使用的 HTTP Live Streaming(HLS) 协议格式的基础,实质是一个媒体播放列表
(Media Playlist),其内部信息记录的是一系列媒体片段资源,顺序播放该片段资源,即可完整展示多媒体资源。其格式如下所示:
#EXTM3U # M3U8文件头
#EXT-X-VERSION:3 # M3U8版本号
#EXT-X-TARGETDURATION:10 # 每个分片TS的最大的时长
#EXT-X-MEDIA-SEQUENCE:1 # 第一个TS分片的序列号
#EXT-X-PLAYLIST-TYPE:VOD # 表明流媒体类型,VOD 表示点播
#EXT-X-KEY:METHOD=AES-128,URI="https://xxx.key",IV=0x00000000000000000000000000000001 # 是否加密解析
#EXTINF:10, # 分片TS的信息,如时长,带宽等;
index480p_00001.ts
#EXT-X-KEY:METHOD=AES-128,URI="https://xxx.key",IV=0x00000000000000000000000000000002
#EXTINF:10,
index480p_00002.ts
#EXT-X-KEY:METHOD=AES-128,URI="https://xxx.key",IV=0x00000000000000000000000000000003
#EXTINF:10,
...
#EXT-X-ENDLIST # 结束,也可用来区分是直播或点播
简单来说,m3u8 是一个 ts 切片列表文件,它记录视频的每个切片的时长与顺序。在播放 M3U8 文件时,并不会将全部 ts 列表文件都请求执行,而是根据视频进度执行对应的 ts 分片文件。
在 M3U8 文件中有一个关键信息 EXT-X-KEY
,如果存在此信息,说明视频是进行过加密解析的,如果不进行解密,ts 分片文件将无法正常播放。
而获取解析密钥的方式是访问 EXT-X-KEY.URI
地址来获取密钥 key,有了密钥 key,才可以解密 ts 分片文件,使得视频正常播放。
当然,在执行 M3U8 ts 切片列表文件时,每个 ts 切片文件在执行前都会执行一次获取密钥 key 的 URI 地址。
下面,我们看看在 Web 端拿到服务端下发的 m3u8 文件后,如何进行视频播放。
video.js
在 Web 端使用 HTML video 标签播放 m3u8 文件时,即使设置 type 为 application/x-mpegURL
也无法正常播放。如下:
<video id="my-video" width="320" height="240" controls>
<source src="https://xxx/index.m3u8" type="application/x-mpegURL">
您的浏览器不支持 HTML5 video 标签。
</video>
不过,video.js 提供了解析播放 m3u8 文件的能力,下面我们引入 video 相关资源即可正常播放:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link href="https://cdn.jsdelivr.net/npm/video.js@7.2.4/dist/video-js.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/video.js@7.4.1/dist/video.js"></script>
</head>
<body>
<video id="my-video" width="320" height="240" controls>
<source src="https://xxx/index.m3u8" type="application/x-mpegURL">
您的浏览器不支持 HTML5 video 标签。
</video>
<script>
const player = videojs('my-video');
</script>
</body>
</html>
播放加密 m3u8 ts 片段文件
上面我们提到,如果 m3u8 是一个经过加密后的文件,在下发的文本内容中会有 EXT-X-KEY
字段信息,如:
#EXT-X-KEY:METHOD=AES-128,URI="https://xxx.key",IV=0x00000000000000000000000000000001 # 是否加密解析
METHOD
表示加密方式为 AES-128
,其中 URI
是用于获取密钥进行解密的 api 接口,这个地址由服务端提供。
而对于这里的设计,我们采用了用户权限校验。也就是说,当访问 URI
获取密钥 key 时,如果用户有权限访问视频,则下发正确的密钥,否则认为无权限,视频无法正常播放,这很好的实现了视频防盗链。
而用户权限校验,可以是在访问 URI
获取密钥 key 时,将用户的登录信息(如:token
)作为 queryString
参数传给 URI
做后续处理。
但是,问题来了,在 Web 端如何能够监听 m3u8 文件内下发的 URI
地址,在被访问时为其添加 queryString
参数呢?
videojs 团队提供了一个库 http-streaming
,可以用来监听每一个 xhr 请求,本质上 获取密钥 key 的 URI
地址发起也属于 xhr。
http-streaming
为每个 xhr 提供了 beforeRequest
函数,它接收一个参数 option,我们可以通过改写 option.uri
来实现 queryString
传参。
代码实现参考如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link href="https://cdn.jsdelivr.net/npm/video.js@7.2.4/dist/video-js.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/video.js@7.4.1/dist/video.js"></script>
<script src="https://unpkg.com/@videojs/http-streaming@2.14.2/dist/videojs-http-streaming.min.js"></script>
</head>
<body>
<video id="my-video" width="320" height="240" controls>
<source src="https://xxx/index.m3u8" type="application/x-mpegURL">
您的浏览器不支持 HTML5 video 标签。
</video>
<script>
const player = videojs('my-video');
setTimeout(() => {
player.tech().vhs.xhr.beforeRequest = function(options) {
console.log('options: ', options);
// 这里可以加判断,只为获取密钥的 `URI` xhr 改写 options.uri
options.uri = `${options.uri}?token=xxx`;
return options;
};
}, 0);
</script>
</body>
</html>
上面方式是监听目标 video 播放下的 xhr 请求,如果想监听全局 video xhr
请求时(当涉及多个 video 时),可以采用:
videojs.Vhs.xhr.beforeRequest = function(options) {
options.uri = `${options.uri}?token=xxx`;
return options;
};
参考
视频加密
videojs 团队(Github)
video.js(Github)
http-streaming(Github)