UNPKG

ffmpeg-stream-manager

Version:

🎥 A powerful TypeScript library for managing multiple simultaneous RTMP streams using FFmpeg. Perfect for streaming to platforms like YouTube Live, Twitch, and others.

226 lines • 8.29 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StreamProcess = void 0; const child_process_1 = require("child_process"); const events_1 = require("events"); const types_1 = require("../types"); const ffmpeg_command_builder_1 = require("../utils/ffmpeg-command-builder"); class StreamProcess extends events_1.EventEmitter { constructor(config, options = {}) { super(); this.config = config; this.ffmpegProcess = null; this._state = types_1.StreamState.STOPPED; this._restartCount = 0; this.autoRestart = options.autoRestart ?? true; this.restartDelay = options.restartDelay ?? 5000; } on(event, listener) { return super.on(event, listener); } emit(event, ...args) { return super.emit(event, ...args); } get state() { return this._state; } get startTime() { return this._startTime; } get uptime() { if (!this._startTime || this._state !== types_1.StreamState.RUNNING) { return 0; } return Math.floor((Date.now() - this._startTime.getTime()) / 1000); } get restartCount() { return this._restartCount; } get lastError() { return this._lastError; } get pid() { return this.ffmpegProcess?.pid; } async start() { if (this._state !== types_1.StreamState.STOPPED) { throw new Error(`Cannot start stream in state: ${this._state}`); } try { ffmpeg_command_builder_1.FFmpegCommandBuilder.validateConfig(this.config); this.setState(types_1.StreamState.STARTING); this.clearRestartTimeout(); const command = ffmpeg_command_builder_1.FFmpegCommandBuilder.buildCommand(this.config); this.log('info', `Starting FFmpeg with command: ffmpeg ${command.join(' ')}`); this.ffmpegProcess = (0, child_process_1.spawn)('ffmpeg', command, { stdio: ['ignore', 'pipe', 'pipe'], detached: false }); this.setupProcessHandlers(); // Give the process a moment to start await new Promise(resolve => setTimeout(resolve, 1000)); if (this.ffmpegProcess && !this.ffmpegProcess.killed) { this._startTime = new Date(); this.setState(types_1.StreamState.RUNNING); this.log('info', 'Stream started successfully'); } else { throw new Error('FFmpeg process failed to start'); } } catch (error) { this._lastError = error instanceof Error ? error.message : 'Unknown error'; this.setState(types_1.StreamState.ERROR); throw error; } } async stop() { if (this._state === types_1.StreamState.STOPPED) { return; } this.clearRestartTimeout(); this.autoRestart = false; // Disable auto-restart when manually stopping if (this.ffmpegProcess) { this.log('info', 'Stopping stream...'); // Try graceful termination first this.ffmpegProcess.kill('SIGTERM'); // Force kill after 5 seconds if still running setTimeout(() => { if (this.ffmpegProcess && !this.ffmpegProcess.killed) { this.log('warning', 'Force killing FFmpeg process'); this.ffmpegProcess.kill('SIGKILL'); } }, 5000); } this.setState(types_1.StreamState.STOPPED); this._startTime = undefined; } async restart() { this.log('info', 'Restarting stream...'); this.setState(types_1.StreamState.RESTARTING); await this.stop(); // Wait a bit before restarting await new Promise(resolve => setTimeout(resolve, this.restartDelay)); this._restartCount++; this.emit('restart'); await this.start(); } updateConfig(newConfig) { if (this._state === types_1.StreamState.RUNNING) { throw new Error('Cannot update config while stream is running. Stop the stream first.'); } ffmpeg_command_builder_1.FFmpegCommandBuilder.validateConfig(newConfig); this.config = { ...newConfig }; this.log('info', 'Stream configuration updated'); } getConfig() { return { ...this.config }; } setState(newState) { if (this._state !== newState) { const oldState = this._state; this._state = newState; this.log('info', `State changed from ${oldState} to ${newState}`); this.emit('state-change', newState); } } setupProcessHandlers() { if (!this.ffmpegProcess) return; // Handle stdout this.ffmpegProcess.stdout?.on('data', (data) => { const message = data.toString().trim(); if (message) { this.log('info', message, 'stdout'); } }); // Handle stderr (FFmpeg writes most output to stderr) this.ffmpegProcess.stderr?.on('data', (data) => { const message = data.toString().trim(); if (message) { // Parse FFmpeg output to determine if it's an error const isError = this.isErrorMessage(message); this.log(isError ? 'error' : 'info', message, 'stderr'); } }); // Handle process exit this.ffmpegProcess.on('exit', (code, signal) => { this.log('info', `FFmpeg process exited with code ${code}, signal ${signal}`); if (code !== 0 && code !== null) { this._lastError = `FFmpeg exited with code ${code}`; this.setState(types_1.StreamState.ERROR); } else { this.setState(types_1.StreamState.STOPPED); } this.ffmpegProcess = null; this._startTime = undefined; // Auto-restart if enabled and not manually stopped if (this.autoRestart && this._state === types_1.StreamState.ERROR) { this.scheduleRestart(); } }); // Handle process errors this.ffmpegProcess.on('error', (error) => { this._lastError = error.message; this.log('error', `FFmpeg process error: ${error.message}`); this.setState(types_1.StreamState.ERROR); this.emit('error', error); }); } isErrorMessage(message) { const errorIndicators = [ 'error', 'failed', 'cannot', 'unable', 'invalid', 'not found', 'permission denied', 'connection refused' ]; return errorIndicators.some(indicator => message.toLowerCase().includes(indicator)); } scheduleRestart() { if (this.restartTimeout) { clearTimeout(this.restartTimeout); } this.log('info', `Scheduling restart in ${this.restartDelay}ms`); this.restartTimeout = setTimeout(async () => { try { await this.restart(); } catch (error) { this.log('error', `Restart failed: ${error instanceof Error ? error.message : 'Unknown error'}`); // Schedule another restart this.scheduleRestart(); } }, this.restartDelay); } clearRestartTimeout() { if (this.restartTimeout) { clearTimeout(this.restartTimeout); this.restartTimeout = undefined; } } log(level, message, source = 'stdout') { const logEntry = { streamId: this.config.id, timestamp: new Date(), level, message, source }; this.emit('log', logEntry); } // Cleanup resources destroy() { this.clearRestartTimeout(); this.removeAllListeners(); if (this.ffmpegProcess && !this.ffmpegProcess.killed) { this.ffmpegProcess.kill('SIGKILL'); } } } exports.StreamProcess = StreamProcess; //# sourceMappingURL=stream-process.js.map