/* import-globals-from manifest.js */ const CLEARKEY_KEYSYSTEM = "org.w3.clearkey"; const gCencMediaKeySystemConfig = [ { initDataTypes: ["cenc"], videoCapabilities: [{ contentType: "video/mp4" }], audioCapabilities: [{ contentType: "audio/mp4" }], }, ]; function IsMacOSSnowLeopardOrEarlier() { var re = /Mac OS X (\d+)\.(\d+)/; var ver = navigator.userAgent.match(re); if (!ver || ver.length != 3) { return false; } var major = ver[1] | 0; var minor = ver[2] | 0; return major == 10 && minor <= 6; } function bail(message) { return function(err) { if (err) { message += "; " + String(err); } ok(false, message); if (err) { info(String(err)); } SimpleTest.finish(); }; } function ArrayBufferToString(arr) { var str = ""; var view = new Uint8Array(arr); for (var i = 0; i < view.length; i++) { str += String.fromCharCode(view[i]); } return str; } function StringToArrayBuffer(str) { var arr = new ArrayBuffer(str.length); var view = new Uint8Array(arr); for (var i = 0; i < str.length; i++) { view[i] = str.charCodeAt(i); } return arr; } function StringToHex(str) { var res = ""; for (var i = 0; i < str.length; ++i) { res += ("0" + str.charCodeAt(i).toString(16)).slice(-2); } return res; } function Base64ToHex(str) { var bin = window.atob(str.replace(/-/g, "+").replace(/_/g, "/")); var res = ""; for (var i = 0; i < bin.length; i++) { res += ("0" + bin.charCodeAt(i).toString(16)).substr(-2); } return res; } function HexToBase64(hex) { var bin = ""; for (var i = 0; i < hex.length; i += 2) { bin += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); } return window .btoa(bin) .replace(/=/g, "") .replace(/\+/g, "-") .replace(/\//g, "_"); } function TimeRangesToString(trs) { var l = trs.length; if (l === 0) { return "-"; } var s = ""; var i = 0; for (;;) { s += trs.start(i) + "-" + trs.end(i); if (++i === l) { return s; } s += ","; } } function SourceBufferToString(sb) { return ( "SourceBuffer{" + "AppendMode=" + (sb.AppendMode || "-") + ", updating=" + (sb.updating ? "true" : "false") + ", buffered=" + TimeRangesToString(sb.buffered) + ", audioTracks=" + (sb.audioTracks ? sb.audioTracks.length : "-") + ", videoTracks=" + (sb.videoTracks ? sb.videoTracks.length : "-") + "}" ); } function SourceBufferListToString(sbl) { return "SourceBufferList[" + sbl.map(SourceBufferToString).join(", ") + "]"; } function GenerateClearKeyLicense(licenseRequest, keyStore) { var msgStr = ArrayBufferToString(licenseRequest); var msg = JSON.parse(msgStr); var keys = []; for (var i = 0; i < msg.kids.length; i++) { var id64 = msg.kids[i]; var idHex = Base64ToHex(msg.kids[i]).toLowerCase(); var key = keyStore[idHex]; if (key) { keys.push({ kty: "oct", kid: id64, k: HexToBase64(key), }); } } return new TextEncoder().encode( JSON.stringify({ keys, type: msg.type || "temporary", }) ); } function UpdateSessionFunc(test, token, sessionType, resolve, reject) { return function(ev) { var license = GenerateClearKeyLicense(ev.message, test.keys); Log( token, "sending update message to CDM: " + new TextDecoder().decode(license) ); ev.target .update(license) .then(function() { Log(token, "MediaKeySession update ok!"); resolve(ev.target); }) .catch(function(reason) { reject(`${token} MediaKeySession update failed: ${reason}`); }); }; } function MaybeCrossOriginURI(test, uri) { if (test.crossOrigin) { return "https://example.com:443/tests/dom/media/test/allowed.sjs?" + uri; } return uri; } function AppendTrack(test, ms, track, token) { return new Promise(function(resolve, reject) { var sb; var curFragment = 0; var fragments = track.fragments; var fragmentFile; function addNextFragment() { if (curFragment >= fragments.length) { Log(token, track.name + ": end of track"); resolve(); return; } fragmentFile = MaybeCrossOriginURI(test, fragments[curFragment++]); var req = new XMLHttpRequest(); req.open("GET", fragmentFile); req.responseType = "arraybuffer"; req.addEventListener("load", function() { Log( token, track.name + ": fetch of " + fragmentFile + " complete, appending" ); sb.appendBuffer(new Uint8Array(req.response)); }); req.addEventListener("error", function() { reject(`${token} - ${track.name}: error fetching ${fragmentFile}`); }); req.addEventListener("abort", function() { reject(`${token} - ${track.name}: aborted fetching ${fragmentFile}`); }); Log( token, track.name + ": addNextFragment() fetching next fragment " + fragmentFile ); req.send(null); } Log(token, track.name + ": addSourceBuffer(" + track.type + ")"); sb = ms.addSourceBuffer(track.type); sb.addEventListener("updateend", function() { Log( token, track.name + ": updateend for " + fragmentFile + ", " + SourceBufferToString(sb) ); addNextFragment(); }); addNextFragment(); }); } //Returns a promise that is resolved when the media element is ready to have //its play() function called; when it's loaded MSE fragments. function LoadTest(test, elem, token, endOfStream = true) { if (!test.tracks) { ok(false, token + " test does not have a tracks list"); return Promise.reject(); } var ms = new MediaSource(); elem.src = URL.createObjectURL(ms); elem.crossOrigin = test.crossOrigin || false; return new Promise(function(resolve, reject) { ms.addEventListener( "sourceopen", function() { Log(token, "sourceopen"); Promise.all( test.tracks.map(function(track) { return AppendTrack(test, ms, track, token); }) ) .then(function() { Log(token, "Tracks loaded, calling MediaSource.endOfStream()"); if (endOfStream) { ms.endOfStream(); } resolve(); }) .catch(reject); }, { once: true } ); }); } function EMEPromise() { var self = this; self.promise = new Promise(function(resolve, reject) { self.resolve = resolve; self.reject = reject; }); } /* * Create a new MediaKeys object. * Return a promise which will be resolved with a new MediaKeys object, * or will be rejected with a string that describes the failure. */ function CreateMediaKeys(v, test, token) { let p = new EMEPromise(); function streamType(type) { var x = test.tracks.find(o => o.name == type); return x ? x.type : undefined; } function onencrypted(ev) { var options = { initDataTypes: [ev.initDataType] }; if (streamType("video")) { options.videoCapabilities = [{ contentType: streamType("video") }]; } if (streamType("audio")) { options.audioCapabilities = [{ contentType: streamType("audio") }]; } navigator.requestMediaKeySystemAccess(CLEARKEY_KEYSYSTEM, [options]).then( keySystemAccess => { keySystemAccess .createMediaKeys() .then(p.resolve, () => p.reject(`${token} Failed to create MediaKeys object.`) ); }, () => p.reject(`${token} Failed to request key system access.`) ); } v.addEventListener("encrypted", onencrypted, { once: true }); return p.promise; } /* * Create a new MediaKeys object and provide it to the media element. * Return a promise which will be resolved if succeeded, or will be rejected * with a string that describes the failure. */ function CreateAndSetMediaKeys(v, test, token) { let p = new EMEPromise(); CreateMediaKeys(v, test, token).then(mediaKeys => { v.setMediaKeys(mediaKeys).then(p.resolve, () => p.reject(`${token} Failed to set MediaKeys on