diff --git a/web/backend/src/MediaPlayer.ts b/web/backend/src/MediaPlayer.ts index ea068a9..767af23 100644 --- a/web/backend/src/MediaPlayer.ts +++ b/web/backend/src/MediaPlayer.ts @@ -76,6 +76,7 @@ export class MediaPlayer { private activeEntryId: number | null = null; private activeUrl: string | null = null; private fallbackSuppressedUntilStart = false; + private fallbackPendingUrl: string | null = null; private browserUrl: string | null = null; private browserFallback = new ChromiumFallback((url) => { if (this.browserUrl === url) { @@ -384,7 +385,10 @@ export class MediaPlayer { } public async append(url: string) { - await this.loadFile(url, "append-play"); + const mode = this.browserUrl || this.fallbackPendingUrl || this.browserFallback.isRunning + ? "append" + : "append-play"; + await this.loadFile(url, mode); } public async replace(url: string) { @@ -740,6 +744,7 @@ export class MediaPlayer { const hadBrowser = !!this.browserUrl || this.browserFallback.isRunning; this.activeAttemptGeneration++; this.fallbackSuppressedUntilStart = false; + this.fallbackPendingUrl = null; this.browserUrl = null; if (hadBrowser) void this.browserFallback.stop(); const entryId = typeof response.playlist_entry_id === "number" @@ -805,6 +810,7 @@ export class MediaPlayer { if (this.fallbackCandidates.has(file) && (entryId === this.activeEntryId || (this.activeEntryId === null && !this.fallbackSuppressedUntilStart)) && attemptGeneration === this.activeAttemptGeneration) { + this.fallbackPendingUrl = file; void this.startBrowserFallback(file, attemptGeneration, entryId); } } @@ -851,6 +857,10 @@ export class MediaPlayer { } } catch (error) { console.error(`Failed to start Chromium fallback for ${url}:`, error); + } finally { + if (generation === this.activeAttemptGeneration && this.fallbackPendingUrl === url) { + this.fallbackPendingUrl = null; + } } } @@ -860,6 +870,7 @@ export class MediaPlayer { this.activeEntryId = null; this.activeUrl = null; this.currentFile = null; + this.fallbackPendingUrl = null; this.browserUrl = null; await this.browserFallback.stop(); } diff --git a/web/backend/test/MediaPlayerFallback.test.cjs b/web/backend/test/MediaPlayerFallback.test.cjs index 2a2f368..f4fc1f6 100644 --- a/web/backend/test/MediaPlayerFallback.test.cjs +++ b/web/backend/test/MediaPlayerFallback.test.cjs @@ -5,6 +5,7 @@ const { MediaPlayer } = require("../build/MediaPlayer"); const makePlayer = (url = "https://example.test/watch?v=abc", playlist = [{ id: 42, filename: url }]) => { const player = Object.create(MediaPlayer.prototype); const starts = []; + const commands = []; let stops = 0; const state = { eventSubscribers: [], @@ -38,13 +39,14 @@ const makePlayer = (url = "https://example.test/watch?v=abc", playlist = [{ id: player.browserFallback.isRunning = false; }, }; - player.writeCommand = async (command) => { + 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, get stops() { return stops; }, set playlist(value) { playlist = value; } }; + return { player, starts, commands, get stops() { return stops; }, set playlist(value) { playlist = value; } }; }; const begin = (player, entryId = 42) => player.receiveData(`${JSON.stringify({ event: "start-file", playlist_entry_id: entryId })}\n`); @@ -84,12 +86,13 @@ test("actual MPV error opens the exact original URL and mirrors indeterminate st 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 } = makePlayer(a, [{ id: 42, filename: a }]); + 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]); @@ -97,6 +100,22 @@ test("queue append leaves active A eligible when queued B is added", async () => 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 }]);