mes-engine
Version:
A powerful and flexible video processing framework for Node.js with support for multiple processing engines, adaptive streaming, and intelligent caching.
247 lines (206 loc) • 9.44 kB
text/typescript
import { expect } from 'chai';
import { VideoProcessor } from '../src/processor';
import { FFmpegEngine } from '../src/engines/FFmpegEngine';
import { FileSystemStorage } from '../src/storage/FileSystemStorage';
import { VideoConfig, QualityLevel } from '../src/core/types';
import { StorageProvider } from '../src/storage/StorageProvider';
import { VideoEngine } from '../src/core/VideoEngine';
import { promises as fs } from 'fs';
import { join, dirname } from 'path';
import { Readable } from 'stream';
import { VideoEvent } from '../src/core/events';
// Mock implementation of a custom engine to test modularity
class CustomVideoEngine extends VideoEngine {
async processChunk(
inputPath: string,
outputPath: string,
startTime: number,
quality: QualityLevel
): Promise<void> {
// Create the directory path for the chunk if it doesn't exist
const chunkDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
await fs.mkdir(dirname(outputPath), { recursive: true });
const chunkData = Buffer.from('test'); // Mock chunk data
// Mock implementation
await fs.writeFile(outputPath, chunkData);
// Save to storage
const storage = new CustomStorageProvider();
await storage.saveChunk(outputPath, chunkData);
}
async getDuration(inputPath: string): Promise<number> {
return 30; // Mock duration
}
}
// Mock implementation of a custom storage provider
class CustomStorageProvider extends StorageProvider {
private static storage = new Map<string, Buffer>();
async saveChunk(chunkPath: string, data: Buffer): Promise<void> {
console.log(`Saving chunk at: ${chunkPath}`);
CustomStorageProvider.storage.set(chunkPath, data);
}
async getChunk(chunkPath: string): Promise<Buffer> {
console.log(`Fetching chunk at: ${chunkPath}`);
const data = CustomStorageProvider.storage.get(chunkPath);
if (!data) throw new Error('Chunk not found');
return data;
}
async deleteChunk(chunkPath: string): Promise<void> {
CustomStorageProvider.storage.delete(chunkPath);
}
// Add a public method to clear the storage
public clearStorage(): void {
CustomStorageProvider.storage.clear();
}
}
const globalStorage = new CustomStorageProvider();
beforeEach(() => {
// Optional: Reset storage if needed
globalStorage.clearStorage();
});
describe('Video Processing Framework', () => {
const testConfig: VideoConfig = {
chunkSize: 10,
cacheDir: './test-cache',
maxCacheSize: 1024 * 1024 * 100, // 100MB
defaultQualities: [
{ height: 720, bitrate: '2000k' },
{ height: 480, bitrate: '1000k' }
]
};
beforeEach(async () => {
// Create test cache directory
await fs.mkdir(testConfig.cacheDir, { recursive: true });
});
afterEach(async () => {
// Cleanup test cache directory
await fs.rm(testConfig.cacheDir, { recursive: true, force: true });
});
describe('Modular Engine System', () => {
it('should work with FFmpegEngine', async () => {
const engine = new FFmpegEngine();
const storage = new FileSystemStorage();
const processor = new VideoProcessor(engine, storage, testConfig);
expect(processor).to.be.instanceOf(VideoProcessor);
});
it('should work with CustomVideoEngine', async () => {
const engine = new CustomVideoEngine();
const storage = new FileSystemStorage();
const processor = new VideoProcessor(engine, storage, testConfig);
expect(processor).to.be.instanceOf(VideoProcessor);
});
});
describe('Abstract Storage Providers', () => {
it('should work with FileSystemStorage', async () => {
const engine = new CustomVideoEngine();
const storage = new FileSystemStorage();
const processor = new VideoProcessor(engine, storage, testConfig);
expect(processor).to.be.instanceOf(VideoProcessor);
});
it('should work with CustomStorageProvider', async () => {
const engine = new CustomVideoEngine();
const storage = new CustomStorageProvider();
const processor = new VideoProcessor(engine, storage, testConfig);
expect(processor).to.be.instanceOf(VideoProcessor);
});
});
describe('Stream Management', () => {
it('should create readable stream for chunk', async () => {
const engine = new CustomVideoEngine();
const storage = new CustomStorageProvider();
const processor = new VideoProcessor(engine, storage, testConfig);
// Mock a video file for testing
const videoId = 'test-video_12345';
const quality = 720;
const chunkNumber = 0;
const chunkData = Buffer.from('test');
// Simulate chunk processing and saving
const chunkPath = join(
testConfig.cacheDir,
videoId,
`${quality}p`,
`chunk_${chunkNumber}.mp4`
);
await storage.saveChunk(chunkPath, chunkData);
// Process a test video first
const manifest = await processor.processVideo('test-video.mp4');
console.log('Manifest:', manifest); // Debugging
expect(manifest.chunks).to.have.length.greaterThan(0); // Ensure chunks exist
// Test streaming a chunk
const stream = await processor.streamChunk(
manifest.videoId,
720,
0
);
// Verify the stream is a readable stream
expect(stream).to.be.instanceOf(Readable);
// Read from the stream to validate its content
const data = await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
stream.on('data', (chunk) => chunks.push(chunk));
stream.on('end', () => resolve(Buffer.concat(chunks)));
stream.on('error', reject);
});
expect(data.toString()).to.equal(chunkData.toString());
expect(stream).to.be.instanceOf(Readable);
});
it('should support range requests', async () => {
const engine = new CustomVideoEngine();
const storage = new CustomStorageProvider();
const processor = new VideoProcessor(engine, storage, testConfig);
const manifest = await processor.processVideo('test-video.mp4');
const stream = await processor.streamChunk(
manifest.videoId,
720,
0,
{ start: 0, end: 100 }
);
expect(stream).to.be.instanceOf(Readable);
});
});
describe('Event System', () => {
it('should emit events during processing', async () => {
const engine = new CustomVideoEngine();
const storage = new CustomStorageProvider();
const processor = new VideoProcessor(engine, storage, testConfig);
const events: string[] = [];
processor.on(VideoEvent.CHUNK_PROCESSED, () => events.push('chunk'));
processor.on(VideoEvent.QUALITY_PROCESSED, () => events.push('quality'));
processor.on(VideoEvent.PROCESSING_COMPLETE, () => events.push('complete'));
await processor.processVideo('test-video.mp4');
expect(events).to.include('chunk');
expect(events).to.include('quality');
expect(events).to.include('complete');
});
});
describe('Configurable Quality Levels', () => {
it('should process video in configured quality levels', async () => {
const engine = new CustomVideoEngine();
const storage = new CustomStorageProvider();
const processor = new VideoProcessor(engine, storage, testConfig);
const manifest = await processor.processVideo('test-video.mp4');
expect(manifest.qualities).to.deep.equal(testConfig.defaultQualities);
expect(manifest.chunks).to.have.length.greaterThan(0);
// Verify chunks exist for each quality
const qualities = new Set(manifest.chunks.map(chunk => chunk.quality));
expect(qualities.size).to.equal(testConfig.defaultQualities.length);
});
});
describe('TypeScript Interfaces', () => {
it('should validate type safety of configuration', () => {
const config: VideoConfig = {
chunkSize: 10,
cacheDir: './test-cache',
maxCacheSize: 1024 * 1024 * 100,
defaultQualities: [
{ height: 720, bitrate: '2000k' }
]
};
expect(config).to.have.all.keys([
'chunkSize',
'cacheDir',
'maxCacheSize',
'defaultQualities'
]);
});
});
});