Files

192 lines
7.4 KiB
JavaScript
Raw Permalink Normal View History

2026-11-07 10:34:25 -08:00
const test = require("node:test");
const assert = require("node:assert/strict");
const { MediaPlayer } = require("../build/MediaPlayer");
const makePlayer = (url = "https://example.test/watch?v=abc", playlist = [{ id: 42, filename: url }]) => {
2026-11-07 10:34:25 -08:00
const player = Object.create(MediaPlayer.prototype);
const starts = [];
const commands = [];
2026-11-07 10:34:25 -08:00
let stops = 0;
const state = {
2026-11-07 10:34:25 -08:00
eventSubscribers: [],
playbackErrors: new Map(),
currentFile: null,
lastLoadCandidate: null,
activeEntryId: null,
activeUrl: null,
2026-11-07 10:34:25 -08:00
currentMediaTitle: null,
currentPlaybackMetadata: {},
dataBuffer: "",
activeAttemptGeneration: 0,
fallbackSuppressedUntilStart: false,
fallbackCandidates: new Set(),
entryCandidates: new Map(),
pendingCandidateUrls: [],
2026-11-07 10:34:25 -08:00
browserUrl: null,
metadata: new Map(),
};
Object.assign(player, state);
player.browserFallback = {
isRunning: false,
start: async (requestedUrl, isCurrent) => {
starts.push(requestedUrl);
if (!isCurrent()) return false;
player.browserFallback.isRunning = true;
return true;
2026-11-07 10:34:25 -08:00
},
stop: async () => {
stops += 1;
player.browserFallback.isRunning = false;
},
};
player.writeCommand = async (command, args) => {
commands.push([command, args]);
if (command === "loadfile") return { error: "success" };
if (command === "get_property") return { data: playlist };
return { error: "success" };
};
player.fetchMetadataAndNotify = async () => undefined;
return { player, starts, commands, get stops() { return stops; }, set playlist(value) { playlist = value; } };
2026-11-07 10:34:25 -08:00
};
const begin = (player, entryId = 42) => player.receiveData(`${JSON.stringify({ event: "start-file", playlist_entry_id: entryId })}\n`);
const send = (player, event, entryId = player.activeEntryId) => player.receiveData(`${JSON.stringify({
event: "end-file", playlist_entry_id: entryId, ...event,
})}\n`);
const settle = async () => { await new Promise((resolve) => setImmediate(resolve)); await new Promise((resolve) => setImmediate(resolve)); };
2026-11-07 10:34:25 -08:00
test("successful MPV completion does not start fallback", async () => {
const { player, starts } = makePlayer();
player.fallbackCandidates.add("https://example.test/watch?v=abc");
player.entryCandidates.set(42, "https://example.test/watch?v=abc");
2026-11-07 10:34:25 -08:00
begin(player);
send(player, { reason: "eof" });
await settle();
assert.deepEqual(starts, []);
});
test("actual MPV error opens the exact original URL and mirrors indeterminate state", async () => {
const url = "https://example.test/watch?v=x&next=1;touch%20/tmp/nope";
const { player, starts } = makePlayer(url);
player.fallbackCandidates.add(url);
player.entryCandidates.set(42, url);
2026-11-07 10:34:25 -08:00
begin(player);
send(player, { reason: "error", file_error: "demux failed" });
await settle();
assert.deepEqual(starts, [url]);
assert.equal(player.browserUrl, url);
assert.equal((await player.getNowPlaying()).filename, url);
assert.equal(await player.getPauseState(), false);
assert.equal(await player.getIdle(), false);
assert.equal(await player.getTimePosition(), null);
assert.equal(await player.getDuration(), null);
assert.equal(await player.getSeekable(), false);
});
test("queue append leaves active A eligible when queued B is added", async () => {
const a = "https://example.test/a";
const b = "https://example.test/b";
const { player, starts, commands } = makePlayer(a, [{ id: 42, filename: a }]);
player.fallbackCandidates.add(a);
player.entryCandidates.set(42, a);
begin(player, 42);
await player.append(b);
await settle();
assert.equal(commands.filter(([name]) => name === "loadfile").at(-1)[1][1], "append-play");
send(player, { reason: "error", file_error: "failed A" }, 42);
await settle();
assert.deepEqual(starts, [a]);
assert.equal(player.browserUrl, a);
assert.equal(player.fallbackCandidates.has(b), true);
});
test("queue append during fallback startup keeps browser A open without starting B", async () => {
const a = "https://example.test/a";
const b = "https://example.test/b";
const { player, starts, commands } = makePlayer(a, [{ id: 42, filename: a }]);
player.fallbackCandidates.add(a);
player.entryCandidates.set(42, a);
begin(player, 42);
send(player, { reason: "error", file_error: "failed A" }, 42);
await player.append(b);
await settle();
const load = commands.filter(([name]) => name === "loadfile").at(-1);
assert.equal(load[1][1], "append");
assert.deepEqual(starts, [a]);
assert.equal(player.browserUrl, a);
});
test("append then skipTo keeps the selected entry eligible for its actual MPV error", async () => {
const url = "https://example.test/shared-original?x=1&y=2";
const { player, starts } = makePlayer(url, [{ id: 42, filename: url }]);
await player.append(url);
await settle();
await player.skipTo(0);
begin(player, 42);
send(player, { reason: "error", file_error: "load failed" }, 42);
await settle();
assert.deepEqual(starts, [url]);
});
test("failure is correlated from MPV end-file entry id without querying a cleared playlist", async () => {
const url = "https://example.test/fails";
const { player, starts } = makePlayer(url);
player.fallbackCandidates.add(url);
player.entryCandidates.set(42, url);
begin(player, 42);
player.writeCommand = async (command) => {
assert.notEqual(command, "get_property", "failure correlation must use the emitted entry id");
return { data: [] };
};
player.activeEntryId = null;
player.activeUrl = null;
player.currentFile = null;
send(player, { reason: "error", file_error: "network unavailable" }, 42);
await settle();
assert.deepEqual(starts, [url]);
assert.equal(player.playbackErrors.get(url), "network unavailable");
});
test("EOF, user stop, cancel, replacement, or stale queue events do not fallback", async () => {
2026-11-07 10:34:25 -08:00
const eof = makePlayer();
eof.player.fallbackCandidates.add("https://example.test/watch?v=abc");
eof.player.entryCandidates.set(42, "https://example.test/watch?v=abc");
begin(eof.player);
2026-11-07 10:34:25 -08:00
send(eof.player, { reason: "eof" });
for (const action of ["stop", "cancel", "replacement", "shutdown"]) {
const replacementUrl = "https://example.test/replacement";
const { player, starts } = makePlayer(replacementUrl);
if (action === "shutdown") await player.shutdown();
else if (action === "stop") await player.stop();
else if (action === "replacement") await player.replace(replacementUrl);
else await player.cancelBrowserFallback();
send(player, { reason: "error" }, 42);
2026-11-07 10:34:25 -08:00
await settle();
assert.deepEqual(starts, [], `${action} must invalidate the callback`);
}
const stale = makePlayer();
stale.player.fallbackCandidates.add("https://example.test/watch?v=abc");
stale.player.entryCandidates.set(42, "https://example.test/watch?v=abc");
begin(stale.player, 42);
begin(stale.player, 43);
send(stale.player, { reason: "error" }, 42);
2026-11-07 10:34:25 -08:00
await settle();
assert.deepEqual(stale.starts, []);
});
test("fallback startup failure leaves MPV state usable", async () => {
const { player, starts } = makePlayer();
const url = "https://example.test/watch?v=abc";
player.fallbackCandidates.add(url);
player.entryCandidates.set(42, url);
2026-11-07 10:34:25 -08:00
player.browserFallback.start = async () => { throw new Error("chromium missing"); };
begin(player);
send(player, { reason: "error" });
await settle();
assert.equal(player.browserUrl, null);
assert.deepEqual(starts, []);
assert.equal(player.playbackErrors.get(url), "Unknown playback error");
2026-11-07 10:34:25 -08:00
});