UNPKG

mes-engine

Version:

A powerful and flexible video processing framework for Node.js with support for multiple processing engines, adaptive streaming, and intelligent caching.

303 lines (289 loc) 9.87 kB
import { EventEmitter } from 'events'; import { spawn } from 'child_process'; import { Readable } from 'stream'; import { promises } from 'fs'; import fetch from 'node-fetch'; import { join } from 'path'; var VideoEvent; (function (VideoEvent) { VideoEvent["CHUNK_PROCESSED"] = "chunkProcessed"; VideoEvent["QUALITY_PROCESSED"] = "qualityProcessed"; VideoEvent["PROCESSING_COMPLETE"] = "processingComplete"; VideoEvent["ERROR"] = "error"; })(VideoEvent || (VideoEvent = {})); // core/VideoEngine.ts class VideoEngine extends EventEmitter { } // engines/FFmpegEngine.ts class FFmpegEngine extends VideoEngine { async processChunk(inputPath, outputPath, startTime, quality) { return new Promise((resolve, reject) => { const ffmpeg = spawn('ffmpeg', [ '-i', inputPath, '-ss', startTime.toString(), '-t', '10', '-vf', `scale=-1:${quality.height}`, '-c:v', 'libx264', '-b:v', quality.bitrate, '-c:a', 'aac', '-b:a', '128k', '-preset', 'fast', '-y', outputPath ]); ffmpeg.on('close', code => { code === 0 ? resolve() : reject(new Error(`FFmpeg error: ${code}`)); }); }); } async getDuration(inputPath) { return new Promise((resolve, reject) => { const ffprobe = spawn('ffprobe', [ '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', inputPath ]); let output = ''; ffprobe.stdout.on('data', data => output += data); ffprobe.on('close', code => { code === 0 ? resolve(parseFloat(output)) : reject(new Error(`FFprobe error: ${code}`)); }); }); } } // streaming/StreamManager.ts class StreamManager { constructor(storage) { this.storage = storage; } async createStream(chunkPath, range) { const data = await this.storage.getChunk(chunkPath); const stream = new Readable(); if (range) { stream.push(data.slice(range.start, range.end + 1)); } else { stream.push(data); } stream.push(null); return stream; } } // engines/GStreamerEngine.ts class GStreamerEngine extends VideoEngine { async processChunk(inputPath, outputPath, startTime, quality) { return new Promise((resolve, reject) => { const gst = spawn('gst-launch-1.0', [ 'filesrc', `location=${inputPath}`, '!', 'decodebin', '!', 'videoconvert', '!', 'videoscale', '!', `video/x-raw,width=-1,height=${quality.height}`, '!', 'x264enc', `bitrate=${quality.bitrate}`, '!', 'mp4mux', '!', 'filesink', `location=${outputPath}` ]); gst.on('close', code => { code === 0 ? resolve() : reject(new Error(`GStreamer error: ${code}`)); }); }); } async getDuration(inputPath) { return new Promise((resolve, reject) => { const gst = spawn('gst-launch-1.0', [ 'filesrc', `location=${inputPath}`, '!', 'decodebin', '!', 'identity', '-debug', 'duration' ]); let output = ''; gst.stdout.on('data', data => output += data); gst.on('close', code => { code === 0 ? resolve(parseFloat(output)) : reject(new Error(`GStreamer error: ${code}`)); }); }); } } // storage/StorageProvider.ts class StorageProvider { } // storage/FileSystemStorage.ts class FileSystemStorage extends StorageProvider { async saveChunk(chunkPath, data) { await promises.writeFile(chunkPath, data); } async getChunk(chunkPath) { return promises.readFile(chunkPath); } async deleteChunk(chunkPath) { await promises.unlink(chunkPath); } } class CacheStrategy { } // cache/LRU.ts class LRU { constructor(maxSize) { this.maxSize = maxSize; this.cache = new Map(); } set(key, value) { if (this.cache.has(key)) { this.cache.delete(key); } else if (this.cache.size >= this.maxSize) { const oldestKey = this.cache.keys().next().value; if (oldestKey !== undefined) { this.cache.delete(oldestKey); } } this.cache.set(key, value); } get(key) { if (!this.cache.has(key)) return undefined; const value = this.cache.get(key); this.cache.delete(key); this.cache.set(key, value); return value; } clear() { this.cache.clear(); } } // cache/internalCache.ts class InternalCache extends CacheStrategy { constructor(options, storage) { super(); this.options = options; this.cache = new LRU(options.maxSize); this.storage = storage; } async set(key, value) { this.cache.set(key, value); } async get(key) { const cached = this.cache.get(key); if (cached) return cached; try { const data = await this.storage.getChunk(key); await this.set(key, data); return data; } catch { return null; } } async preload(key) { if (this.options.preloadNextChunk) { const nextChunkKey = this.getNextChunkKey(key); if (!this.cache.get(nextChunkKey)) { try { const data = await this.storage.getChunk(nextChunkKey); await this.set(nextChunkKey, data); } catch { // Ignore preload failures } } } } async clear() { this.cache.clear(); } getNextChunkKey(currentKey) { const parts = currentKey.split('_'); const currentChunk = parseInt(parts[parts.length - 1]); parts[parts.length - 1] = (currentChunk + 1).toString(); return parts.join('_'); } } // cache/ExternalCache.ts class ExternalCache extends CacheStrategy { constructor(options) { super(); this.options = options; this.baseUrl = options.externalCacheUrl; } async set(key, value) { await fetch(`${this.baseUrl}/cache/${key}`, { method: 'POST', body: value, headers: { 'Content-Type': 'application/octet-stream', 'TTL': this.options.ttl.toString() } }); } async get(key) { const response = await fetch(`${this.baseUrl}/cache/${key}`); return response.ok ? Buffer.from(await response.arrayBuffer()) : null; } async preload(key) { if (this.options.preloadNextChunk) { const nextChunkKey = this.getNextChunkKey(key); await fetch(`${this.baseUrl}/preload/${nextChunkKey}`); } } async clear() { await fetch(`${this.baseUrl}/cache`, { method: 'DELETE' }); } getNextChunkKey(currentKey) { const parts = currentKey.split('_'); const currentChunk = parseInt(parts[parts.length - 1]); parts[parts.length - 1] = (currentChunk + 1).toString(); return parts.join('_'); } } class VideoProcessor extends EventEmitter { constructor(engine, storage, config) { super(); this.engine = engine; this.streamManager = new StreamManager(storage); this.config = config; } async processVideo(inputPath) { const videoId = await this.generateVideoId(inputPath); const manifest = { videoId, qualities: this.config.defaultQualities, chunks: [] }; const duration = await this.engine.getDuration(inputPath); const chunks = Math.ceil(duration / this.config.chunkSize); for (const quality of this.config.defaultQualities) { for (let i = 0; i < chunks; i++) { const chunkPath = this.getChunkPath(videoId, quality.height, i); try { await this.engine.processChunk(inputPath, chunkPath, i * this.config.chunkSize, quality); manifest.chunks.push({ quality: quality.height, number: i, path: chunkPath }); this.emit(VideoEvent.CHUNK_PROCESSED, { quality, chunkNumber: i }); } catch (error) { this.emit(VideoEvent.ERROR, error); throw error; } } this.emit(VideoEvent.QUALITY_PROCESSED, quality); } this.emit(VideoEvent.PROCESSING_COMPLETE, manifest); return manifest; } async streamChunk(videoId, quality, chunkNumber, range) { const chunkPath = this.getChunkPath(videoId, quality, chunkNumber); return this.streamManager.createStream(chunkPath, range); } getChunkPath(videoId, quality, chunkNumber) { return join(this.config.cacheDir, videoId, `${quality}p`, `chunk_${chunkNumber}.mp4`); } async generateVideoId(inputPath) { const stats = await promises.stat(inputPath); return `${inputPath.split('/').pop()?.split('.')[0]}_${stats.mtimeMs}`; } } export { CacheStrategy, ExternalCache, FFmpegEngine, FileSystemStorage, GStreamerEngine, InternalCache, StorageProvider, StreamManager, VideoEngine, VideoEvent, VideoProcessor }; //# sourceMappingURL=index.js.map