mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-23 05:16:23 -07:00
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:
@@ -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 });
|
||||
|
||||
|
||||
@@ -283,6 +283,7 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
||||
let active: ActiveReview | null = null;
|
||||
let currentRequest: ReviewRequestLifecycle | null = null;
|
||||
let pendingPauseRestore: ReviewMpvClient | null = null;
|
||||
let resumeDeferred = false;
|
||||
|
||||
function restorePendingPlayback(): void {
|
||||
const mpvClient = pendingPauseRestore;
|
||||
@@ -292,6 +293,26 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
||||
}
|
||||
}
|
||||
|
||||
function resumeDeferredPlayback(): void {
|
||||
if (!resumeDeferred) return;
|
||||
resumeDeferred = false;
|
||||
const mpvClient = deps.getMpvClient();
|
||||
if (mpvClient?.connected) {
|
||||
mpvClient.send({ command: ['set_property', 'pause', 'no'] });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds an overlay request to resume playback (e.g. an auto-pause released because the
|
||||
* dictionary popup closed) until the pending review ends, then applies it. Returns false
|
||||
* when no review holds playback, so the caller should resume right away.
|
||||
*/
|
||||
function deferPlaybackResume(): boolean {
|
||||
if (!active && !currentRequest) return false;
|
||||
resumeDeferred = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function ensureWindow(
|
||||
review: ActiveReview,
|
||||
range: RemoteMediaWindowRange,
|
||||
@@ -539,6 +560,7 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
||||
} finally {
|
||||
if (currentRequest === lifecycle) {
|
||||
currentRequest = null;
|
||||
resumeDeferredPlayback();
|
||||
}
|
||||
lifecycle.markSettled();
|
||||
}
|
||||
@@ -737,7 +759,8 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
||||
if (!current) return;
|
||||
deps.clearFrameCache?.();
|
||||
void current.preview?.session.then((session) => session.dispose()).catch(() => {});
|
||||
if (current.restorePlayback && current.mpvClient.connected) {
|
||||
if ((current.restorePlayback || resumeDeferred) && current.mpvClient.connected) {
|
||||
resumeDeferred = false;
|
||||
current.mpvClient.send({ command: ['set_property', 'pause', 'no'] });
|
||||
}
|
||||
}
|
||||
@@ -758,6 +781,7 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
||||
getFrame,
|
||||
stopPreview,
|
||||
resolveReview,
|
||||
deferPlaybackResume,
|
||||
dispose,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user