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