dash.js – License acquisition for multiple EME versions

Http Live Streaming (HLS) and Dynamic Adaptive Streaming over HTTP (MPEG-DASH) are the two main formats for adaptive streaming. While HLS is natively supported on most of its target platforms (iOS and MacOSX), we need external players for MPEG-DASH. For browser based environments there are two great open-source options, namely shaka-player and dash.js.  Both are written in JavaScript and use the MediaSourceExtensions (MSE) and the EncryptedMediaExtensions (EME) to enable playback directly in the browser without the need for external plugins. Both offer a wide set of features and have an active community. Shaka-player is maintained and developed by Google, dash.js is the official reference player of the DASH Industry Forum

In this series of blog posts I will focus on dash.js. I will explain how certain features are implemented and how they can be used within applications. Today we are taking a closer look on the license acquisition process and how dash.js supports different versions of the Encrypted Media Extensions.

Why we need support for multiple EME versions

The EME is the API that enables playback of protected content in the browser. It provides the necessary function calls to discover and interact with the underlying DRM system. EME was published as a W3C recommendation at 18.September 2017. However, many companies have adopted EME much earlier. In 2013 Netflix published a blog post in which they shared their intent to move away from Silverlight to an MSE/EME stack. At this time, they already had a working player on the Google Chromebook using the two new APIs. 

Like any other API, EME changed over time and the current version is a lot different from the one in 2013. While desktop and mobile browsers are frequently updated, some embedded devices and set-top boxes are still running on outdated or even customized versions of the EME. For that reason we need a player which detects the EME version on the client and triggers the right API functions.

Different EME versions in dash.js

By default, dash.js comes with support for three different versions of the EME:

  • ProtectionModel_01b.js: Initial implementation of the EME, implemented by Google Chrome prior to version 36. This version of the EME is not promised based and uses outdated or prefixed events like “needkey” or “webkitneedkey”.
  • ProtectionModel_3Feb2014.js:  Implementation of the EME APIs as of the 3 Feb 2014 state of the specification. Implemented by Internet Explorer 11 (Windows 8.1). 
  • ProtectionModel_21Jan2015.js: Most recent EME implementation. Latest changes in the EME specification are added to this model. It supports the promised based EME function calls.

How to select the right EME version

Ideally we want to inject the right EME version only once when the player is initialized. This is exactly how dash.js does it:

 if ((!videoElement || videoElement.onencrypted !== undefined) &&
            (!videoElement || videoElement.mediaKeys !== undefined)) {
            return ProtectionModel_21Jan2015
        } 

else if (getAPI(videoElement, APIS_ProtectionModel_3Feb2014)) {
            return ProtectionModel_3Feb2014 
        } 

else if (getAPI(videoElement, APIS_ProtectionModel_01b)) {
            return ProtectionModel_01b
        } 

For means of simplicity I removed the actual instantiation of the protection models. The player checks for the right EME version in reversed order. That way the latest available EME version is selected and the appropriate ProtectionModel is returned to the controlling entity ( ProtectionController.js) . At this point we could also plug in our own protection model to support customized versions of the EME.

The big picture

Now that we know how the EME version is selected we can step through the complete license acquisition process. 

1. Detecting encrypted content

First of all we need to check if our content is encrypted. In general, the information if and how the content is encrypted can either be part of the manifest file or/and be embedded in the media segments. Let’s assume the latter. If the content is encrypted we will receive an encrypted event from the browser. We register for that type of event and pass the DRM initialization data to our callback function:

 case 'encrypted':
   if (event.initData) {
         let initData = event.initData
         eventBus.trigger(events.NEED_KEY, {key:new NeedKey(initData, event.initDataType)});
     }

By parsing the initialization data we can identify which DRM systems can be used in order to decrypt the content. For instance, one content might require a Playready DRM while another one supports Playready and Widevine.

2. Requesting access to the DRM system

Before trying to decrypt the content, we need to check if our platform supports one of the required DRM systems. For that purpose the requestMediaKeySystemAccess() function of the EME is used. You can find more details on that topic in one of my previous posts. A successful call to this function will return a MediaKeySystemAccess object.

3. Selecting the right DRM system

Some platforms have multiple DRMs available – for instance Playready and Widevine at the same time. From my experience, dash.js will choose the first valid configuration. Hence, to guarantee a consistent behavior, the content provider should write the initialization data (PSSH boxes) in the media segments in chronological order.

4. Generating the payload for the license request

Using the MediaKeySystemAccess object we can now create MediaKeys and assign them to the HTML5 video element. Later, the MediaKeys will be used to decrypt our content:

 keySystemAccess.mksa.createMediaKeys()
    .then(function (mkeys) {
            mediaKeys = mkeys;
            videoElement.setMediaKeys(mediaKeys)
                  .then(function () {             
                     // Cool it worked
                   });
            }

In order to receive a valid license for our content we need to add a payload to our license request. For that purpose, we create a MediaKeySession on which we call the generateRequest() function.

const session = mediaKeys.createSession(sessionType);
session.generateRequest(dataType, initData)
    .then(function () {
         // Request generated
        })
    .catch(function (error) {
        // Ups this is not good 
        });

The browser will forward this request to the underlying Content Decryption Module (CDM). As a result, the CDM generates the payload for our license request.

5. Sending the license request

When the CDM has generated the required payload it forwards this data to the browser. By registering for the message event we are able to grap everything we need:

case 'message':
   let message = ArrayBuffer.isView(event.message) ? event.message.buffer : event.message;                        

Now we can finally issue our license request with reqPayload derived from the previous key message:

doLicenseRequest(url, reqHeaders, reqMethod, responseType, withCredentials, reqPayload,
            LICENSE_SERVER_REQUEST_RETRIES, timeout, onLoad, onAbort, onError);

6. Working with the license response

Let’s say our license server likes what he sees and returns a valid license. All we have to do now is update our MediaKeySession with the data we received from the license server:

session.update(message).catch(function (error) {
    // Please don't throw an error
});

At this point we have everything we need to play our content. The rest is up to the browser and the CDM.

Conclusion

This concludes our small example of the license acquisition in dash.js. In reality the complete process is a little more complicated as you have to deal with different response formats, request headers and so on. Nevertheless, the steps remain the same, at least for the current version of the EME.