mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-23 05:16:23 -07:00
323 lines
12 KiB
TypeScript
323 lines
12 KiB
TypeScript
import { isLogFileEnabled } from '../../shared/log-files';
|
|
import { canConnectSocket } from '../../shared/socket-probe';
|
|
import { buildMpvLaunchModeArgs } from '../../shared/mpv-launch-mode';
|
|
import { buildMpvMsgLevel } from '../../shared/mpv-logging-args';
|
|
import { buildSubminerPluginRuntimeScriptOptParts } from '../../shared/subminer-plugin-script-opts';
|
|
import type { MpvLaunchMode } from '../../types/config';
|
|
import type { SubminerPluginRuntimeScriptOptConfig } from '../../shared/subminer-plugin-script-opts';
|
|
import type { InstalledMpvPluginDetection } from './first-run-setup-plugin';
|
|
import {
|
|
createWindowsMpvPathDeps,
|
|
resolveWindowsMpvPath,
|
|
spawnMpvProcess,
|
|
type WindowsMpvPathDeps,
|
|
} from './mpv-process';
|
|
export {
|
|
getConfiguredWindowsMpvPathStatus,
|
|
resolveWindowsMpvPath,
|
|
type ConfiguredWindowsMpvPathStatus,
|
|
} from './mpv-process';
|
|
|
|
export interface WindowsMpvLaunchDeps extends WindowsMpvPathDeps {
|
|
spawnDetached: (command: string, args: string[], env?: NodeJS.ProcessEnv) => Promise<void>;
|
|
isAppControlServerAvailable?: () => Promise<boolean>;
|
|
sendAppControlCommand?: (
|
|
argv: string[],
|
|
) => Promise<{ ok: boolean; unavailable?: boolean; error?: string }>;
|
|
waitForSocketReady?: (socketPath: string, timeoutMs: number) => Promise<boolean>;
|
|
showError: (title: string, content: string) => void;
|
|
logInfo?: (message: string) => void;
|
|
}
|
|
|
|
export interface WindowsMpvRuntimePluginPolicy {
|
|
detectInstalledMpvPlugin?: (mpvPath: string) => InstalledMpvPluginDetection;
|
|
notifyInstalledPluginDetected?: (detection: InstalledMpvPluginDetection) => void;
|
|
resolveInstalledPluginBeforeLaunch?: (
|
|
detection: InstalledMpvPluginDetection,
|
|
mpvPath: string,
|
|
) => Promise<'removed' | 'continue' | 'cancel'> | 'removed' | 'continue' | 'cancel';
|
|
}
|
|
|
|
function normalizeCandidate(candidate: string | undefined): string {
|
|
return typeof candidate === 'string' ? candidate.trim() : '';
|
|
}
|
|
|
|
const DEFAULT_WINDOWS_MPV_SOCKET = '\\\\.\\pipe\\subminer-socket';
|
|
const RUNNING_APP_ATTACH_SOCKET_WAIT_MS = 10000;
|
|
|
|
async function sleepMs(ms: number): Promise<void> {
|
|
await new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function waitForSocketReady(socketPath: string, timeoutMs: number): Promise<boolean> {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
if (await canConnectSocket(socketPath)) {
|
|
return true;
|
|
}
|
|
await sleepMs(150);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function readExtraArgValue(extraArgs: string[], flag: string): string | undefined {
|
|
let value: string | undefined;
|
|
for (let i = 0; i < extraArgs.length; i += 1) {
|
|
const arg = extraArgs[i];
|
|
if (arg === flag) {
|
|
const next = extraArgs[i + 1];
|
|
if (next && !next.startsWith('-')) {
|
|
value = next;
|
|
i += 1;
|
|
}
|
|
continue;
|
|
}
|
|
if (arg?.startsWith(`${flag}=`)) {
|
|
value = arg.slice(flag.length + 1);
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function resolveWindowsMpvInputIpcServer(extraArgs: string[] = []): string {
|
|
return readExtraArgValue(extraArgs, '--input-ipc-server') ?? DEFAULT_WINDOWS_MPV_SOCKET;
|
|
}
|
|
|
|
export function buildWindowsRunningAppAttachArgs(
|
|
socketPath: string,
|
|
runtimeConfig: SubminerPluginRuntimeScriptOptConfig,
|
|
): string[] {
|
|
const args = ['--start', '--managed-playback'];
|
|
if (runtimeConfig.logLevel && runtimeConfig.logLevel !== 'info') {
|
|
args.push('--log-level', runtimeConfig.logLevel);
|
|
}
|
|
if (runtimeConfig.backend) {
|
|
args.push('--backend', runtimeConfig.backend);
|
|
}
|
|
args.push('--socket', socketPath);
|
|
args.push(
|
|
runtimeConfig.autoStartVisibleOverlay ? '--show-visible-overlay' : '--hide-visible-overlay',
|
|
);
|
|
if (runtimeConfig.texthookerEnabled) {
|
|
args.push('--texthooker');
|
|
}
|
|
return args;
|
|
}
|
|
|
|
export function buildWindowsMpvLaunchArgs(
|
|
targets: string[],
|
|
extraArgs: string[] = [],
|
|
binaryPath?: string,
|
|
pluginEntrypointPath?: string,
|
|
launchMode: MpvLaunchMode = 'normal',
|
|
pluginRuntimeConfig?: SubminerPluginRuntimeScriptOptConfig,
|
|
): string[] {
|
|
const launchIdle = targets.length === 0;
|
|
const inputIpcServer = resolveWindowsMpvInputIpcServer(extraArgs);
|
|
const scriptEntrypoint =
|
|
typeof pluginEntrypointPath === 'string' && pluginEntrypointPath.trim().length > 0
|
|
? `--script=${pluginEntrypointPath.trim()}`
|
|
: null;
|
|
const hasBinaryPath = typeof binaryPath === 'string' && binaryPath.trim().length > 0;
|
|
const shouldPassSubminerScriptOpts = scriptEntrypoint || hasBinaryPath;
|
|
const scriptOptPairs = pluginRuntimeConfig
|
|
? buildSubminerPluginRuntimeScriptOptParts(
|
|
{
|
|
...pluginRuntimeConfig,
|
|
socketPath: inputIpcServer,
|
|
},
|
|
binaryPath ?? '',
|
|
)
|
|
: shouldPassSubminerScriptOpts
|
|
? [`subminer-socket_path=${inputIpcServer.replace(/,/g, '\\,')}`]
|
|
: [];
|
|
const logLevel = pluginRuntimeConfig?.logLevel;
|
|
const hasMsgLevel = readExtraArgValue(extraArgs, '--msg-level') !== undefined;
|
|
const hasLogFile = readExtraArgValue(extraArgs, '--log-file') !== undefined;
|
|
const mpvLogLevelArg =
|
|
logLevel && !hasMsgLevel && (isLogFileEnabled('mpv') || hasLogFile)
|
|
? `--msg-level=${buildMpvMsgLevel(logLevel)}`
|
|
: null;
|
|
if (!pluginRuntimeConfig && hasBinaryPath) {
|
|
scriptOptPairs.unshift(`subminer-binary_path=${binaryPath.trim().replace(/,/g, '\\,')}`);
|
|
}
|
|
const scriptOpts = scriptOptPairs.length > 0 ? `--script-opts=${scriptOptPairs.join(',')}` : null;
|
|
|
|
return [
|
|
'--player-operation-mode=pseudo-gui',
|
|
'--force-window=immediate',
|
|
...(launchIdle ? ['--idle=yes'] : []),
|
|
...(scriptEntrypoint ? [scriptEntrypoint] : []),
|
|
`--input-ipc-server=${inputIpcServer}`,
|
|
'--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
|
'--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
|
'--sub-auto=fuzzy',
|
|
'--sub-file-paths=subs;subtitles',
|
|
'--sid=auto',
|
|
'--secondary-sid=auto',
|
|
'--sub-visibility=no',
|
|
'--secondary-sub-visibility=no',
|
|
...(scriptOpts ? [scriptOpts] : []),
|
|
...buildMpvLaunchModeArgs(launchMode),
|
|
...(mpvLogLevelArg ? [mpvLogLevelArg] : []),
|
|
...extraArgs,
|
|
...targets,
|
|
];
|
|
}
|
|
|
|
export async function launchWindowsMpv(
|
|
targets: string[],
|
|
deps: WindowsMpvLaunchDeps,
|
|
extraArgs: string[] = [],
|
|
binaryPath?: string,
|
|
pluginEntrypointPath?: string,
|
|
configuredMpvPath?: string,
|
|
launchMode: MpvLaunchMode = 'normal',
|
|
runtimePluginPolicy?: WindowsMpvRuntimePluginPolicy,
|
|
pluginRuntimeConfig?: SubminerPluginRuntimeScriptOptConfig,
|
|
): Promise<{ ok: boolean; mpvPath: string }> {
|
|
const normalizedConfiguredPath = normalizeCandidate(configuredMpvPath);
|
|
const mpvPath = resolveWindowsMpvPath(deps, normalizedConfiguredPath);
|
|
if (!mpvPath) {
|
|
deps.showError(
|
|
'SubMiner mpv launcher',
|
|
normalizedConfiguredPath
|
|
? `Configured mpv.executablePath was not found: ${normalizedConfiguredPath}`
|
|
: 'Could not find mpv.exe. Set mpv.executablePath, set SUBMINER_MPV_PATH, or add mpv.exe to PATH.',
|
|
);
|
|
return { ok: false, mpvPath: '' };
|
|
}
|
|
|
|
try {
|
|
let installedPlugin = runtimePluginPolicy?.detectInstalledMpvPlugin?.(mpvPath);
|
|
let installedPluginPrompted = false;
|
|
if (installedPlugin?.installed) {
|
|
const resolution = await runtimePluginPolicy?.resolveInstalledPluginBeforeLaunch?.(
|
|
installedPlugin,
|
|
mpvPath,
|
|
);
|
|
installedPluginPrompted = resolution != null;
|
|
if (resolution === 'cancel') {
|
|
return { ok: false, mpvPath };
|
|
}
|
|
if (resolution === 'removed') {
|
|
installedPlugin = runtimePluginPolicy?.detectInstalledMpvPlugin?.(mpvPath);
|
|
}
|
|
}
|
|
const runtimePluginEntrypointPath = installedPlugin?.installed
|
|
? undefined
|
|
: pluginEntrypointPath;
|
|
if (installedPlugin?.installed && !installedPluginPrompted) {
|
|
runtimePluginPolicy?.notifyInstalledPluginDetected?.(installedPlugin);
|
|
}
|
|
const hasLogLevel = pluginRuntimeConfig?.logLevel !== undefined;
|
|
const hasLogRotation = pluginRuntimeConfig?.logRotation !== undefined;
|
|
const shouldAttachRunningApp =
|
|
targets.length > 0 &&
|
|
pluginRuntimeConfig?.autoStart === true &&
|
|
deps.isAppControlServerAvailable !== undefined &&
|
|
deps.sendAppControlCommand !== undefined &&
|
|
(await deps.isAppControlServerAvailable());
|
|
const effectivePluginRuntimeConfig =
|
|
shouldAttachRunningApp && pluginRuntimeConfig
|
|
? { ...pluginRuntimeConfig, autoStart: false }
|
|
: pluginRuntimeConfig;
|
|
const launchEnv =
|
|
hasLogLevel || hasLogRotation
|
|
? {
|
|
...(hasLogLevel ? { SUBMINER_LOG_LEVEL: pluginRuntimeConfig.logLevel } : {}),
|
|
...(hasLogRotation
|
|
? { SUBMINER_LOG_ROTATION: String(pluginRuntimeConfig.logRotation) }
|
|
: {}),
|
|
}
|
|
: undefined;
|
|
const inputIpcServer = resolveWindowsMpvInputIpcServer(extraArgs);
|
|
const launchArgs = buildWindowsMpvLaunchArgs(
|
|
targets,
|
|
extraArgs,
|
|
binaryPath,
|
|
runtimePluginEntrypointPath,
|
|
launchMode,
|
|
effectivePluginRuntimeConfig,
|
|
);
|
|
deps.logInfo?.(
|
|
[
|
|
`Launching mpv: mpvPath=${mpvPath}`,
|
|
`inputIpcServer=${inputIpcServer}`,
|
|
`bundledPlugin=${runtimePluginEntrypointPath ?? 'not injected'}`,
|
|
`installedPlugin=${installedPlugin?.installed ? (installedPlugin.path ?? 'unknown') : 'none'}`,
|
|
`targets=${targets.length}`,
|
|
`attachRunningApp=${shouldAttachRunningApp ? 'yes' : 'no'}`,
|
|
].join('; '),
|
|
);
|
|
await deps.spawnDetached(mpvPath, launchArgs, launchEnv);
|
|
if (shouldAttachRunningApp && pluginRuntimeConfig) {
|
|
const socketReady = await (deps.waitForSocketReady ?? waitForSocketReady)(
|
|
inputIpcServer,
|
|
RUNNING_APP_ATTACH_SOCKET_WAIT_MS,
|
|
);
|
|
if (!socketReady) {
|
|
deps.logInfo?.(`MPV IPC socket was not ready before running app attach: ${inputIpcServer}`);
|
|
}
|
|
const attachArgs = buildWindowsRunningAppAttachArgs(inputIpcServer, pluginRuntimeConfig);
|
|
const controlResult = await deps.sendAppControlCommand?.(attachArgs);
|
|
if (controlResult?.ok) {
|
|
deps.logInfo?.('Attached launched mpv session to running SubMiner app via control socket');
|
|
} else {
|
|
deps.logInfo?.(
|
|
`Running SubMiner app attach failed: ${controlResult?.error ?? 'unknown error'}`,
|
|
);
|
|
}
|
|
}
|
|
return { ok: true, mpvPath };
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
deps.showError('SubMiner mpv launcher', `Failed to launch mpv.\nPath: ${mpvPath}\n${message}`);
|
|
return { ok: false, mpvPath };
|
|
}
|
|
}
|
|
|
|
export function createWindowsMpvLaunchDeps(options: {
|
|
getEnv?: (name: string) => string | undefined;
|
|
fileExists?: (candidate: string) => boolean;
|
|
isAppControlServerAvailable?: () => Promise<boolean>;
|
|
sendAppControlCommand?: (
|
|
argv: string[],
|
|
) => Promise<{ ok: boolean; unavailable?: boolean; error?: string }>;
|
|
showError: (title: string, content: string) => void;
|
|
logInfo?: (message: string) => void;
|
|
}): WindowsMpvLaunchDeps {
|
|
return {
|
|
...createWindowsMpvPathDeps(options),
|
|
isAppControlServerAvailable: options.isAppControlServerAvailable,
|
|
sendAppControlCommand: options.sendAppControlCommand,
|
|
waitForSocketReady,
|
|
logInfo: options.logInfo,
|
|
spawnDetached: (command, args, env) =>
|
|
new Promise((resolve, reject) => {
|
|
try {
|
|
const child = spawnMpvProcess(
|
|
command,
|
|
args,
|
|
env ? { ...process.env, ...env } : process.env,
|
|
);
|
|
let settled = false;
|
|
child.once('error', (error) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
reject(error);
|
|
});
|
|
child.once('spawn', () => {
|
|
if (settled) return;
|
|
settled = true;
|
|
child.unref();
|
|
resolve();
|
|
});
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
}),
|
|
showError: options.showError,
|
|
};
|
|
}
|