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
JavaScript
;
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