chromium/media/test/data/eme_player_js/player_utils.js

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// The PlayerUtils provides utility functions to binding common media events
// to specific player functions. It also provides functions to load media source
// base on test configurations.
var PlayerUtils = new function() {
}

// Prepares a video element for playback by setting default event handlers
// and source attribute.
PlayerUtils.registerDefaultEventListeners = function(player) {
  Utils.timeLog('Registering video event handlers.');
  // Map from event name to event listener function name.  It is common for
  // event listeners to be named onEventName.
  var eventListenerMap = {
    'encrypted': 'onEncrypted',
  };
  for (eventName in eventListenerMap) {
    var eventListenerFunction = player[eventListenerMap[eventName]];
    if (eventListenerFunction) {
      player.video.addEventListener(eventName, function(e) {
        player[eventListenerMap[e.type]](e);
      });
    }
  }

  player.video.addEventListener('error', function(error) {
    // This most likely happens on pipeline failures (e.g. when the CDM
    // crashes). Don't report a failure if the test is checking that sessions
    // are closed on a crash.
    Utils.timeLog('onHTMLElementError', error.target.error.message);
    if (player.testConfig.keySystem == CRASH_TEST_KEYSYSTEM) {
      // On failure the session should have been closed, so verify.
      player.session.closed.then(
          function(result) {
            Utils.setResultInTitle(EME_SESSION_CLOSED_AND_ERROR);
          },
          function(error) { Utils.failTest(error); });
    } else {
      Utils.failTest(error);
    }
  });
};

