Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

const PAGE_LIVE_STREAM =
/**
* A live stream (e.g. internet radio) is typically served without a
* `Content-Length` header, so Gecko treats its underlying resource as a live
* stream (`MediaDecoder::IsLiveStream()`). Such streams often report a very
* short, or slowly growing, `duration` before enough data has arrived to
* estimate it accurately. The fixture used here starts with a ~1 second
* pre-recorded prelude, then streams in more content that grows the
* reported duration well past the
* `media.mediacontrol.eligible.media.duration.s` threshold, mimicking a
* short jingle in front of a live broadcast. That threshold exists to
* filter out short local sound effects, not live streams, so a live stream
* must become - and remain - controllable regardless of how its reported
* duration changes over time.
*/
add_task(async function testLiveStreamControllableRegardlessOfDuration() {
const threshold = Services.prefs.getFloatPref(
"media.mediacontrol.eligible.media.duration.s"
);
// This test plays for several real seconds so the reported duration can
// grow past the threshold. With real hardware media keys enabled, the
// platform's media-remote integration (e.g. macOS's Now Playing) can send
// an actual Pause command once this stream registers as playing, which
// stalls playback for the rest of the test. Other tests avoid this by
// simulating key events instead of relying on the real backend; this one
// doesn't send any key events at all, so it's simplest to just disable
// the real backend here.
await SpecialPowers.pushPrefEnv({
set: [["media.hardwaremediakeys.enabled", false]],
});
info(`open new tab with live stream`);
const tab = await createLoadedTabWrapper(PAGE_LIVE_STREAM, {
needCheck: false,
});
info(`start playing the live stream`);
await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
const audio = content.document.getElementById("live-stream");
return audio.play();
});
info(`controller should become active despite the stream's short duration`);
await checkOrWaitUntilControllerBecomeActive(tab);
ok(tab.controller.isActive, "live stream activated the media controller");
info(`wait for the prelude's duration to be known`);
await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
const audio = content.document.getElementById("live-stream");
if (audio.readyState >= audio.HAVE_METADATA) {
return Promise.resolve();
}
return new Promise(resolve => {
audio.addEventListener("loadedmetadata", () => resolve(), {
once: true,
});
});
});
const initialDuration = await SpecialPowers.spawn(
tab.linkedBrowser,
[],
() => content.document.getElementById("live-stream").duration
);
Assert.less(
initialDuration,
threshold,
`initial duration ${initialDuration} is below the ${threshold}s threshold`
);
info(`wait for the reported duration to grow past the threshold`);
await SpecialPowers.spawn(tab.linkedBrowser, [threshold], threshold => {
const audio = content.document.getElementById("live-stream");
if (audio.duration > threshold) {
return Promise.resolve();
}
return new Promise(resolve => {
audio.addEventListener("durationchange", function listener() {
if (audio.duration > threshold) {
audio.removeEventListener("durationchange", listener);
resolve();
}
});
});
});
ok(
tab.controller.isActive,
"live stream is still controllable once duration exceeds the threshold"
);
// live_stream.sjs keeps its HTTP response open for a while after
// writing all of its bytes, to mimic a real live stream's connection.
// Wait for the element to report the download as finished (a "suspend"
// event, fired once its network state becomes idle) before tearing
// down the tab, so the server-side response isn't torn down mid-flight.
info(`wait for the live_stream.sjs response to finish on its own`);
await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
const audio = content.document.getElementById("live-stream");
if (audio.networkState == audio.NETWORK_IDLE) {
return Promise.resolve();
}
return new Promise(resolve => {
audio.addEventListener("suspend", () => resolve(), { once: true });
});
});
info(`remove tab`);
await tab.close();
});