The autoplay setting was binary and its default blocked all media, including muted video, leaving sites that rely on muted autoplay visibly broken. Replace it with a tri-state user-agent autoplay policy (allow audio and video, block audio, or block audio and video) defaulting to allowing only inaudible media to autoplay. This is enforced through the media element's "allowed to play" check, so unmuting a muted autoplay or calling `play()` cannot slip audio past the policy; audible playback is permitted once the document has been activated by the user. The policy lives in a dedicated AutoplaySettings consulted from HTMLMediaElement instead of the Permissions Policy "allowed to use feature" check it was previously conflated with.
129 lines
5.5 KiB
HTML
129 lines
5.5 KiB
HTML
<!DOCTYPE html>
|
|
<!-- Allow autoplay so the scripted play() works without a user gesture, independent of the browser's
|
|
autoplay policy that test-web would otherwise inherit. -->
|
|
<script>internals.setAutoplayPolicy("allow-audio-and-video");</script>
|
|
<video id="video"></video>
|
|
<script src="../include.js"></script>
|
|
<script>
|
|
asyncTest(done => {
|
|
function step(name) {
|
|
println(`PASS: ${name}`);
|
|
}
|
|
|
|
function fail(name, error) {
|
|
println(`FAIL: ${name}: ${error}`);
|
|
done();
|
|
}
|
|
|
|
try {
|
|
const video = document.getElementById("video");
|
|
|
|
// Step 1: Check isTypeSupported
|
|
const mimeType = 'video/webm;codecs="vp9,opus"';
|
|
if (!MediaSource.isTypeSupported(mimeType)) {
|
|
fail("isTypeSupported", `${mimeType} not supported`);
|
|
return;
|
|
}
|
|
step("isTypeSupported");
|
|
|
|
// Step 2: Create MediaSource
|
|
const mediaSource = new MediaSource();
|
|
step("created MediaSource");
|
|
|
|
// Step 3: Create object URL and assign to video
|
|
const url = URL.createObjectURL(mediaSource);
|
|
video.src = url;
|
|
step("assigned object URL to video.src");
|
|
|
|
// Step 4: Wait for sourceopen
|
|
mediaSource.addEventListener("sourceopen", async () => {
|
|
try {
|
|
step("sourceopen fired");
|
|
|
|
// Step 5: Add a SourceBuffer
|
|
const sourceBuffer = mediaSource.addSourceBuffer(mimeType);
|
|
step("addSourceBuffer succeeded");
|
|
|
|
// Step 6: Fetch test media
|
|
const response = await fetch("../../../Assets/test-webm.webm");
|
|
if (!response.ok) {
|
|
fail("fetch media", `HTTP ${response.status}`);
|
|
return;
|
|
}
|
|
const data = await response.arrayBuffer();
|
|
step(`loaded media data (${data.byteLength} bytes)`);
|
|
|
|
// Step 7: Append buffer in slices and print buffered ranges
|
|
function printBuffered(label) {
|
|
const buffered = sourceBuffer.buffered;
|
|
if (!buffered) {
|
|
println(`buffered after ${label}: not available`);
|
|
return;
|
|
}
|
|
const ranges = [];
|
|
for (let i = 0; i < buffered.length; i++)
|
|
ranges.push(`${buffered.start(i)}-${buffered.end(i)}`);
|
|
println(`buffered after ${label}: [${ranges.join(", ")}]`);
|
|
}
|
|
|
|
async function appendSlice(slice, label) {
|
|
await new Promise((resolve, reject) => {
|
|
sourceBuffer.addEventListener("updateend", resolve, { once: true });
|
|
sourceBuffer.addEventListener("error", () =>
|
|
reject(new Error("error event on SourceBuffer")), { once: true });
|
|
sourceBuffer.appendBuffer(slice);
|
|
step(`appendBuffer() for ${label}`);
|
|
});
|
|
printBuffered(label);
|
|
}
|
|
|
|
// Embed the data in a padded buffer and append as typed array views, so that if the view slice is
|
|
// ignored by appendBuffer(), it will cause read errors. YouTube uses these typed array views.
|
|
const padding = 1024;
|
|
const paddedBuffer = new ArrayBuffer(padding + data.byteLength + padding);
|
|
new Uint8Array(paddedBuffer).set(new Uint8Array(data), padding);
|
|
const dataView = new Uint8Array(paddedBuffer, padding, data.byteLength);
|
|
|
|
const numSlices = 3;
|
|
const sliceSize = Math.ceil(dataView.length / numSlices);
|
|
let pos = 0;
|
|
for (let i = 0; i < numSlices; i++) {
|
|
const end = Math.min(pos + sliceSize, dataView.length);
|
|
await appendSlice(dataView.subarray(pos, end), `slice ${i + 1}/${numSlices}`);
|
|
step(`slice ${i + 1} appended`);
|
|
pos = end;
|
|
}
|
|
|
|
// Step 8: End of stream and wait for sourceended
|
|
await new Promise(resolve => {
|
|
mediaSource.addEventListener("sourceended", resolve);
|
|
mediaSource.endOfStream();
|
|
});
|
|
step("endOfStream succeeded");
|
|
printBuffered("endOfStream");
|
|
|
|
// Step 9: Play
|
|
video.addEventListener("error", () => {
|
|
fail("playback", video.error?.message ?? "unknown error");
|
|
});
|
|
|
|
await video.play();
|
|
step("play() promise resolved");
|
|
|
|
await new Promise(resolve => {
|
|
if (!video.paused)
|
|
resolve();
|
|
else
|
|
video.addEventListener("playing", resolve);
|
|
});
|
|
step("playing");
|
|
done();
|
|
} catch (e) {
|
|
fail("sourceopen handler", e);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
fail("setup", e);
|
|
}
|
|
});
|
|
</script>
|