// Register the necessary event handlers needed when playing encrypted content.
// Returns a promise that resolves to the player.
PlayerUtils.registerEMEEventListeners = function(player) {
  player.video.addEventListener('encrypted', function(message) {
    function addMediaKeySessionListeners(mediaKeySession) {
      mediaKeySession.addEventListener('message', function(message) {
        Utils.timeLog('MediaKeyMessageEvent: ' + message.messageType);
        player.video.receivedKeyMessage = true;
        if (message.messageType == 'license-request' ||
            message.messageType == 'license-renewal' ||
            message.messageType == 'license-release' ||
            message.messageType == 'individualization-request') {
          player.video.receivedMessageTypes.add(message.messageType);
        } else {
          Utils.failTest('Unexpected message type:' + message.messageType,
                         EME_MESSAGE_UNEXPECTED_TYPE);
        }
        player.onMessage(message);
      });
      mediaKeySession.addEventListener('keystatuseschange', function(e) {
        const result = [];
        for (let item of mediaKeySession.keyStatuses) {
          result.push(`{kid:${
              Utils.base64urlEncode(
                  Utils.convertToUint8Array(item[0]))},status:${item[1]}}`);
        }
        Utils.timeLog('KeyStatusesChange: ' + result.join(','));
        if (player.testConfig.playCount == 0) {
          Utils.setResultInTitle('ENDED');
        }
      });
    }

    // Calls getStatusForPolicy() and returns a resolved promise if the result
    // matches the |expectedResult|, whose value can be:
    // - a valid key status, e.g. "usable", in which case getStatusForPolicy()
    //   must be resolved with |expectedResult|.
    // - "rejected", in which case getStatusForPolicy() must be rejected.
    // - "resolved", in which case getStatusForPolicy() can be resolved by any
    //   value.
    async function getStatusForHdcpPolicy(
        mediaKeys, hdcpVersion, expectedResult) {
      try {
        var keyStatus =
            await mediaKeys.getStatusForPolicy({minHdcpVersion: hdcpVersion});
        if (expectedResult == 'resolved' ||
            (expectedResult != 'rejected' && keyStatus == expectedResult)) {
          return true;
        }

        return Promise.reject(
            'For HDCP version "' + hdcpVersion + '", keyStatus "' + keyStatus +
            '" does not match "' + expectedResult + '"');
      } catch (e) {
        if (expectedResult == 'rejected') {
          return true;
        }

        return Promise.reject('Promise rejected unexpectedly: ' + e);
      }
    }

    // Tests HDCP policy check. Returns a resolved promise if all tests pass.
    function testGetStatusForHdcpPolicy(mediaKeys) {
      const keySystem = this.testConfig.keySystem;
      Utils.timeLog('Key system: ' + keySystem);

      if (keySystem == EXTERNAL_CLEARKEY) {
        // ClearKeyCdm pretends the device is HDCP 2.0 compliant. See
        // ClearKeyCdm::GetStatusForPolicy() for details.
        return Promise.all([
          getStatusForHdcpPolicy(mediaKeys, '', 'usable'),
          getStatusForHdcpPolicy(mediaKeys, '1.0', 'usable'),
          getStatusForHdcpPolicy(mediaKeys, '2.3', 'output-restricted'),
        ]);
      }

      if (keySystem == CLEARKEY) {
        // AesDecryptor does not support getStatusForPolicy() so the promise
        // is always rejected.
        return Promise.all([
          getStatusForHdcpPolicy(mediaKeys, '', 'rejected'),
          getStatusForHdcpPolicy(mediaKeys, '1.0', 'rejected'),
        ]);
      }

      if (keySystem == WIDEVINE_KEYSYSTEM) {
        // Widevine CDM supports getStatusForPolicy() so the promise is always
        // resolved. However the key status depends on the device's HDCP level
        // so we cannot enforce it.
        return Promise.all([
          getStatusForHdcpPolicy(mediaKeys, '', 'usable'),
          getStatusForHdcpPolicy(mediaKeys, '1.0', 'resolved'),
        ]);
      }

      return Promise.reject('Unsupported key system');
    }

    try {
      if (player.testConfig.sessionToLoad) {
        // Create a session to load using a new MediaKeys.
        // TODO(jrummell): Add a test that covers remove().
        player.access.createMediaKeys()
            .then(function(mediaKeys) {
              // As the tests run with a different origin every time, there is
              // no way currently to create a session in one test and then load
              // it in a subsequent test (https://crbug.com/715349).
              // So if |sessionToLoad| is 'PersistentLicense', create a session
              // that can be loaded and use that session to load. Otherwise
              // use the name provided (which allows for testing load() on a
              // session which doesn't exist).
              if (player.testConfig.sessionToLoad == 'PersistentLicense') {
                return Utils.createSessionToLoad(
                    mediaKeys, player.testConfig.sessionToLoad);
              } else {
                return player.testConfig.sessionToLoad;
              }
            })
            .then(function(sessionId) {
              Utils.timeLog('Loading session: ' + sessionId);
              player.session =
                  message.target.mediaKeys.createSession('persistent-license');
              addMediaKeySessionListeners(player.session);
              return player.session.load(sessionId);
            })
            .then(
                function(result) {
                  if (!result)
                    Utils.failTest('Session not found.', EME_SESSION_NOT_FOUND);
                },
                function(error) {
                  Utils.failTest(error, EME_LOAD_FAILED);
                });
      } else if (player.testConfig.policyCheck) {
        // TODO(xhwang): We should be able to move all policy check code to a
        // new test js file once we figure out an easy way to separate the
        // requrestMediaKeySystemAccess() logic from the rest of this file.
        Utils.timeLog('Policy check test.');
        player.access.createMediaKeys().then(function (mediaKeys) {
          // Call getStatusForPolicy() before creating any MediaKeySessions.
          return testGetStatusForHdcpPolicy(mediaKeys);
        }).then(function (result) {
          Utils.timeLog('Policy check test passed.');
          Utils.setResultInTitle(UNIT_TEST_SUCCESS);
        }).catch(function (error) {
          Utils.timeLog('Policy check test failed.');
          Utils.failTest(error, UNIT_TEST_FAILURE);
        });
      } else {
        Utils.timeLog(
            'Creating new media key session for initDataType: ' +
            message.initDataType + ', initData: ' +
            Utils.getHexString(new Uint8Array(message.initData)));
        player.session = message.target.mediaKeys.createSession();
        addMediaKeySessionListeners(player.session);
        player.session.generateRequest(message.initDataType, message.initData)
            .catch(function(error) {
              // Ignore the error if a crash is expected. This ensures that
              // the decoder actually detects and reports the error.
              if (this.testConfig.keySystem != CRASH_TEST_KEYSYSTEM) {
                Utils.failTest(error, EME_GENERATEREQUEST_FAILED);
              }
            });
      }
    } catch (e) {
      Utils.failTest(e);
    }
  });

  this.registerDefaultEventListeners(player);
  player.video.receivedKeyMessage = false;
  player.video.receivedMessageTypes = new Set();
  Utils.timeLog('Setting video media keys: ' + player.testConfig.keySystem);

  var config = {
    audioCapabilities: [],
    videoCapabilities: [],
    persistentState: 'optional',
    sessionTypes: ['temporary'],
  };

  // requestMediaKeySystemAccess() requires at least one of 'audioCapabilities'
  // or 'videoCapabilities' to be specified. It also requires only codecs
  // specific to the capability, so unlike MSE cannot have both audio and
  // video codecs in the contentType.
  if (player.testConfig.mediaType) {
    if (player.testConfig.mediaType.substring(0, 5) == 'video') {
      config.videoCapabilities = [{contentType: player.testConfig.mediaType}];
    } else if (player.testConfig.mediaType.substring(0, 5) == 'audio') {
      config.audioCapabilities = [{contentType: player.testConfig.mediaType}];
    }
    // Handle special cases where both audio and video are needed.
    if (player.testConfig.mediaType == 'video/webm; codecs="vorbis, vp8"') {
      config.audioCapabilities = [{contentType: 'audio/webm; codecs="vorbis"'}];
      config.videoCapabilities = [{contentType: 'video/webm; codecs="vp8"'}];
    } else if (
        player.testConfig.mediaType == 'video/webm; codecs="opus, vp9"') {
      config.audioCapabilities = [{contentType: 'audio/webm; codecs="opus"'}];
      config.videoCapabilities = [{contentType: 'video/webm; codecs="vp9"'}];
    } else if (
        player.testConfig.mediaType ==
        'video/mp4; codecs="mp4a.40.2, avc1.64001E"') {
      config.audioCapabilities =
          [{contentType: 'audio/mp4; codecs="mp4a.40.2"'}];
      config.videoCapabilities =
          [{contentType: 'video/mp4; codecs="avc1.64001E"'}];
    }
  } else {
    // Some tests (e.g. mse_different_containers.html) specify audio and
    // video codecs separately.
    if (player.testConfig.videoFormat) {
      config.videoCapabilities = [{contentType: player.testConfig.videoFormat}];
    }
    if (player.testConfig.audioFormat) {
      config.audioCapabilities = [{contentType: player.testConfig.audioFormat}];
    }
  }

  // The File IO test requires persistent state support.
  if (player.testConfig.keySystem == FILE_IO_TEST_KEYSYSTEM) {
    config.persistentState = 'required';
  } else if (
      player.testConfig.sessionToLoad ||
      player.testConfig.keySystem == STORAGE_ID_TEST_KEYSYSTEM) {
    config.persistentState = 'required';
    config.sessionTypes = ['temporary', 'persistent-license'];
  }

  return navigator
      .requestMediaKeySystemAccess(player.testConfig.keySystem, [config])
      .then(function(access) {
        player.access = access;
        return access.createMediaKeys();
      })
      .then(function(mediaKeys) {
        return player.video.setMediaKeys(mediaKeys);
      })
      .then(function(result) {
        return player;
      })
      .catch(function(error) {
        Utils.failTest(error, NOTSUPPORTEDERROR);
      });
};

