Keep fallback active while adding queued media

This commit is contained in:
2026-11-07 10:59:49 -08:00
parent 946771d406
commit 463bb823d8
2 changed files with 34 additions and 4 deletions
+12 -1
View File
@@ -76,6 +76,7 @@ export class MediaPlayer {
private activeEntryId: number | null = null; private activeEntryId: number | null = null;
private activeUrl: string | null = null; private activeUrl: string | null = null;
private fallbackSuppressedUntilStart = false; private fallbackSuppressedUntilStart = false;
private fallbackPendingUrl: string | null = null;
private browserUrl: string | null = null; private browserUrl: string | null = null;
private browserFallback = new ChromiumFallback((url) => { private browserFallback = new ChromiumFallback((url) => {
if (this.browserUrl === url) { if (this.browserUrl === url) {
@@ -384,7 +385,10 @@ export class MediaPlayer {
} }
public async append(url: string) { 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) { public async replace(url: string) {
@@ -740,6 +744,7 @@ export class MediaPlayer {
const hadBrowser = !!this.browserUrl || this.browserFallback.isRunning; const hadBrowser = !!this.browserUrl || this.browserFallback.isRunning;
this.activeAttemptGeneration++; this.activeAttemptGeneration++;
this.fallbackSuppressedUntilStart = false; this.fallbackSuppressedUntilStart = false;
this.fallbackPendingUrl = null;
this.browserUrl = null; this.browserUrl = null;
if (hadBrowser) void this.browserFallback.stop(); if (hadBrowser) void this.browserFallback.stop();
const entryId = typeof response.playlist_entry_id === "number" const entryId = typeof response.playlist_entry_id === "number"
@@ -805,6 +810,7 @@ export class MediaPlayer {
if (this.fallbackCandidates.has(file) && if (this.fallbackCandidates.has(file) &&
(entryId === this.activeEntryId || (this.activeEntryId === null && !this.fallbackSuppressedUntilStart)) && (entryId === this.activeEntryId || (this.activeEntryId === null && !this.fallbackSuppressedUntilStart)) &&
attemptGeneration === this.activeAttemptGeneration) { attemptGeneration === this.activeAttemptGeneration) {
this.fallbackPendingUrl = file;
void this.startBrowserFallback(file, attemptGeneration, entryId); void this.startBrowserFallback(file, attemptGeneration, entryId);
} }
} }
@@ -851,6 +857,10 @@ export class MediaPlayer {
} }
} catch (error) { } catch (error) {
console.error(`Failed to start Chromium fallback for ${url}:`, 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.activeEntryId = null;
this.activeUrl = null; this.activeUrl = null;
this.currentFile = null; this.currentFile = null;
this.fallbackPendingUrl = null;
this.browserUrl = null; this.browserUrl = null;
await this.browserFallback.stop(); await this.browserFallback.stop();
} }
+22 -3
View File
@@ -5,6 +5,7 @@ const { MediaPlayer } = require("../build/MediaPlayer");
const makePlayer = (url = "https://example.test/watch?v=abc", playlist = [{ id: 42, filename: url }]) => { const makePlayer = (url = "https://example.test/watch?v=abc", playlist = [{ id: 42, filename: url }]) => {
const player = Object.create(MediaPlayer.prototype); const player = Object.create(MediaPlayer.prototype);
const starts = []; const starts = [];
const commands = [];
let stops = 0; let stops = 0;
const state = { const state = {
eventSubscribers: [], eventSubscribers: [],
@@ -38,13 +39,14 @@ const makePlayer = (url = "https://example.test/watch?v=abc", playlist = [{ id:
player.browserFallback.isRunning = false; 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 === "loadfile") return { error: "success" };
if (command === "get_property") return { data: playlist }; if (command === "get_property") return { data: playlist };
return { error: "success" }; return { error: "success" };
}; };
player.fetchMetadataAndNotify = async () => undefined; 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`); 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 () => { test("queue append leaves active A eligible when queued B is added", async () => {
const a = "https://example.test/a"; const a = "https://example.test/a";
const b = "https://example.test/b"; 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.fallbackCandidates.add(a);
player.entryCandidates.set(42, a); player.entryCandidates.set(42, a);
begin(player, 42); begin(player, 42);
await player.append(b); await player.append(b);
await settle(); await settle();
assert.equal(commands.filter(([name]) => name === "loadfile").at(-1)[1][1], "append-play");
send(player, { reason: "error", file_error: "failed A" }, 42); send(player, { reason: "error", file_error: "failed A" }, 42);
await settle(); await settle();
assert.deepEqual(starts, [a]); 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); 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 () => { 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 url = "https://example.test/shared-original?x=1&y=2";
const { player, starts } = makePlayer(url, [{ id: 42, filename: url }]); const { player, starts } = makePlayer(url, [{ id: 42, filename: url }]);