summaryrefslogtreecommitdiffstats
path: root/dom/media/test/eme.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/test/eme.js')
-rw-r--r--dom/media/test/eme.js495
1 files changed, 495 insertions, 0 deletions
diff --git a/dom/media/test/eme.js b/dom/media/test/eme.js
new file mode 100644
index 000000000..81f679762
--- /dev/null
+++ b/dom/media/test/eme.js
@@ -0,0 +1,495 @@
+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 UpdateSessionFunc(test, token, sessionType, resolve, reject) {
+ return function(ev) {
+ var msgStr = ArrayBufferToString(ev.message);
+ var msg = JSON.parse(msgStr);
+
+ Log(token, "got message from CDM: " + msgStr);
+ is(msg.type, sessionType, TimeStamp(token) + " key session type should match");
+ ok(msg.kids, TimeStamp(token) + " message event should contain key ID array");
+
+ var outKeys = [];
+
+ for (var i = 0; i < msg.kids.length; i++) {
+ var id64 = msg.kids[i];
+ var idHex = Base64ToHex(msg.kids[i]).toLowerCase();
+ var key = test.keys[idHex];
+
+ if (key) {
+ Log(token, "found key " + key + " for key id " + idHex);
+ outKeys.push({
+ "kty":"oct",
+ "kid":id64,
+ "k":HexToBase64(key)
+ });
+ } else {
+ bail(token + " couldn't find key for key id " + idHex);
+ }
+ }
+
+ var update = JSON.stringify({
+ "keys" : outKeys,
+ "type" : msg.type
+ });
+ Log(token, "sending update message to CDM: " + update);
+
+ ev.target.update(StringToArrayBuffer(update)).then(function() {
+ Log(token, "MediaKeySession update ok!");
+ resolve(ev.target);
+ }).catch(function(reason) {
+ bail(token + " MediaKeySession update failed")(reason);
+ reject();
+ });
+ }
+}
+
+function MaybeCrossOriginURI(test, uri)
+{
+ if (test.crossOrigin) {
+ return "http://test2.mochi.test:8888/tests/dom/media/test/allowed.sjs?" + uri;
+ } else {
+ return uri;
+ }
+}
+
+function AppendTrack(test, ms, track, token, loadParams)
+{
+ return new Promise(function(resolve, reject) {
+ var sb;
+ var curFragment = 0;
+ var resolved = false;
+ var fragments = track.fragments;
+ var fragmentFile;
+
+ if (loadParams && loadParams.onlyLoadFirstFragments) {
+ fragments = fragments.slice(0, loadParams.onlyLoadFirstFragments);
+ }
+
+ 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(){info(token + " error fetching " + fragmentFile);});
+ req.addEventListener("abort", function(){info(token + " 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() {
+ if (ms.readyState == "ended") {
+ /* We can get another updateevent as a result of calling ms.endOfStream() if
+ the highest end time of our source buffers is different from that of the
+ media source duration. Due to bug 1065207 this can happen because of
+ inaccuracies in the frame duration calculations. Check if we are already
+ "ended" and ignore the update event */
+ Log(token, track.name + ": updateend when readyState already 'ended'");
+ if (!resolved) {
+ // Needed if decoder knows this was the last fragment and ended by itself.
+ Log(token, track.name + ": but promise not resolved yet -> end of track");
+ resolve();
+ resolved = true;
+ }
+ return;
+ }
+ 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, loadParams)
+{
+ 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);
+
+ return new Promise(function (resolve, reject) {
+ var firstOpen = true;
+ ms.addEventListener("sourceopen", function () {
+ if (!firstOpen) {
+ Log(token, "sourceopen again?");
+ return;
+ }
+
+ firstOpen = false;
+ Log(token, "sourceopen");
+ return Promise.all(test.tracks.map(function(track) {
+ return AppendTrack(test, ms, track, token, loadParams);
+ })).then(function() {
+ if (loadParams && loadParams.noEndOfStream) {
+ Log(token, "Tracks loaded");
+ } else {
+ Log(token, "Tracks loaded, calling MediaSource.endOfStream()");
+ ms.endOfStream();
+ }
+ resolve();
+ }).catch(function() {
+ Log(token, "error while loading tracks");
+ });
+ })
+ });
+}
+
+// Same as LoadTest, but manage a token+"_load" start&finished.
+// Also finish main token if loading fails.
+function LoadTestWithManagedLoadToken(test, elem, manager, token, loadParams)
+{
+ manager.started(token + "_load");
+ return LoadTest(test, elem, token, loadParams)
+ .catch(function (reason) {
+ ok(false, TimeStamp(token) + " - Error during load: " + reason);
+ manager.finished(token + "_load");
+ manager.finished(token);
+ })
+ .then(function () {
+ manager.finished(token + "_load");
+ });
+}
+
+function SetupEME(test, token, params)
+{
+ var v = document.createElement("video");
+ v.crossOrigin = test.crossOrigin || false;
+ v.sessions = [];
+
+ v.closeSessions = function() {
+ return Promise.all(v.sessions.map(s => s.close().then(() => s.closed))).then(
+ () => {
+ v.setMediaKeys(null);
+ if (v.parentNode) {
+ v.parentNode.removeChild(v);
+ }
+ v.onerror = null;
+ v.src = null;
+ });
+ };
+
+ // Log events dispatched to make debugging easier...
+ [ "canplay", "canplaythrough", "ended", "error", "loadeddata",
+ "loadedmetadata", "loadstart", "pause", "play", "playing", "progress",
+ "stalled", "suspend", "waiting", "waitingforkey",
+ ].forEach(function (e) {
+ v.addEventListener(e, function(event) {
+ Log(token, "" + e);
+ }, false);
+ });
+
+ // Finish the test when error is encountered.
+ v.onerror = bail(token + " got error event");
+
+ var onSetKeysFail = (params && params.onSetKeysFail)
+ ? params.onSetKeysFail
+ : bail(token + " Failed to set MediaKeys on <video> element");
+
+ // null: No session management in progress, just go ahead and update the session.
+ // [...]: Session management in progress, add {initDataType, initData} to
+ // this queue to get it processed when possible.
+ var initDataQueue = [];
+ function pushInitData(ev)
+ {
+ if (initDataQueue === null) {
+ initDataQueue = [];
+ }
+ initDataQueue.push(ev);
+ if (params && params.onInitDataQueued) {
+ params.onInitDataQueued(ev, ev.initDataType, StringToHex(ArrayBufferToString(ev.initData)));
+ }
+ }
+
+ function processInitDataQueue()
+ {
+ if (initDataQueue === null) { return; }
+ // If we're processed all our init data null the queue to indicate encrypted event handled.
+ if (initDataQueue.length === 0) {
+ initDataQueue = null;
+ return;
+ }
+ var ev = initDataQueue.shift();
+
+ var sessionType = (params && params.sessionType) ? params.sessionType : "temporary";
+ Log(token, "createSession(" + sessionType + ") for (" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + ")");
+ var session = v.mediaKeys.createSession(sessionType);
+ if (params && params.onsessioncreated) {
+ params.onsessioncreated(session);
+ }
+ v.sessions.push(session);
+
+ return new Promise(function (resolve, reject) {
+ session.addEventListener("message", UpdateSessionFunc(test, token, sessionType, resolve, reject));
+ Log(token, "session[" + session.sessionId + "].generateRequest(" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + ")");
+ session.generateRequest(ev.initDataType, ev.initData).catch(function(reason) {
+ // Reject the promise if generateRequest() failed. Otherwise it will
+ // be resolve in UpdateSessionFunc().
+ bail(token + ": session[" + session.sessionId + "].generateRequest(" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + ") failed")(reason);
+ reject();
+ });
+ })
+
+ .then(function(aSession) {
+ Log(token, "session[" + session.sessionId + "].generateRequest(" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + ") succeeded");
+ if (params && params.onsessionupdated) {
+ params.onsessionupdated(aSession);
+ }
+ processInitDataQueue();
+ });
+ }
+
+ function streamType(type) {
+ var x = test.tracks.find(o => o.name == type);
+ return x ? x.type : undefined;
+ }
+
+ // If sessions are to be delayed we won't peform any processing until the
+ // callback the assigned here is called by the test.
+ if (params && params.delaySessions) {
+ params.ProcessSessions = processInitDataQueue;
+ }
+
+ // Is this the first piece of init data we're processing?
+ var firstInitData = true;
+ v.addEventListener("encrypted", function(ev) {
+ if (firstInitData) {
+ Log(token, "got first encrypted(" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + "), setup session");
+ firstInitData = false;
+ pushInitData(ev);
+
+ function chain(promise, onReject) {
+ return promise.then(function(value) {
+ return Promise.resolve(value);
+ }).catch(function(reason) {
+ onReject(reason);
+ return Promise.reject();
+ })
+ }
+
+ var options = { initDataTypes: [ev.initDataType] };
+ if (streamType("video")) {
+ options.videoCapabilities = [{contentType: streamType("video")}];
+ }
+ if (streamType("audio")) {
+ options.audioCapabilities = [{contentType: streamType("audio")}];
+ }
+
+ var p = navigator.requestMediaKeySystemAccess(CLEARKEY_KEYSYSTEM, [options]);
+ var r = bail(token + " Failed to request key system access.");
+ chain(p, r)
+ .then(function(keySystemAccess) {
+ var p = keySystemAccess.createMediaKeys();
+ var r = bail(token + " Failed to create MediaKeys object");
+ return chain(p, r);
+ })
+
+ .then(function(mediaKeys) {
+ Log(token, "created MediaKeys object ok");
+ mediaKeys.sessions = [];
+ var p = v.setMediaKeys(mediaKeys);
+ return chain(p, onSetKeysFail);
+ })
+
+ .then(function() {
+ Log(token, "set MediaKeys on <video> element ok");
+ if (params && params.onMediaKeysSet) {
+ params.onMediaKeysSet();
+ }
+ if (!(params && params.delaySessions)) {
+ processInitDataQueue();
+ }
+ })
+ } else {
+ if (params && params.delaySessions) {
+ Log(token, "got encrypted(" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + ") event, queue it in because we're delaying sessions");
+ pushInitData(ev);
+ } else if (initDataQueue !== null) {
+ Log(token, "got encrypted(" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + ") event, queue it for later session update");
+ pushInitData(ev);
+ } else {
+ Log(token, "got encrypted(" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + ") event, update session now");
+ pushInitData(ev);
+ processInitDataQueue();
+ }
+ }
+ });
+ return v;
+}
+
+function SetupEMEPref(callback) {
+ var prefs = [
+ [ "media.mediasource.enabled", true ],
+ [ "media.eme.apiVisible", true ],
+ [ "media.mediasource.webm.enabled", true ],
+ ];
+
+ if (SpecialPowers.Services.appinfo.name == "B2G" ||
+ !manifestVideo().canPlayType("video/mp4")) {
+ // XXX remove once we have mp4 PlatformDecoderModules on all platforms.
+ prefs.push([ "media.use-blank-decoder", true ]);
+ }
+
+ SpecialPowers.pushPrefEnv({ "set" : prefs }, callback);
+}
+
+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 onceEvent(arg) {
+ target.removeEventListener(name, onceEvent);
+ resolve(arg);
+ });
+ });
+ if (cb) {
+ p.then(cb);
+ }
+ return p;
+}