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" : 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; } else { return uri; } } function AppendTrack(test, ms, track, token) { return new Promise(function(resolve, reject) { var sb; var curFragment = 0; var resolved = false; var fragments = track.fragments; var fragmentFile; function addNextFragment() { if (curFragment >= fragments.length) { Log(token, track.name + ": end of track"); resolve(); resolved = true; 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