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.

174 lines (151 loc) 7.06 kB
// src/processor.ts import { VideoEngine } from './core/VideoEngine'; import { EventEmitter } from 'events'; import { StorageProvider } from './storage/StorageProvider'; import { StreamManager } from './streaming/StreamManager'; import { VideoConfig, VideoManifest, VideoChunk, ProcessingOptions } from './core/types'; import { join, dirname } from 'path'; import { promises as fs } from 'fs'; import { Readable } from 'stream'; import { VideoEvent } from './core/events'; /** * Main orchestrator for video processing. * Handles chunking, transcoding (via engines), and manifest generation. */ export class VideoProcessor extends EventEmitter { private engine: VideoEngine; private streamManager: StreamManager; private config: VideoConfig; constructor( engine: VideoEngine, storage: StorageProvider, config: VideoConfig ) { super(); this.engine = engine; this.streamManager = new StreamManager(storage); this.config = config; } /** * Processes an input video file into multiple quality levels and chunks. * Generates HLS playlists and a JSON manifest. * * @param inputPath - Absolute path to the source video file * @param options - Optional metadata and processing instructions * @returns Promise resolving to the generated VideoManifest * @throws Error if processing fails at any stage */ async processVideo( inputPath: string, options?: ProcessingOptions ): Promise<VideoManifest> { const videoId = await this.generateVideoId(inputPath); const manifest: VideoManifest = { videoId, qualities: this.config.defaultQualities, chunks: [], metadata: { title: options?.title, description: options?.overallDescription, createdAt: new Date().toISOString() } }; // Create base directory structure const baseDir = join(this.config.cacheDir, videoId); const screenshotDir = join(baseDir, 'screenshots'); await fs.mkdir(baseDir, { recursive: true }); await fs.mkdir(screenshotDir, { recursive: true }); const duration = await this.engine.getDuration(inputPath); const chunks = Math.ceil(duration / this.config.chunkSize); for (const quality of this.config.defaultQualities) { // Create quality-specific directory const qualityDir = join(baseDir, `${quality.height}p`); await fs.mkdir(qualityDir, { recursive: true }); let m3u8Content = '#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:' + this.config.chunkSize + '\n#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-PLAYLIST-TYPE:VOD\n'; for (let i = 0; i < chunks; i++) { const chunkPath = this.getChunkPath(videoId, quality.height, i); const screenshotPath = this.getScreenshotPath(videoId, i); try { // Process chunk await this.engine.processChunk( inputPath, chunkPath, i * this.config.chunkSize, quality ); // Extract screenshot (only once per chunk number, e.g., for the first quality) if (quality.height === this.config.defaultQualities[0].height) { await this.engine.extractScreenshot( inputPath, screenshotPath, i * this.config.chunkSize + 1 // 1 second into the chunk ); } const chunk: VideoChunk = { quality: quality.height, number: i, path: chunkPath, screenshotPath: screenshotPath, description: options?.descriptions?.[i] }; manifest.chunks.push(chunk); // Add to M3U8 m3u8Content += `#EXTINF:${this.config.chunkSize}.0,\nchunk_${i}.mp4\n`; this.emit(VideoEvent.CHUNK_PROCESSED, { quality, chunkNumber: i }); } catch (error) { this.emit(VideoEvent.ERROR, error); throw error; } } m3u8Content += '#EXT-X-ENDLIST'; // Save M3U8 for this quality const m3u8Path = join(qualityDir, 'playlist.m3u8'); await fs.writeFile(m3u8Path, m3u8Content); this.emit(VideoEvent.QUALITY_PROCESSED, quality); } // Generate Master M3U8 if (this.config.defaultQualities.length > 1) { let masterM3u8 = '#EXTM3U\n'; for (const quality of this.config.defaultQualities) { masterM3u8 += `#EXT-X-STREAM-INF:BANDWIDTH=${quality.bitrate.replace('k', '000')},RESOLUTION=-1x${quality.height}\n${quality.height}p/playlist.m3u8\n`; } const masterPath = join(baseDir, 'master.m3u8'); await fs.writeFile(masterPath, masterM3u8); } // Save JSON Manifest const manifestPath = join(baseDir, 'manifest.json'); await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2)); this.emit(VideoEvent.PROCESSING_COMPLETE, manifest); return manifest; } /** * Creates a readable stream for a specific video chunk. * Supported for on-demand delivery of processed segments. * * @param videoId - ID of the processed video * @param quality - Target quality (height) * @param chunkNumber - Sequential index of the chunk * @param range - Optional byte range for partial content * @returns Promise resolving to a Readable stream */ async streamChunk( videoId: string, quality: number, chunkNumber: number, range?: { start: number; end: number } ): Promise<Readable> { const chunkPath = this.getChunkPath(videoId, quality, chunkNumber); return this.streamManager.createStream(chunkPath, range); } private getChunkPath(videoId: string, quality: number, chunkNumber: number): string { return join(this.config.cacheDir, videoId, `${quality}p`, `chunk_${chunkNumber}.mp4`); } private getScreenshotPath(videoId: string, chunkNumber: number): string { return join(this.config.cacheDir, videoId, 'screenshots', `chunk_${chunkNumber}.jpg`); } private async generateVideoId(inputPath: string): Promise<string> { const stats = await fs.stat(inputPath); const fileName = inputPath.split(/[\\/]/).pop()?.split('.')[0]; return `${fileName}_${Math.floor(stats.mtimeMs)}`; } }