| 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:'); |
| } |