diff --git a/src/main/runtime/media-timing-review.test.ts b/src/main/runtime/media-timing-review.test.ts index 95f074d6..d557a01d 100644 --- a/src/main/runtime/media-timing-review.test.ts +++ b/src/main/runtime/media-timing-review.test.ts @@ -686,6 +686,64 @@ for (const paused of [true, false]) { }); } +test('media timing review honors an overlay pause that arrives while setup reads the pause state', async () => { + const commands: Array> = []; + const pauseReads: Array<(paused: boolean) => void> = []; + let runtime: ReturnType; + runtime = createMediaTimingReviewRuntime({ + getMpvClient: () => ({ + connected: true, + currentVideoPath: '/video/show.mkv', + requestProperty: (name) => + name === 'pause' + ? new Promise((resolve) => pauseReads.push(resolve)) + : Promise.resolve(name === 'duration' ? 100 : null), + send: ({ command }) => commands.push(command), + }), + getCurrentMediaPath: () => '/video/show.mkv', + getMpvExecutablePath: () => 'mpv', + generateWaveform: async () => [], + createPreviewSession: () => ({ + start: async () => undefined, + play: async () => undefined, + stop: async () => undefined, + onPlaybackEnded: () => undefined, + dispose: () => undefined, + }), + openModal: async (payload) => { + queueMicrotask(() => { + runtime.resolveReview({ reviewId: payload.reviewId, decision: { action: 'use-original' } }); + }); + return true; + }, + showStatus: () => undefined, + }); + const request = { + kind: 'sentence' as const, + text: '字幕', + startTime: 10, + endTime: 12, + audioPadding: 0, + maxMediaDuration: 30, + }; + + const first = runtime.requestReview(request); + runtime.cancelPlaybackResume(); + // mpv answered the read before the overlay's pause reached it. + pauseReads[0]!(false); + await first; + assert.deepEqual(commands, [['set_property', 'pause', 'yes']]); + + commands.length = 0; + const second = runtime.requestReview(request); + pauseReads[1]!(false); + await second; + assert.deepEqual(commands, [ + ['set_property', 'pause', 'yes'], + ['set_property', 'pause', 'no'], + ]); +}); + 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 18acff83..b585c817 100644 --- a/src/main/runtime/media-timing-review.ts +++ b/src/main/runtime/media-timing-review.ts @@ -284,6 +284,8 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep let currentRequest: ReviewRequestLifecycle | null = null; let pendingPauseRestore: ReviewMpvClient | null = null; let resumeDeferred = false; + /** Set when the overlay pauses during the current request, even before its setup reads the pause state. */ + let restoreCancelled = false; function restorePendingPlayback(): void { const mpvClient = pendingPauseRestore; @@ -318,7 +320,9 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep * not resume it: drops both a held overlay resume and the review's own restore. */ function cancelPlaybackResume(): void { + if (!currentRequest) return; resumeDeferred = false; + restoreCancelled = true; if (active) active.restorePlayback = false; } @@ -441,7 +445,8 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep const [pauseRaw, durationRaw, audioTrackRaw, volumeRaw, resolvedSource, videoSource] = setup.values; const pauseState = booleanProperty(pauseRaw); - pendingPauseRestore = pauseState === false ? mpvClient : null; + // The pause read can predate an overlay pause that arrived during setup. + pendingPauseRestore = pauseState === false && !restoreCancelled ? mpvClient : null; mpvClient.send({ command: ['set_property', 'pause', 'yes'] }); if (lifecycle.isCancelled()) { restorePendingPlayback(); @@ -559,6 +564,7 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep } const lifecycle = createReviewRequestLifecycle(); currentRequest = lifecycle; + restoreCancelled = false; try { return await runReview(request, lifecycle); } catch {