feat & improvements & refactor: new structure, better garbage collection, better docs, better performance, added tests
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import type { ChildProcessByStdio } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
import type { Readable } from 'node:stream';
|
||||
|
||||
import { AudioManagerConfigError } from './errors';
|
||||
import type { FfmpegOptions } from './types';
|
||||
|
||||
const requireFromCurrentModule = createRequire(__filename);
|
||||
|
||||
const DEFAULT_INPUT_ARGS = ['-hide_banner', '-loglevel', 'error', '-nostdin'] as const;
|
||||
const DEFAULT_OUTPUT_ARGS = ['-vn', '-f', 's16le', '-ar', '48000', '-ac', '2', 'pipe:1'] as const;
|
||||
const FORCE_KILL_TIMEOUT_MS = 2_000;
|
||||
|
||||
export type FfmpegProcessHandle = {
|
||||
process: ChildProcessByStdio<null, Readable, Readable>;
|
||||
stop(): void;
|
||||
};
|
||||
|
||||
export function resolveFfmpegExecutable(options: FfmpegOptions = {}): string {
|
||||
if (options.executablePath?.trim()) {
|
||||
return options.executablePath;
|
||||
}
|
||||
|
||||
if ((options.mode ?? 'native') === 'native') {
|
||||
return 'ffmpeg';
|
||||
}
|
||||
|
||||
try {
|
||||
const executable = requireFromCurrentModule('ffmpeg-static') as unknown;
|
||||
|
||||
if (typeof executable === 'string' && executable.length > 0) {
|
||||
return executable;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new AudioManagerConfigError(
|
||||
`Unable to resolve ffmpeg-static. Install it or pass ffmpeg.executablePath. Cause: ${String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new AudioManagerConfigError('ffmpeg-static did not expose an executable path.');
|
||||
}
|
||||
|
||||
export function startFfmpeg(input: string, options: FfmpegOptions = {}): FfmpegProcessHandle {
|
||||
const executable = resolveFfmpegExecutable(options);
|
||||
const args = [
|
||||
...(options.inputArgs ?? DEFAULT_INPUT_ARGS),
|
||||
'-i',
|
||||
input,
|
||||
...(options.outputArgs ?? DEFAULT_OUTPUT_ARGS),
|
||||
];
|
||||
const childProcess = spawn(executable, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
|
||||
childProcess.once('error', () => undefined);
|
||||
childProcess.stderr.resume();
|
||||
|
||||
return {
|
||||
process: childProcess,
|
||||
stop: (): void => {
|
||||
stopProcess(childProcess);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function stopProcess(childProcess: ChildProcessByStdio<null, Readable, Readable>): void {
|
||||
childProcess.stdout.destroy();
|
||||
childProcess.stderr.destroy();
|
||||
childProcess.removeAllListeners();
|
||||
|
||||
if (childProcess.killed || childProcess.exitCode !== null || childProcess.signalCode !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
childProcess.kill('SIGTERM');
|
||||
|
||||
const forceKillTimeout = setTimeout(() => {
|
||||
if (!childProcess.killed && childProcess.exitCode === null && childProcess.signalCode === null) {
|
||||
childProcess.kill('SIGKILL');
|
||||
}
|
||||
}, FORCE_KILL_TIMEOUT_MS);
|
||||
|
||||
forceKillTimeout.unref();
|
||||
}
|
||||
Reference in New Issue
Block a user