chromium/third_party/blink/web_tests/media/encrypted-media/encrypted-media-utils.js

var consoleDiv = null;

function consoleWrite(text)
{
    if (!consoleDiv && document.body) {
        consoleDiv = document.createElement('div');
        document.body.appendChild(consoleDiv);
    }
    var span = document.createElement('span');
    span.appendChild(document.createTextNode(text));
    span.appendChild(document.createElement('br'));
    consoleDiv.appendChild(span);
}

// Returns a promise that is fulfilled with true if |initDataType| is supported,
// or false if not.
function isInitDataTypeSupported(initDataType)
{
    return navigator.requestMediaKeySystemAccess(
                        "org.w3.clearkey", getSimpleConfigurationForInitDataType(initDataType))
        .then(function() { return true; }, function() { return false; });
}

function getInitData(initDataType)
{
  if (initDataType == 'webm') {
      return new Uint8Array([
          0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
          0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F
      ]);
  }

  if (initDataType == 'cenc') {
      return new Uint8Array([
          0x00, 0x00, 0x00, 0x34,                          // size = 52
          0x70, 0x73, 0x73, 0x68,                          // 'pssh'
          0x01,                                            // version = 1
          0x00, 0x00, 0x00,                                // flags
          0x10, 0x77, 0xEF, 0xEC, 0xC0, 0xB2, 0x4D, 0x02,  // Common SystemID
          0xAC, 0xE3, 0x3C, 0x1E, 0x52, 0xE2, 0xFB, 0x4B,
          0x00, 0x00, 0x00, 0x01,                          // key count
          0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,  // key
          0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
          0x00, 0x00, 0x00, 0x00                           // datasize
     ]);
  }

  if (initDataType == 'keyids') {
      var keyId = new Uint8Array([
          0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
          0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F
      ]);
      return stringToUint8Array(createKeyIDs(keyId));
  }

  throw 'initDataType ' + initDataType + ' not supported.';
}

// Returns an array of audioCapabilities that includes entries for a set of
// codecs that should cover all user agents.
function getPossibleAudioCapabilities()
{
    return [
        { contentType: 'audio/mp4; codecs="mp4a.40.2"' },
        { contentType: 'audio/webm; codecs="opus"' },
    ];
}

// Returns a trivial MediaKeySystemConfiguration that should be accepted,
// possibly as a subset of the specified capabilities, by all user agents.
function getSimpleConfiguration()
{
    return [ {
        initDataTypes : [ 'webm', 'cenc', 'keyids' ],
        audioCapabilities: getPossibleAudioCapabilities()
    } ];
}

// Returns a MediaKeySystemConfiguration for |initDataType| that should be
// accepted, possibly as a subset of the specified capabilities, by all
// user agents.
function getSimpleConfigurationForInitDataType(initDataType)
{
    return [ {
        initDataTypes: [ initDataType ],
        audioCapabilities: getPossibleAudioCapabilities()
    } ];
}

// Returns a MediaKeySystemConfiguration for |mediaFile| that specifies
// both audio and video capabilities for the specified file..
function getConfigurationForFile(mediaFile)
{
    if (mediaFile.toLowerCase().endsWith('.webm')) {
        return [ {
            initDataTypes: [ 'webm' ],
            audioCapabilities: [ { contentType: 'audio/webm; codecs="opus"' } ],
            videoCapabilities: [ { contentType: 'video/webm; codecs="vp8"' } ]
        } ];
    }

    // NOTE: Supporting other mediaFormats is not currently implemented as
    // Chromium only tests with WebM files.
    throw 'mediaFile ' + mediaFile + ' not supported.';
}

function waitForEventAndRunStep(eventName, element, func, stepTest)
{
    var eventCallback = function(event) {
        if (func)
            func(event);
    }
    if (stepTest)
        eventCallback = stepTest.step_func(eventCallback);

    element.addEventListener(eventName, eventCallback, true);
}

// Copied from LayoutTests/resources/js-test.js.
// See it for details of why this is necessary.
function asyncGC(callback) {
  if (!callback) {
      return new Promise(resolve => asyncGC(resolve));
  }
  var documentsBefore = internals.numberOfLiveDocuments();
  GCController.asyncCollectAll(function () {
      var documentsAfter = internals.numberOfLiveDocuments();
      if (documentsAfter < documentsBefore)
          asyncGC(callback);
      else
          callback();
  });
}

function createGCPromise()
{
    // Run gc() as a promise.
    return new Promise(
        function(resolve, reject) {
            asyncGC(resolve);
        });
}

function delayToAllowEventProcessingPromise()
{
    return new Promise(
        function(resolve, reject) {
            setTimeout(resolve, 0);
        });
}

function stringToUint8Array(str)
{
    var result = new Uint8Array(str.length);
    for(var i = 0; i < str.length; i++) {
        result[i] = str.charCodeAt(i);
    }
    return result;
}

function arrayBufferAsString(buffer)
{
    // MediaKeySession.keyStatuses iterators return an ArrayBuffer,
    // so convert it into a printable string.
    return String.fromCharCode.apply(null, new Uint8Array(buffer));
}

