зеркало из https://github.com/mozilla/gecko-dev.git
497 строки
12 KiB
JavaScript
497 строки
12 KiB
JavaScript
/* 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 SetupEMEPref(callback) {
|
|
var prefs = [
|
|
["media.mediasource.enabled", 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(arg) {
|
|
resolve(arg);
|
|
},
|
|
{ once: true }
|
|
);
|
|
});
|
|
if (cb) {
|
|
p.then(cb);
|
|
}
|
|
return p;
|
|
}
|