summaryrefslogtreecommitdiffstats
path: root/dom/media/test/eme.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/media/test/eme.js479
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;
+}