mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-24 05:16:19 -07:00
fix(anki): honor overlay pause that races timing review setup
- Track overlay pauses per review request so a pause arriving while setup reads mpv's pause state still cancels the playback restore - Ignore cancelPlaybackResume when no review request is in flight - Add a test covering the pause-read race and reset between requests
This commit is contained in:
@@ -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<Array<string | number>> = [];
|
||||||
|
const pauseReads: Array<(paused: boolean) => void> = [];
|
||||||
|
let runtime: ReturnType<typeof createMediaTimingReviewRuntime>;
|
||||||
|
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 () => {
|
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 });
|
||||||
|
|
||||||
|
|||||||
@@ -284,6 +284,8 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
|||||||
let currentRequest: ReviewRequestLifecycle | null = null;
|
let currentRequest: ReviewRequestLifecycle | null = null;
|
||||||
let pendingPauseRestore: ReviewMpvClient | null = null;
|
let pendingPauseRestore: ReviewMpvClient | null = null;
|
||||||
let resumeDeferred = false;
|
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 {
|
function restorePendingPlayback(): void {
|
||||||
const mpvClient = pendingPauseRestore;
|
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.
|
* not resume it: drops both a held overlay resume and the review's own restore.
|
||||||
*/
|
*/
|
||||||
function cancelPlaybackResume(): void {
|
function cancelPlaybackResume(): void {
|
||||||
|
if (!currentRequest) return;
|
||||||
resumeDeferred = false;
|
resumeDeferred = false;
|
||||||
|
restoreCancelled = true;
|
||||||
if (active) active.restorePlayback = false;
|
if (active) active.restorePlayback = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +445,8 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
|||||||
const [pauseRaw, durationRaw, audioTrackRaw, volumeRaw, resolvedSource, videoSource] =
|
const [pauseRaw, durationRaw, audioTrackRaw, volumeRaw, resolvedSource, videoSource] =
|
||||||
setup.values;
|
setup.values;
|
||||||
const pauseState = booleanProperty(pauseRaw);
|
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'] });
|
mpvClient.send({ command: ['set_property', 'pause', 'yes'] });
|
||||||
if (lifecycle.isCancelled()) {
|
if (lifecycle.isCancelled()) {
|
||||||
restorePendingPlayback();
|
restorePendingPlayback();
|
||||||
@@ -559,6 +564,7 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
|||||||
}
|
}
|
||||||
const lifecycle = createReviewRequestLifecycle();
|
const lifecycle = createReviewRequestLifecycle();
|
||||||
currentRequest = lifecycle;
|
currentRequest = lifecycle;
|
||||||
|
restoreCancelled = false;
|
||||||
try {
|
try {
|
||||||
return await runReview(request, lifecycle);
|
return await runReview(request, lifecycle);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user