fix(anki): hold playback paused while media timing review is open

- Defer overlay resume requests (popup closed, hover left) during an active review and apply them when the review closes
- Add deferPlaybackResume to the media timing review runtime and gate sendRendererMpvCommand on it
- Document the paused-playback behavior and add a changelog fragment
This commit is contained in:
2026-09-23 00:32:35 -07:00
parent 294e3a777e
commit 430120cc63
5 changed files with 72 additions and 3 deletions
+28 -2
View File
@@ -98,6 +98,7 @@ async function startActiveMediaTimingReview(
} = {},
) {
const previewCalls: Array<[number, number]> = [];
const commands: Array<Array<string | number>> = [];
let publishPayload!: (payload: MediaTimingReviewOpenPayload) => void;
const openedPayload = new Promise<MediaTimingReviewOpenPayload>((resolve) => {
publishPayload = resolve;
@@ -107,7 +108,7 @@ async function startActiveMediaTimingReview(
connected: true,
currentVideoPath: '/video/show.mkv',
requestProperty: async (name) => (name === 'duration' ? 100 : name === 'pause' ? true : null),
send: () => undefined,
send: ({ command }) => commands.push(command),
}),
getCurrentMediaPath: () => '/video/show.mkv',
getMpvExecutablePath: () => 'mpv',
@@ -138,7 +139,7 @@ async function startActiveMediaTimingReview(
maxMediaDuration: options.maxMediaDuration ?? 30,
});
return { runtime, payload: await openedPayload, pendingDecision, previewCalls };
return { runtime, payload: await openedPayload, pendingDecision, previewCalls, commands };
}
test('media timing review pauses playback, resolves exact timing, and restores playing state', async () => {
@@ -642,6 +643,31 @@ test('collectMediaTimingContextLines falls back to played history when no cues a
assert.deepEqual(context.next, []);
});
test('media timing review keeps an already-paused video paused when nothing asks to resume', async () => {
const { runtime, payload, pendingDecision, commands } = await startActiveMediaTimingReview();
runtime.resolveReview({ reviewId: payload.reviewId, decision: { action: 'use-original' } });
await pendingDecision;
assert.deepEqual(commands, [['set_property', 'pause', 'yes']]);
});
test('media timing review holds overlay resume requests until the review closes', async () => {
const { runtime, payload, pendingDecision, commands } = await startActiveMediaTimingReview();
assert.equal(runtime.deferPlaybackResume(), true);
assert.deepEqual(commands, [['set_property', 'pause', 'yes']]);
runtime.resolveReview({ reviewId: payload.reviewId, decision: { action: 'use-original' } });
await pendingDecision;
assert.deepEqual(commands, [
['set_property', 'pause', 'yes'],
['set_property', 'pause', 'no'],
]);
assert.equal(runtime.deferPlaybackResume(), false);
});
test('media timing review watchdog falls back when the renderer stops responding', async () => {
const { pendingDecision } = await startActiveMediaTimingReview({ decisionTimeoutMs: 0 });