chromium/media/test/data/eme_player_js/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.

// Utils provide logging functions and other JS functions commonly used by the
// app and media players.
var Utils = new function() {
  this.titleChanged = false;
};

// Adds options to document element.
Utils.addOptions = function(elementID, keyValueOptions, disabledOptions) {
  disabledOptions = disabledOptions || [];
  var selectElement = document.getElementById(elementID);
  var keys = Object.keys(keyValueOptions);
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var option = new Option(key, keyValueOptions[key]);
    option.title = keyValueOptions[key];
    if (disabledOptions.indexOf(key) >= 0)
      option.disabled = true;
    selectElement.options.add(option);
  }
};

Utils.convertToArray = function(input) {
  if (Array.isArray(input))
    return input;
  return [input];
};

Utils.convertToUint8Array = function(msg) {
  if (typeof msg == 'string') {
    var ans = new Uint8Array(msg.length);
    for (var i = 0; i < msg.length; i++) {
      ans[i] = msg.charCodeAt(i);
    }
    return ans;
  }
  // Assume it is an ArrayBuffer or ArrayBufferView. If it already is a
  // Uint8Array, this will just make a copy of the view.
  return new Uint8Array(msg);
};

// Encodes data (Uint8Array) into base64url string. There is no '=' padding,
// and the characters '-' and '_' must be used instead of '+' and '/',
// respectively.
Utils.base64urlEncode = function(data) {
  var result = btoa(String.fromCharCode.apply(null, data));
  return result.replace(/=+$/g, '').replace(/\+/g, '-').replace(/\//g, '_');
};

Utils.createJWKData = function(keyId, key) {
  // JWK routines copied from third_party/WebKit/LayoutTests/media/
  //   encrypted-media/encrypted-media-utils.js

  // Creates a JWK from raw key ID and key.
  function createJWK(keyId, key) {
    var jwk = '{"kty":"oct","kid":"';
    jwk += Utils.base64urlEncode(Utils.convertToUint8Array(keyId));
    jwk += '","k":"';
    jwk += Utils.base64urlEncode(Utils.convertToUint8Array(key));
    jwk += '"}';
    return jwk;
  }

  // Creates a JWK Set from an array of JWK(s).
  function createJWKSet() {
    var jwkSet = '{"keys":[';
    for (var i = 0; i < arguments.length; i++) {
      if (i != 0)
        jwkSet += ',';
      jwkSet += arguments[i];
    }
    jwkSet += '], "type":"temporary"}';
    return jwkSet;
  }

  return Utils.convertToUint8Array(createJWKSet(createJWK(keyId, key)));
};

Utils.createKeyIdsInitializationData = function(keyId) {
  var initData = '{"kids":["';
  initData += Utils.base64urlEncode(Utils.convertToUint8Array(keyId));
  initData += '"]}';
  return Utils.convertToUint8Array(initData);
};

function convertToString(data) {
  return String.fromCharCode.apply(null, Utils.convertToUint8Array(data));
}

Utils.extractFirstLicenseKeyId = function(message) {
  // Decodes data (Uint8Array) from base64url string.
  function base64urlDecode(data) {
    return atob(data.replace(/\-/g, "+").replace(/\_/g, "/"));
  }

  try {
    var json = JSON.parse(convertToString(message));
    // Decode the first element of 'kids', return it as an Uint8Array.
    return Utils.convertToUint8Array(base64urlDecode(json.kids[0]));
  } catch (error) {
    // Not valid JSON, so return message untouched as Uint8Array.
    return Utils.convertToUint8Array(message);
  }
};

Utils.verifyUsageRecord =
    function(message, expectNullTime) {
  try {
    var json = JSON.parse(convertToString(message));
    if (expectNullTime && (json.firstTime != null || json.latestTime != null)) {
      Utils.failTest(
          'Expecting null time for usage record but got firstTime=' +
          json.firstTime + ', latestTime=' + json.latestTime);
    } else if (!expectNullTime) {
      var first_decrypt_time = new Date(json.firstTime);
      var last_decrypt_time = new Date(json.latestTime);
      Utils.timeLog('First decrypt time: ' + first_decrypt_time.toISOString());
      Utils.timeLog('Last decrypt time: ' + last_decrypt_time.toISOString());

      var delta = json.latestTime - json.firstTime;
      // The video used for the tests is roughly 2.5 seconds.
      if (delta < 2000 || delta > 3000) {
        Utils.failTest(
            'The usage record reported by the CDM was not in the ' +
            'expected range')
      }
    }

  } catch (error) {
    Utils.failTest(
        'Fail to extract first decrypt time from license-release' +
        'message');
  }
}

    Utils.documentLog = function(log, success, time) {
  if (!docLogs)
    return;
  time = time || Utils.getCurrentTimeString();
  var timeLog = '<span style="color: green">' + time + '</span>';
  var logColor = !success ? 'red' : 'black'; // default is true.
  log = '<span style="color: "' + logColor + '>' + log + '</span>';
  docLogs.innerHTML = timeLog + ' - ' + log + '<br>' + docLogs.innerHTML;
};

Utils.ensureOptionInList = function(listID, option) {
  var selectElement = document.getElementById(listID);
  for (var i = 0; i < selectElement.length; i++) {
    if (selectElement.options[i].value == option) {
      selectElement.value = option;
      return;
    }
  }
  // The list does not have the option, let's add it and select it.
  var optionElement = new Option(option, option);
  optionElement.title = option;
  selectElement.options.add(optionElement);
  selectElement.value = option;
};

Utils.failTest = function(msg, newTitle) {
  var failMessage = 'FAIL: ';
  var title = 'FAILED';
  // Handle exception messages;
  if (msg.message) {
    title = msg.name || 'Error';
    failMessage += title + ' ' + msg.message;
  } else if (msg instanceof Event) {
    // Handle failing events.
    failMessage = msg.target + '.' + msg.type;
    title = msg.type;
  } else {
    failMessage += msg;
  }
  // Force newTitle if passed.
  title = newTitle || title;
  // Log failure.
  Utils.documentLog(failMessage, false);
  console.log(failMessage, msg);
  Utils.setResultInTitle(title);
};

Utils.getCurrentTimeString = function() {
  var date = new Date();
  var hours = ('0' + date.getHours()).slice(-2);
  var minutes = ('0' + date.getMinutes()).slice(-2);
  var secs = ('0' + date.getSeconds()).slice(-2);
  var milliSecs = ('00' + date.getMilliseconds()).slice(-3);
  return hours + ':' + minutes + ':' + secs + '.' + milliSecs;
};

Utils.getDefaultKey = function(forceInvalidResponse) {
  if (forceInvalidResponse) {
    Utils.timeLog('Forcing invalid key data.');
    return new Uint8Array([0xAA]);
  }
  return KEY;
};

Utils.getHexString = function(uintArray) {
  var hex_str = '';
  for (var i = 0; i < uintArray.length; i++) {
    var hex = uintArray[i].toString(16);
    if (hex.length == 1)
      hex = '0' + hex;
    hex_str += hex;
  }
  return hex_str;
};

Utils.hasPrefix = function(msg, prefix) {
  var message = String.fromCharCode.apply(null, Utils.convertToUint8Array(msg));
  return message.substring(0, prefix.length) == prefix;
};

Utils.installTitleEventHandler = function(element, event) {
  element.addEventListener(event, function(e) {
    Utils.setResultInTitle(e.type.toUpperCase());
  }, false);
};

Utils.resetTitleChange = function() {
  this.titleChanged = false;
  document.title = '';
};

Utils.sendRequest = function(
    requestType, responseType, message, serverURL, onResponseCallbackFn,
    forceInvalidResponse) {
  var requestAttemptCount = 0;
  var REQUEST_RETRY_DELAY_MS = 3000;
  var REQUEST_TIMEOUT_MS = 1000;

  function sendRequestAttempt() {
    // No limit on the number of retries. This will retry on failures
    // until the test framework stops the test.
    requestAttemptCount++;
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.responseType = responseType;
    xmlhttp.open(requestType, serverURL, true);
    xmlhttp.onerror = function(e) {
      Utils.timeLog('Request status: ' + this.statusText);
      Utils.timeLog('FAILED: License request XHR failed with network error.');
      Utils.timeLog('Retrying request in ' + REQUEST_RETRY_DELAY_MS + 'ms');
      setTimeout(sendRequestAttempt, REQUEST_RETRY_DELAY_MS);
    };
    xmlhttp.onload = function(e) {
      if (this.status == 200) {
        onResponseCallbackFn(this.response);
      } else if (this.status == 404 && serverURL == DEFAULT_LICENSE_SERVER) {
        // If using the default license server, no page available means there
        // is no license server configured.
        onResponseCallbackFn(Utils.convertToUint8Array("No license."));
      } else {
        Utils.timeLog('Bad response status: ' + this.status);
        Utils.timeLog('Bad response: ' + this.response);
        Utils.timeLog('Retrying request in ' + REQUEST_RETRY_DELAY_MS + 'ms');
        setTimeout(sendRequestAttempt, REQUEST_RETRY_DELAY_MS);
      }
    };
    xmlhttp.timeout = REQUEST_TIMEOUT_MS;
    xmlhttp.ontimeout = function(e) {
      Utils.timeLog('Request timeout');
      Utils.timeLog('Retrying request in ' + REQUEST_RETRY_DELAY_MS + 'ms');
      setTimeout(sendRequestAttempt, REQUEST_RETRY_DELAY_MS);
    }
    Utils.timeLog('Attempt (' + requestAttemptCount +
                  '): sending request to server: ' + serverURL);
    xmlhttp.send(message);
  }

  if (forceInvalidResponse) {
    Utils.timeLog('Not sending request - forcing an invalid response.');
    return onResponseCallbackFn(Utils.convertToUint8Array("Invalid response."));
  }
  sendRequestAttempt();
};

Utils.setResultInTitle = function(title) {
  // If document title is 'ENDED', then update it with new title to possibly
  // mark a test as failure.  Otherwise, keep the first title change in place.
  if (!this.titleChanged || document.title == 'ENDED')
    document.title = title;
  Utils.timeLog('Set document title to: ' + title + ', updated title: ' +
                document.title);
  this.titleChanged = true;
};

Utils.timeLog = function(/**/) {
  if (arguments.length == 0)
    return;
  var time = Utils.getCurrentTimeString();
  // Log to document.
  Utils.documentLog(arguments[0], time);
  // Log to JS console.
  var logString = time + ' - ';
  for (var i = 0; i < arguments.length; i++) {
    logString += ' ' + arguments[i];
  }
  console.log(logString);
};

// Convert an event into a promise. When |event| is fired on |object|,
// call |func| to handle the event and either resolve or reject the promise.
// If |func| is not specified, the promise will simply be resolved with the
// event when the event happens.
Utils.waitForEvent = function(object, event, func) {
  return new Promise(function(resolve, reject) {
    object.addEventListener(event, function listener(e) {
      object.removeEventListener(event, listener);
      if (func) {
        func(e, resolve, reject);
      } else {
        // No |func| is specified, so simply resolve the promise passing
        // |event| in case the caller is interested in it.
        resolve(event);
      }
    });
  });
};

// Create a loadable session and return the session ID of it as a promise.
Utils.createSessionToLoad = function(mediaKeys, request) {
  // Create a persistent session and on the message event initialize it
  // with key ID |KEY_ID| and key |KEY|. Then close the session, and resolve
  // the promise providing the session ID.
  const keySession = mediaKeys.createSession('persistent-license');
  var promise =
      Utils.waitForEvent(keySession, 'message', function(e, resolve, reject) {
        const session = e.target;
        return session.update(Utils.createJWKData(KEY_ID, KEY))
            .then(function(result) {
              Utils.timeLog(
                  'Persistent session ' + session.sessionId + ' created');
              return session.close();
            })
            .then(function(result) {
              // Make sure the session is properly closed before continuing on.
              return session.closed;
            })
            .then(function(result) {
              Utils.timeLog(
                  'Persistent session ' + session.sessionId +
                  ' saved and closed');
              resolve(session.sessionId);
            });
      });
  return keySession
      .generateRequest('keyids', Utils.createKeyIdsInitializationData(KEY_ID))
      .then(function() {
        return promise;
      });
};

// Verify that |keyStatuses| contains just the keys in the array |expected|.
// Each entry specifies the keyId and status expected.
// Example call: verifyKeyStatuses(mediaKeySession.keyStatuses,
//   [{keyId: key1, status: 'usable'}, {keyId: key2, status: 'released'}]);
Utils.verifyKeyStatuses = function(keyStatuses, expected) {
  // |keyStatuses| should have same size as number of |keys.expected|.
  if (keyStatuses.size !== expected.length) {
    Utils.failTest(
        'keystatuses should have expected size of ' + expected.length +
        ' but has size ' + keyStatuses.size);
  }

  // All |expected| should be found.
  expected.map(function(item) {
    if (!keyStatuses.has(Utils.convertToUint8Array(item.keyId))) {
      Utils.failTest('missing keyID ' + item.keyId);
    }
    if (keyStatuses.get(Utils.convertToUint8Array(item.keyId)) !==
        item.status) {
      Utils.failTest(
          'keyId ' + item.keyId + ' has status ' +
          keyStatuses.get(Utils.convertToUint8Array(item.keyId)) +
          ', expected ' + item.status);
    }
  });
};