Yard


hlsjs

1 什么是 hls

HTTP Live Streaming(缩写是 HLS)是一个由苹果公司提出的基于 HTTP 的流媒体网络传输协议。是苹果公司 QuickTime X 和 iPhone 软件系统的一部分。它的工作原理是把整个流分成一个个小的基于 HTTP 的文件来下载,每次只下载一些。当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源,允许流媒体会话适应不同的数据速率。在开始一个流媒体会话时,客户端会下载一个包含元数据的 extended M3U (m3u8)playlist 文件,用于寻找可用的媒体流。 \ HLS 只请求基本的 HTTP 报文,与实时传输协议(RTP)不同,HLS 可以穿过任何允许 HTTP 数据通过的防火墙或者代理服务器。它也很容易使用内容分发网络来传输媒体流。

HLS 协议规定:

  1. 视频的封装格式是 TS。
  2. 视频的编码格式为 H264,音频编码格式为 MP3、AAC 或者 AC-3。
  3. 除了 TS 视频文件本身,还定义了用来控制播放的 m3u8 文件(文本文件)。

2 M3U8 文件

以下是一个简略的 M3U8 文件

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-KEY:METHOD=AES-128,URI="https://ipc-camera.fast-cn.wgine.com/api/cloud/key?devId=6c4db6784fffcc3892nxmp&magic=qKr6mxwDmUbJ8EYnHCdmAnD3488CsMaj",IV=0x7b84a718bbac5e2053d64b3295ca2dce
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:10
#EXT-X-PROGRAM-DATE-TIME:2020-01-08T20:01:16.000+00:00
#EXTINF:10,
bobf339vmg9aa815lus0zTCWOElTj5jd_0.ts?token=5160f1deec8742b4c5ecd9891f5bcf934290b7d4ea3f29284a1ebce9bab2efab
#EXT-X-PROGRAM-DATE-TIME:2020-01-08T20:01:26.000+00:00
#EXTINF:10,
bobf339vmg9aa815lus0zTCWOElTj5jd_1.ts?token=9588f1926541d09bbc06db2f9ffd944fa9b2f2062e9e4186938ed64e7869b5f3
#EXT-X-PROGRAM-DATE-TIME:2020-01-08T20:01:36.000+00:00
#EXTINF:10,
#EXT-X-ENDLIST

我们解析下这里的每个标签是干什么的?

2.1 EXTM3U

每个 M3U 文件第一行必须是这个 tag,起标示作用。

2.2 EXT-X-VERSION

用以标示协议版本。

2.3 EXT-X-KEY

这个标示了当前 M3U8 的解密方式。

2.4 EXT-X-MEDIA-SEQUENCE

每一个 media URI 在 PlayList 中只有唯一的序号,相邻之间序号+1, 一个 media URI 并不是必须要包含的,如果没有,默认为 0。(因为存在多个 m3u8 的情况,视频太大时减少 m3u8 大小)

2.5 EXT-X-TARGETDURATION

每一份媒体文件的时间, 以秒为单位, 这里是 10 秒一份

2.6 EXTINF

每一份媒体文件的具体数据,包括文件 url,持续时间等

2.7 EXT-X-PROGRAM-DATE-TIME

播放的绝对时间。(这里我们用来更新进度条)

3 hls.js 播放 m3u8

https://github.com/video-dev/hls.js/

这是目前使用最广的前端 client。使用非常简单

var video = document.getElementById("video");
if (Hls.isSupported()) {
  // 新建一个hls实例
  var hls = new Hls();
  // load m3u8文件
  hls.loadSource("https://video-dev.github.io/streams/x36xhzz/x36xhzz.m3u8");
  // 将hls stream attach到video
  hls.attachMedia(video);
  // 监听MANIFEST_PARSED事件,通知video开始播放
  hls.on(Hls.Events.MANIFEST_PARSED, function() {
    video.play();
  });
}

4 hls 普通加密方式

hls 只有一个 m3u8 地址,只要能访问就可以直接播放视频,但是在商业用途上,视频肯定是需要加密的,那么 hls 是如何加密的呢?

我们使用一种加密模式给 TS 文件加密,这样如果没有解密方法,这个视频就无法播放。那么怎么解密呢?

这里就提到了上面讲的 EXT-X-KEY。

这个字段里面包含了一个 url 和一个 IV,在 hlsjs 的实现里面,会去调用这个方法去获取 key,然后使用这个 key 和这个 IV 对 TS 文件进行解密,那么就可以播放了。

我们的问题就在此产生了:这个 key 需要一个二进制,而且进制的转换是在 hlsjs 代码里面做掉了,而我们的 网关服务都不支持二进制的格式返回数据。

翻了一遍源码找到了以下的片段

function loadsuccess(
  response: LoaderResponse,
  stats: LoaderStats,
  context: KeyLoaderContext
) {
  let frag = context.frag;
  if (!frag.decryptdata) {
    logger.error("after key load, decryptdata unset");
    return;
  }
  this.decryptkey = frag.decryptdata.key = new Uint8Array(
    response.data as ArrayBuffer
  );

  // detach fragment loader on load success
  frag.loader = undefined;
  delete this.loaders[frag.type];
  this.hls.trigger(Event.KEY_LOADED, { frag: frag });
}

那么如何修改这个 func 使得可以在解密呢?

我们在 hls 的配置项里面找到了这个 https://github.com/video-dev/hls.js/blob/master/docs/API.md#loader

loader
(default: standard XMLHttpRequest-based URL loader)

Override standard URL loader by a custom one. Use composition and wrap internal implementation which could be exported by Hls.DefaultConfig.loader. Could be useful for P2P or stubbing (testing).

Use this, if you want to overwrite both the fragment and the playlist loader.

Note: If fLoader or pLoader are used, they overwrite loader!

我们试着通过改写这个 loader 方法来完成我们的业务场景:

const configure = {
  loader() {
    const loader = new Hls.DefaultConfig.loader(configure);
    this.abort = () => loader.abort();
    this.destroy = () => loader.destroy();

    this.load = (context, config, callbacks) => {
      const { type } = context;
      const onSuccess = callbacks.onSuccess;
      callbacks.onSuccess = (response, stats, context1, networkDetails) => {
        // 标示key方法 因为请求中m3u8请求和ts请求都在这里
        if (type !== "manifest" && context1.url.includes("key?devId")) {
          response.data = _base64ToArrayBuffer(ab2str(response.data));
        }
        onSuccess(response, stats, context, networkDetails);
      };
      loader.load(context, config, callbacks);
    };
  }
};

保留了原来的各类方法,又将我们需要的处理函数注入到代码中,这样就解决了自定义注入key的问题。