Share via


Using Reactive License Acquisition

This topic describes the basic steps required to acquire a license for PlayReady protected media content reactively. With reactive license acquisition, your Web app responds to a needKey event from a media element. The following figure shows the basic steps required to acquire a PlayReady license reactively.

Note

The keyError event can occur at any time during the license acquisition process.

Reactive license acquisition consists of two operations: needKey and keyMessage event handling.

To handle the needKey event

Your Web app receives a needKey event along with the payload from the media element.

Note

With PlayReady content protection, your Web app must handle the first needKey event, but it must then ignore any other needKey event that occurs.

Your Web app handles the needKey event by:

  1. Extracting the initData from the event payload. Optionally your Web app could also overwrite the initData with different values.
  2. Optionally creating PlayReady CDM-specific data that contains PlayReady CDM-specific information such as custom data.
  3. Creating a MediaKeys object (based on the protection scheme extracted from the event payload or based on prior knowledge, mime type, and so on) if it does not exist.
  4. Calling MediaKeys::createSession to create a MediaKeySession object and passing it the initData and CDMData (both are optional but at least one of them must be set).

To handle the keyMessage event

  1. The CDM session (MediaKeySession) sends a keyMessage event, along with its payload, to your Web app.
  2. Your Web app handles the keyMessage event by sending the license acquisition challenge in the payload to a license server and getting back a license response.
  3. Your Web app calls MediaKeySession::Update along with the license response.
  4. The CDM session sends your Web app either a keyAdded event if successful or a keyError event if an error occurs.

Note

If initData contains a PlayReady object that contains an OnDemand header, only a keyAdded event is returned (as opposed to a keyMessage event as described in the Encrypted Media Extension draft). Similarly, if initData contains a PlayReady object that contains a key identifier in the hashed data storage (HDS), only a keyAdded event is returned.

The following sample demonstrates PlayReady reactive license acquisition.

//==============================================================================
// PlayReady reactive license acquisition
//==============================================================================

function PlayReadyManager(vid) {
  this.vid = vid;

  var that = this;
  vid.addEventListener(this.NEEDKEY_EVENT, function (e) {
    that.needPlayReadyKey(e);
  }, false);
}

PlayReadyManager.prototype = {
  NEEDKEY_EVENT: "msneedkey",
  KEYMESSAGE_EVENT: "mskeymessage",
  KEYADDED_EVENT: "mskeyadded",
  KEYERROR_EVENT: "mskeyerror",
  KEY_SYSTEM: "com.microsoft.playready",

  needPlayReadyKey: function (e) {
    var that = this;

    console.log("Received needkey message");

    if (!video.msKeys) {
      console.log("Creating a new MediaKeys(\"" + this.KEY_SYSTEM + "\")");
      try {
        video.msSetMediaKeys(new MSMediaKeys(this.KEY_SYSTEM));
      } catch (e) {
        throw "Unable to create MediaKeys(\"" + this.KEY_SYSTEM + "\"). Verify the components are installed and functional. Original error: " + e.message;
      }
    } else {
      return;
    }

    var session = video.msKeys.createSession("video/mp4", e.initData);
    if (!session) {
      throw "Could not create key session";
    }

    session.addEventListener(this.KEYMESSAGE_EVENT, function (e) {
      console.log("Processing key message");
      that.downloadPlayReadyKey(e.destinationURL, String.fromCharCode.apply(null, new Uint16Array(e.message.buffer)), function (data) {
        session.update(data);
      });
    });

    session.addEventListener(this.KEYADDED_EVENT, function () {
      console.log("Key successfully added");
    });

    session.addEventListener(this.KEYERROR_EVENT, function () {
      throw "Unexpected 'keyerror' event from key session. Code: " + session.error.code + ", systemCode: " + session.error.systemCode;
    });
  },

  downloadPlayReadyKey: function (url, keyMessage, callback) {
    console.log("Parsing key message XML");
    var keyMessageXML = new DOMParser().parseFromString(keyMessage, "application/xml");

    var challenge;
    if (keyMessageXML.getElementsByTagName("Challenge")[0]) {
      challenge = atob(keyMessageXML.getElementsByTagName("Challenge")[0].childNodes[0].nodeValue);
    } else {
      throw "Can not find <Challenge> in key message";
    }

    var headerNames = keyMessageXML.getElementsByTagName("name");
    var headerValues = keyMessageXML.getElementsByTagName("value");
    if (headerNames.length !== headerValues.length) {
      throw "Mismatched header <name>/<value> pair in key message";
    }

    var xhr = new XMLHttpRequest();
    xhr.open("POST", url);
    xhr.responseType = "arraybuffer";
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          callback(new Uint8Array(xhr.response));
        } else {
          throw "XHR failed (" + url + "). Status: " + xhr.status + " (" + xhr.statusText + ")";
        }
      }
    }
    for (var i = 0; i < headerNames.length; i++) {
      xhr.setRequestHeader(headerNames[i].childNodes[0].nodeValue, headerValues[i].childNodes[0].nodeValue);
    }

    console.log("Loading PlayReady key from: " + url);
    xhr.send(challenge);
  }
}