mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-25 12:55:18 -07:00
fix: drop stale deferred autoplay-ready signals on media change
- autoplay-ready gate now stamps pending signal with mediaPath and discards it on flush if media has changed - tokenization warm release skips signaling when current media path is cleared (null) - tighten regex matchers in main-wiring and overlay-legacy-cleanup tests
This commit is contained in:
@@ -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 \(\) => \{(?<body>[\s\S]*?)\n \},/,
|
||||
/markActiveVideoWatched:\s*async\s*\(\)\s*=>\s*\{(?<body>[\s\S]*?)\}\s*,/,
|
||||
)?.groups?.body;
|
||||
|
||||
assert.ok(actionBlock);
|
||||
|
||||
@@ -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<Array<string | boolean>> = [];
|
||||
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<string | boolean> }) => {
|
||||
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, []);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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, []);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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*\{(?<body>[\s\S]*?)\n\}/,
|
||||
/\.subtitle-sidebar-content\s*\{(?<body>[\s\S]*?)\s*\}/,
|
||||
)?.groups?.body;
|
||||
|
||||
assert.ok(sidebarContentBlock);
|
||||
|
||||
Reference in New Issue
Block a user