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.
246 lines • 10.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.FFmpegCommandBuilder = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
class FFmpegCommandBuilder {
static buildCommand(config) {
const args = [];
// Global options
args.push('-y'); // Overwrite output files
args.push('-nostdin'); // Disable interaction
args.push('-hide_banner'); // Hide banner
args.push('-loglevel', 'info');
// Handle multiple input files
const inputPaths = Array.isArray(config.inputPath) ? config.inputPath : [config.inputPath];
// Input configuration
if (config.inputType === 'video') {
if (inputPaths.length === 1) {
// Single video file
if (config.loop) {
args.push('-stream_loop', '-1');
}
args.push('-re'); // Read input at native frame rate
args.push('-i', inputPaths[0]);
}
else {
// Multiple video files - use concat demuxer
const concatFile = this.createConcatFile(inputPaths);
args.push('-f', 'concat');
args.push('-safe', '0');
if (config.loop) {
args.push('-stream_loop', '-1');
}
args.push('-re');
args.push('-i', concatFile);
}
}
else {
// Audio file(s) with static image
if (!config.staticImagePath) {
throw new Error('Static image path is required for audio input type');
}
// Loop the image
args.push('-loop', '1');
args.push('-i', config.staticImagePath);
if (inputPaths.length === 1) {
// Single audio file
if (config.loop) {
args.push('-stream_loop', '-1');
}
args.push('-re');
args.push('-i', inputPaths[0]);
}
else {
// Multiple audio files - use concat demuxer
const concatFile = this.createConcatFile(inputPaths);
args.push('-f', 'concat');
args.push('-safe', '0');
if (config.loop) {
args.push('-stream_loop', '-1');
}
args.push('-re');
args.push('-i', concatFile);
}
}
// Video encoding settings
args.push('-c:v', config.videoConfig.codec);
args.push('-preset', config.presetConfig.preset);
if (config.presetConfig.tune) {
args.push('-tune', config.presetConfig.tune);
}
// Video quality settings
args.push('-b:v', config.videoConfig.bitrate);
args.push('-maxrate', config.presetConfig.maxrate || config.videoConfig.bitrate);
args.push('-bufsize', config.presetConfig.bufsize || this.calculateBufsize(config.videoConfig.bitrate));
// Video dimensions and framerate
args.push('-s', `${config.videoConfig.width}x${config.videoConfig.height}`);
args.push('-r', config.videoConfig.framerate.toString());
// Video profile and level
if (config.videoConfig.profile) {
args.push('-profile:v', config.videoConfig.profile);
}
if (config.videoConfig.level) {
args.push('-level:v', config.videoConfig.level);
}
// Audio encoding settings
args.push('-c:a', config.audioConfig.codec);
args.push('-b:a', config.audioConfig.bitrate);
args.push('-ar', config.audioConfig.sampleRate.toString());
args.push('-ac', config.audioConfig.channels.toString());
// Pixel format for RTMP compatibility
args.push('-pix_fmt', 'yuv420p');
// Streaming specific settings
args.push('-g', (config.videoConfig.framerate * 2).toString()); // Keyframe interval (2 seconds)
args.push('-keyint_min', config.videoConfig.framerate.toString());
args.push('-sc_threshold', '0');
// RTMP specific settings
args.push('-f', 'flv');
// Output URL
args.push(config.rtmpUrl);
return args;
}
static calculateBufsize(bitrate) {
// Extract numeric value from bitrate (e.g., "2500k" -> 2500)
const bitrateValue = parseInt(bitrate.replace(/[^\d]/g, ''));
const unit = bitrate.replace(/[\d]/g, '').toLowerCase();
let bitrateInKbps = bitrateValue;
if (unit === 'm') {
bitrateInKbps = bitrateValue * 1000;
}
// Buffer size should be 2x the bitrate for stable streaming
return `${bitrateInKbps * 2}k`;
}
static buildYouTubeRTMPUrl(streamKey, server) {
const rtmpServer = server || 'rtmp://a.rtmp.youtube.com/live2/';
return `${rtmpServer}${streamKey}`;
}
static validateConfig(config) {
if (!config.id) {
throw new Error('Stream ID is required');
}
if (!config.rtmpUrl) {
throw new Error('RTMP URL is required');
}
if (!config.inputPath || (Array.isArray(config.inputPath) && config.inputPath.length === 0)) {
throw new Error('Input path is required');
}
if (config.inputType === 'audio' && !config.staticImagePath) {
throw new Error('Static image path is required for audio input type');
}
// Validate all input files exist
const inputPaths = Array.isArray(config.inputPath) ? config.inputPath : [config.inputPath];
for (const inputPath of inputPaths) {
if (!fs.existsSync(inputPath)) {
throw new Error(`Input file does not exist: ${inputPath}`);
}
}
// Validate video config
if (config.videoConfig.width <= 0 || config.videoConfig.height <= 0) {
throw new Error('Invalid video dimensions');
}
if (config.videoConfig.framerate <= 0) {
throw new Error('Invalid framerate');
}
// Validate audio config
if (config.audioConfig.sampleRate <= 0) {
throw new Error('Invalid sample rate');
}
if (config.audioConfig.channels <= 0) {
throw new Error('Invalid channel count');
}
}
/**
* Creates a temporary concat file for FFmpeg to handle multiple input files
*/
static createConcatFile(inputPaths) {
const tempDir = os.tmpdir();
const concatFileName = `ffmpeg_concat_${Date.now()}_${Math.random().toString(36).substring(7)}.txt`;
const concatFilePath = path.join(tempDir, concatFileName);
let concatContent = '';
// Add each file to the concat list
for (const inputPath of inputPaths) {
// Escape single quotes and handle paths with spaces
const escapedPath = inputPath.replace(/'/g, "'\"'\"'");
concatContent += `file '${escapedPath}'\n`;
}
// Write the concat file
fs.writeFileSync(concatFilePath, concatContent, 'utf8');
return concatFilePath;
}
/**
* Cleans up temporary concat files
*/
static cleanupConcatFile(concatFilePath) {
try {
if (fs.existsSync(concatFilePath) && concatFilePath.includes('ffmpeg_concat_')) {
fs.unlinkSync(concatFilePath);
}
}
catch (error) {
// Ignore cleanup errors
console.warn(`Failed to cleanup concat file: ${concatFilePath}`, error);
}
}
/**
* Validates that all files in the array have compatible formats
*/
static validateFileCompatibility(inputPaths, inputType) {
const videoExtensions = ['.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.ts', '.m4v'];
const audioExtensions = ['.mp3', '.wav', '.aac', '.flac', '.ogg', '.m4a', '.wma'];
const allowedExtensions = inputType === 'video' ? videoExtensions : audioExtensions;
for (const inputPath of inputPaths) {
const ext = path.extname(inputPath).toLowerCase();
if (!allowedExtensions.includes(ext)) {
throw new Error(`Unsupported file format for ${inputType}: ${ext}. Supported formats: ${allowedExtensions.join(', ')}`);
}
}
}
/**
* Shuffles array for random playlist mode
*/
static shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
}
exports.FFmpegCommandBuilder = FFmpegCommandBuilder;
//# sourceMappingURL=ffmpeg-command-builder.js.map