mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-12 03:13:39 -07:00
refactor(main): extract autoplay subtitle priming runtime from main.ts
This commit is contained in:
+36
-180
@@ -103,7 +103,6 @@ import type {
|
|||||||
RuntimeOptionState,
|
RuntimeOptionState,
|
||||||
SessionActionDispatchRequest,
|
SessionActionDispatchRequest,
|
||||||
SecondarySubMode,
|
SecondarySubMode,
|
||||||
SubtitleCue,
|
|
||||||
SubtitleData,
|
SubtitleData,
|
||||||
SubtitleMiningContext,
|
SubtitleMiningContext,
|
||||||
SubtitlePosition,
|
SubtitlePosition,
|
||||||
@@ -378,7 +377,7 @@ import {
|
|||||||
createYoutubePrimarySubtitleNotificationRuntime,
|
createYoutubePrimarySubtitleNotificationRuntime,
|
||||||
} from './main/runtime/youtube-primary-subtitle-notification';
|
} from './main/runtime/youtube-primary-subtitle-notification';
|
||||||
import { createAutoplayReadyGate } from './main/runtime/autoplay-ready-gate';
|
import { createAutoplayReadyGate } from './main/runtime/autoplay-ready-gate';
|
||||||
import { selectAutoplayStartupCue } from './main/runtime/autoplay-subtitle-primer';
|
import { createAutoplaySubtitlePrimingRuntime } from './main/runtime/autoplay-subtitle-priming-runtime';
|
||||||
import { createAutoplayTokenizationWarmRelease } from './main/runtime/autoplay-tokenization-warm-release';
|
import { createAutoplayTokenizationWarmRelease } from './main/runtime/autoplay-tokenization-warm-release';
|
||||||
import { isVisibleOverlayAutoplayTargetReady } from './main/runtime/visible-overlay-autoplay-readiness';
|
import { isVisibleOverlayAutoplayTargetReady } from './main/runtime/visible-overlay-autoplay-readiness';
|
||||||
import { createManagedLocalSubtitleSelectionRuntime } from './main/runtime/local-subtitle-selection';
|
import { createManagedLocalSubtitleSelectionRuntime } from './main/runtime/local-subtitle-selection';
|
||||||
@@ -519,10 +518,7 @@ import { handleCharacterDictionaryAutoSyncComplete } from './main/runtime/charac
|
|||||||
import { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/character-dictionary-auto-sync-notifications';
|
import { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/character-dictionary-auto-sync-notifications';
|
||||||
import { openCharacterDictionaryManagerWithConfigGate } from './main/runtime/character-dictionary-manager-gate';
|
import { openCharacterDictionaryManagerWithConfigGate } from './main/runtime/character-dictionary-manager-gate';
|
||||||
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
|
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
|
||||||
import {
|
import { resolveCurrentSubtitleForRenderer } from './main/runtime/current-subtitle-snapshot';
|
||||||
primeVisibleOverlaySubtitleFromMpv,
|
|
||||||
resolveCurrentSubtitleForRenderer,
|
|
||||||
} from './main/runtime/current-subtitle-snapshot';
|
|
||||||
import { restoreLinuxOverlayWindowShape } from './main/runtime/linux-overlay-window-shape';
|
import { restoreLinuxOverlayWindowShape } from './main/runtime/linux-overlay-window-shape';
|
||||||
import { createOverlayGeometryRuntime } from './main/runtime/overlay-geometry-runtime';
|
import { createOverlayGeometryRuntime } from './main/runtime/overlay-geometry-runtime';
|
||||||
import { createJellyfinSubtitleCacheIo } from './main/runtime/jellyfin-subtitle-cache-io';
|
import { createJellyfinSubtitleCacheIo } from './main/runtime/jellyfin-subtitle-cache-io';
|
||||||
@@ -595,10 +591,7 @@ import {
|
|||||||
createSubtitlePrefetchService,
|
createSubtitlePrefetchService,
|
||||||
type SubtitlePrefetchService,
|
type SubtitlePrefetchService,
|
||||||
} from './core/services/subtitle-prefetch';
|
} from './core/services/subtitle-prefetch';
|
||||||
import {
|
import { buildSubtitleSidebarSourceKey } from './main/runtime/subtitle-prefetch-source';
|
||||||
buildSubtitleSidebarSourceKey,
|
|
||||||
resolveSubtitleSourcePath,
|
|
||||||
} from './main/runtime/subtitle-prefetch-source';
|
|
||||||
import { createSubtitlePrefetchInitController } from './main/runtime/subtitle-prefetch-init';
|
import { createSubtitlePrefetchInitController } from './main/runtime/subtitle-prefetch-init';
|
||||||
import {
|
import {
|
||||||
loadSubtitleSourceText,
|
loadSubtitleSourceText,
|
||||||
@@ -1773,7 +1766,6 @@ const subtitleProcessingController = createSubtitleProcessingController(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let subtitlePrefetchService: SubtitlePrefetchService | null = null;
|
let subtitlePrefetchService: SubtitlePrefetchService | null = null;
|
||||||
let subtitlePrefetchRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
let lastObservedTimePos = 0;
|
let lastObservedTimePos = 0;
|
||||||
let lastObservedPrimarySubtitleTrackId: number | null = null;
|
let lastObservedPrimarySubtitleTrackId: number | null = null;
|
||||||
let cancelLinuxMpvFullscreenOverlayRefreshBurst: CancelLinuxMpvFullscreenOverlayRefreshBurst | null =
|
let cancelLinuxMpvFullscreenOverlayRefreshBurst: CancelLinuxMpvFullscreenOverlayRefreshBurst | null =
|
||||||
@@ -1786,166 +1778,48 @@ const linuxVisibleOverlayOwnerBindingQueues = new WeakMap<BrowserWindow, Promise
|
|||||||
let linuxVisibleOverlayWindowModeSwitchToken = 0;
|
let linuxVisibleOverlayWindowModeSwitchToken = 0;
|
||||||
let subtitleSidebarRequestedOpen = false;
|
let subtitleSidebarRequestedOpen = false;
|
||||||
const SEEK_THRESHOLD_SECONDS = 3;
|
const SEEK_THRESHOLD_SECONDS = 3;
|
||||||
const AUTOPLAY_SUBTITLE_PRIME_LOOKAHEAD_SECONDS = 2;
|
|
||||||
let autoplaySubtitlePrimedMediaPath: string | null = null;
|
|
||||||
let visibleOverlaySubtitleRefreshAfterFirstPaintTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
const VISIBLE_OVERLAY_SUBTITLE_REFRESH_AFTER_FIRST_PAINT_DELAY_MS = 100;
|
|
||||||
|
|
||||||
function getCurrentAutoplayMediaPath(): string | null {
|
const autoplaySubtitlePrimingRuntime = createAutoplaySubtitlePrimingRuntime({
|
||||||
return appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null;
|
getCurrentMediaPath: () => appState.currentMediaPath,
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
subtitlePrefetchService?.pause();
|
|
||||||
const cachedPayload = subtitleProcessingController.consumeCachedSubtitle(text);
|
|
||||||
if (cachedPayload) {
|
|
||||||
subtitleProcessingController.onSubtitleChange(text);
|
|
||||||
emitSubtitlePayload(cachedPayload);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 primeCurrentSubtitleForVisibleOverlay(): Promise<void> {
|
|
||||||
await primeVisibleOverlaySubtitleFromMpv({
|
|
||||||
getMpvClient: () => appState.mpvClient,
|
getMpvClient: () => appState.mpvClient,
|
||||||
setCurrentSubText: (text) => {
|
setCurrentSubText: (text) => {
|
||||||
appState.currentSubText = text;
|
appState.currentSubText = text;
|
||||||
},
|
},
|
||||||
|
getCurrentSubText: () => appState.currentSubText,
|
||||||
getCurrentSubtitleData: () => appState.currentSubtitleData,
|
getCurrentSubtitleData: () => appState.currentSubtitleData,
|
||||||
consumeCachedSubtitle: (text) => subtitleProcessingController.consumeCachedSubtitle(text),
|
setActiveParsedSubtitleMediaPath: (mediaPath) => {
|
||||||
onSubtitleChange: (text) => {
|
appState.activeParsedSubtitleMediaPath = mediaPath;
|
||||||
subtitlePrefetchService?.pause();
|
|
||||||
subtitlePrefetchService?.onSeek(lastObservedTimePos);
|
|
||||||
subtitleProcessingController.onSubtitleChange(text);
|
|
||||||
},
|
|
||||||
refreshCurrentSubtitle: (text) => {
|
|
||||||
subtitlePrefetchService?.pause();
|
|
||||||
subtitlePrefetchService?.onSeek(lastObservedTimePos);
|
|
||||||
subtitleProcessingController.refreshCurrentSubtitle(text);
|
|
||||||
},
|
|
||||||
deferUncachedRefresh: true,
|
|
||||||
emitSubtitle: (payload) => emitSubtitlePayload(payload),
|
|
||||||
setCurrentSecondarySubText: (text) => {
|
|
||||||
if (appState.mpvClient) {
|
|
||||||
appState.mpvClient.currentSecondarySubText = text;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
subtitleProcessingController,
|
||||||
|
emitSubtitlePayload: (payload) => emitSubtitlePayload(payload),
|
||||||
|
getSubtitlePrefetchService: () => subtitlePrefetchService,
|
||||||
|
getLastObservedTimePos: () => lastObservedTimePos,
|
||||||
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
emitSecondarySubtitle: (text) => {
|
emitSecondarySubtitle: (text) => {
|
||||||
broadcastToOverlayWindows('secondary-subtitle:set', text);
|
broadcastToOverlayWindows('secondary-subtitle:set', text);
|
||||||
},
|
},
|
||||||
|
initSubtitlePrefetch: (sourcePath, currentTimePos, sourceKey) =>
|
||||||
|
subtitlePrefetchInitController.initSubtitlePrefetch(sourcePath, currentTimePos, sourceKey),
|
||||||
|
refreshSubtitlePrefetchFromActiveTrack: () => refreshSubtitlePrefetchFromActiveTrackHandler(),
|
||||||
logDebug: (message) => {
|
logDebug: (message) => {
|
||||||
logger.debug(message);
|
logger.debug(message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function primeCurrentSubtitleForVisibleOverlay(): Promise<void> {
|
||||||
|
return autoplaySubtitlePrimingRuntime.primeCurrentSubtitleForVisibleOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelVisibleOverlaySubtitleRefreshAfterFirstPaint(): void {
|
function cancelVisibleOverlaySubtitleRefreshAfterFirstPaint(): void {
|
||||||
if (!visibleOverlaySubtitleRefreshAfterFirstPaintTimer) {
|
autoplaySubtitlePrimingRuntime.cancelVisibleOverlaySubtitleRefreshAfterFirstPaint();
|
||||||
return;
|
|
||||||
}
|
|
||||||
clearTimeout(visibleOverlaySubtitleRefreshAfterFirstPaintTimer);
|
|
||||||
visibleOverlaySubtitleRefreshAfterFirstPaintTimer = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleVisibleOverlaySubtitleRefreshAfterFirstPaint(): void {
|
function scheduleVisibleOverlaySubtitleRefreshAfterFirstPaint(): void {
|
||||||
if (visibleOverlaySubtitleRefreshAfterFirstPaintTimer) {
|
autoplaySubtitlePrimingRuntime.scheduleVisibleOverlaySubtitleRefreshAfterFirstPaint();
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!overlayManager.getVisibleOverlayVisible() || !appState.currentSubText.trim()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
visibleOverlaySubtitleRefreshAfterFirstPaintTimer = setTimeout(() => {
|
|
||||||
visibleOverlaySubtitleRefreshAfterFirstPaintTimer = null;
|
|
||||||
if (!overlayManager.getVisibleOverlayVisible()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const text = appState.currentSubText;
|
|
||||||
if (!text.trim()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
subtitlePrefetchService?.pause();
|
|
||||||
subtitlePrefetchService?.onSeek(lastObservedTimePos);
|
|
||||||
subtitleProcessingController.refreshCurrentSubtitle(text);
|
|
||||||
}, VISIBLE_OVERLAY_SUBTITLE_REFRESH_AFTER_FIRST_PAINT_DELAY_MS);
|
|
||||||
visibleOverlaySubtitleRefreshAfterFirstPaintTimer.unref?.();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function primeAutoplaySubtitleFromParsedCues(
|
function scheduleSubtitlePrefetchRefresh(delayMs = 0): void {
|
||||||
mediaPath: string,
|
autoplaySubtitlePrimingRuntime.scheduleSubtitlePrefetchRefresh(delayMs);
|
||||||
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) {
|
|
||||||
clearTimeout(subtitlePrefetchRefreshTimer);
|
|
||||||
subtitlePrefetchRefreshTimer = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelPendingLinuxMpvFullscreenOverlayRefreshBurst(): void {
|
function cancelPendingLinuxMpvFullscreenOverlayRefreshBurst(): void {
|
||||||
@@ -1976,9 +1850,11 @@ const subtitlePrefetchInitController = createSubtitlePrefetchInitController({
|
|||||||
if (!cues?.length) {
|
if (!cues?.length) {
|
||||||
appState.activeParsedSubtitleMediaPath = null;
|
appState.activeParsedSubtitleMediaPath = null;
|
||||||
}
|
}
|
||||||
const mediaPath = getCurrentAutoplayMediaPath();
|
const mediaPath = autoplaySubtitlePrimingRuntime.getCurrentAutoplayMediaPath();
|
||||||
if (mediaPath && cues?.length) {
|
if (mediaPath && cues?.length) {
|
||||||
void primeAutoplaySubtitleFromParsedCues(mediaPath, cues).catch((error) => {
|
void autoplaySubtitlePrimingRuntime
|
||||||
|
.primeAutoplaySubtitleFromParsedCues(mediaPath, cues)
|
||||||
|
.catch((error) => {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`[autoplay-subtitle-prime] failed to prime from parsed cues: ${
|
`[autoplay-subtitle-prime] failed to prime from parsed cues: ${
|
||||||
error instanceof Error ? error.message : String(error)
|
error instanceof Error ? error.message : String(error)
|
||||||
@@ -1994,22 +1870,6 @@ const resolveActiveSubtitleSidebarSourceHandler = createResolveActiveSubtitleSid
|
|||||||
extractInternalSubtitleTrackToTempFile(ffmpegPath, videoPath, track),
|
extractInternalSubtitleTrackToTempFile(ffmpegPath, videoPath, track),
|
||||||
});
|
});
|
||||||
|
|
||||||
async function refreshSubtitleSidebarFromSource(
|
|
||||||
sourcePath: string,
|
|
||||||
mediaPath?: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const normalizedSourcePath = resolveSubtitleSourcePath(sourcePath.trim());
|
|
||||||
if (!normalizedSourcePath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextMediaPath = mediaPath?.trim() || getCurrentAutoplayMediaPath();
|
|
||||||
await subtitlePrefetchInitController.initSubtitlePrefetch(
|
|
||||||
normalizedSourcePath,
|
|
||||||
lastObservedTimePos,
|
|
||||||
normalizedSourcePath,
|
|
||||||
);
|
|
||||||
appState.activeParsedSubtitleMediaPath = nextMediaPath;
|
|
||||||
}
|
|
||||||
const refreshSubtitlePrefetchFromActiveTrackHandler =
|
const refreshSubtitlePrefetchFromActiveTrackHandler =
|
||||||
createRefreshSubtitlePrefetchFromActiveTrackHandler({
|
createRefreshSubtitlePrefetchFromActiveTrackHandler({
|
||||||
getMpvClient: () => appState.mpvClient,
|
getMpvClient: () => appState.mpvClient,
|
||||||
@@ -2019,21 +1879,16 @@ const refreshSubtitlePrefetchFromActiveTrackHandler =
|
|||||||
resolveActiveSubtitleSidebarSource: (input) => resolveActiveSubtitleSidebarSourceHandler(input),
|
resolveActiveSubtitleSidebarSource: (input) => resolveActiveSubtitleSidebarSourceHandler(input),
|
||||||
});
|
});
|
||||||
|
|
||||||
function scheduleSubtitlePrefetchRefresh(delayMs = 0): void {
|
|
||||||
clearScheduledSubtitlePrefetchRefresh();
|
|
||||||
subtitlePrefetchRefreshTimer = setTimeout(() => {
|
|
||||||
subtitlePrefetchRefreshTimer = null;
|
|
||||||
void refreshSubtitlePrefetchFromActiveTrackHandler();
|
|
||||||
}, delayMs);
|
|
||||||
}
|
|
||||||
const subtitlePrefetchRuntime = {
|
const subtitlePrefetchRuntime = {
|
||||||
cancelPendingInit: () => subtitlePrefetchInitController.cancelPendingInit(),
|
cancelPendingInit: () => subtitlePrefetchInitController.cancelPendingInit(),
|
||||||
initSubtitlePrefetch: subtitlePrefetchInitController.initSubtitlePrefetch,
|
initSubtitlePrefetch: subtitlePrefetchInitController.initSubtitlePrefetch,
|
||||||
refreshSubtitleSidebarFromSource: (sourcePath: string, mediaPath?: string) =>
|
refreshSubtitleSidebarFromSource: (sourcePath: string, mediaPath?: string) =>
|
||||||
refreshSubtitleSidebarFromSource(sourcePath, mediaPath),
|
autoplaySubtitlePrimingRuntime.refreshSubtitleSidebarFromSource(sourcePath, mediaPath),
|
||||||
refreshSubtitlePrefetchFromActiveTrack: () => refreshSubtitlePrefetchFromActiveTrackHandler(),
|
refreshSubtitlePrefetchFromActiveTrack: () => refreshSubtitlePrefetchFromActiveTrackHandler(),
|
||||||
scheduleSubtitlePrefetchRefresh: (delayMs?: number) => scheduleSubtitlePrefetchRefresh(delayMs),
|
scheduleSubtitlePrefetchRefresh: (delayMs?: number) =>
|
||||||
clearScheduledSubtitlePrefetchRefresh: () => clearScheduledSubtitlePrefetchRefresh(),
|
autoplaySubtitlePrimingRuntime.scheduleSubtitlePrefetchRefresh(delayMs),
|
||||||
|
clearScheduledSubtitlePrefetchRefresh: () =>
|
||||||
|
autoplaySubtitlePrimingRuntime.clearScheduledSubtitlePrefetchRefresh(),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const overlayShortcutsRuntime = createOverlayShortcutsRuntimeService(
|
const overlayShortcutsRuntime = createOverlayShortcutsRuntimeService(
|
||||||
@@ -4984,7 +4839,7 @@ const {
|
|||||||
topX: frequencyDictionary.topX,
|
topX: frequencyDictionary.topX,
|
||||||
mode: frequencyDictionary.mode,
|
mode: frequencyDictionary.mode,
|
||||||
};
|
};
|
||||||
autoplaySubtitlePrimedMediaPath = null;
|
autoplaySubtitlePrimingRuntime.resetAutoplaySubtitlePrime();
|
||||||
lastObservedTimePos = 0;
|
lastObservedTimePos = 0;
|
||||||
appState.currentSubText = '';
|
appState.currentSubText = '';
|
||||||
appState.currentSubAssText = '';
|
appState.currentSubAssText = '';
|
||||||
@@ -5300,7 +5155,8 @@ signalAutoplayReadyFromWarmTokenization = createAutoplayTokenizationWarmRelease(
|
|||||||
},
|
},
|
||||||
getCurrentMediaPath: () =>
|
getCurrentMediaPath: () =>
|
||||||
appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null,
|
appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null,
|
||||||
primeCurrentSubtitle: (mediaPath) => primeCurrentSubtitleForAutoplay(mediaPath),
|
primeCurrentSubtitle: (mediaPath) =>
|
||||||
|
autoplaySubtitlePrimingRuntime.primeCurrentSubtitleForAutoplay(mediaPath),
|
||||||
signalAutoplayReady: () => signalCurrentSubtitleAutoplayReady(),
|
signalAutoplayReady: () => signalCurrentSubtitleAutoplayReady(),
|
||||||
warn: (message, error) => logger.warn(message, error),
|
warn: (message, error) => logger.warn(message, error),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -150,9 +150,9 @@ test('all visible overlay hide paths clear stale overlay input state', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('subtitle sidebar media path tag is assigned after prefetch succeeds', () => {
|
test('subtitle sidebar media path tag is assigned after prefetch succeeds', () => {
|
||||||
const source = readMainSource();
|
const source = readSource('src/main/runtime/autoplay-subtitle-priming-runtime.ts');
|
||||||
const actionBlock = source.match(
|
const actionBlock = source.match(
|
||||||
/async function refreshSubtitleSidebarFromSource\([\s\S]*?\): Promise<void> \{(?<body>[\s\S]*?)\n\}/,
|
/async function refreshSubtitleSidebarFromSource\([\s\S]*?\): Promise<void> \{(?<body>[\s\S]*?)\n \}/,
|
||||||
)?.groups?.body;
|
)?.groups?.body;
|
||||||
|
|
||||||
assert.ok(actionBlock);
|
assert.ok(actionBlock);
|
||||||
@@ -161,8 +161,8 @@ test('subtitle sidebar media path tag is assigned after prefetch succeeds', () =
|
|||||||
/const nextMediaPath = mediaPath\?\.trim\(\) \|\| getCurrentAutoplayMediaPath\(\);/,
|
/const nextMediaPath = mediaPath\?\.trim\(\) \|\| getCurrentAutoplayMediaPath\(\);/,
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
actionBlock.indexOf('subtitlePrefetchInitController.initSubtitlePrefetch') <
|
actionBlock.indexOf('deps.initSubtitlePrefetch(') <
|
||||||
actionBlock.indexOf('appState.activeParsedSubtitleMediaPath = nextMediaPath;'),
|
actionBlock.indexOf('deps.setActiveParsedSubtitleMediaPath(nextMediaPath);'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -211,9 +211,9 @@ test('subtitle change re-prioritizes prefetch around live playback before tokeni
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('autoplay subtitle prime emits cached annotations and avoids raw fallback overlay flashes', () => {
|
test('autoplay subtitle prime emits cached annotations and avoids raw fallback overlay flashes', () => {
|
||||||
const source = readMainSource();
|
const source = readSource('src/main/runtime/autoplay-subtitle-priming-runtime.ts');
|
||||||
const actionBlock = source.match(
|
const actionBlock = source.match(
|
||||||
/function emitAutoplayPrimedSubtitle\([\s\S]*?\): boolean \{(?<body>[\s\S]*?)\n\}/,
|
/function emitAutoplayPrimedSubtitle\([\s\S]*?\): boolean \{(?<body>[\s\S]*?)\n \}/,
|
||||||
)?.groups?.body;
|
)?.groups?.body;
|
||||||
|
|
||||||
assert.ok(actionBlock);
|
assert.ok(actionBlock);
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
import type { SubtitleCue, SubtitleData } from '../../types';
|
||||||
|
import { selectAutoplayStartupCue } from './autoplay-subtitle-primer';
|
||||||
|
import { primeVisibleOverlaySubtitleFromMpv } from './current-subtitle-snapshot';
|
||||||
|
import { resolveSubtitleSourcePath } from './subtitle-prefetch-source';
|
||||||
|
|
||||||
|
const AUTOPLAY_SUBTITLE_PRIME_LOOKAHEAD_SECONDS = 2;
|
||||||
|
const VISIBLE_OVERLAY_SUBTITLE_REFRESH_AFTER_FIRST_PAINT_DELAY_MS = 100;
|
||||||
|
|
||||||
|
type AutoplaySubtitlePrimingMpvClient = {
|
||||||
|
connected?: boolean;
|
||||||
|
requestProperty: (name: string) => Promise<unknown>;
|
||||||
|
currentVideoPath?: string;
|
||||||
|
currentTimePos?: number;
|
||||||
|
currentSecondarySubText?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AutoplaySubtitlePrimingPrefetchService = {
|
||||||
|
pause: () => void;
|
||||||
|
onSeek: (timePos: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AutoplaySubtitlePrimingRuntimeDeps {
|
||||||
|
getCurrentMediaPath: () => string | null | undefined;
|
||||||
|
getMpvClient: () => AutoplaySubtitlePrimingMpvClient | null;
|
||||||
|
setCurrentSubText: (text: string) => void;
|
||||||
|
getCurrentSubText: () => string;
|
||||||
|
getCurrentSubtitleData: () => SubtitleData | null;
|
||||||
|
setActiveParsedSubtitleMediaPath: (mediaPath: string | null) => void;
|
||||||
|
subtitleProcessingController: {
|
||||||
|
consumeCachedSubtitle: (text: string) => SubtitleData | null;
|
||||||
|
onSubtitleChange: (text: string) => void;
|
||||||
|
refreshCurrentSubtitle: (text: string) => void;
|
||||||
|
};
|
||||||
|
emitSubtitlePayload: (payload: SubtitleData) => void;
|
||||||
|
getSubtitlePrefetchService: () => AutoplaySubtitlePrimingPrefetchService | null;
|
||||||
|
getLastObservedTimePos: () => number;
|
||||||
|
getVisibleOverlayVisible: () => boolean;
|
||||||
|
emitSecondarySubtitle: (text: string) => void;
|
||||||
|
initSubtitlePrefetch: (
|
||||||
|
sourcePath: string,
|
||||||
|
currentTimePos: number,
|
||||||
|
sourceKey?: string,
|
||||||
|
) => Promise<void>;
|
||||||
|
refreshSubtitlePrefetchFromActiveTrack: () => Promise<void>;
|
||||||
|
logDebug: (message: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAutoplaySubtitlePrimingRuntime(deps: AutoplaySubtitlePrimingRuntimeDeps) {
|
||||||
|
const { subtitleProcessingController, emitSubtitlePayload } = deps;
|
||||||
|
|
||||||
|
let subtitlePrefetchRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let autoplaySubtitlePrimedMediaPath: string | null = null;
|
||||||
|
let visibleOverlaySubtitleRefreshAfterFirstPaintTimer: ReturnType<typeof setTimeout> | null =
|
||||||
|
null;
|
||||||
|
|
||||||
|
function getCurrentAutoplayMediaPath(): string | null {
|
||||||
|
return (
|
||||||
|
deps.getCurrentMediaPath()?.trim() || deps.getMpvClient()?.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 resetAutoplaySubtitlePrime(): void {
|
||||||
|
autoplaySubtitlePrimedMediaPath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitAutoplayPrimedSubtitle(mediaPath: string, text: string): boolean {
|
||||||
|
if (!text.trim() || !isCurrentAutoplayMediaPath(mediaPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!markAutoplaySubtitlePrimeConsumed(mediaPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.setCurrentSubText(text);
|
||||||
|
deps.getSubtitlePrefetchService()?.pause();
|
||||||
|
const cachedPayload = subtitleProcessingController.consumeCachedSubtitle(text);
|
||||||
|
if (cachedPayload) {
|
||||||
|
subtitleProcessingController.onSubtitleChange(text);
|
||||||
|
emitSubtitlePayload(cachedPayload);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
subtitleProcessingController.onSubtitleChange(text);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function primeCurrentSubtitleForAutoplay(mediaPath: string): Promise<void> {
|
||||||
|
const client = deps.getMpvClient();
|
||||||
|
if (!client?.connected || !isCurrentAutoplayMediaPath(mediaPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subTextRaw = await client.requestProperty('sub-text').catch((error) => {
|
||||||
|
deps.logDebug(
|
||||||
|
`[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 primeCurrentSubtitleForVisibleOverlay(): Promise<void> {
|
||||||
|
await primeVisibleOverlaySubtitleFromMpv({
|
||||||
|
getMpvClient: () => deps.getMpvClient(),
|
||||||
|
setCurrentSubText: (text) => {
|
||||||
|
deps.setCurrentSubText(text);
|
||||||
|
},
|
||||||
|
getCurrentSubtitleData: () => deps.getCurrentSubtitleData(),
|
||||||
|
consumeCachedSubtitle: (text) => subtitleProcessingController.consumeCachedSubtitle(text),
|
||||||
|
onSubtitleChange: (text) => {
|
||||||
|
deps.getSubtitlePrefetchService()?.pause();
|
||||||
|
deps.getSubtitlePrefetchService()?.onSeek(deps.getLastObservedTimePos());
|
||||||
|
subtitleProcessingController.onSubtitleChange(text);
|
||||||
|
},
|
||||||
|
refreshCurrentSubtitle: (text) => {
|
||||||
|
deps.getSubtitlePrefetchService()?.pause();
|
||||||
|
deps.getSubtitlePrefetchService()?.onSeek(deps.getLastObservedTimePos());
|
||||||
|
subtitleProcessingController.refreshCurrentSubtitle(text);
|
||||||
|
},
|
||||||
|
deferUncachedRefresh: true,
|
||||||
|
emitSubtitle: (payload) => emitSubtitlePayload(payload),
|
||||||
|
setCurrentSecondarySubText: (text) => {
|
||||||
|
const client = deps.getMpvClient();
|
||||||
|
if (client) {
|
||||||
|
client.currentSecondarySubText = text;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emitSecondarySubtitle: (text) => {
|
||||||
|
deps.emitSecondarySubtitle(text);
|
||||||
|
},
|
||||||
|
logDebug: (message) => {
|
||||||
|
deps.logDebug(message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelVisibleOverlaySubtitleRefreshAfterFirstPaint(): void {
|
||||||
|
if (!visibleOverlaySubtitleRefreshAfterFirstPaintTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(visibleOverlaySubtitleRefreshAfterFirstPaintTimer);
|
||||||
|
visibleOverlaySubtitleRefreshAfterFirstPaintTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleVisibleOverlaySubtitleRefreshAfterFirstPaint(): void {
|
||||||
|
if (visibleOverlaySubtitleRefreshAfterFirstPaintTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!deps.getVisibleOverlayVisible() || !deps.getCurrentSubText().trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleOverlaySubtitleRefreshAfterFirstPaintTimer = setTimeout(() => {
|
||||||
|
visibleOverlaySubtitleRefreshAfterFirstPaintTimer = null;
|
||||||
|
if (!deps.getVisibleOverlayVisible()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = deps.getCurrentSubText();
|
||||||
|
if (!text.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deps.getSubtitlePrefetchService()?.pause();
|
||||||
|
deps.getSubtitlePrefetchService()?.onSeek(deps.getLastObservedTimePos());
|
||||||
|
subtitleProcessingController.refreshCurrentSubtitle(text);
|
||||||
|
}, VISIBLE_OVERLAY_SUBTITLE_REFRESH_AFTER_FIRST_PAINT_DELAY_MS);
|
||||||
|
visibleOverlaySubtitleRefreshAfterFirstPaintTimer.unref?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function primeAutoplaySubtitleFromParsedCues(
|
||||||
|
mediaPath: string,
|
||||||
|
cues: SubtitleCue[],
|
||||||
|
): Promise<void> {
|
||||||
|
if (
|
||||||
|
cues.length === 0 ||
|
||||||
|
autoplaySubtitlePrimedMediaPath === mediaPath ||
|
||||||
|
!isCurrentAutoplayMediaPath(mediaPath)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = deps.getMpvClient();
|
||||||
|
const timePosRaw = await client?.requestProperty('time-pos').catch(() => null);
|
||||||
|
const currentTimeSeconds = Number(
|
||||||
|
timePosRaw ?? client?.currentTimePos ?? deps.getLastObservedTimePos() ?? 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) {
|
||||||
|
clearTimeout(subtitlePrefetchRefreshTimer);
|
||||||
|
subtitlePrefetchRefreshTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshSubtitleSidebarFromSource(
|
||||||
|
sourcePath: string,
|
||||||
|
mediaPath?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const normalizedSourcePath = resolveSubtitleSourcePath(sourcePath.trim());
|
||||||
|
if (!normalizedSourcePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextMediaPath = mediaPath?.trim() || getCurrentAutoplayMediaPath();
|
||||||
|
await deps.initSubtitlePrefetch(
|
||||||
|
normalizedSourcePath,
|
||||||
|
deps.getLastObservedTimePos(),
|
||||||
|
normalizedSourcePath,
|
||||||
|
);
|
||||||
|
deps.setActiveParsedSubtitleMediaPath(nextMediaPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleSubtitlePrefetchRefresh(delayMs = 0): void {
|
||||||
|
clearScheduledSubtitlePrefetchRefresh();
|
||||||
|
subtitlePrefetchRefreshTimer = setTimeout(() => {
|
||||||
|
subtitlePrefetchRefreshTimer = null;
|
||||||
|
void deps.refreshSubtitlePrefetchFromActiveTrack();
|
||||||
|
}, delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCurrentAutoplayMediaPath,
|
||||||
|
resetAutoplaySubtitlePrime,
|
||||||
|
primeCurrentSubtitleForAutoplay,
|
||||||
|
primeCurrentSubtitleForVisibleOverlay,
|
||||||
|
cancelVisibleOverlaySubtitleRefreshAfterFirstPaint,
|
||||||
|
scheduleVisibleOverlaySubtitleRefreshAfterFirstPaint,
|
||||||
|
primeAutoplaySubtitleFromParsedCues,
|
||||||
|
clearScheduledSubtitlePrefetchRefresh,
|
||||||
|
refreshSubtitleSidebarFromSource,
|
||||||
|
scheduleSubtitlePrefetchRefresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user