Surface ffmpeg startup failures
This commit is contained in:
+51
-2
@@ -3,7 +3,7 @@ import type { ChildProcessByStdio } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
import type { Readable } from 'node:stream';
|
||||
|
||||
import { AudioManagerConfigError } from './errors';
|
||||
import { AudioManagerConfigError, FfmpegProcessError } from './errors';
|
||||
import type { FfmpegOptions } from './types';
|
||||
|
||||
const requireFromCurrentModule = createRequire(__filename);
|
||||
@@ -11,9 +11,11 @@ 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;
|
||||
const STDERR_TAIL_BYTES = 4_096;
|
||||
|
||||
export type FfmpegProcessHandle = {
|
||||
process: ChildProcessByStdio<null, Readable, Readable>;
|
||||
ready: Promise<void>;
|
||||
stop(): void;
|
||||
};
|
||||
|
||||
@@ -50,18 +52,65 @@ export function startFfmpeg(input: string, options: FfmpegOptions = {}): FfmpegP
|
||||
...(options.outputArgs ?? DEFAULT_OUTPUT_ARGS),
|
||||
];
|
||||
const childProcess = spawn(executable, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
const ready = waitForFfmpegOutput(childProcess);
|
||||
|
||||
childProcess.once('error', () => undefined);
|
||||
childProcess.stderr.resume();
|
||||
|
||||
return {
|
||||
process: childProcess,
|
||||
ready,
|
||||
stop: (): void => {
|
||||
stopProcess(childProcess);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function waitForFfmpegOutput(childProcess: ChildProcessByStdio<null, Readable, Readable>): Promise<void> {
|
||||
let stderrTail = '';
|
||||
|
||||
const appendStderr = (chunk: Buffer | string): void => {
|
||||
stderrTail = (stderrTail + String(chunk)).slice(-STDERR_TAIL_BYTES);
|
||||
};
|
||||
|
||||
childProcess.stderr.on('data', appendStderr);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const cleanup = (): void => {
|
||||
childProcess.off('error', onError);
|
||||
childProcess.off('exit', onExit);
|
||||
childProcess.stdout.off('readable', onReadable);
|
||||
};
|
||||
|
||||
const fail = (message: string, cause?: unknown): void => {
|
||||
cleanup();
|
||||
reject(new FfmpegProcessError(addStderrTail(message, stderrTail), cause));
|
||||
};
|
||||
|
||||
const onError = (error: Error): void => {
|
||||
fail(`Unable to start ffmpeg. Cause: ${error.message}`, error);
|
||||
};
|
||||
|
||||
const onExit = (code: number | null, signal: NodeJS.Signals | null): void => {
|
||||
fail(`ffmpeg exited before producing audio. Exit code: ${code ?? 'none'}, signal: ${signal ?? 'none'}.`);
|
||||
};
|
||||
|
||||
const onReadable = (): void => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
|
||||
childProcess.once('error', onError);
|
||||
childProcess.once('exit', onExit);
|
||||
childProcess.stdout.once('readable', onReadable);
|
||||
});
|
||||
}
|
||||
|
||||
function addStderrTail(message: string, stderrTail: string): string {
|
||||
const trimmedTail = stderrTail.trim();
|
||||
|
||||
return trimmedTail ? `${message} stderr: ${trimmedTail}` : message;
|
||||
}
|
||||
|
||||
function stopProcess(childProcess: ChildProcessByStdio<null, Readable, Readable>): void {
|
||||
childProcess.stdout.destroy();
|
||||
childProcess.stderr.destroy();
|
||||
|
||||
Reference in New Issue
Block a user