Source: lib/polyfill/patchedmediakeys_v01b.js

/**
 * @license
 * Copyright 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

goog.provide('shaka.polyfill.PatchedMediaKeys.v01b');

goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.FakeEventTarget');
goog.require('shaka.util.PublicPromise');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.Uint8ArrayUtils');


/**
 * Install a polyfill to implement {@link http://goo.gl/blgtZZ EME draft
 * 12 March 2015} on top of {@link http://goo.gl/FSpoAo EME v0.1b}.
 */
shaka.polyfill.PatchedMediaKeys.v01b.install = function() {
  shaka.log.debug('v01b.install');

  goog.asserts.assert(HTMLMediaElement.prototype.webkitGenerateKeyRequest,
                      'v01b APIs not available!');

  // Alias.
  var v01b = shaka.polyfill.PatchedMediaKeys.v01b;

  // Construct fake key ID.  This is not done at load-time to avoid exceptions
  // on unsupported browsers.  This particular fake key ID was suggested in
  // w3c/encrypted-media#32.
  v01b.MediaKeyStatusMap.KEY_ID_ = (new Uint8Array([0])).buffer;

  // Install patches.
  navigator.requestMediaKeySystemAccess = v01b.requestMediaKeySystemAccess;
  // Delete mediaKeys to work around strict mode compatibility issues.
  delete HTMLMediaElement.prototype['mediaKeys'];
  // Work around read-only declaration for mediaKeys by using a string.
  HTMLMediaElement.prototype['mediaKeys'] = null;
  HTMLMediaElement.prototype.setMediaKeys = v01b.setMediaKeys;
  window.MediaKeys = v01b.MediaKeys;
  window.MediaKeySystemAccess = v01b.MediaKeySystemAccess;
};


/**
 * An implementation of navigator.requestMediaKeySystemAccess.
 * Retrieve a MediaKeySystemAccess object.
 *
 * @this {!Navigator}
 * @param {string} keySystem
 * @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
 * @return {!Promise.<!MediaKeySystemAccess>}
 * @suppress {unnecessaryCasts}
 */
shaka.polyfill.PatchedMediaKeys.v01b.requestMediaKeySystemAccess =
    function(keySystem, supportedConfigurations) {
  shaka.log.debug('v01b.requestMediaKeySystemAccess');
  goog.asserts.assert(this == navigator,
                      'bad "this" for requestMediaKeySystemAccess');

  // Alias.
  var v01b = shaka.polyfill.PatchedMediaKeys.v01b;
  try {
    var access = new v01b.MediaKeySystemAccess(keySystem,
                                               supportedConfigurations);
    return Promise.resolve(/** @type {!MediaKeySystemAccess} */ (access));
  } catch (exception) {
    return Promise.reject(exception);
  }
};


/**
 * An implementation of HTMLMediaElement.prototype.setMediaKeys.
 * Attach a MediaKeys object to the media element.
 *
 * @this {!HTMLMediaElement}
 * @param {MediaKeys} mediaKeys
 * @return {!Promise}
 */
shaka.polyfill.PatchedMediaKeys.v01b.setMediaKeys = function(mediaKeys) {
  shaka.log.debug('v01b.setMediaKeys');
  goog.asserts.assert(this instanceof HTMLMediaElement,
                      'bad "this" for setMediaKeys');

  // Alias.
  var v01b = shaka.polyfill.PatchedMediaKeys.v01b;

  var newMediaKeys =
      /** @type {shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys} */ (
          mediaKeys);
  var oldMediaKeys =
      /** @type {shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys} */ (
          this.mediaKeys);

  if (oldMediaKeys && oldMediaKeys != newMediaKeys) {
    goog.asserts.assert(oldMediaKeys instanceof v01b.MediaKeys,
                        'non-polyfill instance of oldMediaKeys');
    // Have the old MediaKeys stop listening to events on the video tag.
    oldMediaKeys.setMedia(null);
  }

  delete this['mediaKeys'];  // in case there is an existing getter
  this['mediaKeys'] = mediaKeys;  // work around read-only declaration

  if (newMediaKeys) {
    goog.asserts.assert(newMediaKeys instanceof v01b.MediaKeys,
                        'non-polyfill instance of newMediaKeys');
    newMediaKeys.setMedia(this);
  }

  return Promise.resolve();
};


/**
 * For some of this polyfill's implementation, we need to query a video element.
 * But for some embedded systems, it is memory-expensive to create multiple
 * video elements.  Therefore, we check the document to see if we can borrow one
 * to query before we fall back to creating one temporarily.
 *
 * @return {!HTMLVideoElement}
 * @private
 */
