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.

322 lines • 11.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FFmpegStreamManager = void 0; const events_1 = require("events"); const uuid_1 = require("uuid"); const types_1 = require("./types"); const stream_process_1 = require("./stream/stream-process"); const ffmpeg_command_builder_1 = require("./utils/ffmpeg-command-builder"); class FFmpegStreamManager extends events_1.EventEmitter { constructor(options = {}) { super(); this.streams = new Map(); this.options = { maxConcurrentStreams: options.maxConcurrentStreams ?? 10, autoRestart: options.autoRestart ?? true, restartDelay: options.restartDelay ?? 5000, logLevel: options.logLevel ?? 'info' }; } on(event, listener) { return super.on(event, listener); } emit(event, ...args) { return super.emit(event, ...args); } /** * Start a new stream with the given configuration */ async startStream(config) { if (this.streams.size >= this.options.maxConcurrentStreams) { throw new types_1.StreamError(`Maximum concurrent streams limit reached (${this.options.maxConcurrentStreams})`, 'manager'); } const streamId = (0, uuid_1.v4)(); const fullConfig = { ...config, id: streamId }; try { const streamProcess = new stream_process_1.StreamProcess(fullConfig, { autoRestart: this.options.autoRestart, restartDelay: this.options.restartDelay }); this.setupStreamHandlers(streamProcess); this.streams.set(streamId, streamProcess); await streamProcess.start(); this.emit('stream-started', streamId); this.log('info', `Stream ${streamId} started successfully`); return streamId; } catch (error) { this.streams.delete(streamId); const streamError = new types_1.StreamError(`Failed to start stream: ${error instanceof Error ? error.message : 'Unknown error'}`, streamId); this.emit('stream-error', streamId, streamError); throw streamError; } } /** * Start a stream for YouTube Live */ async startYouTubeStream(config, youtubeConfig) { const rtmpUrl = ffmpeg_command_builder_1.FFmpegCommandBuilder.buildYouTubeRTMPUrl(youtubeConfig.streamKey, youtubeConfig.server); return this.startStream({ ...config, rtmpUrl }); } /** * Stop a stream by ID */ async stopStream(streamId) { const stream = this.streams.get(streamId); if (!stream) { throw new types_1.StreamError(`Stream not found: ${streamId}`, streamId); } try { await stream.stop(); this.streams.delete(streamId); this.emit('stream-stopped', streamId); this.log('info', `Stream ${streamId} stopped successfully`); } catch (error) { const streamError = new types_1.StreamError(`Failed to stop stream: ${error instanceof Error ? error.message : 'Unknown error'}`, streamId); this.emit('stream-error', streamId, streamError); throw streamError; } } /** * Restart a stream by ID */ async restartStream(streamId) { const stream = this.streams.get(streamId); if (!stream) { throw new types_1.StreamError(`Stream not found: ${streamId}`, streamId); } try { await stream.restart(); this.log('info', `Stream ${streamId} restarted successfully`); } catch (error) { const streamError = new types_1.StreamError(`Failed to restart stream: ${error instanceof Error ? error.message : 'Unknown error'}`, streamId); this.emit('stream-error', streamId, streamError); throw streamError; } } /** * Update stream configuration (stream must be stopped) */ updateStreamConfig(streamId, updates) { const stream = this.streams.get(streamId); if (!stream) { throw new types_1.StreamError(`Stream not found: ${streamId}`, streamId); } try { const currentConfig = stream.getConfig(); const newConfig = { ...currentConfig, ...updates, videoConfig: { ...currentConfig.videoConfig, ...updates.videoConfig }, audioConfig: { ...currentConfig.audioConfig, ...updates.audioConfig }, presetConfig: { ...currentConfig.presetConfig, ...updates.presetConfig } }; stream.updateConfig(newConfig); this.log('info', `Stream ${streamId} configuration updated`); } catch (error) { const streamError = new types_1.StreamError(`Failed to update stream config: ${error instanceof Error ? error.message : 'Unknown error'}`, streamId); this.emit('stream-error', streamId, streamError); throw streamError; } } /** * Get status of a specific stream */ getStreamStatus(streamId) { const stream = this.streams.get(streamId); if (!stream) { throw new types_1.StreamError(`Stream not found: ${streamId}`, streamId); } return { id: streamId, status: stream.state, startTime: stream.startTime, uptime: stream.uptime, restartCount: stream.restartCount, lastError: stream.lastError, ffmpegPid: stream.pid, config: stream.getConfig() }; } /** * Get status of all streams */ getAllStreamStatuses() { return Array.from(this.streams.entries()).map(([streamId, stream]) => ({ id: streamId, status: stream.state, startTime: stream.startTime, uptime: stream.uptime, restartCount: stream.restartCount, lastError: stream.lastError, ffmpegPid: stream.pid, config: stream.getConfig() })); } /** * Get list of active stream IDs */ getActiveStreamIds() { return Array.from(this.streams.keys()).filter(streamId => { const stream = this.streams.get(streamId); return stream && stream.state === types_1.StreamState.RUNNING; }); } /** * Get total number of streams */ getStreamCount() { return this.streams.size; } /** * Check if a stream exists */ hasStream(streamId) { return this.streams.has(streamId); } /** * Stop all streams */ async stopAllStreams() { const streamIds = Array.from(this.streams.keys()); const stopPromises = streamIds.map(id => this.stopStream(id)); await Promise.allSettled(stopPromises); this.log('info', 'All streams stopped'); } /** * Get default configurations for common use cases */ static getDefaultConfigs() { return { youTube1080p: { videoConfig: { width: 1920, height: 1080, bitrate: '4500k', framerate: 30, codec: 'libx264', profile: 'high', level: '4.1' }, audioConfig: { codec: 'aac', bitrate: '128k', sampleRate: 44100, channels: 2 }, presetConfig: { preset: 'fast', tune: 'zerolatency', bufsize: '9000k', maxrate: '4500k' } }, youTube720p: { videoConfig: { width: 1280, height: 720, bitrate: '2500k', framerate: 30, codec: 'libx264', profile: 'high', level: '3.1' }, audioConfig: { codec: 'aac', bitrate: '128k', sampleRate: 44100, channels: 2 }, presetConfig: { preset: 'fast', tune: 'zerolatency', bufsize: '5000k', maxrate: '2500k' } }, youTube480p: { videoConfig: { width: 854, height: 480, bitrate: '1000k', framerate: 30, codec: 'libx264', profile: 'main', level: '3.0' }, audioConfig: { codec: 'aac', bitrate: '96k', sampleRate: 44100, channels: 2 }, presetConfig: { preset: 'fast', tune: 'zerolatency', bufsize: '2000k', maxrate: '1000k' } } }; } setupStreamHandlers(stream) { stream.on('state-change', (state) => { const streamId = stream.getConfig().id; this.log('info', `Stream ${streamId} state changed to ${state}`); }); stream.on('log', (log) => { if (this.shouldLogLevel(log.level)) { this.emit('log', log); } }); stream.on('error', (error) => { const streamId = stream.getConfig().id; this.emit('stream-error', streamId, error); }); stream.on('restart', () => { const streamId = stream.getConfig().id; this.emit('stream-restarted', streamId); }); } shouldLogLevel(level) { const levels = ['debug', 'info', 'warning', 'error']; const currentLevelIndex = levels.indexOf(this.options.logLevel); const messageLevelIndex = levels.indexOf(level); return messageLevelIndex >= currentLevelIndex; } log(level, message) { if (this.shouldLogLevel(level)) { const logEntry = { streamId: 'manager', timestamp: new Date(), level, message, source: 'stdout' }; this.emit('log', logEntry); } } /** * Cleanup all resources */ async destroy() { await this.stopAllStreams(); // Clean up all stream processes for (const stream of this.streams.values()) { stream.destroy(); } this.streams.clear(); this.removeAllListeners(); } } exports.FFmpegStreamManager = FFmpegStreamManager; //# sourceMappingURL=ffmpeg-stream-manager.js.map