function dumpKeyStatuses(keyStatuses)
{
    consoleWrite("for (var entry of keyStatuses)");
    for (var entry of keyStatuses) {
        consoleWrite(arrayBufferAsString(entry[0]) + ": " + entry[1]);
    }
    consoleWrite("for (var keyId of keyStatuses.keys())");
    for (var keyId of keyStatuses.keys()) {
        consoleWrite(arrayBufferAsString(keyId));
    }
    consoleWrite("for (var status of keyStatuses.values())");
    for (var status of keyStatuses.values()) {
        consoleWrite(status);
    }
    consoleWrite("for (var entry of keyStatuses.entries())");
    for (var entry of keyStatuses.entries()) {
        consoleWrite(arrayBufferAsString(entry[0]) + ": " + entry[1]);
    }
    consoleWrite("keyStatuses.forEach()");
    keyStatuses.forEach(function(status, keyId) {
        consoleWrite(arrayBufferAsString(keyId) + ": " + status);
    });
}

// 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'}]);
function verifyKeyStatuses(keyStatuses, expected) {
  // |keyStatuses| should have same size as number of |keys.expected|.
  assert_equals(keyStatuses.size, expected.length);

  // All |expected| should be found.
  expected.map(function(item) {
    assert_true(keyStatuses.has(item.keyId));
    assert_equals(keyStatuses.get(item.keyId), item.status);
  });
}

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

// Decode |encoded| using base64url decoding.
function base64urlDecode(encoded)
{
    return atob(encoded.replace(/\-/g, "+").replace(/\_/g, "/"));
}

// For Clear Key, the License Format is a JSON Web Key (JWK) Set, which contains
// a set of cryptographic keys represented by JSON. These helper functions help
// wrap raw keys into a JWK set.
// See:
// https://w3c.github.io/encrypted-media/#clear-key-license-format
// http://tools.ietf.org/html/draft-ietf-jose-json-web-key
//
// Creates a JWK from raw key ID and key.
// |keyId| and |key| are expected to be ArrayBufferViews, not base64-encoded.
function createJWK(keyId, key)
{
    var jwk = '{"kty":"oct","alg":"A128KW","kid":"';
    jwk += base64urlEncode(keyId);
    jwk += '","k":"';
    jwk += base64urlEncode(key);
    jwk += '"}';
    return jwk;
}

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

// Clear Key can also support Key IDs Initialization Data.
// ref: http://w3c.github.io/encrypted-media/keyids-format.html
// Each parameter is expected to be a key id in an Uint8Array.
function createKeyIDs()
{
    var keyIds = '{"kids":["';
    for (var i = 0; i < arguments.length; i++) {
        if (i != 0)
            keyIds += '","';
        keyIds += base64urlEncode(arguments[i]);
    }
    keyIds += '"]}';
    return keyIds;
}

function forceTestFailureFromPromise(test, error, message)
{
    // Promises convert exceptions into rejected Promises. Since there is
    // currently no way to report a failed test in the test harness, errors
    // are reported using force_timeout().
    if (message)
        consoleWrite(message + ': ' + error.message);
    else if (error)
        consoleWrite(error);

    test.force_timeout();
    test.done();
}

function extractSingleKeyIdFromMessage(message)
{
    var json = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(message)));
    // Decode the first element of 'kids'.
    assert_equals(1, json.kids.length);
    var decoded_key = base64urlDecode(json.kids[0]);
    // Convert to an Uint8Array and return it.
    return stringToUint8Array(decoded_key);
}

// Create a MediaKeys object for Clear Key with 1 session. KeyId and key
// required for the video are already known and provided. Returns a promise
// that resolves to the MediaKeys object created.
function createClearKeyMediaKeysAndInitializeWithOneKey(keyId, key)
{
    var mediaKeys;
    var mediaKeySession;
    var request = stringToUint8Array(createKeyIDs(keyId));
    var jwkSet = stringToUint8Array(createJWKSet(createJWK(keyId, key)));

    return navigator.requestMediaKeySystemAccess('org.w3.clearkey', getSimpleConfigurationForInitDataType('keyids')).then(function(access) {
        return access.createMediaKeys();
    }).then(function(result) {
        mediaKeys = result;
        mediaKeySession = mediaKeys.createSession();
        return mediaKeySession.generateRequest('keyids', request);
    }).then(function() {
        return mediaKeySession.update(jwkSet);
    }).then(function() {
        return Promise.resolve(mediaKeys);
    });
}

// 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.
// The event is only fired once.
function waitForSingleEvent(object, event, func) {
  return new Promise(function(resolve, reject) {
    object.addEventListener(event, function listener(e) {
      object.removeEventListener(event, listener);
      func(e, resolve, reject);
    });
  });
};

// Play the specified |content| on |video|. Returns a promise that is resolved
// after the video plays for |duration| seconds.
function playVideoAndWaitForTimeupdate(video, content, duration)
{
    video.src = content;
    video.play();
    return new Promise(function(resolve) {
        video.addEventListener('timeupdate', function listener(event) {
            if (event.target.currentTime < duration)
                return;
            video.removeEventListener('timeupdate', listener);
            resolve('success');
        });
    });
}

// Verifies that the number of existing MediaKey and MediaKeySession objects
// match what is expected.
function verifyMediaKeyAndMediaKeySessionCount(
    expectedMediaKeysCount, expectedMediaKeySessionCount, description)
{
    assert_equals(internals.mediaKeysCount(),
                  expectedMediaKeysCount,
                  description + ', MediaKeys:');
    assert_equals(internals.mediaKeySessionCount(),
                  expectedMediaKeySessionCount,
                  description + ', MediaKeySession:');
}