shaka.polyfill.PatchedMediaKeys.v01b.getVideoElement_ = function() {
  var videos = document.getElementsByTagName('video');
  var tmpVideo = videos.length ? videos[0] : document.createElement('video');
  return /** @type {!HTMLVideoElement} */(tmpVideo);
};



/**
 * An implementation of MediaKeySystemAccess.
 *
 * @constructor
 * @struct
 * @param {string} keySystem
 * @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
 * @implements {MediaKeySystemAccess}
 * @throws {Error} if the key system is not supported.
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySystemAccess =
    function(keySystem, supportedConfigurations) {
  shaka.log.debug('v01b.MediaKeySystemAccess');

  /** @type {string} */
  this.keySystem = keySystem;

  /** @private {string} */
  this.internalKeySystem_ = keySystem;

  /** @private {!MediaKeySystemConfiguration} */
  this.configuration_;

  // This is only a guess, since we don't really know from the prefixed API.
  var allowPersistentState = true;

  if (keySystem == 'org.w3.clearkey') {
    // ClearKey's string must be prefixed in v0.1b.
    this.internalKeySystem_ = 'webkit-org.w3.clearkey';
    // ClearKey doesn't support persistence.
    allowPersistentState = false;
  }

  var success = false;
  var tmpVideo = shaka.polyfill.PatchedMediaKeys.v01b.getVideoElement_();
  for (var i = 0; i < supportedConfigurations.length; ++i) {
    var cfg = supportedConfigurations[i];

    // Create a new config object and start adding in the pieces which we
    // find support for.  We will return this from getConfiguration() if
    // asked.
    /** @type {!MediaKeySystemConfiguration} */
    var newCfg = {
      'audioCapabilities': [],
      'videoCapabilities': [],
      // It is technically against spec to return these as optional, but we
      // don't truly know their values from the prefixed API:
      'persistentState': 'optional',
      'distinctiveIdentifier': 'optional',
      // Pretend the requested init data types are supported, since we don't
      // really know that either:
      'initDataTypes': cfg.initDataTypes,
      'sessionTypes': ['temporary'],
      'label': cfg.label
    };

    // v0.1b tests for key system availability with an extra argument on
    // canPlayType.
    var ranAnyTests = false;
    if (cfg.audioCapabilities) {
      for (var j = 0; j < cfg.audioCapabilities.length; ++j) {
        var cap = cfg.audioCapabilities[j];
        if (cap.contentType) {
          ranAnyTests = true;
          // In Chrome <= 40, if you ask about Widevine-encrypted audio support,
          // you get a false-negative when you specify codec information.
          // Work around this by stripping codec info for audio types.
          var contentType = cap.contentType.split(';')[0];
          if (tmpVideo.canPlayType(contentType, this.internalKeySystem_)) {
            newCfg.audioCapabilities.push(cap);
            success = true;
          }
        }
      }
    }
    if (cfg.videoCapabilities) {
      for (var j = 0; j < cfg.videoCapabilities.length; ++j) {
        var cap = cfg.videoCapabilities[j];
        if (cap.contentType) {
          ranAnyTests = true;
          if (tmpVideo.canPlayType(cap.contentType, this.internalKeySystem_)) {
            newCfg.videoCapabilities.push(cap);
            success = true;
          }
        }
      }
    }

    if (!ranAnyTests) {
      // If no specific types were requested, we check all common types to find
      // out if the key system is present at all.
      success = tmpVideo.canPlayType('video/mp4', this.internalKeySystem_) ||
                tmpVideo.canPlayType('video/webm', this.internalKeySystem_);
    }
    if (cfg.persistentState == 'required') {
      if (allowPersistentState) {
        newCfg.persistentState = 'required';
        newCfg.sessionTypes = ['persistent-license'];
      } else {
        success = false;
      }
    }

    if (success) {
      this.configuration_ = newCfg;
      return;
    }
  }  // for each cfg in supportedConfigurations

  var message = 'Unsupported keySystem';
  if (keySystem == 'org.w3.clearkey' || keySystem == 'com.widevine.alpha') {
    message = 'None of the requested configurations were supported.';
  }
  var unsupportedError = new Error(message);
  unsupportedError.name = 'NotSupportedError';
  unsupportedError.code = DOMException.NOT_SUPPORTED_ERR;
  throw unsupportedError;
};


