mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 12:55:16 -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', () => {
|
test('manual watched session action starts immersion tracker before marking watched', () => {
|
||||||
const source = readMainSource();
|
const source = readMainSource();
|
||||||
const actionBlock = source.match(
|
const actionBlock = source.match(
|
||||||
/markActiveVideoWatched: async \(\) => \{(?<body>[\s\S]*?)\n \},/,
|
/markActiveVideoWatched:\s*async\s*\(\)\s*=>\s*\{(?<body>[\s\S]*?)\}\s*,/,
|
||||||
)?.groups?.body;
|
)?.groups?.body;
|
||||||
|
|
||||||
assert.ok(actionBlock);
|
assert.ok(actionBlock);
|
||||||
|
|||||||
@@ -192,3 +192,43 @@ test('autoplay ready gate defers plugin readiness until the signal target is rea
|
|||||||
true,
|
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 autoPlayReadySignalMediaPath: string | null = null;
|
||||||
let autoPlayReadySignalGeneration = 0;
|
let autoPlayReadySignalGeneration = 0;
|
||||||
let pendingAutoplayReadySignal: {
|
let pendingAutoplayReadySignal: {
|
||||||
|
mediaPath: string;
|
||||||
payload: SubtitleData;
|
payload: SubtitleData;
|
||||||
options?: { forceWhilePaused?: boolean };
|
options?: { forceWhilePaused?: boolean };
|
||||||
} | null = null;
|
} | null = null;
|
||||||
@@ -35,6 +36,9 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) {
|
|||||||
|
|
||||||
const isSignalTargetReady = (): boolean => deps.isSignalTargetReady?.() ?? true;
|
const isSignalTargetReady = (): boolean => deps.isSignalTargetReady?.() ?? true;
|
||||||
|
|
||||||
|
const getSignalMediaPath = (): string =>
|
||||||
|
deps.getCurrentMediaPath()?.trim() || deps.getCurrentVideoPath()?.trim() || '__unknown__';
|
||||||
|
|
||||||
const maybeSignalPluginAutoplayReady = (
|
const maybeSignalPluginAutoplayReady = (
|
||||||
payload: SubtitleData,
|
payload: SubtitleData,
|
||||||
options?: { forceWhilePaused?: boolean },
|
options?: { forceWhilePaused?: boolean },
|
||||||
@@ -47,8 +51,7 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaPath =
|
const mediaPath = getSignalMediaPath();
|
||||||
deps.getCurrentMediaPath()?.trim() || deps.getCurrentVideoPath()?.trim() || '__unknown__';
|
|
||||||
const duplicateMediaSignal = autoPlayReadySignalMediaPath === mediaPath;
|
const duplicateMediaSignal = autoPlayReadySignalMediaPath === mediaPath;
|
||||||
const releaseRetryDelayMs = 200;
|
const releaseRetryDelayMs = 200;
|
||||||
const maxReleaseAttempts = resolveAutoplayReadyMaxReleaseAttempts({
|
const maxReleaseAttempts = resolveAutoplayReadyMaxReleaseAttempts({
|
||||||
@@ -116,7 +119,7 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isSignalTargetReady()) {
|
if (!isSignalTargetReady()) {
|
||||||
pendingAutoplayReadySignal = { payload, options };
|
pendingAutoplayReadySignal = { mediaPath, payload, options };
|
||||||
deps.logDebug(
|
deps.logDebug(
|
||||||
`[autoplay-ready] deferred until signal target is ready for media ${mediaPath}`,
|
`[autoplay-ready] deferred until signal target is ready for media ${mediaPath}`,
|
||||||
);
|
);
|
||||||
@@ -137,6 +140,12 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) {
|
|||||||
|
|
||||||
const pendingSignal = pendingAutoplayReadySignal;
|
const pendingSignal = pendingAutoplayReadySignal;
|
||||||
pendingAutoplayReadySignal = null;
|
pendingAutoplayReadySignal = null;
|
||||||
|
if (getSignalMediaPath() !== pendingSignal.mediaPath) {
|
||||||
|
deps.logDebug(
|
||||||
|
`[autoplay-ready] dropped deferred signal for stale media ${pendingSignal.mediaPath}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
maybeSignalPluginAutoplayReady(pendingSignal.payload, pendingSignal.options);
|
maybeSignalPluginAutoplayReady(pendingSignal.payload, pendingSignal.options);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -67,3 +67,20 @@ test('autoplay tokenization warm release skips stale media after warmup resolves
|
|||||||
|
|
||||||
assert.deepEqual(calls, ['warmup']);
|
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 {
|
}): (mediaPath: string | null | undefined) => void {
|
||||||
const signalIfCurrent = (mediaPath: string): void => {
|
const signalIfCurrent = (mediaPath: string): void => {
|
||||||
const currentMediaPath = normalizeMediaPath(deps.getCurrentMediaPath());
|
const currentMediaPath = normalizeMediaPath(deps.getCurrentMediaPath());
|
||||||
if (currentMediaPath && currentMediaPath !== mediaPath) {
|
if (!currentMediaPath || currentMediaPath !== mediaPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
deps.signalAutoplayReady();
|
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', () => {
|
test('subtitle sidebar stylesheet keeps quoted font fallbacks and generic family', () => {
|
||||||
const cssSource = readWorkspaceFile('src/renderer/style.css');
|
const cssSource = readWorkspaceFile('src/renderer/style.css');
|
||||||
const sidebarContentBlock = cssSource.match(
|
const sidebarContentBlock = cssSource.match(
|
||||||
/\.subtitle-sidebar-content\s*\{(?<body>[\s\S]*?)\n\}/,
|
/\.subtitle-sidebar-content\s*\{(?<body>[\s\S]*?)\s*\}/,
|
||||||
)?.groups?.body;
|
)?.groups?.body;
|
||||||
|
|
||||||
assert.ok(sidebarContentBlock);
|
assert.ok(sidebarContentBlock);
|
||||||
|
|||||||
Reference in New Issue
Block a user