PlayerUtils.setVideoSource = function(player) {
  if (player.testConfig.useMSE) {
    Utils.timeLog('Loading media using MSE.');
    var mediaSource =
        MediaSourceUtils.loadMediaSourceFromTestConfig(player.testConfig);
    player.video.src = window.URL.createObjectURL(mediaSource);
  } else {
    Utils.timeLog('Loading media using src.');
    player.video.src = player.testConfig.mediaFile;
  }
  Utils.timeLog('video.src has been set to ' + player.video.src);
};

// Initialize the player to play encrypted content. Returns a promise that
// resolves to the player.
PlayerUtils.initEMEPlayer = function(player) {
  return player.registerEventListeners().then(function(result) {
    PlayerUtils.setVideoSource(player);
    Utils.timeLog('initEMEPlayer() done');
    return player;
  });
};

// Return the appropriate player based on test configuration.
PlayerUtils.createPlayer = function(video, testConfig) {
  function getPlayerType(keySystem) {
    switch (keySystem) {
      case WIDEVINE_KEYSYSTEM:
        return WidevinePlayer;
      case CLEARKEY:
      case EXTERNAL_CLEARKEY:
      case MEDIAFOUNDATION_CLEARKEY:
      case MESSAGE_TYPE_TEST_KEYSYSTEM:
      case CRASH_TEST_KEYSYSTEM:
        return ClearKeyPlayer;
      case FILE_IO_TEST_KEYSYSTEM:
      case OUTPUT_PROTECTION_TEST_KEYSYSTEM:
      case PLATFORM_VERIFICATION_TEST_KEYSYSTEM:
      case VERIFY_HOST_FILES_TEST_KEYSYSTEM:
      case STORAGE_ID_TEST_KEYSYSTEM:
        return UnitTestPlayer;
      default:
        Utils.timeLog(keySystem + ' is not a known key system');
        return ClearKeyPlayer;
    }
  }
  var Player = getPlayerType(testConfig.keySystem);
  return new Player(video, testConfig);
};

PlayerUtils.removeSession = async function(player) {
  // Once remove() is called, another 'keystatuseschange' and 'message' events
  // will happen.
  const waitForKeyStatusChangePromise =
      Utils.waitForEvent(player.session, 'keystatuseschange');

  const waitForMessagePromise = Utils.waitForEvent(
      player.session, 'message', function(e, resolve, reject) {
        Utils.timeLog(e.messageType);
        resolve();
      });

  Utils.timeLog('Calling remove()');
  const removePromise = player.session.remove();

  return Promise.all(
      [removePromise, waitForKeyStatusChangePromise, waitForMessagePromise]);
}