/**
 * @override
 * @suppress {unnecessaryCasts}
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySystemAccess.prototype.
    createMediaKeys = function() {
  shaka.log.debug('v01b.MediaKeySystemAccess.createMediaKeys');

  // Alias.
  var v01b = shaka.polyfill.PatchedMediaKeys.v01b;
  var mediaKeys = new v01b.MediaKeys(this.internalKeySystem_);
  return Promise.resolve(/** @type {!MediaKeys} */ (mediaKeys));
};


/** @override */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySystemAccess.prototype.
    getConfiguration = function() {
  shaka.log.debug('v01b.MediaKeySystemAccess.getConfiguration');
  return this.configuration_;
};



/**
 * An implementation of MediaKeys.
 *
 * @constructor
 * @struct
 * @param {string} keySystem
 * @implements {MediaKeys}
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys = function(keySystem) {
  shaka.log.debug('v01b.MediaKeys');

  /** @private {string} */
  this.keySystem_ = keySystem;

  /** @private {HTMLMediaElement} */
  this.media_ = null;

  /** @private {!shaka.util.EventManager} */
  this.eventManager_ = new shaka.util.EventManager();

  /**
   * @private {!Array.<!shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession>}
   */
  this.newSessions_ = [];

  /**
   * @private {!Object.<string,
   *                    !shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession>}
   */
  this.sessionMap_ = {};
};


/**
 * @param {HTMLMediaElement} media
 * @protected
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.setMedia =
    function(media) {
  this.media_ = media;

  // Remove any old listeners.
  this.eventManager_.removeAll();

  if (media) {
    // Intercept and translate these prefixed EME events.
    this.eventManager_.listen(media, 'webkitneedkey',
        /** @type {shaka.util.EventManager.ListenerType} */ (
            this.onWebkitNeedKey_.bind(this)));

    this.eventManager_.listen(media, 'webkitkeymessage',
        /** @type {shaka.util.EventManager.ListenerType} */ (
            this.onWebkitKeyMessage_.bind(this)));

    this.eventManager_.listen(media, 'webkitkeyadded',
        /** @type {shaka.util.EventManager.ListenerType} */ (
            this.onWebkitKeyAdded_.bind(this)));

    this.eventManager_.listen(media, 'webkitkeyerror',
        /** @type {shaka.util.EventManager.ListenerType} */ (
            this.onWebkitKeyError_.bind(this)));
  }
};


/** @override */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.createSession =
    function(opt_sessionType) {
  shaka.log.debug('v01b.MediaKeys.createSession');

  var sessionType = opt_sessionType || 'temporary';
  // TODO: Consider adding support for persistent-release once Chrome has
  // implemented it natively.  http://crbug.com/448888
  // This is a non-issue if we've deprecated the polyfill by then, since
  // prefixed EME is on its way out.
  if (sessionType != 'temporary' && sessionType != 'persistent-license') {
    throw new TypeError('Session type ' + opt_sessionType +
                        ' is unsupported on this platform.');
  }

  // Alias.
  var v01b = shaka.polyfill.PatchedMediaKeys.v01b;

  // Unprefixed EME allows for session creation without a video tag or src.
  // Prefixed EME requires both a valid HTMLMediaElement and a src.
  var media = this.media_ || /** @type {!HTMLMediaElement} */(
      document.createElement('video'));
  if (!media.src) media.src = 'about:blank';

  var session = new v01b.MediaKeySession(media, this.keySystem_, sessionType);
  this.newSessions_.push(session);
  return session;
};


/** @override */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.setServerCertificate =
    function(serverCertificate) {
  shaka.log.debug('v01b.MediaKeys.setServerCertificate');

  // There is no equivalent in v0.1b, so return failure.
  return Promise.reject(new Error(
      'setServerCertificate not supported on this platform.'));
};


