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.

246 lines • 10.3 kB
"use strict"; 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