fix: unblock autoplay on tokenization-ready and defer annotation loading

This commit is contained in:
2026-03-02 02:43:09 -08:00
parent 5167e3a494
commit e744fab067
9 changed files with 440 additions and 71 deletions

View File

@@ -80,17 +80,22 @@ function M.create(ctx)
state.auto_play_ready_osd_timer = nil
end
local function disarm_auto_play_ready_gate()
local function disarm_auto_play_ready_gate(options)
local should_resume = options == nil or options.resume_playback ~= false
local was_armed = state.auto_play_ready_gate_armed
clear_auto_play_ready_timeout()
clear_auto_play_ready_osd_timer()
state.auto_play_ready_gate_armed = false
if was_armed and should_resume then
mp.set_property_native("pause", false)
end
end
local function release_auto_play_ready_gate(reason)
if not state.auto_play_ready_gate_armed then
return
end
disarm_auto_play_ready_gate()
disarm_auto_play_ready_gate({ resume_playback = false })
mp.set_property_native("pause", false)
show_osd(AUTO_PLAY_READY_READY_OSD)
subminer_log("info", "process", "Resuming playback after startup gate: " .. tostring(reason or "ready"))
@@ -270,6 +275,23 @@ function M.create(ctx)
if state.overlay_running then
if overrides.auto_start_trigger == true then
subminer_log("debug", "process", "Auto-start ignored because overlay is already running")
local socket_path = overrides.socket_path or opts.socket_path
local should_pause_until_ready = (
resolve_visible_overlay_startup()
and resolve_pause_until_ready()
and has_matching_mpv_ipc_socket(socket_path)
)
if should_pause_until_ready then
arm_auto_play_ready_gate()
else
disarm_auto_play_ready_gate()
end
local visibility_action = resolve_visible_overlay_startup()
and "show-visible-overlay"
or "hide-visible-overlay"
run_control_command_async(visibility_action, {
log_level = overrides.log_level,
})
return
end
subminer_log("info", "process", "Overlay already running")

View File

