fix(anki): keep playback paused after overlay pause during timing review

- Treat overlay pause requests during an open media timing review as final: drop held resumes and the review's own playback restore
- Add cancelPlaybackResume to the media timing review runtime
- Test both paused and playing starting states
This commit is contained in:
2026-09-23 01:36:11 -07:00
parent 430120cc63
commit 33e694bc61
3 changed files with 41 additions and 9 deletions
+12 -8
View File
@@ -2051,20 +2051,24 @@ function isExplicitMpvSeekCommand(command: readonly (string | number)[]): boolea
return command[0] === 'seek' || command[0] === 'sub-seek'; return command[0] === 'seek' || command[0] === 'sub-seek';
} }
function isMpvResumeCommand(command: readonly (string | number)[]): boolean { function getMpvPauseCommandValue(command: readonly (string | number)[]): 'yes' | 'no' | null {
return ( if ((command[0] !== 'set_property' && command[0] !== 'set') || command[1] !== 'pause') {
(command[0] === 'set_property' || command[0] === 'set') && return null;
command[1] === 'pause' && }
command[2] === 'no' return command[2] === 'yes' || command[2] === 'no' ? command[2] : null;
);
} }
function sendRendererMpvCommand(rawCommand: (string | number)[]): void { function sendRendererMpvCommand(rawCommand: (string | number)[]): void {
// Overlay auto-pause releases (popup closed, hover left) must not resume playback // Overlay auto-pause releases (popup closed, hover left) must not resume playback
// behind an open timing review; the review applies them when it closes. // behind an open timing review; the review applies them when it closes. A pause
if (isMpvResumeCommand(rawCommand) && mediaTimingReviewRuntime.deferPlaybackResume()) { // request during the review keeps playback paused after it closes.
const pauseValue = getMpvPauseCommandValue(rawCommand);
if (pauseValue === 'no' && mediaTimingReviewRuntime.deferPlaybackResume()) {
return; return;
} }
if (pauseValue === 'yes') {
mediaTimingReviewRuntime.cancelPlaybackResume();
}
const command = const command =
resolveSanitizedSubtitleSeekCommand( resolveSanitizedSubtitleSeekCommand(
rawCommand, rawCommand,
+19 -1
View File
@@ -95,6 +95,7 @@ async function startActiveMediaTimingReview(
decisionTimeoutMs?: number; decisionTimeoutMs?: number;
generateWaveform?: () => Promise<number[]>; generateWaveform?: () => Promise<number[]>;
play?: () => Promise<void>; play?: () => Promise<void>;
paused?: boolean;
} = {}, } = {},
) { ) {
const previewCalls: Array<[number, number]> = []; const previewCalls: Array<[number, number]> = [];
@@ -107,7 +108,8 @@ async function startActiveMediaTimingReview(
getMpvClient: () => ({ getMpvClient: () => ({
connected: true, connected: true,
currentVideoPath: '/video/show.mkv', 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), send: ({ command }) => commands.push(command),
}), }),
getCurrentMediaPath: () => '/video/show.mkv', 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); 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 () => { test('media timing review watchdog falls back when the renderer stops responding', async () => {
const { pendingDecision } = await startActiveMediaTimingReview({ decisionTimeoutMs: 0 }); const { pendingDecision } = await startActiveMediaTimingReview({ decisionTimeoutMs: 0 });
+10
View File
@@ -313,6 +313,15 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
return true; 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( function ensureWindow(
review: ActiveReview, review: ActiveReview,
range: RemoteMediaWindowRange, range: RemoteMediaWindowRange,
@@ -782,6 +791,7 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
stopPreview, stopPreview,
resolveReview, resolveReview,
deferPlaybackResume, deferPlaybackResume,
cancelPlaybackResume,
dispose, dispose,
}; };
} }