diff options
Diffstat (limited to 'dom/media/test/eme.js')
-rw-r--r-- | dom/media/test/eme.js | 479 |
1 files changed, 479 insertions, 0 deletions
diff --git a/dom/media/test/eme.js b/dom/media/test/eme.js new file mode 100644 index 0000000000..927c99876a --- /dev/null +++ b/dom/media/test/eme.js @@ -0,0 +1,479 @@ +/* import-globals-from manifest.js */ + +const CLEARKEY_KEYSYSTEM = "org.w3.clearkey"; + +const gCencMediaKeySystemConfig = [ + { + initDataTypes: ["cenc"], + videoCapabilities: [{ contentType: "video/mp4" }], + audioCapabilities: [{ contentType: "audio/mp4" }], + }, +]; + +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 <video> element.`) + ); + }, p.reject); + + return p.promise; +} + +/* + * Collect the init data from 'encrypted' events. + * Return a promise which will be resolved with the init data when collection + * is completed (specified by test.sessionCount). + */ +function LoadInitData(v, test, token) { + let p = new EMEPromise(); + let initDataQueue = []; + + // Call SimpleTest._originalSetTimeout() to bypass the flaky timeout checker. + let timer = SimpleTest._originalSetTimeout.call( + window, + () => { + p.reject(`${token} Timed out in waiting for the init data.`); + }, + 60000 + ); + + function onencrypted(ev) { + initDataQueue.push(ev); + Log( + token, + `got encrypted(${ev.initDataType}, ` + + `${StringToHex(ArrayBufferToString(ev.initData))}) event.` + ); + if (test.sessionCount == initDataQueue.length) { + p.resolve(initDataQueue); + clearTimeout(timer); + } + } + + v.addEventListener("encrypted", onencrypted); + return p.promise; +} + +/* + * Generate a license request and update the session. + * Return a promsise which will be resolved with the updated session + * or rejected with a string that describes the failure. + */ +function MakeRequest(test, token, ev, session, sessionType) { + sessionType = sessionType || "temporary"; + let p = new EMEPromise(); + let str = + `session[${session.sessionId}].generateRequest(` + + `${ev.initDataType}, ${StringToHex(ArrayBufferToString(ev.initData))})`; + + session.addEventListener( + "message", + UpdateSessionFunc(test, token, sessionType, p.resolve, p.reject) + ); + + Log(token, str); + session.generateRequest(ev.initDataType, ev.initData).catch(reason => { + // Reject the promise if generateRequest() failed. + // Otherwise it will be resolved in UpdateSessionFunc(). + p.reject(`${token}: ${str} failed; ${reason}`); + }); + + return p.promise; +} + +/* + * Process the init data by calling MakeRequest(). + * Return a promise which will be resolved with the updated sessions + * when all init data are processed or rejected if any failure. + */ +function ProcessInitData(v, test, token, initData, sessionType) { + return Promise.all( + initData.map(ev => { + let session = v.mediaKeys.createSession(sessionType); + return MakeRequest(test, token, ev, session, sessionType); + }) + ); +} + +/* + * Clean up the |v| element. + */ +function CleanUpMedia(v) { + v.setMediaKeys(null); + v.remove(); + v.removeAttribute("src"); + v.load(); +} + +/* + * Close all sessions and clean up the |v| element. + */ +function CloseSessions(v, sessions) { + return Promise.all(sessions.map(s => s.close())).then(CleanUpMedia(v)); +} + +/* + * Set up media keys and source buffers for the media element. + * Return a promise resolved when all key sessions are updated or rejected + * if any failure. + */ +function SetupEME(v, test, token) { + let p = new EMEPromise(); + + v.onerror = function () { + p.reject(`${token} got an error event.`); + }; + + Promise.all([ + LoadInitData(v, test, token), + CreateAndSetMediaKeys(v, test, token), + LoadTest(test, v, token), + ]) + .then(values => { + let initData = values[0]; + return ProcessInitData(v, test, token, initData); + }) + .then(p.resolve, p.reject); + + return p.promise; +} + +function fetchWithXHR(uri, onLoadFunction) { + var p = new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", uri, true); + xhr.responseType = "arraybuffer"; + xhr.addEventListener("load", function () { + is( + xhr.status, + 200, + "fetchWithXHR load uri='" + uri + "' status=" + xhr.status + ); + resolve(xhr.response); + }); + xhr.send(); + }); + + if (onLoadFunction) { + p.then(onLoadFunction); + } + + return p; +} + +function once(target, name, cb) { + var p = new Promise(function (resolve, reject) { + target.addEventListener( + name, + function (arg) { + resolve(arg); + }, + { once: true } + ); + }); + if (cb) { + p.then(cb); + } + return p; +} |