/**
 * @param {!MediaKeyEvent} event
 * @private
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.onWebkitNeedKey_ =
    function(event) {
  shaka.log.debug('v01b.onWebkitNeedKey_', event);
  goog.asserts.assert(this.media_, 'media_ not set in onWebkitNeedKey_');

  var event2 = new shaka.util.FakeEvent('encrypted', {
    initDataType: 'webm',  // not used by v0.1b EME, but given a valid value
    initData: event.initData
  });

  this.media_.dispatchEvent(event2);
};


/**
 * @param {!MediaKeyEvent} event
 * @private
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.onWebkitKeyMessage_ =
    function(event) {
  shaka.log.debug('v01b.onWebkitKeyMessage_', event);

  var session = this.findSession_(event.sessionId);
  if (!session) {
    shaka.log.error('Session not found', event.sessionId);
    return;
  }

  var isNew = session.keyStatuses.getStatus() == undefined;

  var event2 = new shaka.util.FakeEvent('message', {
    messageType: isNew ? 'licenserequest' : 'licenserenewal',
    message: event.message
  });

  session.generated();
  session.dispatchEvent(event2);
};


/**
 * @param {!MediaKeyEvent} event
 * @private
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.onWebkitKeyAdded_ =
    function(event) {
  shaka.log.debug('v01b.onWebkitKeyAdded_', event);

  var session = this.findSession_(event.sessionId);
  goog.asserts.assert(session, 'unable to find session in onWebkitKeyAdded_');
  if (session) {
    session.ready();
  }
};


/**
 * @param {!MediaKeyEvent} event
 * @private
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.onWebkitKeyError_ =
    function(event) {
  shaka.log.debug('v01b.onWebkitKeyError_', event);

  var session = this.findSession_(event.sessionId);
  goog.asserts.assert(session, 'unable to find session in onWebkitKeyError_');
  if (session) {
    session.handleError(event);
  }
};


/**
 * @param {string} sessionId
 * @return {shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession}
 * @private
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.findSession_ =
    function(sessionId) {
  var session = this.sessionMap_[sessionId];
  if (session) {
    shaka.log.debug('v01b.MediaKeys.findSession_', session);
    return session;
  }

  session = this.newSessions_.shift();
  if (session) {
    session.sessionId = sessionId;
    this.sessionMap_[sessionId] = session;
    shaka.log.debug('v01b.MediaKeys.findSession_', session);
    return session;
  }

  return null;
};



/**
 * An implementation of MediaKeySession.
 *
 * @param {!HTMLMediaElement} media
 * @param {string} keySystem
 * @param {string} sessionType
 *
 * @constructor
 * @struct
 * @implements {MediaKeySession}
 * @extends {shaka.util.FakeEventTarget}
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession =
    function(media, keySystem, sessionType) {
  shaka.log.debug('v01b.MediaKeySession');
  shaka.util.FakeEventTarget.call(this);

  /** @private {!HTMLMediaElement} */
  this.media_ = media;

  /** @private {boolean} */
  this.initialized_ = false;

  /** @private {shaka.util.PublicPromise} */
  this.generatePromise_ = null;

  /** @private {shaka.util.PublicPromise} */
  this.updatePromise_ = null;

  /** @private {string} */
  this.keySystem_ = keySystem;

  /** @private {string} */
  this.type_ = sessionType;

  /** @type {string} */
  this.sessionId = '';

  /** @type {number} */
  this.expiration = NaN;

  /** @type {!shaka.util.PublicPromise} */
  this.closed = new shaka.util.PublicPromise();

  /** @type {!MediaKeyStatusMap} */
  this.keyStatuses =
      new shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap();
};
goog.inherits(shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession,
              shaka.util.FakeEventTarget);


