diff --git a/src/main.ts b/src/main.ts index 6c7ae8f5..9b68c833 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2051,20 +2051,24 @@ function isExplicitMpvSeekCommand(command: readonly (string | number)[]): boolea return command[0] === 'seek' || command[0] === 'sub-seek'; } -function isMpvResumeCommand(command: readonly (string | number)[]): boolean { - return ( - (command[0] === 'set_property' || command[0] === 'set') && - command[1] === 'pause' && - command[2] === 'no' - ); +function getMpvPauseCommandValue(command: readonly (string | number)[]): 'yes' | 'no' | null { + if ((command[0] !== 'set_property' && command[0] !== 'set') || command[1] !== 'pause') { + return null; + } + return command[2] === 'yes' || command[2] === 'no' ? command[2] : null; } function sendRendererMpvCommand(rawCommand: (string | number)[]): void { // Overlay auto-pause releases (popup closed, hover left) must not resume playback - // behind an open timing review; the review applies them when it closes. - if (isMpvResumeCommand(rawCommand) && mediaTimingReviewRuntime.deferPlaybackResume()) { + // behind an open timing review; the review applies them when it closes. A pause + // request during the review keeps playback paused after it closes. + const pauseValue = getMpvPauseCommandValue(rawCommand); + if (pauseValue === 'no' && mediaTimingReviewRuntime.deferPlaybackResume()) { return; } + if (pauseValue === 'yes') { + mediaTimingReviewRuntime.cancelPlaybackResume(); + } const command = resolveSanitizedSubtitleSeekCommand( rawCommand, diff --git a/src/main/runtime/media-timing-review.test.ts b/src/main/runtime/media-timing-review.test.ts index e3400d38..95f074d6 100644 --- a/src/main/runtime/media-timing-review.test.ts +++ b/src/main/runtime/media-timing-review.test.ts @@ -95,6 +95,7 @@ async function startActiveMediaTimingReview( decisionTimeoutMs?: number; generateWaveform?: () => Promise; play?: () => Promise; + paused?: boolean; } = {}, ) { const previewCalls: Array<[number, number]> = []; @@ -107,7 +108,8 @@ async function startActiveMediaTimingReview( getMpvClient: () => ({ connected: true, currentVideoPath: '/video/show.mkv', - requestProperty: async (name) => (name === 'duration' ? 100 : name === 'pause' ? true : null), + requestProperty: async (name) => + name === 'duration' ? 100 : name === 'pause' ? (options.paused ?? true) : null, send: ({ command }) => commands.push(command), }), getCurrentMediaPath: () => '/video/show.mkv', @@ -668,6 +670,22 @@ test('media timing review holds overlay resume requests until the review closes' assert.equal(runtime.deferPlaybackResume(), false); }); +for (const paused of [true, false]) { + test(`media timing review stays paused after an overlay pause request (paused before: ${paused})`, async () => { + const { runtime, payload, pendingDecision, commands } = await startActiveMediaTimingReview({ + paused, + }); + + assert.equal(runtime.deferPlaybackResume(), true); + runtime.cancelPlaybackResume(); + + runtime.resolveReview({ reviewId: payload.reviewId, decision: { action: 'use-original' } }); + await pendingDecision; + + assert.deepEqual(commands, [['set_property', 'pause', 'yes']]); + }); +} + test('media timing review watchdog falls back when the renderer stops responding', async () => { const { pendingDecision } = await startActiveMediaTimingReview({ decisionTimeoutMs: 0 }); diff --git a/src/main/runtime/media-timing-review.ts b/src/main/runtime/media-timing-review.ts index 65826ea4..18acff83 100644 --- a/src/main/runtime/media-timing-review.ts +++ b/src/main/runtime/media-timing-review.ts @@ -313,6 +313,15 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep return true; } + /** + * The overlay asked to pause while a review holds playback, so closing the review must + * not resume it: drops both a held overlay resume and the review's own restore. + */ + function cancelPlaybackResume(): void { + resumeDeferred = false; + if (active) active.restorePlayback = false; + } + function ensureWindow( review: ActiveReview, range: RemoteMediaWindowRange, @@ -782,6 +791,7 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep stopPreview, resolveReview, deferPlaybackResume, + cancelPlaybackResume, dispose, }; }