UNPKG

@eleven-am/transcoder

Version:

High-performance HLS transcoding library with hardware acceleration, intelligent client management, and distributed processing support for Node.js

227 lines 7.2 kB
"use strict"; /* * @eleven-am/transcoder * Copyright (C) 2025 Roy OSSAI * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.FfmpegCommand = void 0; exports.default = ffmpeg; const child_process_1 = require("child_process"); const utils_1 = require("./utils"); class FfmpegCommand extends utils_1.ExtendedEventEmitter { constructor(inputPath) { super(); this.inputOpts = []; this.outputOpts = []; this.outputPath = ''; this.process = null; this.isRunning = false; this.processTimeout = null; this.processTimeoutMs = 300000; // 5 minutes this.inputPath = inputPath; } /** * Add input options to the command * @param options Array of input options */ inputOptions(options) { this.inputOpts.push(...options); return this; } /** * Add output options to the command * @param options Array of output options */ outputOptions(options) { this.outputOpts.push(...options); return this; } /** * Add video filters to the command * @param filters Video filter string */ videoFilters(filters) { this.videoFilterOpts = filters; return this; } /** * Set the output path * @param outputPath Path for the output file */ output(outputPath) { this.outputPath = outputPath; return this; } /** * Execute the FFmpeg command */ run() { if (this.isRunning) { return; } this.isRunning = true; const args = this.buildArgs(); this.process = (0, child_process_1.spawn)('ffmpeg', args); let stderrBuffer = ''; // Set up timeout this.processTimeout = setTimeout(() => { if (this.isRunning) { this.kill('SIGTERM'); this.emit('error', new Error('FFmpeg process timed out')); } }, this.processTimeoutMs); this.emit('start', { command: `ffmpeg ${args.join(' ')}`, }); this.process.stderr.on('data', (data) => { const output = data.toString(); stderrBuffer += output; }); this.process.stdout.on('data', (data) => { const output = data.toString(); const regex = /segment-(?<segment>\d+)\.ts/; const match = output.match(regex); if (match && match.groups) { const segment = parseInt(match.groups.segment, 10); this.emit('progress', { segment }); } }); this.process.on('close', (code) => { this.cleanup(); if (code === 0 || code === null) { this.emit('end', undefined); } else { const error = new Error(`FFmpeg process exited with code ${code} and output: ${stderrBuffer}`); this.emit('error', error); } }); this.process.on('error', (err) => { this.cleanup(); this.emit('error', err); }); // Clean up on process exit this.process.on('exit', () => { this.cleanup(); }); } /** * Kill the FFmpeg process * @param signal Signal to send to the process */ kill(signal = 'SIGKILL') { if (this.process && this.isRunning) { this.process.kill(signal); this.cleanup(); } } /** * Cleanup resources */ cleanup() { this.isRunning = false; if (this.processTimeout) { clearTimeout(this.processTimeout); this.processTimeout = null; } // Remove all listeners to prevent memory leaks if (this.process) { this.process.removeAllListeners(); this.process.stderr.removeAllListeners(); this.process.stdout.removeAllListeners(); this.process = null; } } /** * Pipe the FFmpeg output to a stream rather than a file * @returns The stdout stream from the FFmpeg process */ pipe() { if (this.isRunning) { throw new Error('FFmpeg process is already running'); } this.outputPath = '-'; const args = this.buildArgs(); this.process = (0, child_process_1.spawn)('ffmpeg', args); this.isRunning = true; let stderrBuffer = ''; this.emit('start', { command: `ffmpeg ${args.join(' ')}`, }); // Set up timeout for pipe mode this.processTimeout = setTimeout(() => { if (this.isRunning) { this.kill('SIGTERM'); this.emit('error', new Error('FFmpeg process timed out')); } }, this.processTimeoutMs); this.process.stderr.on('data', (data) => { const output = data.toString(); stderrBuffer += output; const progressRegex = /time=(\d+:\d+:\d+.\d+)/; const match = output.match(progressRegex); if (match) { this.emit('progress', { segment: 0 }); } }); this.process.on('error', (err) => { this.cleanup(); this.emit('error', err); }); this.process.on('close', (code) => { this.cleanup(); if (code === 0 || code === null) { this.emit('end', undefined); } else { const error = new Error(`FFmpeg process exited with code ${code} and output: ${stderrBuffer}`); this.emit('error', error); } }); // Clean up on process exit this.process.on('exit', () => { this.cleanup(); }); return this.process.stdout; } /** * Build the complete FFmpeg command arguments */ buildArgs() { const args = [ ...this.inputOpts, '-i', this.inputPath, ]; if (this.videoFilterOpts) { args.push('-vf', this.videoFilterOpts); } args.push(...this.outputOpts); if (this.outputPath) { args.push(this.outputPath); } return args; } } exports.FfmpegCommand = FfmpegCommand; /** * Factory function that mimics the fluent-ffmpeg module's interface * @param inputPath Input file path */ function ffmpeg(inputPath) { return new FfmpegCommand(inputPath); } //# sourceMappingURL=ffmpeg.js.map