mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
fix: prime startup subtitle before autoplay resumes
- Add `selectAutoplayStartupCue` to pick active or imminent cue at startup - Call `primeCurrentSubtitle` in warm-release before signaling autoplay ready - Reset primed state on media path change to avoid stale cue leaks
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: overlay
|
||||
|
||||
- Primed the first startup subtitle before autoplay resumes so the overlay can render text before video playback begins.
|
||||
+2
-2
@@ -50,8 +50,8 @@
|
||||
"test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-session-bindings.lua && lua scripts/test-plugin-binary-windows.lua",
|
||||
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
||||
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/mpv.test.ts launcher/picker.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src",
|
||||
"test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/settings/settings-model.test.ts src/settings/settings-field-layout.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/ipc.test.ts src/core/services/anki-jimaku-ipc.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/setup-gate.test.ts stats/src/lib/api-client.test.ts stats/src/hooks/useExcludedWords.test.ts",
|
||||
"test:core:dist": "bun test dist/preload-settings.test.js dist/settings/settings-anki-controls.test.js dist/settings/settings-model.test.js dist/settings/settings-field-layout.test.js dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js",
|
||||
"test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/settings/settings-model.test.ts src/settings/settings-field-layout.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/ipc.test.ts src/core/services/anki-jimaku-ipc.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/main/runtime/autoplay-tokenization-warm-release.test.ts src/main/runtime/autoplay-subtitle-primer.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/setup-gate.test.ts stats/src/lib/api-client.test.ts stats/src/hooks/useExcludedWords.test.ts",
|
||||
"test:core:dist": "bun test dist/preload-settings.test.js dist/settings/settings-anki-controls.test.js dist/settings/settings-model.test.js dist/settings/settings-field-layout.test.js dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/main/runtime/autoplay-tokenization-warm-release.test.js dist/main/runtime/autoplay-subtitle-primer.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js",
|
||||
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
||||
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
||||
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
|
||||
|
||||
+105
-1
@@ -104,6 +104,7 @@ import type {
|
||||
RuntimeOptionState,
|
||||
SessionActionDispatchRequest,
|
||||
SecondarySubMode,
|
||||
SubtitleCue,
|
||||
SubtitleData,
|
||||
SubtitlePosition,
|
||||
UpdateChannel,
|
||||
@@ -363,6 +364,7 @@ import {
|
||||
createYoutubePrimarySubtitleNotificationRuntime,
|
||||
} from './main/runtime/youtube-primary-subtitle-notification';
|
||||
import { createAutoplayReadyGate } from './main/runtime/autoplay-ready-gate';
|
||||
import { selectAutoplayStartupCue } from './main/runtime/autoplay-subtitle-primer';
|
||||
import { createAutoplayTokenizationWarmRelease } from './main/runtime/autoplay-tokenization-warm-release';
|
||||
import { createManagedLocalSubtitleSelectionRuntime } from './main/runtime/local-subtitle-selection';
|
||||
import {
|
||||
@@ -1625,6 +1627,88 @@ let lastObservedTimePos = 0;
|
||||
let cancelLinuxMpvFullscreenOverlayRefreshBurst: CancelLinuxMpvFullscreenOverlayRefreshBurst | null =
|
||||
null;
|
||||
const SEEK_THRESHOLD_SECONDS = 3;
|
||||
const AUTOPLAY_SUBTITLE_PRIME_LOOKAHEAD_SECONDS = 2;
|
||||
let autoplaySubtitlePrimedMediaPath: string | null = null;
|
||||
|
||||
function getCurrentAutoplayMediaPath(): string | null {
|
||||
return appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null;
|
||||
}
|
||||
|
||||
function isCurrentAutoplayMediaPath(mediaPath: string): boolean {
|
||||
return getCurrentAutoplayMediaPath() === mediaPath;
|
||||
}
|
||||
|
||||
function markAutoplaySubtitlePrimeConsumed(mediaPath: string): boolean {
|
||||
if (autoplaySubtitlePrimedMediaPath === mediaPath) {
|
||||
return false;
|
||||
}
|
||||
autoplaySubtitlePrimedMediaPath = mediaPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
function emitAutoplayPrimedSubtitle(mediaPath: string, text: string): boolean {
|
||||
if (!text.trim() || !isCurrentAutoplayMediaPath(mediaPath)) {
|
||||
return false;
|
||||
}
|
||||
if (!markAutoplaySubtitlePrimeConsumed(mediaPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
appState.currentSubText = text;
|
||||
const rawPayload = withCurrentSubtitleTiming({ text, tokens: null });
|
||||
appState.currentSubtitleData = rawPayload;
|
||||
broadcastToOverlayWindows('subtitle:set', rawPayload);
|
||||
subtitlePrefetchService?.pause();
|
||||
subtitleProcessingController.onSubtitleChange(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function primeCurrentSubtitleForAutoplay(mediaPath: string): Promise<void> {
|
||||
const client = appState.mpvClient;
|
||||
if (!client?.connected || !isCurrentAutoplayMediaPath(mediaPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subTextRaw = await client.requestProperty('sub-text').catch((error) => {
|
||||
logger.debug(
|
||||
`[autoplay-subtitle-prime] failed to read sub-text: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
return null;
|
||||
});
|
||||
const text = typeof subTextRaw === 'string' ? subTextRaw : '';
|
||||
emitAutoplayPrimedSubtitle(mediaPath, text);
|
||||
}
|
||||
|
||||
async function primeAutoplaySubtitleFromParsedCues(
|
||||
mediaPath: string,
|
||||
cues: SubtitleCue[],
|
||||
): Promise<void> {
|
||||
if (
|
||||
cues.length === 0 ||
|
||||
autoplaySubtitlePrimedMediaPath === mediaPath ||
|
||||
!isCurrentAutoplayMediaPath(mediaPath)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = appState.mpvClient;
|
||||
const timePosRaw = await client?.requestProperty('time-pos').catch(() => null);
|
||||
const currentTimeSeconds = Number(
|
||||
timePosRaw ?? client?.currentTimePos ?? lastObservedTimePos ?? 0,
|
||||
);
|
||||
const cue = selectAutoplayStartupCue(
|
||||
cues,
|
||||
Number.isFinite(currentTimeSeconds) ? currentTimeSeconds : 0,
|
||||
AUTOPLAY_SUBTITLE_PRIME_LOOKAHEAD_SECONDS,
|
||||
);
|
||||
if (!cue) {
|
||||
return;
|
||||
}
|
||||
|
||||
emitAutoplayPrimedSubtitle(mediaPath, cue.text);
|
||||
}
|
||||
|
||||
function clearScheduledSubtitlePrefetchRefresh(): void {
|
||||
if (subtitlePrefetchRefreshTimer) {
|
||||
@@ -1657,6 +1741,16 @@ const subtitlePrefetchInitController = createSubtitlePrefetchInitController({
|
||||
onParsedSubtitleCuesChanged: (cues, sourceKey) => {
|
||||
appState.activeParsedSubtitleCues = cues ?? [];
|
||||
appState.activeParsedSubtitleSource = sourceKey;
|
||||
const mediaPath = getCurrentAutoplayMediaPath();
|
||||
if (mediaPath && cues?.length) {
|
||||
void primeAutoplaySubtitleFromParsedCues(mediaPath, cues).catch((error) => {
|
||||
logger.debug(
|
||||
`[autoplay-subtitle-prime] failed to prime from parsed cues: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
const resolveActiveSubtitleSidebarSourceHandler = createResolveActiveSubtitleSidebarSourceHandler({
|
||||
@@ -4150,6 +4244,14 @@ const {
|
||||
tokenizeSubtitleForImmersion: async (text): Promise<SubtitleData | null> =>
|
||||
tokenizeSubtitleDeferred ? await tokenizeSubtitleDeferred(text) : null,
|
||||
updateCurrentMediaPath: (path) => {
|
||||
const normalizedPath = path.trim();
|
||||
const previousPath = appState.currentMediaPath?.trim() || null;
|
||||
if ((normalizedPath || null) !== previousPath) {
|
||||
autoplaySubtitlePrimedMediaPath = null;
|
||||
appState.currentSubText = '';
|
||||
appState.currentSubAssText = '';
|
||||
appState.currentSubtitleData = null;
|
||||
}
|
||||
autoplayReadyGate.invalidatePendingAutoplayReadyFallbacks();
|
||||
currentMediaTokenizationGate.updateCurrentMediaPath(path);
|
||||
managedLocalSubtitleSelectionRuntime.handleMediaPathChange(path);
|
||||
@@ -4159,7 +4261,8 @@ const {
|
||||
youtubePrimarySubtitleNotificationRuntime.handleMediaPathChange(path);
|
||||
if (path) {
|
||||
ensureImmersionTrackerStarted();
|
||||
// Delay slightly to allow MPV's track-list to be populated.
|
||||
void subtitlePrefetchRuntime.refreshSubtitlePrefetchFromActiveTrack();
|
||||
// Retry after a short delay because MPV can populate track-list after path.
|
||||
subtitlePrefetchRuntime.scheduleSubtitlePrefetchRefresh(500);
|
||||
}
|
||||
mediaRuntime.updateCurrentMediaPath(path);
|
||||
@@ -4410,6 +4513,7 @@ signalAutoplayReadyFromWarmTokenization = createAutoplayTokenizationWarmRelease(
|
||||
},
|
||||
getCurrentMediaPath: () =>
|
||||
appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null,
|
||||
primeCurrentSubtitle: (mediaPath) => primeCurrentSubtitleForAutoplay(mediaPath),
|
||||
signalAutoplayReady: () => {
|
||||
autoplayReadyGate.maybeSignalPluginAutoplayReady(
|
||||
{ text: '__warm__', tokens: null },
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { selectAutoplayStartupCue } from './autoplay-subtitle-primer';
|
||||
|
||||
test('selectAutoplayStartupCue returns the active cue at the current time', () => {
|
||||
assert.deepEqual(
|
||||
selectAutoplayStartupCue(
|
||||
[
|
||||
{ startTime: 1, endTime: 3, text: 'first' },
|
||||
{ startTime: 4, endTime: 5, text: 'second' },
|
||||
],
|
||||
2,
|
||||
1,
|
||||
),
|
||||
{ startTime: 1, endTime: 3, text: 'first' },
|
||||
);
|
||||
});
|
||||
|
||||
test('selectAutoplayStartupCue returns the next imminent cue before playback starts', () => {
|
||||
assert.deepEqual(
|
||||
selectAutoplayStartupCue(
|
||||
[
|
||||
{ startTime: 1.2, endTime: 3, text: 'first' },
|
||||
{ startTime: 4, endTime: 5, text: 'second' },
|
||||
],
|
||||
0,
|
||||
2,
|
||||
),
|
||||
{ startTime: 1.2, endTime: 3, text: 'first' },
|
||||
);
|
||||
});
|
||||
|
||||
test('selectAutoplayStartupCue does not reveal far future subtitle text', () => {
|
||||
assert.equal(
|
||||
selectAutoplayStartupCue([{ startTime: 12, endTime: 15, text: 'later' }], 0, 2),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test('selectAutoplayStartupCue skips blank cues', () => {
|
||||
assert.deepEqual(
|
||||
selectAutoplayStartupCue(
|
||||
[
|
||||
{ startTime: 0, endTime: 1, text: ' ' },
|
||||
{ startTime: 0.5, endTime: 2, text: 'visible' },
|
||||
],
|
||||
0.75,
|
||||
1,
|
||||
),
|
||||
{ startTime: 0.5, endTime: 2, text: 'visible' },
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { SubtitleCue } from '../../types';
|
||||
|
||||
export function selectAutoplayStartupCue(
|
||||
cues: SubtitleCue[],
|
||||
currentTimeSeconds: number,
|
||||
lookaheadSeconds: number,
|
||||
): SubtitleCue | null {
|
||||
const currentTime = Number.isFinite(currentTimeSeconds) ? currentTimeSeconds : 0;
|
||||
const lookahead = Math.max(0, Number.isFinite(lookaheadSeconds) ? lookaheadSeconds : 0);
|
||||
const latestStartTime = currentTime + lookahead;
|
||||
|
||||
for (const cue of cues) {
|
||||
if (!cue.text.trim()) {
|
||||
continue;
|
||||
}
|
||||
if (cue.startTime <= currentTime && cue.endTime > currentTime) {
|
||||
return cue;
|
||||
}
|
||||
}
|
||||
|
||||
for (const cue of cues) {
|
||||
if (!cue.text.trim()) {
|
||||
continue;
|
||||
}
|
||||
if (cue.startTime >= currentTime && cue.startTime <= latestStartTime) {
|
||||
return cue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -19,6 +19,60 @@ test('autoplay tokenization warm release signals immediately when warmups are re
|
||||
assert.deepEqual(calls, ['signal']);
|
||||
});
|
||||
|
||||
test('autoplay tokenization warm release primes subtitles before waiting for warmups', async () => {
|
||||
const calls: string[] = [];
|
||||
let resolveWarmup!: () => void;
|
||||
const warmup = new Promise<void>((resolve) => {
|
||||
resolveWarmup = resolve;
|
||||
});
|
||||
const release = createAutoplayTokenizationWarmRelease({
|
||||
isTokenizationWarmupReady: () => false,
|
||||
startTokenizationWarmups: async () => {
|
||||
calls.push('warmup');
|
||||
await warmup;
|
||||
},
|
||||
getCurrentMediaPath: () => '/tmp/video.mkv',
|
||||
primeCurrentSubtitle: () => {
|
||||
calls.push('prime');
|
||||
},
|
||||
signalAutoplayReady: () => calls.push('signal'),
|
||||
warn: () => {},
|
||||
});
|
||||
|
||||
release('/tmp/video.mkv');
|
||||
await Promise.resolve();
|
||||
assert.deepEqual(calls, ['prime', 'warmup']);
|
||||
|
||||
resolveWarmup();
|
||||
await warmup;
|
||||
await Promise.resolve();
|
||||
|
||||
assert.deepEqual(calls, ['prime', 'warmup', 'signal']);
|
||||
});
|
||||
|
||||
test('autoplay tokenization warm release does not await subtitle priming before signaling ready media', async () => {
|
||||
const calls: string[] = [];
|
||||
const never = new Promise<void>(() => {});
|
||||
const release = createAutoplayTokenizationWarmRelease({
|
||||
isTokenizationWarmupReady: () => true,
|
||||
startTokenizationWarmups: async () => {
|
||||
calls.push('warmup');
|
||||
},
|
||||
getCurrentMediaPath: () => '/tmp/video.mkv',
|
||||
primeCurrentSubtitle: () => {
|
||||
calls.push('prime');
|
||||
return never;
|
||||
},
|
||||
signalAutoplayReady: () => calls.push('signal'),
|
||||
warn: () => {},
|
||||
});
|
||||
|
||||
release('/tmp/video.mkv');
|
||||
await Promise.resolve();
|
||||
|
||||
assert.deepEqual(calls, ['prime', 'signal']);
|
||||
});
|
||||
|
||||
test('autoplay tokenization warm release waits for warmups before signaling current media', async () => {
|
||||
const calls: string[] = [];
|
||||
let resolveWarmup!: () => void;
|
||||
|
||||
@@ -10,6 +10,7 @@ export function createAutoplayTokenizationWarmRelease(deps: {
|
||||
isTokenizationWarmupReady: () => boolean;
|
||||
startTokenizationWarmups: () => Promise<void>;
|
||||
getCurrentMediaPath: () => string | null | undefined;
|
||||
primeCurrentSubtitle?: (mediaPath: string) => void | Promise<void>;
|
||||
signalAutoplayReady: () => void;
|
||||
warn: (message: string, error: unknown) => void;
|
||||
}): (mediaPath: string | null | undefined) => void {
|
||||
@@ -26,6 +27,13 @@ export function createAutoplayTokenizationWarmRelease(deps: {
|
||||
if (!normalizedPath) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
void Promise.resolve(deps.primeCurrentSubtitle?.(normalizedPath)).catch((error) => {
|
||||
deps.warn('Startup subtitle priming failed before autoplay readiness release:', error);
|
||||
});
|
||||
} catch (error) {
|
||||
deps.warn('Startup subtitle priming failed before autoplay readiness release:', error);
|
||||
}
|
||||
if (deps.isTokenizationWarmupReady()) {
|
||||
signalIfCurrent(normalizedPath);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user