/**
 * Signals that the license request has been generated.  This resolves the
 * 'generateRequest' promise.
 *
 * @protected
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.generated =
    function() {
  shaka.log.debug('v01b.MediaKeySession.generated');

  if (this.generatePromise_) {
    this.generatePromise_.resolve();
    this.generatePromise_ = null;
  }
};


/**
 * Signals that the session is 'ready', which is the terminology used in older
 * versions of EME.  The new signal is to resolve the 'update' promise.  This
 * translates between the two.
 *
 * @protected
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.ready =
    function() {
  shaka.log.debug('v01b.MediaKeySession.ready');

  this.updateKeyStatus_('usable');

  if (this.updatePromise_) {
    this.updatePromise_.resolve();
  }
  this.updatePromise_ = null;
};


/**
 * Either rejects a promise, or dispatches an error event, as appropriate.
 *
 * @param {!MediaKeyEvent} event
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.handleError =
    function(event) {
  shaka.log.debug('v01b.MediaKeySession.handleError', event);

  // This does not match the DOMException we get in current WD EME, but it will
  // at least provide some information which can be used to look into the
  // problem.
  var error = new Error('EME v0.1b key error');
  error.errorCode = event.errorCode;
  error.errorCode.systemCode = event.systemCode;

  // The presence or absence of sessionId indicates whether this corresponds to
  // generateRequest() or update().
  if (!event.sessionId && this.generatePromise_) {
    error.method = 'generateRequest';
    if (event.systemCode == 45) {
      error.message = 'Unsupported session type.';
    }
    this.generatePromise_.reject(error);
    this.generatePromise_ = null;
  } else if (event.sessionId && this.updatePromise_) {
    error.method = 'update';
    this.updatePromise_.reject(error);
    this.updatePromise_ = null;
  } else {
    // This mapping of key statuses is imperfect at best.
    var code = event.errorCode.code;
    var systemCode = event.systemCode;
    if (code == MediaKeyError['MEDIA_KEYERR_OUTPUT']) {
      this.updateKeyStatus_('output-restricted');
    } else if (systemCode == 1) {
      this.updateKeyStatus_('expired');
    } else {
      this.updateKeyStatus_('internal-error');
    }
  }
};


/**
 * Logic which is shared between generateRequest() and load(), both of which
 * are ultimately implemented with webkitGenerateKeyRequest in prefixed EME.
 *
 * @param {?BufferSource} initData
 * @param {?string} offlineSessionId
 * @return {!Promise}
 * @private
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.generate_ =
    function(initData, offlineSessionId) {
  if (this.initialized_) {
    return Promise.reject(new Error('The session is already initialized.'));
  }

  this.initialized_ = true;

  /** @type {!Uint8Array} */
  var mangledInitData;

  try {
    if (this.type_ == 'persistent-license') {
      var StringUtils = shaka.util.StringUtils;
      if (!offlineSessionId) {
        // Persisting the initial license.
        // Prefix the init data with a tag to indicate persistence.
        var prefix = StringUtils.toUTF8('PERSISTENT|');
        var result = new Uint8Array(prefix.byteLength + initData.byteLength);
        result.set(new Uint8Array(prefix), 0);
        result.set(new Uint8Array(initData), prefix.byteLength);
        mangledInitData = result;
      } else {
        // Loading a stored license.
        // Prefix the init data (which is really a session ID) with a tag to
        // indicate that we are loading a persisted session.
        mangledInitData = new Uint8Array(
            StringUtils.toUTF8('LOAD_SESSION|' + offlineSessionId));
      }
    } else {
      // Streaming.
      goog.asserts.assert(this.type_ == 'temporary',
                          'expected temporary session');
      goog.asserts.assert(!offlineSessionId,
                          'unexpected offline session ID');
      mangledInitData = new Uint8Array(initData);
    }

    goog.asserts.assert(mangledInitData,
                        'init data not set!');
  } catch (exception) {
    return Promise.reject(exception);
  }

  goog.asserts.assert(this.generatePromise_ == null,
                      'generatePromise_ should be null');
  this.generatePromise_ = new shaka.util.PublicPromise();

  // Because we are hacking media.src in createSession to better emulate
  // unprefixed EME's ability to create sessions and license requests without a
  // video tag, we can get ourselves into trouble.  It seems that sometimes,
  // the setting of media.src hasn't been processed by some other thread, and
  // GKR can throw an exception.  If this occurs, wait 10 ms and try again at
  // most once.  This situation should only occur when init data is available
  // ahead of the 'needkey' event.
  try {
    this.media_.webkitGenerateKeyRequest(this.keySystem_, mangledInitData);
  } catch (exception) {
    if (exception.name != 'InvalidStateError') {
      this.generatePromise_ = null;
      return Promise.reject(exception);
    }

    setTimeout(function() {
      try {
        this.media_.webkitGenerateKeyRequest(this.keySystem_, mangledInitData);
      } catch (exception) {
        this.generatePromise_.reject(exception);
        this.generatePromise_ = null;
      }
    }.bind(this), 10);
  }

  return this.generatePromise_;
};


/**
 * An internal version of update which defers new calls while old ones are in
 * progress.
 *
 * @param {!shaka.util.PublicPromise} promise  The promise associated with this
 *   call.
 * @param {?BufferSource} response
 * @private
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.update_ =
    function(promise, response) {
  if (this.updatePromise_) {
    // We already have an update in-progress, so defer this one until after the
    // old one is resolved.  Execute this whether the original one succeeds or
    // fails.
    this.updatePromise_.then(
        this.update_.bind(this, promise, response)
    ).catch(
        this.update_.bind(this, promise, response)
    );
    return;
  }

  this.updatePromise_ = promise;

  var key;
  var keyId;

  if (this.keySystem_ == 'webkit-org.w3.clearkey') {
    // The current EME version of clearkey wants a structured JSON response.
    // The v0.1b version wants just a raw key.  Parse the JSON response and
    // extract the key and key ID.
    var StringUtils = shaka.util.StringUtils;
    var Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
    var licenseString = StringUtils.fromUTF8(response);
    var jwkSet = /** @type {JWKSet} */ (JSON.parse(licenseString));
    var kty = jwkSet.keys[0].kty;
    if (kty != 'oct') {
      // Reject the promise.
      var error = new Error('Response is not a valid JSON Web Key Set.');
      this.updatePromise_.reject(error);
      this.updatePromise_ = null;
    }
    key = Uint8ArrayUtils.fromBase64(jwkSet.keys[0].k);
    keyId = Uint8ArrayUtils.fromBase64(jwkSet.keys[0].kid);
  } else {
    // The key ID is not required.
    key = new Uint8Array(response);
    keyId = null;
  }

  try {
    this.media_.webkitAddKey(this.keySystem_, key, keyId, this.sessionId);
  } catch (exception) {
    // Reject the promise.
    this.updatePromise_.reject(exception);
    this.updatePromise_ = null;
  }
};


