blob: d31ead572dcd4ee9c94576a288c0c80e9fd0bd9e [file] [edit]
function once(target, name, callback) {
let promise = new Promise((resolve, reject) => {
target.addEventListener(name, (event) => {
resolve(event);
}, { once: true });
});
if (callback)
promise.then(callback);
return promise;
}
function fetchWithXHR(uri, onLoadFunction) {
let p = new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open("GET", uri, true);
xhr.responseType = "arraybuffer";
xhr.addEventListener("load", () => {
resolve(xhr.response);
});
xhr.send();
});
if (onLoadFunction)
p.then(onLoadFunction);
return p;
};
function loadSegment(sb, typedArrayOrArrayBuffer) {
let typedArray = (typedArrayOrArrayBuffer instanceof ArrayBuffer)
? new Uint8Array(typedArrayOrArrayBuffer)
: typedArrayOrArrayBuffer;
return new Promise((resolve, reject) => {
once(sb, 'update').then(() => { resolve(); });
sb.appendBuffer(typedArray);
});
}
function fetchAndLoad(sb, prefix, chunks, suffix) {
// Fetch the buffers in parallel.
let buffers = {};
let fetches = [];
for (var chunk of chunks)
fetches.push(fetchWithXHR(prefix + chunk + suffix).then(((c, x) => buffers[c] = x).bind(null, chunk)));
// Load them in series, as required per spec.
return Promise.all(fetches).then(() => {
let rv = Promise.resolve();
for (let chunk of chunks)
rv = rv.then(loadSegment.bind(null, sb, buffers[chunk]));
return rv;
});
}
const delay = ms => new Promise(res => setTimeout(res, ms));
function waitForVideoFrame(video, cb) {
const p = new Promise((resolve) => {
video.requestVideoFrameCallback((now, metadata) => resolve([now, metadata]));
});
if (cb)
p.then(cb);
return p;
}
function waitForVideoFrameWithTimeout(video, time, message) {
const framePromise = new Promise((resolve) => {
video.requestVideoFrameCallback((now, metadata) => resolve([now, metadata]));
});
const timeoutPromise = new Promise((resolve) => {
setTimeout(resolve, time, 'timeout');
});
return Promise.race([
framePromise,
timeoutPromise,
]).then(result => {
if (result === 'timeout')
return Promise.reject(new Error(message));
return Promise.resolve(result);
});
}
function waitForVideoFrameUntil(video, time, cb) {
const p = new Promise(resolve => {
const callback = ((now, metadata) => {
if (metadata.mediaTime >= time) {
resolve([now, metadata]);
return;
}
video.requestVideoFrameCallback(callback);
});
video.requestVideoFrameCallback(callback);
});
if (cb)
p.then(cb);
return p;
}
function timeRangesToString(timeRanges) {
if (!timeRanges?.length)
return "[]";
const ranges = [];
for (let i = 0; i < timeRanges?.length; i++) {
const range = "[" + [timeRanges.start(i) + ", " + timeRanges.end(i)] + ")";
ranges.push(range);
}
return ranges.toString();
}
// Play a media element inside a user gesture, ignoring the AbortError that play() rejects with when
// a still-pending play() is interrupted by a later pause() or by teardown (spec behavior, not a
// failure). Any other rejection is rethrown so a genuine play() failure still surfaces. Requires the
// Internals API.
function playIgnoringAbort(mediaElement) {
internals.withUserGesture(() => {
mediaElement.play().catch(error => {
if (error.name !== "AbortError")
throw error;
});
});
}
// Create a near-silent AudioContext under a user gesture and wait until it is actually running before
// returning it, so a caller polling internals.audioSessionActive() is timing the audio-session
// activation and not WebAudio context startup. The tone renders (activating the audio session) but is
// routed through a 0.001 gain so it is inaudible at a desk. Requires the Internals API. The caller owns
// the returned context and should close() it when done.
async function startNearSilentAudioContext() {
let context;
let resumePromise;
internals.withUserGesture(() => {
context = new AudioContext();
const oscillator = context.createOscillator();
const gain = context.createGain();
gain.gain.value = 0.001;
oscillator.connect(gain);
gain.connect(context.destination);
oscillator.start();
resumePromise = context.resume();
});
await resumePromise;
return context;
}
function waitForAudioSessionActiveState(active) {
return new Promise((resolve, reject) => {
if (!window.internals) {
reject(new Error("waitForAudioSessionActiveState requires window.internals"));
return;
}
let tries = 0;
const check = () => {
if (internals.audioSessionActive() === active)
resolve();
else if (++tries >= 200)
reject(new Error(`audio session did not reach active=${active}`));
else
setTimeout(check, 10);
};
check();
});
}
// Waits for the category applied to the real audio session, which the process owning it derives from
// what every web process reported, so it can lag this process asking for its own category.
async function waitForSystemAudioSessionCategory(category) {
if (!window.internals)
throw new Error("waitForSystemAudioSessionCategory requires window.internals");
let observed;
for (let tries = 0; tries < 200; ++tries) {
observed = await internals.systemAudioSessionCategory();
if (observed === category)
return;
await new Promise(resolve => setTimeout(resolve, 10));
}
throw new Error(`system audio session category is "${observed}", expected "${category}"`);
}
// Waits for the category this process computed for its own sessions. The category is applied from a
// task, so a caller that has just changed what a session reports has to wait for it.
async function waitForAudioSessionCategory(category, description) {
if (!window.internals)
throw new Error("waitForAudioSessionCategory requires window.internals");
let observed;
for (let tries = 0; tries < 200; ++tries) {
observed = internals.audioSessionCategory();
if (observed === category)
return;
await new Promise(resolve => setTimeout(resolve, 10));
}
throw new Error(`audio session category is "${observed}", expected "${category}"${description ? ` (${description})` : ""}`);
}
// Polls a predicate until it returns true, or gives up after the attempts run out (callers then assert the
// specific conditions they expected, so the failure names which one).
async function waitUntil(predicate, { tries = 200, intervalMs = 10 } = {}) {
for (let i = 0; i < tries; ++i) {
if (await predicate())
return;
await delay(intervalMs);
}
}