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.
Files changed (40) hide show
  1. package/CONTRIBUTING.md +199 -0
  2. package/README.md +86 -0
  3. package/dist/index.js +301 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/types/bandwidth.d.ts +8 -0
  6. package/dist/types/cache/ExternalCache.d.ts +11 -0
  7. package/dist/types/cache/LRU.d.ts +8 -0
  8. package/dist/types/cache/cacheStrategy.d.ts +12 -0
  9. package/dist/types/cache/internalCache.d.ts +13 -0
  10. package/dist/types/core/VideoEngine.d.ts +6 -0
  11. package/dist/types/core/events.d.ts +6 -0
  12. package/dist/types/core/types.d.ts +20 -0
  13. package/dist/types/engines/FFmpegEngine.d.ts +6 -0
  14. package/dist/types/index.d.ts +10 -0
  15. package/dist/types/processor.d.ts +19 -0
  16. package/dist/types/storage/FileSystemStorage.d.ts +6 -0
  17. package/dist/types/storage/StorageProvider.d.ts +5 -0
  18. package/dist/types/streaming/StreamManager.d.ts +10 -0
  19. package/docs/README.md +170 -0
  20. package/docs/engines.md +58 -0
  21. package/package.json +48 -0
  22. package/rollup.config.js +24 -0
  23. package/src/bandwidth.ts +30 -0
  24. package/src/cache/ExternalCache.ts +49 -0
  25. package/src/cache/LRU.ts +35 -0
  26. package/src/cache/cacheStrategy.ts +15 -0
  27. package/src/cache/internalCache.ts +60 -0
  28. package/src/core/VideoEngine.ts +16 -0
  29. package/src/core/events.ts +7 -0
  30. package/src/core/types.ts +26 -0
  31. package/src/engines/FFmpegEngine.ts +51 -0
  32. package/src/engines/GStreamerEngine.ts +45 -0
  33. package/src/index.ts +13 -0
  34. package/src/processor.ts +90 -0
  35. package/src/storage/FileSystemStorage.ts +19 -0
  36. package/src/storage/StorageProvider.ts +7 -0
  37. package/src/streaming/StreamManager.ts +26 -0
  38. package/tests/video-processor.test.ts +247 -0
  39. package/tsconfig.json +16 -0
  40. 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';
@@ -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,7 @@
1
+
2
+ // storage/StorageProvider.ts
3
+ export abstract class StorageProvider {
4
+ abstract saveChunk(chunkPath: string, data: Buffer): Promise<void>;
5
+ abstract getChunk(chunkPath: string): Promise<Buffer>;
6
+ abstract deleteChunk(chunkPath: string): Promise<void>;
7
+ }
@@ -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
+ }