/**
 * Update key status and dispatch a 'keystatuseschange' event.
 *
 * @param {string} status
 * @private
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.
    updateKeyStatus_ = function(status) {
  this.keyStatuses.setStatus(status);
  var event = new shaka.util.FakeEvent('keystatuseschange');
  this.dispatchEvent(event);
};


/** @override */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.generateRequest =
    function(initDataType, initData) {
  shaka.log.debug('v01b.MediaKeySession.generateRequest');
  return this.generate_(initData, null);
};


/** @override */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.load =
    function(sessionId) {
  shaka.log.debug('v01b.MediaKeySession.load');
  if (this.type_ == 'persistent-license') {
    return this.generate_(null, sessionId);
  } else {
    return Promise.reject(new Error('Not a persistent session.'));
  }
};


/** @override */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.update =
    function(response) {
  shaka.log.debug('v01b.MediaKeySession.update', response);
  goog.asserts.assert(this.sessionId, 'update without session ID');

  var nextUpdatePromise = new shaka.util.PublicPromise();
  this.update_(nextUpdatePromise, response);
  return nextUpdatePromise;
};


/** @override */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.close =
    function() {
  shaka.log.debug('v01b.MediaKeySession.close');

  // This will remove a persistent session, but it's also the only way to
  // free CDM resources on v0.1b.
  if (this.type_ != 'persistent-license') {
    // sessionId may reasonably be null if no key request has been generated
    // yet.  Unprefixed EME will return a rejected promise in this case.
    // We will use the same error message that Chrome 41 uses in its EME
    // implementation.
    if (!this.sessionId) {
      this.closed.reject(new Error('The session is not callable.'));
      return this.closed;
    }
    this.media_.webkitCancelKeyRequest(this.keySystem_, this.sessionId);
  }

  // Resolve the 'closed' promise and return it.
  this.closed.resolve();
  return this.closed;
};


/** @override */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.remove =
    function() {
  shaka.log.debug('v01b.MediaKeySession.remove');

  if (this.type_ != 'persistent-license') {
    return Promise.reject(new Error('Not a persistent session.'));
  }

  return this.close();
};



/**
 * An implementation of MediaKeyStatusMap.
 * This fakes a map with a single key ID.
 *
 * @constructor
 * @struct
 * @implements {MediaKeyStatusMap}
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap = function() {
  /**
   * @type {number}
   */
  this.size = 0;

  /**
   * @private {string|undefined}
   */
  this.status_ = undefined;
};


/**
 * @const {!ArrayBuffer}
 * @private
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.KEY_ID_;


/**
 * An internal method used by the session to set key status.
 * @param {string|undefined} status
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.prototype.setStatus =
    function(status) {
  this.size = status == undefined ? 0 : 1;
  this.status_ = status;
};


/**
 * An internal method used by the session to get key status.
 * @return {string|undefined}
 */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.prototype.getStatus =
    function() {
  return this.status_;
};


/** @override */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.prototype.forEach =
    function(fn) {
  if (this.status_) {
    var fakeKeyId =
        shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.KEY_ID_;
    fn(fakeKeyId, this.status_);
  }
};


/** @override */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.prototype.get =
    function(keyId) {
  if (this.has(keyId)) {
    return this.status_;
  }
  return undefined;
};


/** @override */
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.prototype.has =
    function(keyId) {
  var fakeKeyId =
      shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.KEY_ID_;
  if (this.status_ &&
      shaka.util.Uint8ArrayUtils.equal(
          new Uint8Array(keyId), new Uint8Array(fakeKeyId))) {
    return true;
  }
  return false;
};