mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-10 03:13:32 -07:00
fix(startup): release autoplay gate before first subtitle line
- Send synthetic `__warm__` payload when no current subtitle exists so the gate can release without waiting for a subtitle event that can't fire while paused - Visible-overlay readiness accepts `__warm__` once the overlay is content-ready, rejects it otherwise - Autoplay gate self-retries via scheduled polling when signal target isn't ready, removing reliance on an external flush event - Skip duplicate desktop notification when overlay or startup sequencer already delivered it
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Fixed startup pause-until-ready so SubMiner releases playback after tokenization and overlay content are ready even when playback starts before the first subtitle line.
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: fixed
|
|
||||||
area: overlay
|
|
||||||
|
|
||||||
- Fixed pause-until-overlay-ready startup on macOS so the initial renderer subtitle snapshot can release the mpv startup gate after the overlay paints annotations.
|
|
||||||
@@ -64,6 +64,16 @@ prefetch work and re-centers prefetch around the live playback time.
|
|||||||
- If secondary `requestProperty` fails, the primary flow stays complete and only a debug line is
|
- If secondary `requestProperty` fails, the primary flow stays complete and only a debug line is
|
||||||
written.
|
written.
|
||||||
|
|
||||||
|
## Startup Ready Release
|
||||||
|
|
||||||
|
- `mpv.pauseUntilOverlayReady` waits for tokenization warmup plus visible-overlay readiness before
|
||||||
|
releasing the mpv startup gate.
|
||||||
|
- If mpv is already on a subtitle, SubMiner still prefers the resolved current subtitle payload and
|
||||||
|
waits for a fresh measured subtitle rectangle before signaling readiness.
|
||||||
|
- If mpv is before the first subtitle, SubMiner sends a synthetic warm readiness payload after
|
||||||
|
tokenization warmup and visible overlay content-ready. This releases playback without waiting for
|
||||||
|
a later subtitle event that cannot happen while mpv is paused.
|
||||||
|
|
||||||
## Linux/X11 Window Shape
|
## Linux/X11 Window Shape
|
||||||
|
|
||||||
- `restoreLinuxOverlayWindowShape()` reads `BrowserWindow.getBounds()` and calls `setShape()` with
|
- `restoreLinuxOverlayWindowShape()` reads `BrowserWindow.getBounds()` and calls `setShape()` with
|
||||||
|
|||||||
+8
-2
@@ -1896,10 +1896,16 @@ async function resolveSentenceSearchHeadwords(term: string): Promise<string[]> {
|
|||||||
function signalCurrentSubtitleAutoplayReady(): void {
|
function signalCurrentSubtitleAutoplayReady(): void {
|
||||||
autoplayReadyGate.flushPendingAutoplayReadySignal();
|
autoplayReadyGate.flushPendingAutoplayReadySignal();
|
||||||
const payload = getCurrentAutoplaySubtitlePayload();
|
const payload = getCurrentAutoplaySubtitlePayload();
|
||||||
if (!payload) {
|
if (payload) {
|
||||||
|
autoplayReadyGate.maybeSignalPluginAutoplayReady(payload, { forceWhilePaused: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
autoplayReadyGate.maybeSignalPluginAutoplayReady(payload, { forceWhilePaused: true });
|
if (!appState.currentSubText.trim()) {
|
||||||
|
autoplayReadyGate.maybeSignalPluginAutoplayReady(
|
||||||
|
{ text: '__warm__', tokens: null },
|
||||||
|
{ forceWhilePaused: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const buildSubtitleProcessingControllerMainDepsHandler =
|
const buildSubtitleProcessingControllerMainDepsHandler =
|
||||||
createBuildSubtitleProcessingControllerMainDepsHandler({
|
createBuildSubtitleProcessingControllerMainDepsHandler({
|
||||||
|
|||||||
@@ -216,11 +216,14 @@ test('subtitle sidebar open state is restored for replacement visible overlay wi
|
|||||||
assert.match(depsBlock, /subtitleSidebarRequestedOpen/);
|
assert.match(depsBlock, /subtitleSidebarRequestedOpen/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('warm tokenization release reuses current subtitle payload instead of synthetic readiness', () => {
|
test('warm tokenization release can signal readiness before the first subtitle appears', () => {
|
||||||
const source = readMainSource();
|
const source = readMainSource();
|
||||||
const warmReleaseBlock = source.match(
|
const warmReleaseBlock = source.match(
|
||||||
/signalAutoplayReadyFromWarmTokenization = createAutoplayTokenizationWarmRelease\(\{(?<body>[\s\S]*?)\n\}\);/,
|
/signalAutoplayReadyFromWarmTokenization = createAutoplayTokenizationWarmRelease\(\{(?<body>[\s\S]*?)\n\}\);/,
|
||||||
)?.groups?.body;
|
)?.groups?.body;
|
||||||
|
const signalBlock = source.match(
|
||||||
|
/function signalCurrentSubtitleAutoplayReady\(\): void \{(?<body>[\s\S]*?)\n\}/,
|
||||||
|
)?.groups?.body;
|
||||||
const currentPayloadBlock = source.match(
|
const currentPayloadBlock = source.match(
|
||||||
/function getCurrentAutoplaySubtitlePayload\(\): SubtitleData \| null \{(?<body>[\s\S]*?)\n\}/,
|
/function getCurrentAutoplaySubtitlePayload\(\): SubtitleData \| null \{(?<body>[\s\S]*?)\n\}/,
|
||||||
)?.groups?.body;
|
)?.groups?.body;
|
||||||
@@ -230,7 +233,12 @@ test('warm tokenization release reuses current subtitle payload instead of synth
|
|||||||
warmReleaseBlock,
|
warmReleaseBlock,
|
||||||
/signalAutoplayReady: \(\) => signalCurrentSubtitleAutoplayReady\(\)/,
|
/signalAutoplayReady: \(\) => signalCurrentSubtitleAutoplayReady\(\)/,
|
||||||
);
|
);
|
||||||
assert.doesNotMatch(warmReleaseBlock, /__warm__/);
|
|
||||||
|
assert.ok(signalBlock);
|
||||||
|
assert.match(signalBlock, /const payload = getCurrentAutoplaySubtitlePayload\(\);/);
|
||||||
|
assert.match(signalBlock, /if \(payload\) \{/);
|
||||||
|
assert.match(signalBlock, /if \(!appState\.currentSubText\.trim\(\)\) \{/);
|
||||||
|
assert.match(signalBlock, /text: '__warm__'/);
|
||||||
|
|
||||||
assert.ok(currentPayloadBlock);
|
assert.ok(currentPayloadBlock);
|
||||||
assert.match(currentPayloadBlock, /appState\.currentSubtitleData/);
|
assert.match(currentPayloadBlock, /appState\.currentSubtitleData/);
|
||||||
|
|||||||
@@ -314,6 +314,57 @@ test('autoplay ready gate defers plugin readiness until the signal target is rea
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('autoplay ready gate retries deferred readiness without an external flush event', async () => {
|
||||||
|
const commands: Array<Array<string | boolean>> = [];
|
||||||
|
const scheduled: Array<() => void> = [];
|
||||||
|
let targetReady = false;
|
||||||
|
|
||||||
|
const gate = createAutoplayReadyGate({
|
||||||
|
isAppOwnedFlowInFlight: () => false,
|
||||||
|
getCurrentMediaPath: () => '/media/video.mkv',
|
||||||
|
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) => {
|
||||||
|
scheduled.push(callback);
|
||||||
|
return 1 as never;
|
||||||
|
},
|
||||||
|
logDebug: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
gate.maybeSignalPluginAutoplayReady({ text: '字幕', tokens: null }, { forceWhilePaused: true });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
assert.deepEqual(commands, []);
|
||||||
|
assert.equal(scheduled.length, 1);
|
||||||
|
|
||||||
|
targetReady = true;
|
||||||
|
scheduled.shift()?.();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
commands.filter((command) => command[0] === 'script-message'),
|
||||||
|
[['script-message', 'subminer-autoplay-ready']],
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
commands.some(
|
||||||
|
(command) => command[0] === 'set_property' && command[1] === 'pause' && command[2] === false,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('autoplay ready gate drops deferred readiness after media changes before flush', async () => {
|
test('autoplay ready gate drops deferred readiness after media changes before flush', async () => {
|
||||||
const commands: Array<Array<string | boolean>> = [];
|
const commands: Array<Array<string | boolean>> = [];
|
||||||
let targetReady = false;
|
let targetReady = false;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { SubtitleData } from '../../types';
|
import type { SubtitleData } from '../../types';
|
||||||
import { resolveAutoplayReadyMaxReleaseAttempts } from './startup-autoplay-release-policy';
|
import { resolveAutoplayReadyMaxReleaseAttempts } from './startup-autoplay-release-policy';
|
||||||
|
|
||||||
|
const PENDING_AUTOPLAY_READY_RETRY_DELAY_MS = 200;
|
||||||
|
const MAX_PENDING_AUTOPLAY_READY_RETRY_ATTEMPTS = 75;
|
||||||
|
|
||||||
type MpvClientLike = {
|
type MpvClientLike = {
|
||||||
connected?: boolean;
|
connected?: boolean;
|
||||||
requestProperty: (property: string) => Promise<unknown>;
|
requestProperty: (property: string) => Promise<unknown>;
|
||||||
@@ -34,12 +37,22 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) {
|
|||||||
let autoPlayReadySignalMediaPath: string | null = null;
|
let autoPlayReadySignalMediaPath: string | null = null;
|
||||||
let autoPlayReadySignalGeneration = 0;
|
let autoPlayReadySignalGeneration = 0;
|
||||||
let pendingAutoplayReadySignal: AutoplayReadySignal | null = null;
|
let pendingAutoplayReadySignal: AutoplayReadySignal | null = null;
|
||||||
|
let pendingAutoplayReadyRetryToken = 0;
|
||||||
|
let pendingAutoplayReadyRetryAttempts = 0;
|
||||||
|
let scheduledPendingAutoplayReadyRetryToken: number | null = null;
|
||||||
const now = deps.now ?? (() => Date.now());
|
const now = deps.now ?? (() => Date.now());
|
||||||
|
|
||||||
|
const invalidatePendingAutoplayReadyRetry = (): void => {
|
||||||
|
pendingAutoplayReadyRetryToken += 1;
|
||||||
|
pendingAutoplayReadyRetryAttempts = 0;
|
||||||
|
scheduledPendingAutoplayReadyRetryToken = null;
|
||||||
|
};
|
||||||
|
|
||||||
const invalidatePendingAutoplayReadyFallbacks = (): void => {
|
const invalidatePendingAutoplayReadyFallbacks = (): void => {
|
||||||
autoPlayReadySignalMediaPath = null;
|
autoPlayReadySignalMediaPath = null;
|
||||||
pendingAutoplayReadySignal = null;
|
pendingAutoplayReadySignal = null;
|
||||||
autoPlayReadySignalGeneration += 1;
|
autoPlayReadySignalGeneration += 1;
|
||||||
|
invalidatePendingAutoplayReadyRetry();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSignalTargetReady = (signal: AutoplayReadySignal): boolean =>
|
const isSignalTargetReady = (signal: AutoplayReadySignal): boolean =>
|
||||||
@@ -52,18 +65,43 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) {
|
|||||||
pendingAutoplayReadySignal = null;
|
pendingAutoplayReadySignal = null;
|
||||||
autoPlayReadySignalMediaPath = getSignalMediaPath();
|
autoPlayReadySignalMediaPath = getSignalMediaPath();
|
||||||
autoPlayReadySignalGeneration += 1;
|
autoPlayReadySignalGeneration += 1;
|
||||||
|
invalidatePendingAutoplayReadyRetry();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setPendingAutoplayReadySignal = (signal: AutoplayReadySignal): void => {
|
const setPendingAutoplayReadySignal = (signal: AutoplayReadySignal): boolean => {
|
||||||
if (
|
if (
|
||||||
pendingAutoplayReadySignal &&
|
pendingAutoplayReadySignal &&
|
||||||
pendingAutoplayReadySignal.mediaPath === signal.mediaPath &&
|
pendingAutoplayReadySignal.mediaPath === signal.mediaPath &&
|
||||||
pendingAutoplayReadySignal.payload.text === signal.payload.text &&
|
pendingAutoplayReadySignal.payload.text === signal.payload.text &&
|
||||||
pendingAutoplayReadySignal.requestedAtMs <= signal.requestedAtMs
|
pendingAutoplayReadySignal.requestedAtMs <= signal.requestedAtMs
|
||||||
) {
|
) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
pendingAutoplayReadySignal = signal;
|
pendingAutoplayReadySignal = signal;
|
||||||
|
pendingAutoplayReadyRetryAttempts = 0;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const schedulePendingAutoplayReadyRetry = (): void => {
|
||||||
|
if (scheduledPendingAutoplayReadyRetryToken === pendingAutoplayReadyRetryToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pendingAutoplayReadyRetryAttempts >= MAX_PENDING_AUTOPLAY_READY_RETRY_ATTEMPTS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryToken = pendingAutoplayReadyRetryToken;
|
||||||
|
pendingAutoplayReadyRetryAttempts += 1;
|
||||||
|
scheduledPendingAutoplayReadyRetryToken = retryToken;
|
||||||
|
deps.schedule(() => {
|
||||||
|
if (scheduledPendingAutoplayReadyRetryToken === retryToken) {
|
||||||
|
scheduledPendingAutoplayReadyRetryToken = null;
|
||||||
|
}
|
||||||
|
if (retryToken !== pendingAutoplayReadyRetryToken || !pendingAutoplayReadySignal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
flushPendingAutoplayReadySignal();
|
||||||
|
}, PENDING_AUTOPLAY_READY_RETRY_DELAY_MS);
|
||||||
};
|
};
|
||||||
|
|
||||||
const releaseAutoplayReadySignal = (signal: AutoplayReadySignal): void => {
|
const releaseAutoplayReadySignal = (signal: AutoplayReadySignal): void => {
|
||||||
@@ -139,6 +177,7 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pendingAutoplayReadySignal = null;
|
pendingAutoplayReadySignal = null;
|
||||||
|
invalidatePendingAutoplayReadyRetry();
|
||||||
autoPlayReadySignalMediaPath = mediaPath;
|
autoPlayReadySignalMediaPath = mediaPath;
|
||||||
const playbackGeneration = ++autoPlayReadySignalGeneration;
|
const playbackGeneration = ++autoPlayReadySignalGeneration;
|
||||||
deps.signalPluginAutoplayReady();
|
deps.signalPluginAutoplayReady();
|
||||||
@@ -152,10 +191,13 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isSignalTargetReady(signal)) {
|
if (!isSignalTargetReady(signal)) {
|
||||||
setPendingAutoplayReadySignal(signal);
|
const pendingSignalChanged = setPendingAutoplayReadySignal(signal);
|
||||||
|
schedulePendingAutoplayReadyRetry();
|
||||||
|
if (pendingSignalChanged) {
|
||||||
deps.logDebug(
|
deps.logDebug(
|
||||||
`[autoplay-ready] deferred until signal target is ready for media ${signal.mediaPath}`,
|
`[autoplay-ready] deferred until signal target is ready for media ${signal.mediaPath}`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
notifyCharacterDictionaryAutoSyncStatus,
|
notifyCharacterDictionaryAutoSyncStatus,
|
||||||
type CharacterDictionaryAutoSyncNotificationEvent,
|
type CharacterDictionaryAutoSyncNotificationEvent,
|
||||||
} from './character-dictionary-auto-sync-notifications';
|
} from './character-dictionary-auto-sync-notifications';
|
||||||
|
import { createStartupOsdSequencer } from './startup-osd-sequencer';
|
||||||
|
|
||||||
function makeEvent(
|
function makeEvent(
|
||||||
phase: CharacterDictionaryAutoSyncNotificationEvent['phase'],
|
phase: CharacterDictionaryAutoSyncNotificationEvent['phase'],
|
||||||
@@ -70,7 +71,7 @@ test('auto sync notifications send osd updates for progress phases', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('auto sync notifications route both to overlay and system only', () => {
|
test('auto sync notifications prefer overlay delivery for both when overlay is available', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
|
||||||
notifyCharacterDictionaryAutoSyncStatus(makeEvent('syncing', 'syncing'), {
|
notifyCharacterDictionaryAutoSyncStatus(makeEvent('syncing', 'syncing'), {
|
||||||
@@ -100,9 +101,7 @@ test('auto sync notifications route both to overlay and system only', () => {
|
|||||||
|
|
||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, [
|
||||||
'overlay:character-dictionary-auto-sync:Character dictionary:syncing:pin',
|
'overlay:character-dictionary-auto-sync:Character dictionary:syncing:pin',
|
||||||
'desktop:SubMiner:syncing',
|
|
||||||
'overlay:character-dictionary-auto-sync:Character dictionary:ready:auto',
|
'overlay:character-dictionary-auto-sync:Character dictionary:ready:auto',
|
||||||
'desktop:SubMiner:ready',
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -187,3 +186,29 @@ test('auto sync notifications send osd-system desktop updates with startup seque
|
|||||||
|
|
||||||
assert.deepEqual(calls, ['sequencer:importing:importing', 'desktop:SubMiner:importing']);
|
assert.deepEqual(calls, ['sequencer:importing:importing', 'desktop:SubMiner:importing']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('auto sync notifications let startup sequencer own osd-system desktop delivery', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const startupOsdSequencer = createStartupOsdSequencer({
|
||||||
|
getNotificationType: () => 'osd-system',
|
||||||
|
showOsd: (message) => {
|
||||||
|
calls.push(`osd:${message}`);
|
||||||
|
},
|
||||||
|
showDesktopNotification: (title, options) => {
|
||||||
|
calls.push(`desktop:${title}:${options.body ?? ''}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
startupOsdSequencer.markTokenizationReady();
|
||||||
|
|
||||||
|
notifyCharacterDictionaryAutoSyncStatus(makeEvent('importing', 'importing'), {
|
||||||
|
getNotificationType: () => 'osd-system',
|
||||||
|
showOsd: (message) => {
|
||||||
|
calls.push(`direct-osd:${message}`);
|
||||||
|
},
|
||||||
|
showDesktopNotification: (title, options) =>
|
||||||
|
calls.push(`direct-desktop:${title}:${options.body ?? ''}`),
|
||||||
|
startupOsdSequencer,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(calls, ['osd:importing', 'desktop:SubMiner:importing']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export function notifyCharacterDictionaryAutoSyncStatus(
|
|||||||
): void {
|
): void {
|
||||||
const type = deps.getNotificationType() ?? 'overlay';
|
const type = deps.getNotificationType() ?? 'overlay';
|
||||||
if (type === 'none') return;
|
if (type === 'none') return;
|
||||||
|
let overlayShown = false;
|
||||||
|
let startupSequencerShown = false;
|
||||||
|
|
||||||
if (shouldShowOverlay(type)) {
|
if (shouldShowOverlay(type)) {
|
||||||
if (deps.showOverlayNotification) {
|
if (deps.showOverlayNotification) {
|
||||||
@@ -45,6 +47,7 @@ export function notifyCharacterDictionaryAutoSyncStatus(
|
|||||||
variant: overlayVariantForPhase(event.phase),
|
variant: overlayVariantForPhase(event.phase),
|
||||||
persistent: !isTerminalPhase(event.phase),
|
persistent: !isTerminalPhase(event.phase),
|
||||||
});
|
});
|
||||||
|
overlayShown = true;
|
||||||
} else if (!shouldShowDesktop(type)) {
|
} else if (!shouldShowDesktop(type)) {
|
||||||
deps.showDesktopNotification('SubMiner', { body: event.message });
|
deps.showDesktopNotification('SubMiner', { body: event.message });
|
||||||
}
|
}
|
||||||
@@ -52,7 +55,7 @@ export function notifyCharacterDictionaryAutoSyncStatus(
|
|||||||
|
|
||||||
if (shouldShowOsd(type)) {
|
if (shouldShowOsd(type)) {
|
||||||
if (deps.startupOsdSequencer) {
|
if (deps.startupOsdSequencer) {
|
||||||
deps.startupOsdSequencer.notifyCharacterDictionaryStatus({
|
startupSequencerShown = deps.startupOsdSequencer.notifyCharacterDictionaryStatus({
|
||||||
phase: event.phase,
|
phase: event.phase,
|
||||||
message: event.message,
|
message: event.message,
|
||||||
});
|
});
|
||||||
@@ -61,7 +64,7 @@ export function notifyCharacterDictionaryAutoSyncStatus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldShowDesktop(type)) {
|
if (shouldShowDesktop(type) && !overlayShown && !startupSequencerShown) {
|
||||||
deps.showDesktopNotification('SubMiner', { body: event.message });
|
deps.showDesktopNotification('SubMiner', { body: event.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,41 @@ test('visible overlay autoplay target falls back when interactive rects have no
|
|||||||
assert.equal(ready, true);
|
assert.equal(ready, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('visible overlay autoplay target rejects synthetic warmup readiness', () => {
|
test('visible overlay autoplay target accepts synthetic warmup readiness after content-ready', () => {
|
||||||
|
const ready = isVisibleOverlayAutoplayTargetReady(
|
||||||
|
{
|
||||||
|
getVisibleOverlayVisible: () => true,
|
||||||
|
isOverlayWindowReady: () => true,
|
||||||
|
getLatestVisibleMeasurement: () => null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mediaPath: '/media/video.mkv',
|
||||||
|
payload: { text: '__warm__', tokens: null },
|
||||||
|
requestedAtMs: 1_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(ready, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('visible overlay autoplay target waits for content-ready before synthetic warmup readiness', () => {
|
||||||
|
const ready = isVisibleOverlayAutoplayTargetReady(
|
||||||
|
{
|
||||||
|
getVisibleOverlayVisible: () => true,
|
||||||
|
isOverlayWindowReady: () => false,
|
||||||
|
getLatestVisibleMeasurement: () => visibleMeasurement(2_000),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mediaPath: '/media/video.mkv',
|
||||||
|
payload: { text: '__warm__', tokens: null },
|
||||||
|
requestedAtMs: 1_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(ready, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('visible overlay autoplay target rejects empty readiness payloads', () => {
|
||||||
const ready = isVisibleOverlayAutoplayTargetReady(
|
const ready = isVisibleOverlayAutoplayTargetReady(
|
||||||
{
|
{
|
||||||
getVisibleOverlayVisible: () => true,
|
getVisibleOverlayVisible: () => true,
|
||||||
@@ -71,7 +105,7 @@ test('visible overlay autoplay target rejects synthetic warmup readiness', () =>
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
mediaPath: '/media/video.mkv',
|
mediaPath: '/media/video.mkv',
|
||||||
payload: { text: '__warm__', tokens: null },
|
payload: { text: '', tokens: null },
|
||||||
requestedAtMs: 1_000,
|
requestedAtMs: 1_000,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function isVisibleOverlayAutoplayTargetReady(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const subtitleText = signal.payload.text.trim();
|
const subtitleText = signal.payload.text.trim();
|
||||||
if (!subtitleText || subtitleText === '__warm__') {
|
if (!subtitleText) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +39,10 @@ export function isVisibleOverlayAutoplayTargetReady(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subtitleText === '__warm__') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const measurement = deps.getLatestVisibleMeasurement();
|
const measurement = deps.getLatestVisibleMeasurement();
|
||||||
if (!measurement || measurement.measuredAtMs < signal.requestedAtMs) {
|
if (!measurement || measurement.measuredAtMs < signal.requestedAtMs) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user