@@ -304,6 +304,26 @@ local function find_control_call(async_calls, flag)
return nil
end
local function count_control_calls(async_calls, flag)
local count = 0
for _, call in ipairs(async_calls) do
local args = call.args or {}
local has_flag = false
local has_start = false
for _, value in ipairs(args) do
if value == flag then
has_flag = true
elseif value == "--start" then
has_start = true
end
end
if has_flag and not has_start then
count = count + 1
end
end
return count
end
local function call_has_arg(call, target)
local args = (call and call.args) or {}
for _, value in ipairs(args) do
@@ -375,6 +395,16 @@ local function count_osd_message(messages, target)
return count
end
local function count_property_set(property_sets, name, value)
local count = 0
for _, call in ipairs(property_sets) do
if call.name == name and call.value == value then
count = count + 1
end
end
return count
end
local function fire_event(recorded, name)
local listeners = recorded.events[name] or {}
for _, listener in ipairs(listeners) do
@@ -516,12 +546,64 @@ do
count_start_calls(recorded.async_calls) == 1,
"duplicate file-loaded events should not issue duplicate --start commands while overlay is already running"
)
assert_true(
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 2,
"duplicate auto-start should re-assert visible overlay state when overlay is already running"
)
assert_true(
count_osd_message(recorded.osd, "SubMiner: Already running") == 0,
"duplicate auto-start events should not show Already running OSD"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "yes",
auto_start_visible_overlay = "yes",
auto_start_pause_until_ready = "yes",
socket_path = "/tmp/subminer-socket",
},
input_ipc_server = "/tmp/subminer-socket",
media_title = "Random Movie",
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for duplicate auto-start pause-until-ready scenario: " .. tostring(err))
fire_event(recorded, "file-loaded")
assert_true(recorded.script_messages["subminer-autoplay-ready"] ~= nil, "subminer-autoplay-ready script message not registered")
recorded.script_messages["subminer-autoplay-ready"]()
fire_event(recorded, "file-loaded")
recorded.script_messages["subminer-autoplay-ready"]()
assert_true(
count_start_calls(recorded.async_calls) == 1,
"duplicate pause-until-ready auto-start should not issue duplicate --start commands while overlay is already running"
)
assert_true(
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 2,
"duplicate pause-until-ready auto-start should still re-assert visible overlay state"
)
assert_true(
count_osd_message(recorded.osd, "SubMiner: Loading subtitle tokenization...") == 2,
"duplicate pause-until-ready auto-start should arm tokenization loading gate for each file"
)
assert_true(
count_osd_message(recorded.osd, "SubMiner: Subtitle tokenization ready") == 2,
"duplicate pause-until-ready auto-start should release tokenization gate for each file"
)
assert_true(
count_property_set(recorded.property_sets, "pause", true) == 2,
"duplicate pause-until-ready auto-start should force pause for each file"
)
assert_true(
count_property_set(recorded.property_sets, "pause", false) == 2,
"duplicate pause-until-ready auto-start should resume playback for each file"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
@@ -572,6 +654,35 @@ do
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "yes",
auto_start_visible_overlay = "yes",
auto_start_pause_until_ready = "yes",
socket_path = "/tmp/subminer-socket",
},
input_ipc_server = "/tmp/subminer-socket",
media_title = "Random Movie",
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for pause cleanup scenario: " .. tostring(err))
fire_event(recorded, "file-loaded")
fire_event(recorded, "end-file")
assert_true(
count_property_set(recorded.property_sets, "pause", true) == 1,
"pause cleanup scenario should force pause while waiting for tokenization"
)
assert_true(
count_property_set(recorded.property_sets, "pause", false) == 1,
"ending file while gate is armed should clear forced pause state"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",

View File

@@ -297,6 +297,60 @@ test('tokenizeSubtitle starts Yomitan frequency lookup and MeCab enrichment in p
assert.equal(result.tokens?.[0]?.frequencyRank, 77);
});
test('tokenizeSubtitle can signal tokenization-ready before enrichment completes', async () => {
const frequencyDeferred = createDeferred<unknown[]>();
const mecabDeferred = createDeferred<null>();
let tokenizationReadyText: string | null = null;
const pendingResult = tokenizeSubtitle(
'猫',
makeDeps({
onTokenizationReady: (text) => {
tokenizationReadyText = text;
},
getFrequencyDictionaryEnabled: () => true,
getYomitanExt: () => ({ id: 'dummy-ext' }) as any,
getYomitanParserWindow: () =>
({
isDestroyed: () => false,
webContents: {
executeJavaScript: async (script: string) => {
if (script.includes('getTermFrequencies')) {
return await frequencyDeferred.promise;
}
return [
{
source: 'scanning-parser',
index: 0,
content: [
[
{
text: '猫',
reading: 'ねこ',
headwords: [[{ term: '猫' }]],
},
],
],
},
];
},
},
}) as unknown as Electron.BrowserWindow,
tokenizeWithMecab: async () => {
return await mecabDeferred.promise;
},
}),
);
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(tokenizationReadyText, '猫');
frequencyDeferred.resolve([]);
mecabDeferred.resolve(null);
await pendingResult;
});
test('tokenizeSubtitle appends trailing kana to merged Yomitan readings when headword equals surface', async () => {
const result = await tokenizeSubtitle(
'断じて見ていない',

View File

@@ -51,6 +51,7 @@ export interface TokenizerServiceDeps {
getYomitanGroupDebugEnabled?: () => boolean;
tokenizeWithMecab: (text: string) => Promise<MergedToken[] | null>;
enrichTokensWithMecab?: MecabTokenEnrichmentFn;
onTokenizationReady?: (text: string) => void;
}
interface MecabTokenizerLike {
@@ -78,6 +79,7 @@ export interface TokenizerDepsRuntimeOptions {
getMinSentenceWordsForNPlusOne?: () => number;
getYomitanGroupDebugEnabled?: () => boolean;
getMecabTokenizer: () => MecabTokenizerLike | null;
onTokenizationReady?: (text: string) => void;
}
interface TokenizerAnnotationOptions {
@@ -215,6 +217,7 @@ export function createTokenizerDepsRuntime(
},
enrichTokensWithMecab: async (tokens, mecabTokens) =>
enrichTokensWithMecabAsync(tokens, mecabTokens),
onTokenizationReady: options.onTokenizationReady,
};
}
@@ -477,6 +480,7 @@ async function parseWithYomitanInternalParser(
if (deps.getYomitanGroupDebugEnabled?.() === true) {
logSelectedYomitanGroups(text, normalizedSelectedTokens);
}
deps.onTokenizationReady?.(text);
const frequencyRankPromise: Promise<Map<string, number>> = options.frequencyEnabled
? (async () => {

View File

@@ -854,21 +854,30 @@ const subsyncRuntime = createMainSubsyncRuntime(buildMainSubsyncRuntimeMainDepsH
let autoPlayReadySignalMediaPath: string | null = null;
let autoPlayReadySignalGeneration = 0;
function maybeSignalPluginAutoplayReady(payload: SubtitleData): void {
function maybeSignalPluginAutoplayReady(
payload: SubtitleData,
options?: { forceWhilePaused?: boolean },
): void {
if (!payload.text.trim()) {
return;
}
const mediaPath = appState.currentMediaPath;
if (!mediaPath) {
return;
}
if (autoPlayReadySignalMediaPath === mediaPath) {
const mediaPath =
appState.currentMediaPath?.trim() ||
appState.mpvClient?.currentVideoPath?.trim() ||
'__unknown__';
const duplicateMediaSignal = autoPlayReadySignalMediaPath === mediaPath;
const allowDuplicateWhilePaused =
options?.forceWhilePaused === true && appState.playbackPaused !== false;
if (duplicateMediaSignal && !allowDuplicateWhilePaused) {
return;
}
autoPlayReadySignalMediaPath = mediaPath;
const playbackGeneration = ++autoPlayReadySignalGeneration;
logger.debug(`[autoplay-ready] signaling mpv for media: ${mediaPath}`);
sendMpvCommandRuntime(appState.mpvClient, ['script-message', 'subminer-autoplay-ready']);
const signalPluginAutoplayReady = (): void => {
logger.debug(`[autoplay-ready] signaling mpv for media: ${mediaPath}`);
sendMpvCommandRuntime(appState.mpvClient, ['script-message', 'subminer-autoplay-ready']);
};
signalPluginAutoplayReady();
const isPlaybackPaused = async (client: {
requestProperty: (property: string) => Promise<unknown>;
}): Promise<boolean> => {
@@ -892,55 +901,52 @@ function maybeSignalPluginAutoplayReady(payload: SubtitleData): void {
return true;
};
// Fallback: unpause directly in case plugin readiness handler is unavailable/outdated.
void (async () => {
const mpvClient = appState.mpvClient;
if (!mpvClient?.connected) {
logger.debug('[autoplay-ready] skipped unpause fallback; mpv not connected');
return;
}
// Fallback: repeatedly try to release pause for a short window in case startup
// gate arming and tokenization-ready signal arrive out of order.
const maxReleaseAttempts = options?.forceWhilePaused === true ? 14 : 3;
const releaseRetryDelayMs = 200;
const attemptRelease = (attempt: number): void => {
void (async () => {
if (
autoPlayReadySignalMediaPath !== mediaPath ||
playbackGeneration !== autoPlayReadySignalGeneration
) {
return;
}
const shouldUnpause = await isPlaybackPaused(mpvClient);
logger.debug(`[autoplay-ready] mpv paused before fallback for ${mediaPath}: ${shouldUnpause}`);
if (!shouldUnpause) {
logger.debug('[autoplay-ready] mpv already playing; no fallback unpause needed');
return;
}
mpvClient.send({ command: ['set_property', 'pause', false] });
setTimeout(() => {
void (async () => {
if (
autoPlayReadySignalMediaPath !== mediaPath ||
playbackGeneration !== autoPlayReadySignalGeneration
) {
return;
const mpvClient = appState.mpvClient;
if (!mpvClient?.connected) {
if (attempt < maxReleaseAttempts) {
setTimeout(() => attemptRelease(attempt + 1), releaseRetryDelayMs);
}
return;
}
const followupClient = appState.mpvClient;
if (!followupClient?.connected) {
return;
const shouldUnpause = await isPlaybackPaused(mpvClient);
logger.debug(
`[autoplay-ready] mpv paused before fallback attempt ${attempt} for ${mediaPath}: ${shouldUnpause}`,
);
if (!shouldUnpause) {
if (attempt === 0) {
logger.debug('[autoplay-ready] mpv already playing; no fallback unpause needed');
}
return;
}
const shouldUnpauseFollowup = await isPlaybackPaused(followupClient);
if (!shouldUnpauseFollowup) {
return;
}
followupClient.send({ command: ['set_property', 'pause', false] });
})();
}, 500);
logger.debug('[autoplay-ready] issued direct mpv unpause fallback');
})();
signalPluginAutoplayReady();
mpvClient.send({ command: ['set_property', 'pause', false] });
if (attempt < maxReleaseAttempts) {
setTimeout(() => attemptRelease(attempt + 1), releaseRetryDelayMs);
}
})();
};
attemptRelease(0);
}
let appTray: Tray | null = null;
const buildSubtitleProcessingControllerMainDepsHandler =
createBuildSubtitleProcessingControllerMainDepsHandler({
tokenizeSubtitle: async (text: string) => {
if (getOverlayWindows().length === 0 && !subtitleWsService.hasClients()) {
return null;
}
return await tokenizeSubtitle(text);
},
emitSubtitle: (payload) => {
@@ -951,7 +957,6 @@ const buildSubtitleProcessingControllerMainDepsHandler =
topX: getResolvedConfig().subtitleStyle.frequencyDictionary.topX,
mode: getResolvedConfig().subtitleStyle.frequencyDictionary.mode,
});
maybeSignalPluginAutoplayReady(payload);
},
logDebug: (message) => {
logger.debug(`[subtitle-processing] ${message}`);
@@ -2335,9 +2340,7 @@ const {
ensureImmersionTrackerStarted();
},
updateCurrentMediaPath: (path) => {
if (appState.currentMediaPath !== path) {
autoPlayReadySignalMediaPath = null;
}
autoPlayReadySignalMediaPath = null;
if (path) {
ensureImmersionTrackerStarted();
}
@@ -2443,6 +2446,9 @@ const {
getFrequencyRank: (text) => appState.frequencyRankLookup(text),
getYomitanGroupDebugEnabled: () => appState.overlayDebugVisualizationEnabled,
getMecabTokenizer: () => appState.mecabTokenizer,
onTokenizationReady: (text) => {
maybeSignalPluginAutoplayReady({ text, tokens: null }, { forceWhilePaused: true });
},
},
createTokenizerRuntimeDeps: (deps) =>
createTokenizerDepsRuntime(deps as Parameters<typeof createTokenizerDepsRuntime>[0]),

View File

@@ -521,8 +521,8 @@ test('composeMpvRuntimeHandlers runs tokenization warmup once across sequential
assert.deepEqual(tokenizeCalls, ['first', 'second']);
assert.equal(yomitanWarmupCalls, 1);
assert.equal(prewarmJlptCalls, 1);
assert.equal(prewarmFrequencyCalls, 1);
assert.equal(prewarmJlptCalls, 0);
assert.equal(prewarmFrequencyCalls, 0);
});
test('composeMpvRuntimeHandlers does not block first tokenization on dictionary or MeCab warmup', async () => {
@@ -658,3 +658,151 @@ test('composeMpvRuntimeHandlers does not block first tokenization on dictionary
await tokenizePromise;
await composed.startTokenizationWarmups();
});
test(
'composeMpvRuntimeHandlers shows annotation loading OSD after tokenization-ready when dictionary warmup is still pending',
async () => {
const jlptDeferred = createDeferred();
const frequencyDeferred = createDeferred();
const osdMessages: string[] = [];
const composed = composeMpvRuntimeHandlers<
{ connect: () => void; on: () => void },
{ onTokenizationReady?: (text: string) => void },
{ text: string }
>({
bindMpvMainEventHandlersMainDeps: {
appState: {
initialArgs: null,
overlayRuntimeInitialized: true,
mpvClient: null,
immersionTracker: null,
subtitleTimingTracker: null,
currentSubText: '',
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: null,
},
getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},
quitApp: () => {},
reportJellyfinRemoteStopped: () => {},
syncOverlayMpvSubtitleSuppression: () => {},
maybeRunAnilistPostWatchUpdate: async () => {},
logSubtitleTimingError: () => {},
broadcastToOverlayWindows: () => {},
onSubtitleChange: () => {},
refreshDiscordPresence: () => {},
ensureImmersionTrackerInitialized: () => {},
updateCurrentMediaPath: () => {},
restoreMpvSubVisibility: () => {},
getCurrentAnilistMediaKey: () => null,
resetAnilistMediaTracking: () => {},
maybeProbeAnilistDuration: () => {},
ensureAnilistMediaGuess: () => {},
syncImmersionMediaState: () => {},
updateCurrentMediaTitle: () => {},
resetAnilistMediaGuessState: () => {},
reportJellyfinRemoteProgress: () => {},
updateSubtitleRenderMetrics: () => {},
},
mpvClientRuntimeServiceFactoryMainDeps: {
createClient: class {
connect(): void {}
on(): void {}
},
getSocketPath: () => '/tmp/mpv.sock',
getResolvedConfig: () => ({ auto_start_overlay: false }),
isAutoStartOverlayEnabled: () => false,
setOverlayVisible: () => {},
isVisibleOverlayVisible: () => false,
getReconnectTimer: () => null,
setReconnectTimer: () => {},
},
updateMpvSubtitleRenderMetricsMainDeps: {
getCurrentMetrics: () => BASE_METRICS,
setCurrentMetrics: () => {},
applyPatch: (current, patch) => ({ next: { ...current, ...patch }, changed: true }),
broadcastMetrics: () => {},
},
tokenizer: {
buildTokenizerDepsMainDeps: {
getYomitanExt: () => null,
getYomitanParserWindow: () => null,
setYomitanParserWindow: () => {},
getYomitanParserReadyPromise: () => null,
setYomitanParserReadyPromise: () => {},
getYomitanParserInitPromise: () => null,
setYomitanParserInitPromise: () => {},
isKnownWord: () => false,
recordLookup: () => {},
getKnownWordMatchMode: () => 'headword',
getNPlusOneEnabled: () => false,
getMinSentenceWordsForNPlusOne: () => 3,
getJlptLevel: () => null,
getJlptEnabled: () => true,
getFrequencyDictionaryEnabled: () => true,
getFrequencyDictionaryMatchMode: () => 'headword',
getFrequencyRank: () => null,
getYomitanGroupDebugEnabled: () => false,
getMecabTokenizer: () => null,
},
createTokenizerRuntimeDeps: (deps) =>
deps as unknown as { onTokenizationReady?: (text: string) => void },
tokenizeSubtitle: async (text, deps) => {
deps.onTokenizationReady?.(text);
return { text };
},
createMecabTokenizerAndCheckMainDeps: {
getMecabTokenizer: () => null,
setMecabTokenizer: () => {},
createMecabTokenizer: () => ({ id: 'mecab' }),
checkAvailability: async () => {},
},
prewarmSubtitleDictionariesMainDeps: {
ensureJlptDictionaryLookup: async () => jlptDeferred.promise,
ensureFrequencyDictionaryLookup: async () => frequencyDeferred.promise,
showMpvOsd: (message) => {
osdMessages.push(message);
},
},
},
warmups: {
launchBackgroundWarmupTaskMainDeps: {
now: () => 0,
logDebug: () => {},
logWarn: () => {},
},
startBackgroundWarmupsMainDeps: {
getStarted: () => false,
setStarted: () => {},
isTexthookerOnlyMode: () => false,
ensureYomitanExtensionLoaded: async () => undefined,
shouldWarmupMecab: () => false,
shouldWarmupYomitanExtension: () => false,
shouldWarmupSubtitleDictionaries: () => false,
shouldWarmupJellyfinRemoteSession: () => false,
shouldAutoConnectJellyfinRemote: () => false,
startJellyfinRemoteSession: async () => {},
},
},
});
const warmupPromise = composed.startTokenizationWarmups();
await new Promise<void>((resolve) => setImmediate(resolve));
assert.deepEqual(osdMessages, []);
await composed.tokenizeSubtitle('first line');
assert.deepEqual(osdMessages, ['Loading subtitle annotations |']);
jlptDeferred.resolve();
frequencyDeferred.resolve();
await warmupPromise;
await new Promise<void>((resolve) => setImmediate(resolve));
assert.deepEqual(osdMessages, [
'Loading subtitle annotations |',
'Subtitle annotations loaded',
]);
},
);

View File

@@ -141,6 +141,12 @@ export function composeMpvRuntimeHandlers<
options.tokenizer.buildTokenizerDepsMainDeps.getFrequencyDictionaryEnabled() !== false;
return nPlusOneEnabled || jlptEnabled || frequencyEnabled;
};
const shouldWarmupAnnotationDictionaries = (): boolean => {
const jlptEnabled = options.tokenizer.buildTokenizerDepsMainDeps.getJlptEnabled() !== false;
const frequencyEnabled =
options.tokenizer.buildTokenizerDepsMainDeps.getFrequencyDictionaryEnabled() !== false;
return jlptEnabled || frequencyEnabled;
};
let tokenizationWarmupInFlight: Promise<void> | null = null;
let tokenizationPrerequisiteWarmupInFlight: Promise<void> | null = null;
let tokenizationPrerequisiteWarmupCompleted = false;
@@ -174,7 +180,9 @@ export function composeMpvRuntimeHandlers<
) {
warmupTasks.push(createMecabTokenizerAndCheck().catch(() => {}));
}
warmupTasks.push(prewarmSubtitleDictionaries({ showLoadingOsd: true }).catch(() => {}));
if (shouldWarmupAnnotationDictionaries()) {
warmupTasks.push(prewarmSubtitleDictionaries().catch(() => {}));
}
await Promise.all(warmupTasks);
tokenizationWarmupCompleted = true;
})().finally(() => {
@@ -186,9 +194,19 @@ export function composeMpvRuntimeHandlers<
const tokenizeSubtitle = async (text: string): Promise<TTokenizedSubtitle> => {
if (!tokenizationWarmupCompleted) void startTokenizationWarmups();
await ensureTokenizationPrerequisites();
const tokenizerMainDeps = buildTokenizerDepsHandler();
if (shouldWarmupAnnotationDictionaries()) {
const onTokenizationReady = tokenizerMainDeps.onTokenizationReady;
tokenizerMainDeps.onTokenizationReady = (tokenizedText: string): void => {
onTokenizationReady?.(tokenizedText);
if (!tokenizationWarmupCompleted) {
void prewarmSubtitleDictionaries({ showLoadingOsd: true }).catch(() => {});
}
};
}
return options.tokenizer.tokenizeSubtitle(
text,
options.tokenizer.createTokenizerRuntimeDeps(buildTokenizerDepsHandler()),
options.tokenizer.createTokenizerRuntimeDeps(tokenizerMainDeps),
);
};

View File

@@ -167,22 +167,28 @@ test('dictionary prewarm can show OSD while awaiting background-started load', a
assert.deepEqual(osdMessages, ['Loading subtitle annotations |', 'Subtitle annotations loaded']);
});
test('dictionary prewarm does not show OSD when notifications are disabled', async () => {
const osdMessages: string[] = [];
test(
'dictionary prewarm shows OSD when loading indicator is requested even if notification predicate is disabled',
async () => {
const osdMessages: string[] = [];
const prewarm = createPrewarmSubtitleDictionariesMainHandler({
ensureJlptDictionaryLookup: async () => undefined,
ensureFrequencyDictionaryLookup: async () => undefined,
shouldShowOsdNotification: () => false,
showMpvOsd: (message) => {
osdMessages.push(message);
},
});
const prewarm = createPrewarmSubtitleDictionariesMainHandler({
ensureJlptDictionaryLookup: async () => undefined,
ensureFrequencyDictionaryLookup: async () => undefined,
shouldShowOsdNotification: () => false,
showMpvOsd: (message) => {
osdMessages.push(message);
},
});
await prewarm({ showLoadingOsd: true });
await prewarm({ showLoadingOsd: true });
assert.deepEqual(osdMessages, []);
});
assert.deepEqual(osdMessages, [
'Loading subtitle annotations |',
'Subtitle annotations loaded',
]);
},
);
test('dictionary prewarm clears loading OSD timer even if notifications are disabled before completion', async () => {
const clearedTimers: unknown[] = [];

View File

@@ -48,6 +48,7 @@ export function createBuildTokenizerDepsMainHandler(deps: TokenizerMainDeps) {
getFrequencyRank: (text: string) => deps.getFrequencyRank(text),
getYomitanGroupDebugEnabled: () => deps.getYomitanGroupDebugEnabled(),
getMecabTokenizer: () => deps.getMecabTokenizer(),
onTokenizationReady: (text: string) => deps.onTokenizationReady?.(text),
});
}
@@ -81,7 +82,6 @@ export function createPrewarmSubtitleDictionariesMainHandler(deps: {
let loadingOsdFrame = 0;
let loadingOsdTimer: unknown = null;
const showMpvOsd = deps.showMpvOsd;
const shouldShowOsdNotification = deps.shouldShowOsdNotification ?? (() => false);
const setIntervalHandler =
deps.setInterval ??
((callback: () => void, delayMs: number): unknown => setInterval(callback, delayMs));
@@ -91,7 +91,7 @@ export function createPrewarmSubtitleDictionariesMainHandler(deps: {
const spinnerFrames = ['|', '/', '-', '\\'];
const beginLoadingOsd = (): boolean => {
if (!showMpvOsd || !shouldShowOsdNotification()) {
if (!showMpvOsd) {
return false;
}
loadingOsdDepth += 1;