diff --git a/src/main/main-wiring.test.ts b/src/main/main-wiring.test.ts
index 77335e99..03edb3a6 100644
--- a/src/main/main-wiring.test.ts
+++ b/src/main/main-wiring.test.ts
@@ -10,7 +10,7 @@ function readMainSource(): string {
test('manual watched session action starts immersion tracker before marking watched', () => {
const source = readMainSource();
const actionBlock = source.match(
- /markActiveVideoWatched: async \(\) => \{(?
[\s\S]*?)\n \},/,
+ /markActiveVideoWatched:\s*async\s*\(\)\s*=>\s*\{(?[\s\S]*?)\}\s*,/,
)?.groups?.body;
assert.ok(actionBlock);
diff --git a/src/main/runtime/autoplay-ready-gate.test.ts b/src/main/runtime/autoplay-ready-gate.test.ts
index 07e0a381..202f1441 100644
--- a/src/main/runtime/autoplay-ready-gate.test.ts
+++ b/src/main/runtime/autoplay-ready-gate.test.ts
@@ -192,3 +192,43 @@ test('autoplay ready gate defers plugin readiness until the signal target is rea
true,
);
});
+
+test('autoplay ready gate drops deferred readiness after media changes before flush', async () => {
+ const commands: Array> = [];
+ let targetReady = false;
+ let currentMediaPath = '/media/video-1.mkv';
+
+ const gate = createAutoplayReadyGate({
+ isAppOwnedFlowInFlight: () => false,
+ getCurrentMediaPath: () => currentMediaPath,
+ getCurrentVideoPath: () => null,
+ getPlaybackPaused: () => true,
+ getMpvClient: () =>
+ ({
+ connected: true,
+ requestProperty: async () => true,
+ send: ({ command }: { command: Array }) => {
+ commands.push(command);
+ },
+ }) as never,
+ signalPluginAutoplayReady: () => {
+ commands.push(['script-message', 'subminer-autoplay-ready']);
+ },
+ isSignalTargetReady: () => targetReady,
+ schedule: (callback) => {
+ queueMicrotask(callback);
+ return 1 as never;
+ },
+ logDebug: () => {},
+ });
+
+ gate.maybeSignalPluginAutoplayReady({ text: '字幕', tokens: null }, { forceWhilePaused: true });
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ currentMediaPath = '/media/video-2.mkv';
+ targetReady = true;
+ gate.flushPendingAutoplayReadySignal();
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ assert.deepEqual(commands, []);
+});
diff --git a/src/main/runtime/autoplay-ready-gate.ts b/src/main/runtime/autoplay-ready-gate.ts
index 56fe369e..b5044d8d 100644
--- a/src/main/runtime/autoplay-ready-gate.ts
+++ b/src/main/runtime/autoplay-ready-gate.ts
@@ -23,6 +23,7 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) {
let autoPlayReadySignalMediaPath: string | null = null;
let autoPlayReadySignalGeneration = 0;
let pendingAutoplayReadySignal: {
+ mediaPath: string;
payload: SubtitleData;
options?: { forceWhilePaused?: boolean };
} | null = null;
@@ -35,6 +36,9 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) {
const isSignalTargetReady = (): boolean => deps.isSignalTargetReady?.() ?? true;
+ const getSignalMediaPath = (): string =>
+ deps.getCurrentMediaPath()?.trim() || deps.getCurrentVideoPath()?.trim() || '__unknown__';
+
const maybeSignalPluginAutoplayReady = (
payload: SubtitleData,
options?: { forceWhilePaused?: boolean },
@@ -47,8 +51,7 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) {
return;
}
- const mediaPath =
- deps.getCurrentMediaPath()?.trim() || deps.getCurrentVideoPath()?.trim() || '__unknown__';
+ const mediaPath = getSignalMediaPath();
const duplicateMediaSignal = autoPlayReadySignalMediaPath === mediaPath;
const releaseRetryDelayMs = 200;
const maxReleaseAttempts = resolveAutoplayReadyMaxReleaseAttempts({
@@ -116,7 +119,7 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) {
return;
}
if (!isSignalTargetReady()) {
- pendingAutoplayReadySignal = { payload, options };
+ pendingAutoplayReadySignal = { mediaPath, payload, options };
deps.logDebug(
`[autoplay-ready] deferred until signal target is ready for media ${mediaPath}`,
);
@@ -137,6 +140,12 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) {
const pendingSignal = pendingAutoplayReadySignal;
pendingAutoplayReadySignal = null;
+ if (getSignalMediaPath() !== pendingSignal.mediaPath) {
+ deps.logDebug(
+ `[autoplay-ready] dropped deferred signal for stale media ${pendingSignal.mediaPath}`,
+ );
+ return;
+ }
maybeSignalPluginAutoplayReady(pendingSignal.payload, pendingSignal.options);
};
diff --git a/src/main/runtime/autoplay-tokenization-warm-release.test.ts b/src/main/runtime/autoplay-tokenization-warm-release.test.ts
index be5faa5a..350401ff 100644
--- a/src/main/runtime/autoplay-tokenization-warm-release.test.ts
+++ b/src/main/runtime/autoplay-tokenization-warm-release.test.ts
@@ -67,3 +67,20 @@ test('autoplay tokenization warm release skips stale media after warmup resolves
assert.deepEqual(calls, ['warmup']);
});
+
+test('autoplay tokenization warm release skips signaling when current media is cleared', () => {
+ const calls: string[] = [];
+ const release = createAutoplayTokenizationWarmRelease({
+ isTokenizationWarmupReady: () => true,
+ startTokenizationWarmups: async () => {
+ calls.push('warmup');
+ },
+ getCurrentMediaPath: () => null,
+ signalAutoplayReady: () => calls.push('signal'),
+ warn: () => {},
+ });
+
+ release('/tmp/video.mkv');
+
+ assert.deepEqual(calls, []);
+});
diff --git a/src/main/runtime/autoplay-tokenization-warm-release.ts b/src/main/runtime/autoplay-tokenization-warm-release.ts
index 13c1bd9b..ae6112e4 100644
--- a/src/main/runtime/autoplay-tokenization-warm-release.ts
+++ b/src/main/runtime/autoplay-tokenization-warm-release.ts
@@ -15,7 +15,7 @@ export function createAutoplayTokenizationWarmRelease(deps: {
}): (mediaPath: string | null | undefined) => void {
const signalIfCurrent = (mediaPath: string): void => {
const currentMediaPath = normalizeMediaPath(deps.getCurrentMediaPath());
- if (currentMediaPath && currentMediaPath !== mediaPath) {
+ if (!currentMediaPath || currentMediaPath !== mediaPath) {
return;
}
deps.signalAutoplayReady();
diff --git a/src/renderer/overlay-legacy-cleanup.test.ts b/src/renderer/overlay-legacy-cleanup.test.ts
index dd971443..3d888a0d 100644
--- a/src/renderer/overlay-legacy-cleanup.test.ts
+++ b/src/renderer/overlay-legacy-cleanup.test.ts
@@ -43,7 +43,7 @@ test('renderer stylesheet only hides visible focus chrome on top-level overlay f
test('subtitle sidebar stylesheet keeps quoted font fallbacks and generic family', () => {
const cssSource = readWorkspaceFile('src/renderer/style.css');
const sidebarContentBlock = cssSource.match(
- /\.subtitle-sidebar-content\s*\{(?[\s\S]*?)\n\}/,
+ /\.subtitle-sidebar-content\s*\{(?[\s\S]*?)\s*\}/,
)?.groups?.body;
assert.ok(sidebarContentBlock);