mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-06 22:19:22 -07:00
fix: force X11 mpv fallback for launcher-managed playback (#47)
This commit is contained in:
@@ -7,6 +7,8 @@ import net from 'node:net';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { Args } from './types';
|
||||
import {
|
||||
buildMpvBackendArgs,
|
||||
buildMpvEnv,
|
||||
cleanupPlaybackSession,
|
||||
detectBackend,
|
||||
findAppBinary,
|
||||
@@ -125,6 +127,113 @@ test('detectBackend resolves windows on win32 auto mode', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('buildMpvEnv forces X11 by dropping Wayland hints when backend resolves to x11', () => {
|
||||
withPlatform('linux', () => {
|
||||
const env = buildMpvEnv(makeArgs({ backend: 'x11' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
HYPRLAND_INSTANCE_SIGNATURE: 'hypr',
|
||||
SWAYSOCK: '/tmp/sway.sock',
|
||||
});
|
||||
|
||||
assert.equal(env.DISPLAY, ':1');
|
||||
assert.equal(env.WAYLAND_DISPLAY, undefined);
|
||||
assert.equal(env.XDG_SESSION_TYPE, 'x11');
|
||||
assert.equal(env.HYPRLAND_INSTANCE_SIGNATURE, undefined);
|
||||
assert.equal(env.SWAYSOCK, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
test('buildMpvEnv auto mode falls back to X11 when no supported Wayland tracker backend is detected', () => {
|
||||
withPlatform('linux', () => {
|
||||
const env = buildMpvEnv(makeArgs({ backend: 'auto' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
XDG_CURRENT_DESKTOP: 'KDE',
|
||||
XDG_SESSION_DESKTOP: 'plasma',
|
||||
});
|
||||
|
||||
assert.equal(env.DISPLAY, ':1');
|
||||
assert.equal(env.WAYLAND_DISPLAY, undefined);
|
||||
assert.equal(env.XDG_SESSION_TYPE, 'x11');
|
||||
});
|
||||
});
|
||||
|
||||
test('buildMpvEnv preserves native Wayland env for supported Hyprland and Sway auto backends', () => {
|
||||
withPlatform('linux', () => {
|
||||
const hyprEnv = buildMpvEnv(makeArgs({ backend: 'auto' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
HYPRLAND_INSTANCE_SIGNATURE: 'hypr',
|
||||
});
|
||||
assert.equal(hyprEnv.WAYLAND_DISPLAY, 'wayland-0');
|
||||
assert.equal(hyprEnv.XDG_SESSION_TYPE, 'wayland');
|
||||
|
||||
const swayEnv = buildMpvEnv(makeArgs({ backend: 'auto' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
SWAYSOCK: '/tmp/sway.sock',
|
||||
});
|
||||
assert.equal(swayEnv.WAYLAND_DISPLAY, 'wayland-0');
|
||||
assert.equal(swayEnv.XDG_SESSION_TYPE, 'wayland');
|
||||
});
|
||||
});
|
||||
|
||||
test('buildMpvBackendArgs forces an explicit X11 renderer stack when backend resolves to x11', () => {
|
||||
withPlatform('linux', () => {
|
||||
assert.deepEqual(
|
||||
buildMpvBackendArgs(makeArgs({ backend: 'x11' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
}),
|
||||
['--vo=gpu', '--gpu-api=opengl', '--gpu-context=x11egl,x11'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('buildMpvBackendArgs forces the same X11 renderer stack for unsupported Wayland auto fallback', () => {
|
||||
withPlatform('linux', () => {
|
||||
assert.deepEqual(
|
||||
buildMpvBackendArgs(makeArgs({ backend: 'auto' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
XDG_CURRENT_DESKTOP: 'KDE',
|
||||
XDG_SESSION_DESKTOP: 'plasma',
|
||||
}),
|
||||
['--vo=gpu', '--gpu-api=opengl', '--gpu-context=x11egl,x11'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('buildMpvBackendArgs keeps supported Hyprland and Sway auto backends unchanged', () => {
|
||||
withPlatform('linux', () => {
|
||||
assert.deepEqual(
|
||||
buildMpvBackendArgs(makeArgs({ backend: 'auto' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
HYPRLAND_INSTANCE_SIGNATURE: 'hypr',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
assert.deepEqual(
|
||||
buildMpvBackendArgs(makeArgs({ backend: 'auto' }), {
|
||||
DISPLAY: ':1',
|
||||
WAYLAND_DISPLAY: 'wayland-0',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
SWAYSOCK: '/tmp/sway.sock',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('launchTexthookerOnly exits non-zero when app binary cannot be spawned', () => {
|
||||
const error = withProcessExitIntercept(() => {
|
||||
launchTexthookerOnly('/definitely-missing-subminer-binary', makeArgs());
|
||||
@@ -485,6 +594,40 @@ function withAccessSyncStub(
|
||||
}
|
||||
}
|
||||
|
||||
function withExistsAndStatSyncStubs(
|
||||
options: {
|
||||
existingPaths?: string[];
|
||||
directoryPaths?: string[];
|
||||
},
|
||||
run: () => void,
|
||||
): void {
|
||||
const existingPaths = new Set(options.existingPaths ?? []);
|
||||
const directoryPaths = new Set(options.directoryPaths ?? []);
|
||||
const originalExistsSync = fs.existsSync;
|
||||
const originalStatSync = fs.statSync;
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(fs as any).existsSync = (filePath: string): boolean =>
|
||||
existingPaths.has(filePath) || directoryPaths.has(filePath);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(fs as any).statSync = (filePath: string) => {
|
||||
if (directoryPaths.has(filePath)) {
|
||||
return { isDirectory: () => true };
|
||||
}
|
||||
if (existingPaths.has(filePath)) {
|
||||
return { isDirectory: () => false };
|
||||
}
|
||||
throw Object.assign(new Error(`ENOENT: ${filePath}`), { code: 'ENOENT' });
|
||||
};
|
||||
run();
|
||||
} finally {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(fs as any).existsSync = originalExistsSync;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(fs as any).statSync = originalStatSync;
|
||||
}
|
||||
}
|
||||
|
||||
function withRealpathSyncStub(resolvePath: (filePath: string) => string, run: () => void): void {
|
||||
const originalRealpathSync = fs.realpathSync;
|
||||
try {
|
||||
@@ -497,6 +640,75 @@ function withRealpathSyncStub(resolvePath: (filePath: string) => string, run: ()
|
||||
}
|
||||
}
|
||||
|
||||
function listRepoRootWindowsTempArtifacts(): string[] {
|
||||
return fs
|
||||
.readdirSync(process.cwd())
|
||||
.filter((entry) => /^\\tmp\\subminer-test-win-/.test(entry))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function runFindAppBinaryWindowsPathCase(): void {
|
||||
const baseDir = 'C:\\Users\\tester\\subminer-test-win-path';
|
||||
const originalHomedir = os.homedir;
|
||||
const originalPath = process.env.PATH;
|
||||
try {
|
||||
os.homedir = () => baseDir;
|
||||
const binDir = path.win32.join(baseDir, 'bin');
|
||||
const wrapperPath = path.win32.join(binDir, 'SubMiner.exe');
|
||||
process.env.PATH = `${binDir}${path.win32.delimiter}${originalPath ?? ''}`;
|
||||
|
||||
withFindAppBinaryPlatformSandbox('win32', (pathModule) => {
|
||||
withAccessSyncStub(
|
||||
(filePath) => filePath === wrapperPath,
|
||||
() => {
|
||||
const result = findAppBinary(
|
||||
pathModule.join(baseDir, 'launcher', 'SubMiner.exe'),
|
||||
pathModule,
|
||||
);
|
||||
assert.equal(result, wrapperPath);
|
||||
},
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
os.homedir = originalHomedir;
|
||||
process.env.PATH = originalPath;
|
||||
}
|
||||
}
|
||||
|
||||
function runFindAppBinaryWindowsInstallDirCase(): void {
|
||||
const baseDir = 'C:\\Users\\tester\\subminer-test-win-dir';
|
||||
const originalHomedir = os.homedir;
|
||||
const originalSubminerBinaryPath = process.env.SUBMINER_BINARY_PATH;
|
||||
try {
|
||||
os.homedir = () => baseDir;
|
||||
const installDir = path.win32.join(baseDir, 'Programs', 'SubMiner');
|
||||
const appExe = path.win32.join(installDir, 'SubMiner.exe');
|
||||
process.env.SUBMINER_BINARY_PATH = installDir;
|
||||
|
||||
withPlatform('win32', () => {
|
||||
withExistsAndStatSyncStubs(
|
||||
{ existingPaths: [appExe], directoryPaths: [installDir] },
|
||||
() => {
|
||||
withAccessSyncStub(
|
||||
(filePath) => filePath === appExe,
|
||||
() => {
|
||||
const result = findAppBinary(path.win32.join(baseDir, 'launcher', 'SubMiner.exe'), path.win32);
|
||||
assert.equal(result, appExe);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
os.homedir = originalHomedir;
|
||||
if (originalSubminerBinaryPath === undefined) {
|
||||
delete process.env.SUBMINER_BINARY_PATH;
|
||||
} else {
|
||||
process.env.SUBMINER_BINARY_PATH = originalSubminerBinaryPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test(
|
||||
'findAppBinary resolves ~/.local/bin/SubMiner.AppImage when it exists',
|
||||
{ concurrency: false },
|
||||
@@ -657,71 +869,31 @@ test('findAppBinary resolves Windows install paths when present', { concurrency:
|
||||
}
|
||||
});
|
||||
|
||||
test('findAppBinary resolves SubMiner.exe on PATH on Windows', { concurrency: false }, () => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-win-path-'));
|
||||
const originalHomedir = os.homedir;
|
||||
const originalPath = process.env.PATH;
|
||||
try {
|
||||
os.homedir = () => baseDir;
|
||||
const binDir = path.win32.join(baseDir, 'bin');
|
||||
const wrapperPath = path.win32.join(binDir, 'SubMiner.exe');
|
||||
makeExecutable(wrapperPath);
|
||||
process.env.PATH = `${binDir}${path.win32.delimiter}${originalPath ?? ''}`;
|
||||
test(
|
||||
'findAppBinary Windows cases do not leak backslash temp artifacts on POSIX',
|
||||
{ concurrency: false },
|
||||
() => {
|
||||
if (path.sep === '\\') {
|
||||
return;
|
||||
}
|
||||
|
||||
withFindAppBinaryPlatformSandbox('win32', (pathModule) => {
|
||||
withAccessSyncStub(
|
||||
(filePath) => filePath === wrapperPath,
|
||||
() => {
|
||||
const result = findAppBinary(
|
||||
pathModule.join(baseDir, 'launcher', 'SubMiner.exe'),
|
||||
pathModule,
|
||||
);
|
||||
assert.equal(result, wrapperPath);
|
||||
},
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
os.homedir = originalHomedir;
|
||||
process.env.PATH = originalPath;
|
||||
fs.rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
const before = listRepoRootWindowsTempArtifacts();
|
||||
runFindAppBinaryWindowsPathCase();
|
||||
runFindAppBinaryWindowsInstallDirCase();
|
||||
const after = listRepoRootWindowsTempArtifacts();
|
||||
|
||||
assert.deepEqual(after, before);
|
||||
},
|
||||
);
|
||||
|
||||
test('findAppBinary resolves SubMiner.exe on PATH on Windows', { concurrency: false }, () => {
|
||||
runFindAppBinaryWindowsPathCase();
|
||||
});
|
||||
|
||||
test(
|
||||
'findAppBinary resolves a Windows install directory to SubMiner.exe',
|
||||
{ concurrency: false },
|
||||
() => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-win-dir-'));
|
||||
const originalHomedir = os.homedir;
|
||||
const originalSubminerBinaryPath = process.env.SUBMINER_BINARY_PATH;
|
||||
try {
|
||||
os.homedir = () => baseDir;
|
||||
const installDir = path.win32.join(baseDir, 'Programs', 'SubMiner');
|
||||
const appExe = path.win32.join(installDir, 'SubMiner.exe');
|
||||
process.env.SUBMINER_BINARY_PATH = installDir;
|
||||
fs.mkdirSync(installDir, { recursive: true });
|
||||
fs.writeFileSync(appExe, '#!/bin/sh\nexit 0\n');
|
||||
fs.chmodSync(appExe, 0o755);
|
||||
|
||||
const originalPlatform = process.platform;
|
||||
try {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
||||
const result = findAppBinary(
|
||||
path.win32.join(baseDir, 'launcher', 'SubMiner.exe'),
|
||||
path.win32,
|
||||
);
|
||||
assert.equal(result, appExe);
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
|
||||
}
|
||||
} finally {
|
||||
os.homedir = originalHomedir;
|
||||
if (originalSubminerBinaryPath === undefined) {
|
||||
delete process.env.SUBMINER_BINARY_PATH;
|
||||
} else {
|
||||
process.env.SUBMINER_BINARY_PATH = originalSubminerBinaryPath;
|
||||
}
|
||||
fs.rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
runFindAppBinaryWindowsInstallDirCase();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -225,27 +225,65 @@ export function makeTempDir(prefix: string): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
}
|
||||
|
||||
export function detectBackend(backend: Backend): Exclude<Backend, 'auto'> {
|
||||
export function detectBackend(
|
||||
backend: Backend,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): Exclude<Backend, 'auto'> {
|
||||
if (backend !== 'auto') return backend;
|
||||
if (process.platform === 'win32') return 'windows';
|
||||
if (process.platform === 'darwin') return 'macos';
|
||||
const xdgCurrentDesktop = (process.env.XDG_CURRENT_DESKTOP || '').toLowerCase();
|
||||
const xdgSessionDesktop = (process.env.XDG_SESSION_DESKTOP || '').toLowerCase();
|
||||
const xdgSessionType = (process.env.XDG_SESSION_TYPE || '').toLowerCase();
|
||||
const hasWayland = Boolean(process.env.WAYLAND_DISPLAY) || xdgSessionType === 'wayland';
|
||||
const linuxDesktopEnv = getLinuxDesktopEnv(env);
|
||||
|
||||
if (
|
||||
process.env.HYPRLAND_INSTANCE_SIGNATURE ||
|
||||
xdgCurrentDesktop.includes('hyprland') ||
|
||||
xdgSessionDesktop.includes('hyprland')
|
||||
env.HYPRLAND_INSTANCE_SIGNATURE ||
|
||||
linuxDesktopEnv.xdgCurrentDesktop.includes('hyprland') ||
|
||||
linuxDesktopEnv.xdgSessionDesktop.includes('hyprland')
|
||||
) {
|
||||
return 'hyprland';
|
||||
}
|
||||
if (hasWayland && commandExists('hyprctl')) return 'hyprland';
|
||||
if (process.env.DISPLAY) return 'x11';
|
||||
if (linuxDesktopEnv.hasWayland && commandExists('hyprctl')) return 'hyprland';
|
||||
if (env.DISPLAY) return 'x11';
|
||||
fail('Could not detect display backend');
|
||||
}
|
||||
|
||||
type LinuxDesktopEnv = {
|
||||
xdgCurrentDesktop: string;
|
||||
xdgSessionDesktop: string;
|
||||
hasWayland: boolean;
|
||||
};
|
||||
|
||||
function getLinuxDesktopEnv(env: NodeJS.ProcessEnv): LinuxDesktopEnv {
|
||||
const xdgCurrentDesktop = (env.XDG_CURRENT_DESKTOP || '').toLowerCase();
|
||||
const xdgSessionDesktop = (env.XDG_SESSION_DESKTOP || '').toLowerCase();
|
||||
const xdgSessionType = (env.XDG_SESSION_TYPE || '').toLowerCase();
|
||||
return {
|
||||
xdgCurrentDesktop,
|
||||
xdgSessionDesktop,
|
||||
hasWayland: Boolean(env.WAYLAND_DISPLAY) || xdgSessionType === 'wayland',
|
||||
};
|
||||
}
|
||||
|
||||
function shouldForceX11MpvBackend(
|
||||
args: Pick<Args, 'backend'>,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): boolean {
|
||||
if (process.platform !== 'linux' || !env.DISPLAY?.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const linuxDesktopEnv = getLinuxDesktopEnv(env);
|
||||
const supportedWaylandBackend =
|
||||
Boolean(env.HYPRLAND_INSTANCE_SIGNATURE || env.SWAYSOCK) ||
|
||||
linuxDesktopEnv.xdgCurrentDesktop.includes('hyprland') ||
|
||||
linuxDesktopEnv.xdgCurrentDesktop.includes('sway') ||
|
||||
linuxDesktopEnv.xdgSessionDesktop.includes('hyprland') ||
|
||||
linuxDesktopEnv.xdgSessionDesktop.includes('sway');
|
||||
return (
|
||||
args.backend === 'x11' ||
|
||||
(args.backend === 'auto' && linuxDesktopEnv.hasWayland && !supportedWaylandBackend)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAppBinaryCandidate(candidate: string, pathModule: PathModule = path): string {
|
||||
const direct = resolveBinaryPathCandidate(candidate);
|
||||
if (!direct) return '';
|
||||
@@ -637,6 +675,7 @@ export async function startMpv(
|
||||
const mpvArgs: string[] = [];
|
||||
if (args.profile) mpvArgs.push(`--profile=${args.profile}`);
|
||||
mpvArgs.push(...DEFAULT_MPV_SUBMINER_ARGS);
|
||||
mpvArgs.push(...buildMpvBackendArgs(args));
|
||||
if (targetKind === 'url' && isYoutubeTarget(target)) {
|
||||
log('info', args.logLevel, 'Applying URL playback options');
|
||||
mpvArgs.push('--ytdl=yes');
|
||||
@@ -712,7 +751,10 @@ export async function startMpv(
|
||||
const mpvTarget = resolveCommandInvocation('mpv', mpvArgs, {
|
||||
normalizeWindowsShellArgs: false,
|
||||
});
|
||||
state.mpvProc = spawn(mpvTarget.command, mpvTarget.args, { stdio: 'inherit' });
|
||||
state.mpvProc = spawn(mpvTarget.command, mpvTarget.args, {
|
||||
stdio: 'inherit',
|
||||
env: buildMpvEnv(args),
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForOverlayStartCommandSettled(
|
||||
@@ -889,9 +931,9 @@ function stopManagedOverlayApp(args: Args): void {
|
||||
}
|
||||
}
|
||||
|
||||
function buildAppEnv(): NodeJS.ProcessEnv {
|
||||
function buildAppEnv(baseEnv: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
||||
const env: Record<string, string | undefined> = {
|
||||
...process.env,
|
||||
...baseEnv,
|
||||
SUBMINER_APP_LOG: getAppLogPath(),
|
||||
SUBMINER_MPV_LOG: getMpvLogPath(),
|
||||
};
|
||||
@@ -911,6 +953,32 @@ function buildAppEnv(): NodeJS.ProcessEnv {
|
||||
return env;
|
||||
}
|
||||
|
||||
export function buildMpvEnv(
|
||||
args: Pick<Args, 'backend'>,
|
||||
baseEnv: NodeJS.ProcessEnv = process.env,
|
||||
): NodeJS.ProcessEnv {
|
||||
const env = buildAppEnv(baseEnv);
|
||||
if (!shouldForceX11MpvBackend(args, env)) {
|
||||
return env;
|
||||
}
|
||||
|
||||
delete env.WAYLAND_DISPLAY;
|
||||
delete env.HYPRLAND_INSTANCE_SIGNATURE;
|
||||
delete env.SWAYSOCK;
|
||||
env.XDG_SESSION_TYPE = 'x11';
|
||||
return env;
|
||||
}
|
||||
|
||||
export function buildMpvBackendArgs(
|
||||
args: Pick<Args, 'backend'>,
|
||||
baseEnv: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
if (!shouldForceX11MpvBackend(args, baseEnv)) {
|
||||
return [];
|
||||
}
|
||||
return ['--vo=gpu', '--gpu-api=opengl', '--gpu-context=x11egl,x11'];
|
||||
}
|
||||
|
||||
function appendCapturedAppOutput(kind: 'STDOUT' | 'STDERR', chunk: string): void {
|
||||
const normalized = chunk.replace(/\r\n/g, '\n');
|
||||
for (const line of normalized.split('\n')) {
|
||||
@@ -1144,6 +1212,7 @@ export function launchMpvIdleDetached(
|
||||
const mpvArgs: string[] = [];
|
||||
if (args.profile) mpvArgs.push(`--profile=${args.profile}`);
|
||||
mpvArgs.push(...DEFAULT_MPV_SUBMINER_ARGS);
|
||||
mpvArgs.push(...buildMpvBackendArgs(args));
|
||||
if (args.mpvArgs) {
|
||||
mpvArgs.push(...parseMpvArgString(args.mpvArgs));
|
||||
}
|
||||
@@ -1159,6 +1228,7 @@ export function launchMpvIdleDetached(
|
||||
const proc = spawn(mpvTarget.command, mpvTarget.args, {
|
||||
stdio: 'ignore',
|
||||
detached: true,
|
||||
env: buildMpvEnv(args),
|
||||
});
|
||||
if (typeof proc.pid === 'number' && proc.pid > 0) {
|
||||
trackDetachedMpvPid(proc.pid);
|
||||
|
||||
Reference in New Issue
Block a user