mes-engine 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +199 -0
- package/README.md +86 -0
- package/dist/index.js +301 -0
- package/dist/index.js.map +1 -0
- package/dist/types/bandwidth.d.ts +8 -0
- package/dist/types/cache/ExternalCache.d.ts +11 -0
- package/dist/types/cache/LRU.d.ts +8 -0
- package/dist/types/cache/cacheStrategy.d.ts +12 -0
- package/dist/types/cache/internalCache.d.ts +13 -0
- package/dist/types/core/VideoEngine.d.ts +6 -0
- package/dist/types/core/events.d.ts +6 -0
- package/dist/types/core/types.d.ts +20 -0
- package/dist/types/engines/FFmpegEngine.d.ts +6 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/processor.d.ts +19 -0
- package/dist/types/storage/FileSystemStorage.d.ts +6 -0
- package/dist/types/storage/StorageProvider.d.ts +5 -0
- package/dist/types/streaming/StreamManager.d.ts +10 -0
- package/docs/README.md +170 -0
- package/docs/engines.md +58 -0
- package/package.json +48 -0
- package/rollup.config.js +24 -0
- package/src/bandwidth.ts +30 -0
- package/src/cache/ExternalCache.ts +49 -0
- package/src/cache/LRU.ts +35 -0
- package/src/cache/cacheStrategy.ts +15 -0
- package/src/cache/internalCache.ts +60 -0
- package/src/core/VideoEngine.ts +16 -0
- package/src/core/events.ts +7 -0
- package/src/core/types.ts +26 -0
- package/src/engines/FFmpegEngine.ts +51 -0
- package/src/engines/GStreamerEngine.ts +45 -0
- package/src/index.ts +13 -0
- package/src/processor.ts +90 -0
- package/src/storage/FileSystemStorage.ts +19 -0
- package/src/storage/StorageProvider.ts +7 -0
- package/src/streaming/StreamManager.ts +26 -0
- package/tests/video-processor.test.ts +247 -0
- package/tsconfig.json +16 -0
- package/tsconfig.test.json +16 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
// engines/FFmpegEngine.ts
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { VideoEngine } from '../core/VideoEngine';
|
|
5
|
+
import { QualityLevel } from '../core/types';
|
|
6
|
+
|
|
7
|
+
export class FFmpegEngine extends VideoEngine {
|
|
8
|
+
async processChunk(
|
|
9
|
+
inputPath: string,
|
|
10
|
+
outputPath: string,
|
|
11
|
+
startTime: number,
|
|
12
|
+
quality: QualityLevel
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const ffmpeg = spawn('ffmpeg', [
|
|
16
|
+
'-i', inputPath,
|
|
17
|
+
'-ss', startTime.toString(),
|
|
18
|
+
'-t', '10',
|
|
19
|
+
'-vf', `scale=-1:${quality.height}`,
|
|
20
|
+
'-c:v', 'libx264',
|
|
21
|
+
'-b:v', quality.bitrate,
|
|
22
|
+
'-c:a', 'aac',
|
|
23
|
+
'-b:a', '128k',
|
|
24
|
+
'-preset', 'fast',
|
|
25
|
+
'-y',
|
|
26
|
+
outputPath
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
ffmpeg.on('close', code => {
|
|
30
|
+
code === 0 ? resolve() : reject(new Error(`FFmpeg error: ${code}`));
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getDuration(inputPath: string): Promise<number> {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const ffprobe = spawn('ffprobe', [
|
|
38
|
+
'-v', 'error',
|
|
39
|
+
'-show_entries', 'format=duration',
|
|
40
|
+
'-of', 'default=noprint_wrappers=1:nokey=1',
|
|
41
|
+
inputPath
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
let output = '';
|
|
45
|
+
ffprobe.stdout.on('data', data => output += data);
|
|
46
|
+
ffprobe.on('close', code => {
|
|
47
|
+
code === 0 ? resolve(parseFloat(output)) : reject(new Error(`FFprobe error: ${code}`));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// engines/GStreamerEngine.ts
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { VideoEngine } from '../core/VideoEngine';
|
|
4
|
+
import { QualityLevel } from '../core/types';
|
|
5
|
+
|
|
6
|
+
export class GStreamerEngine extends VideoEngine {
|
|
7
|
+
async processChunk(
|
|
8
|
+
inputPath: string,
|
|
9
|
+
outputPath: string,
|
|
10
|
+
startTime: number,
|
|
11
|
+
quality: QualityLevel
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const gst = spawn('gst-launch-1.0', [
|
|
15
|
+
'filesrc', `location=${inputPath}`,
|
|
16
|
+
'!', 'decodebin',
|
|
17
|
+
'!', 'videoconvert',
|
|
18
|
+
'!', 'videoscale',
|
|
19
|
+
'!', `video/x-raw,width=-1,height=${quality.height}`,
|
|
20
|
+
'!', 'x264enc', `bitrate=${quality.bitrate}`,
|
|
21
|
+
'!', 'mp4mux', '!', 'filesink', `location=${outputPath}`
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
gst.on('close', code => {
|
|
25
|
+
code === 0 ? resolve() : reject(new Error(`GStreamer error: ${code}`));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async getDuration(inputPath: string): Promise<number> {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const gst = spawn('gst-launch-1.0', [
|
|
33
|
+
'filesrc', `location=${inputPath}`,
|
|
34
|
+
'!', 'decodebin',
|
|
35
|
+
'!', 'identity', '-debug', 'duration'
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
let output = '';
|
|
39
|
+
gst.stdout.on('data', data => output += data);
|
|
40
|
+
gst.on('close', code => {
|
|
41
|
+
code === 0 ? resolve(parseFloat(output)) : reject(new Error(`GStreamer error: ${code}`));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// index.ts
|
|
2
|
+
export * from './core/types';
|
|
3
|
+
export * from './core/events';
|
|
4
|
+
export * from './core/VideoEngine';
|
|
5
|
+
export * from './engines/FFmpegEngine';
|
|
6
|
+
export * from './streaming/StreamManager';
|
|
7
|
+
export * from './engines/GStreamerEngine';
|
|
8
|
+
export * from './storage/StorageProvider';
|
|
9
|
+
export * from './storage/FileSystemStorage';
|
|
10
|
+
export * from './cache/cacheStrategy';
|
|
11
|
+
export * from './cache/internalCache';
|
|
12
|
+
export * from './cache/ExternalCache';
|
|
13
|
+
export * from './processor';
|
package/src/processor.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
|
|
2
|
+
// processor.ts
|
|
3
|
+
import { VideoEngine } from './core/VideoEngine';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
import { StorageProvider } from './storage/StorageProvider';
|
|
6
|
+
import { StreamManager } from './streaming/StreamManager';
|
|
7
|
+
import { VideoConfig, VideoManifest } from './core/types';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { promises as fs } from 'fs';
|
|
10
|
+
import { Readable } from 'stream';
|
|
11
|
+
import { VideoEvent } from './core/events';
|
|
12
|
+
|
|
13
|
+
export class VideoProcessor extends EventEmitter {
|
|
14
|
+
private engine: VideoEngine;
|
|
15
|
+
private streamManager: StreamManager;
|
|
16
|
+
private config: VideoConfig;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
engine: VideoEngine,
|
|
20
|
+
storage: StorageProvider,
|
|
21
|
+
config: VideoConfig
|
|
22
|
+
) {
|
|
23
|
+
super();
|
|
24
|
+
this.engine = engine;
|
|
25
|
+
this.streamManager = new StreamManager(storage);
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async processVideo(inputPath: string): Promise<VideoManifest> {
|
|
30
|
+
const videoId = await this.generateVideoId(inputPath);
|
|
31
|
+
const manifest: VideoManifest = {
|
|
32
|
+
videoId,
|
|
33
|
+
qualities: this.config.defaultQualities,
|
|
34
|
+
chunks: []
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const duration = await this.engine.getDuration(inputPath);
|
|
38
|
+
const chunks = Math.ceil(duration / this.config.chunkSize);
|
|
39
|
+
|
|
40
|
+
for (const quality of this.config.defaultQualities) {
|
|
41
|
+
for (let i = 0; i < chunks; i++) {
|
|
42
|
+
const chunkPath = this.getChunkPath(videoId, quality.height, i);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await this.engine.processChunk(
|
|
46
|
+
inputPath,
|
|
47
|
+
chunkPath,
|
|
48
|
+
i * this.config.chunkSize,
|
|
49
|
+
quality
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
manifest.chunks.push({
|
|
53
|
+
quality: quality.height,
|
|
54
|
+
number: i,
|
|
55
|
+
path: chunkPath
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.emit(VideoEvent.CHUNK_PROCESSED, { quality, chunkNumber: i });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
this.emit(VideoEvent.ERROR, error);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.emit(VideoEvent.QUALITY_PROCESSED, quality);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.emit(VideoEvent.PROCESSING_COMPLETE, manifest);
|
|
69
|
+
return manifest;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async streamChunk(
|
|
73
|
+
videoId: string,
|
|
74
|
+
quality: number,
|
|
75
|
+
chunkNumber: number,
|
|
76
|
+
range?: { start: number; end: number }
|
|
77
|
+
): Promise<Readable> {
|
|
78
|
+
const chunkPath = this.getChunkPath(videoId, quality, chunkNumber);
|
|
79
|
+
return this.streamManager.createStream(chunkPath, range);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private getChunkPath(videoId: string, quality: number, chunkNumber: number): string {
|
|
83
|
+
return join(this.config.cacheDir, videoId, `${quality}p`, `chunk_${chunkNumber}.mp4`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async generateVideoId(inputPath: string): Promise<string> {
|
|
87
|
+
const stats = await fs.stat(inputPath);
|
|
88
|
+
return `${inputPath.split('/').pop()?.split('.')[0]}_${stats.mtimeMs}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
// storage/FileSystemStorage.ts
|
|
4
|
+
import { promises as fs } from 'fs';
|
|
5
|
+
import { StorageProvider } from './StorageProvider';
|
|
6
|
+
|
|
7
|
+
export class FileSystemStorage extends StorageProvider {
|
|
8
|
+
async saveChunk(chunkPath: string, data: Buffer): Promise<void> {
|
|
9
|
+
await fs.writeFile(chunkPath, data);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async getChunk(chunkPath: string): Promise<Buffer> {
|
|
13
|
+
return fs.readFile(chunkPath);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async deleteChunk(chunkPath: string): Promise<void> {
|
|
17
|
+
await fs.unlink(chunkPath);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
// streaming/StreamManager.ts
|
|
3
|
+
import { Readable } from 'stream';
|
|
4
|
+
import { StorageProvider } from '../storage/StorageProvider';
|
|
5
|
+
|
|
6
|
+
export class StreamManager {
|
|
7
|
+
private storage: StorageProvider;
|
|
8
|
+
|
|
9
|
+
constructor(storage: StorageProvider) {
|
|
10
|
+
this.storage = storage;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async createStream(chunkPath: string, range?: { start: number; end: number }): Promise<Readable> {
|
|
14
|
+
const data = await this.storage.getChunk(chunkPath);
|
|
15
|
+
const stream = new Readable();
|
|
16
|
+
|
|
17
|
+
if (range) {
|
|
18
|
+
stream.push(data.slice(range.start, range.end + 1));
|
|
19
|
+
} else {
|
|
20
|
+
stream.push(data);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
stream.push(null);
|
|
24
|
+
return stream;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { VideoProcessor } from '../src/processor';
|
|
3
|
+
import { FFmpegEngine } from '../src/engines/FFmpegEngine';
|
|
4
|
+
import { FileSystemStorage } from '../src/storage/FileSystemStorage';
|
|
5
|
+
import { VideoConfig, QualityLevel } from '../src/core/types';
|
|
6
|
+
import { StorageProvider } from '../src/storage/StorageProvider';
|
|
7
|
+
import { VideoEngine } from '../src/core/VideoEngine';
|
|
8
|
+
import { promises as fs } from 'fs';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { Readable } from 'stream';
|
|
11
|
+
import { VideoEvent } from '../src/core/events';
|
|
12
|
+
|
|
13
|
+
// Mock implementation of a custom engine to test modularity
|
|
14
|
+
class CustomVideoEngine extends VideoEngine {
|
|
15
|
+
async processChunk(
|
|
16
|
+
inputPath: string,
|
|
17
|
+
outputPath: string,
|
|
18
|
+
startTime: number,
|
|
19
|
+
quality: QualityLevel
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
// Create the directory path for the chunk if it doesn't exist
|
|
22
|
+
const chunkDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
|
|
23
|
+
await fs.mkdir(dirname(outputPath), { recursive: true });
|
|
24
|
+
|
|
25
|
+
const chunkData = Buffer.from('test'); // Mock chunk data
|
|
26
|
+
|
|
27
|
+
// Mock implementation
|
|
28
|
+
await fs.writeFile(outputPath, chunkData);
|
|
29
|
+
|
|
30
|
+
// Save to storage
|
|
31
|
+
const storage = new CustomStorageProvider();
|
|
32
|
+
await storage.saveChunk(outputPath, chunkData);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getDuration(inputPath: string): Promise<number> {
|
|
36
|
+
return 30; // Mock duration
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Mock implementation of a custom storage provider
|
|
41
|
+
class CustomStorageProvider extends StorageProvider {
|
|
42
|
+
private static storage = new Map<string, Buffer>();
|
|
43
|
+
|
|
44
|
+
async saveChunk(chunkPath: string, data: Buffer): Promise<void> {
|
|
45
|
+
console.log(`Saving chunk at: ${chunkPath}`);
|
|
46
|
+
CustomStorageProvider.storage.set(chunkPath, data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async getChunk(chunkPath: string): Promise<Buffer> {
|
|
50
|
+
console.log(`Fetching chunk at: ${chunkPath}`);
|
|
51
|
+
const data = CustomStorageProvider.storage.get(chunkPath);
|
|
52
|
+
if (!data) throw new Error('Chunk not found');
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async deleteChunk(chunkPath: string): Promise<void> {
|
|
57
|
+
CustomStorageProvider.storage.delete(chunkPath);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Add a public method to clear the storage
|
|
61
|
+
public clearStorage(): void {
|
|
62
|
+
CustomStorageProvider.storage.clear();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const globalStorage = new CustomStorageProvider();
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
// Optional: Reset storage if needed
|
|
70
|
+
globalStorage.clearStorage();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('Video Processing Framework', () => {
|
|
74
|
+
const testConfig: VideoConfig = {
|
|
75
|
+
chunkSize: 10,
|
|
76
|
+
cacheDir: './test-cache',
|
|
77
|
+
maxCacheSize: 1024 * 1024 * 100, // 100MB
|
|
78
|
+
defaultQualities: [
|
|
79
|
+
{ height: 720, bitrate: '2000k' },
|
|
80
|
+
{ height: 480, bitrate: '1000k' }
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
beforeEach(async () => {
|
|
85
|
+
// Create test cache directory
|
|
86
|
+
await fs.mkdir(testConfig.cacheDir, { recursive: true });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(async () => {
|
|
90
|
+
// Cleanup test cache directory
|
|
91
|
+
await fs.rm(testConfig.cacheDir, { recursive: true, force: true });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('Modular Engine System', () => {
|
|
95
|
+
it('should work with FFmpegEngine', async () => {
|
|
96
|
+
const engine = new FFmpegEngine();
|
|
97
|
+
const storage = new FileSystemStorage();
|
|
98
|
+
const processor = new VideoProcessor(engine, storage, testConfig);
|
|
99
|
+
expect(processor).to.be.instanceOf(VideoProcessor);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should work with CustomVideoEngine', async () => {
|
|
103
|
+
const engine = new CustomVideoEngine();
|
|
104
|
+
const storage = new FileSystemStorage();
|
|
105
|
+
const processor = new VideoProcessor(engine, storage, testConfig);
|
|
106
|
+
expect(processor).to.be.instanceOf(VideoProcessor);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('Abstract Storage Providers', () => {
|
|
111
|
+
it('should work with FileSystemStorage', async () => {
|
|
112
|
+
const engine = new CustomVideoEngine();
|
|
113
|
+
const storage = new FileSystemStorage();
|
|
114
|
+
const processor = new VideoProcessor(engine, storage, testConfig);
|
|
115
|
+
expect(processor).to.be.instanceOf(VideoProcessor);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should work with CustomStorageProvider', async () => {
|
|
119
|
+
const engine = new CustomVideoEngine();
|
|
120
|
+
const storage = new CustomStorageProvider();
|
|
121
|
+
const processor = new VideoProcessor(engine, storage, testConfig);
|
|
122
|
+
expect(processor).to.be.instanceOf(VideoProcessor);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('Stream Management', () => {
|
|
127
|
+
it('should create readable stream for chunk', async () => {
|
|
128
|
+
const engine = new CustomVideoEngine();
|
|
129
|
+
const storage = new CustomStorageProvider();
|
|
130
|
+
const processor = new VideoProcessor(engine, storage, testConfig);
|
|
131
|
+
|
|
132
|
+
// Mock a video file for testing
|
|
133
|
+
const videoId = 'test-video_12345';
|
|
134
|
+
const quality = 720;
|
|
135
|
+
const chunkNumber = 0;
|
|
136
|
+
const chunkData = Buffer.from('test');
|
|
137
|
+
|
|
138
|
+
// Simulate chunk processing and saving
|
|
139
|
+
const chunkPath = join(
|
|
140
|
+
testConfig.cacheDir,
|
|
141
|
+
videoId,
|
|
142
|
+
`${quality}p`,
|
|
143
|
+
`chunk_${chunkNumber}.mp4`
|
|
144
|
+
);
|
|
145
|
+
await storage.saveChunk(chunkPath, chunkData);
|
|
146
|
+
|
|
147
|
+
// Process a test video first
|
|
148
|
+
const manifest = await processor.processVideo('test-video.mp4');
|
|
149
|
+
console.log('Manifest:', manifest); // Debugging
|
|
150
|
+
|
|
151
|
+
expect(manifest.chunks).to.have.length.greaterThan(0); // Ensure chunks exist
|
|
152
|
+
|
|
153
|
+
// Test streaming a chunk
|
|
154
|
+
const stream = await processor.streamChunk(
|
|
155
|
+
manifest.videoId,
|
|
156
|
+
720,
|
|
157
|
+
0
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Verify the stream is a readable stream
|
|
161
|
+
expect(stream).to.be.instanceOf(Readable);
|
|
162
|
+
|
|
163
|
+
// Read from the stream to validate its content
|
|
164
|
+
const data = await new Promise<Buffer>((resolve, reject) => {
|
|
165
|
+
const chunks: Buffer[] = [];
|
|
166
|
+
stream.on('data', (chunk) => chunks.push(chunk));
|
|
167
|
+
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
|
168
|
+
stream.on('error', reject);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(data.toString()).to.equal(chunkData.toString());
|
|
172
|
+
|
|
173
|
+
expect(stream).to.be.instanceOf(Readable);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should support range requests', async () => {
|
|
177
|
+
const engine = new CustomVideoEngine();
|
|
178
|
+
const storage = new CustomStorageProvider();
|
|
179
|
+
const processor = new VideoProcessor(engine, storage, testConfig);
|
|
180
|
+
|
|
181
|
+
const manifest = await processor.processVideo('test-video.mp4');
|
|
182
|
+
const stream = await processor.streamChunk(
|
|
183
|
+
manifest.videoId,
|
|
184
|
+
720,
|
|
185
|
+
0,
|
|
186
|
+
{ start: 0, end: 100 }
|
|
187
|
+
);
|
|
188
|
+
expect(stream).to.be.instanceOf(Readable);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('Event System', () => {
|
|
193
|
+
it('should emit events during processing', async () => {
|
|
194
|
+
const engine = new CustomVideoEngine();
|
|
195
|
+
const storage = new CustomStorageProvider();
|
|
196
|
+
const processor = new VideoProcessor(engine, storage, testConfig);
|
|
197
|
+
|
|
198
|
+
const events: string[] = [];
|
|
199
|
+
|
|
200
|
+
processor.on(VideoEvent.CHUNK_PROCESSED, () => events.push('chunk'));
|
|
201
|
+
processor.on(VideoEvent.QUALITY_PROCESSED, () => events.push('quality'));
|
|
202
|
+
processor.on(VideoEvent.PROCESSING_COMPLETE, () => events.push('complete'));
|
|
203
|
+
|
|
204
|
+
await processor.processVideo('test-video.mp4');
|
|
205
|
+
|
|
206
|
+
expect(events).to.include('chunk');
|
|
207
|
+
expect(events).to.include('quality');
|
|
208
|
+
expect(events).to.include('complete');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('Configurable Quality Levels', () => {
|
|
213
|
+
it('should process video in configured quality levels', async () => {
|
|
214
|
+
const engine = new CustomVideoEngine();
|
|
215
|
+
const storage = new CustomStorageProvider();
|
|
216
|
+
const processor = new VideoProcessor(engine, storage, testConfig);
|
|
217
|
+
|
|
218
|
+
const manifest = await processor.processVideo('test-video.mp4');
|
|
219
|
+
|
|
220
|
+
expect(manifest.qualities).to.deep.equal(testConfig.defaultQualities);
|
|
221
|
+
expect(manifest.chunks).to.have.length.greaterThan(0);
|
|
222
|
+
|
|
223
|
+
// Verify chunks exist for each quality
|
|
224
|
+
const qualities = new Set(manifest.chunks.map(chunk => chunk.quality));
|
|
225
|
+
expect(qualities.size).to.equal(testConfig.defaultQualities.length);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('TypeScript Interfaces', () => {
|
|
230
|
+
it('should validate type safety of configuration', () => {
|
|
231
|
+
const config: VideoConfig = {
|
|
232
|
+
chunkSize: 10,
|
|
233
|
+
cacheDir: './test-cache',
|
|
234
|
+
maxCacheSize: 1024 * 1024 * 100,
|
|
235
|
+
defaultQualities: [
|
|
236
|
+
{ height: 720, bitrate: '2000k' }
|
|
237
|
+
]
|
|
238
|
+
};
|
|
239
|
+
expect(config).to.have.all.keys([
|
|
240
|
+
'chunkSize',
|
|
241
|
+
'cacheDir',
|
|
242
|
+
'maxCacheSize',
|
|
243
|
+
'defaultQualities'
|
|
244
|
+
]);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"types": ["node", "mocha", "chai"],
|
|
12
|
+
"lib": ["dom", "es2015"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*", "tests/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"types": ["node", "mocha", "chai"],
|
|
12
|
+
"lib": ["dom", "es2015"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*", "tests/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|