Custom key acquisition for encrypted HLS in VideoJS

The basic problem is fairly simple: intercept the key acquisition XHR request, update URL and/or add authorization header. This way people won’t be able to just simply put the .m3u8 files into their video players and simply play it. This is a solution for people who want a little more protection for their EHLS videos while sacrificing native behaviour is acceptable.

Please note that browsers without Media Source Extension might have difficulties working with this solution (like iOS safari).

What do we have for starter

Using a special, non-standard scheme(key://) will help us cathing the key requests because our XHR interceptor will get all the segment requests as well. You can stick with standard protocols like http but using special makes our job easier.

We have our index.m3u8 file that contains 3 quality levels with bitrate and dimensions for each:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=1600000,RESOLUTION=854x480
sd/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3200000,RESOLUTION=1280x720
hd/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5300000,RESOLUTION=1920x1080
fhd/index.m3u8

Here is a quality level manifest file (the end was truncated):

# EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="key://0.key",IV=0x9C2403588BE41A691A73E686170DD85A
#EXTINF:5.760000,
index0.ts
#EXTINF:5.760000,
index1.ts
#EXT-X-KEY:METHOD=AES-128,URI="key://2.key",IV=0x372F336A8156359866D2AA7419A9B0F3
#EXTINF:3.840000,
index2.ts
...

Initalizing the player

The are many different versions for VideoJS but we recommend the latest (v7+ at the moment) because it has built in HLS support without the need of any plugin. Unlike previous versions.

So, here is the basic code:

When you try to run this one you should encounter something like this (which is okay for now):

Failed to load key://0.key: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

Adding XHR interceptor

Okay, now we need to register our XHR interceptor. We have to be careful, because lot of the events won’t provide us reliable context. Events we tried, outcomes we found:

  • player.ready will fire when the player is ready but the manifest file is not yet been downloaded. This is important, becase VideoJS will only load http-streaming module (HLS) after the main manifest finished downloading.
  • player.on("loadedmatadata", (e) => {})is too late because it gets triggered after the selected quality manifest has been loaded and right after that the first key might get loaded instatly based on your settings (poster, preload etc).
  • player.on("loadstart") is being triggered after http-streaming module is loaded therefore the XHR object will be available trough player.tech().hls property.

Finding the right events might be tricky because there is no diagram how the internals work in VideoJS but you can check out all the events in the source here (just search for @event): https://github.com/videojs/video.js/blob/master/src/js/player.js

Conclusion

It was fun finding the VideoJS version (oh boy, even the documentation has 3 different approche from which only one works) and events.

We also tried HLS.JS which is a more modern solution and we did the same XHR thing but the audio was (for some reason) broken.

If you have any comment, correction or anytign to add to this article please let me know.

Choosing the unknown