diff --git a/src/main/runtime/youtube-playback-launch.test.ts b/src/main/runtime/youtube-playback-launch.test.ts index 52e3cad..a5ae968 100644 --- a/src/main/runtime/youtube-playback-launch.test.ts +++ b/src/main/runtime/youtube-playback-launch.test.ts @@ -18,7 +18,12 @@ test('prepare youtube playback skips load when current path already matches exac const ok = await prepare({ url: 'https://www.youtube.com/watch?v=abc123' }); assert.equal(ok, true); - assert.deepEqual(commands, []); + assert.deepEqual(commands, [ + ['set_property', 'pause', 'yes'], + ['set_property', 'sub-auto', 'no'], + ['set_property', 'sid', 'no'], + ['set_property', 'secondary-sid', 'no'], + ]); }); test('prepare youtube playback treats matching video IDs as already loaded', async () => { @@ -33,7 +38,12 @@ test('prepare youtube playback treats matching video IDs as already loaded', asy const ok = await prepare({ url: 'https://www.youtube.com/watch?v=abc123' }); assert.equal(ok, true); - assert.deepEqual(commands, []); + assert.deepEqual(commands, [ + ['set_property', 'pause', 'yes'], + ['set_property', 'sub-auto', 'no'], + ['set_property', 'sid', 'no'], + ['set_property', 'secondary-sid', 'no'], + ]); }); test('prepare youtube playback does not mark matching target ready until tracks exist', async () => { @@ -59,7 +69,12 @@ test('prepare youtube playback does not mark matching target ready until tracks }); assert.equal(ok, true); - assert.deepEqual(commands, []); + assert.deepEqual(commands, [ + ['set_property', 'pause', 'yes'], + ['set_property', 'sub-auto', 'no'], + ['set_property', 'sid', 'no'], + ['set_property', 'secondary-sid', 'no'], + ]); }); test('prepare youtube playback replaces media and waits for path switch', async () => { @@ -185,13 +200,17 @@ test('prepare youtube playback accepts a non-youtube resolved path once playable }); const ok = await prepare({ - url: 'https://www.youtube.com/watch?v=newvid', + url: 'https://rr16---sn.example.googlevideo.com/videoplayback?id=abc', timeoutMs: 1500, pollIntervalMs: 1, }); assert.equal(ok, true); - assert.deepEqual(commands[4], ['loadfile', 'https://www.youtube.com/watch?v=newvid', 'replace']); + assert.deepEqual(commands[4], [ + 'loadfile', + 'https://rr16---sn.example.googlevideo.com/videoplayback?id=abc', + 'replace', + ]); }); test('prepare youtube playback does not accept a different youtube video after path change', async () => { @@ -234,3 +253,39 @@ test('prepare youtube playback does not accept a different youtube video after p 'replace', ]); }); + +test('prepare youtube playback accepts a fresh-start path change when the direct target matches exactly', async () => { + const commands: Array> = []; + const observedPaths = [ + '', + 'https://rr16---sn.example.googlevideo.com/videoplayback?id=abc', + ]; + const observedTrackLists = [[], [{ type: 'video', id: 1 }, { type: 'audio', id: 2 }]]; + let requestCount = 0; + const prepare = createPrepareYoutubePlaybackInMpvHandler({ + requestPath: async () => { + const value = observedPaths[Math.min(requestCount, observedPaths.length - 1)] ?? null; + requestCount += 1; + return value; + }, + requestProperty: async (name) => { + if (name !== 'track-list') return null; + return observedTrackLists[Math.min(requestCount - 1, observedTrackLists.length - 1)] ?? []; + }, + sendMpvCommand: (command) => commands.push(command), + wait: createWaitStub(), + }); + + const ok = await prepare({ + url: 'https://rr16---sn.example.googlevideo.com/videoplayback?id=abc', + timeoutMs: 1500, + pollIntervalMs: 1, + }); + + assert.equal(ok, true); + assert.deepEqual(commands[4], [ + 'loadfile', + 'https://rr16---sn.example.googlevideo.com/videoplayback?id=abc', + 'replace', + ]); +}); diff --git a/src/main/runtime/youtube-playback-launch.ts b/src/main/runtime/youtube-playback-launch.ts index afc24d7..085ae68 100644 --- a/src/main/runtime/youtube-playback-launch.ts +++ b/src/main/runtime/youtube-playback-launch.ts @@ -79,6 +79,13 @@ function hasPlayableMediaTracks(trackListRaw: unknown): boolean { }); } +function sendPlaybackPrepCommands(sendMpvCommand: (command: Array) => void): void { + sendMpvCommand(['set_property', 'pause', 'yes']); + sendMpvCommand(['set_property', 'sub-auto', 'no']); + sendMpvCommand(['set_property', 'sid', 'no']); + sendMpvCommand(['set_property', 'secondary-sid', 'no']); +} + export function createPrepareYoutubePlaybackInMpvHandler(deps: YoutubePlaybackLaunchDeps) { const now = deps.now ?? (() => Date.now()); return async (input: YoutubePlaybackLaunchInput): Promise => { @@ -95,6 +102,8 @@ export function createPrepareYoutubePlaybackInMpvHandler(deps: YoutubePlaybackLa // Ignore transient path request failures and continue with bootstrap commands. } + sendPlaybackPrepCommands(deps.sendMpvCommand); + const alreadyTarget = pathMatchesYoutubeTarget(previousPath, targetUrl); if (alreadyTarget) { if (!deps.requestProperty) { @@ -109,10 +118,6 @@ export function createPrepareYoutubePlaybackInMpvHandler(deps: YoutubePlaybackLa // Keep polling; mpv can report the target path before tracks are ready. } } else { - deps.sendMpvCommand(['set_property', 'pause', 'yes']); - deps.sendMpvCommand(['set_property', 'sub-auto', 'no']); - deps.sendMpvCommand(['set_property', 'sid', 'no']); - deps.sendMpvCommand(['set_property', 'secondary-sid', 'no']); deps.sendMpvCommand(['loadfile', targetUrl, 'replace']); } @@ -139,12 +144,13 @@ export function createPrepareYoutubePlaybackInMpvHandler(deps: YoutubePlaybackLa // Continue polling until media tracks are actually available. } } - if (previousPath && currentPath !== previousPath) { - if (isYoutubeMediaPath(currentPath) && isYoutubeMediaPath(targetUrl)) { - if (!pathMatchesYoutubeTarget(currentPath, targetUrl)) { - continue; - } - } + const pathChanged = currentPath !== previousPath; + const matchesChangedTarget = + currentPath === targetUrl || + (isYoutubeMediaPath(currentPath) && + isYoutubeMediaPath(targetUrl) && + pathMatchesYoutubeTarget(currentPath, targetUrl)); + if (pathChanged && matchesChangedTarget) { if (deps.requestProperty) { try { const trackList = await deps.requestProperty('track-list');