av-kit
Version:
AVFoundation Recorder kit for Node.js
167 lines (147 loc) • 4.8 kB
text/typescript
import { ChildProcess, spawn } from "child_process";
import { ffmpegPath } from "./utils";
/**
* FFmpegProcessManager - Handles the creation, management and termination of FFmpeg processes
*/
export class FFmpegProcessManager {
private processes: Map<string, ChildProcess> = new Map();
/**
* Start a new FFmpeg process
*
* @param processName Unique identifier for the process
* @param args FFmpeg command line arguments
* @returns Promise that resolves when the process has started successfully
*/
public startProcess(processName: string, args: string[]): Promise<void> {
// Don't allow duplicate process names
if (this.processes.has(processName)) {
return Promise.reject(new Error(`Process ${processName} already exists`));
}
// Start the process
const process = spawn(ffmpegPath, args);
this.processes.set(processName, process);
// Set up error handling for process startup
return new Promise<void>((resolve, reject) => {
// Handle immediate errors
process.on("error", (error: Error) => {
console.error(`Error starting ${processName}:`, error);
this.processes.delete(processName);
reject(error);
});
// Install early exit handler
let earlyExitHandlerActive = true;
process.once("exit", (code: number | null) => {
if (earlyExitHandlerActive && code !== 0 && code !== null) {
const errorMsg = `${processName} exited early with code ${code}`;
console.error(errorMsg);
this.processes.delete(processName);
reject(new Error(errorMsg));
}
});
// After a short delay, assume process started successfully
setTimeout(() => {
// Disable the early exit handler by setting the flag instead of removing
earlyExitHandlerActive = false;
// Set up regular exit handler
process.on("exit", (code: number | null) => {
if (code !== 0 && code !== null) {
console.error(`${processName} exited with code ${code}`);
}
this.processes.delete(processName);
});
resolve();
}, 500);
// Set up stderr handling to check for startup errors
let stderrData = "";
process.stderr.on("data", (data: Buffer) => {
stderrData += data.toString();
// Check for common ffmpeg errors
if (
stderrData.includes("Cannot open") ||
stderrData.includes("Error") ||
stderrData.includes("Invalid")
) {
console.error(`FFmpeg error: ${stderrData}`);
}
});
});
}
/**
* Stop a specific process
*
* @param processName Identifier of the process to stop
* @returns Promise that resolves when the process has been stopped
*/
public stopProcess(processName: string): Promise<void> {
return new Promise((resolve) => {
const process = this.processes.get(processName);
if (!process) {
resolve();
return;
}
// Define the cleanup function
const cleanup = () => {
process.removeAllListeners();
this.processes.delete(processName);
resolve();
};
// Handle normal exit
process.on("exit", cleanup);
// Gracefully terminate the process with SIGTERM
if (!process.killed) {
process.kill("SIGTERM");
// Set a timeout to force kill if needed
setTimeout(() => {
if (!process.killed) {
try {
process.kill("SIGKILL");
} catch (e) {
console.error(`Error killing ${processName}:`, e);
}
}
cleanup();
}, 2000);
} else {
cleanup();
}
});
}
/**
* Stop all processes
*
* @returns Promise that resolves when all processes have been stopped
*/
public stopAllProcesses(): Promise<void[]> {
const processes = Array.from(this.processes.keys());
const stopPromises = processes.map((processName) =>
this.stopProcess(processName)
);
return Promise.all(stopPromises);
}
/**
* Check if a process exists
*
* @param processName Name of the process to check
* @returns true if the process exists
*/
public hasProcess(processName: string): boolean {
return this.processes.has(processName);
}
/**
* Get a process by name
*
* @param processName Name of the process to get
* @returns The ChildProcess or undefined if not found
*/
public getProcess(processName: string): ChildProcess | undefined {
return this.processes.get(processName);
}
/**
* Get the count of running processes
*
* @returns Number of running processes
*/
public getProcessCount(): number {
return this.processes.size;
}
}