mes-engine 0.0.3 → 1.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 (66) hide show
  1. package/.mocharc.json +7 -0
  2. package/README.md +93 -85
  3. package/dist/index.js +243 -24
  4. package/dist/index.js.map +1 -1
  5. package/dist/{types → src}/core/VideoEngine.d.ts +2 -1
  6. package/dist/{types → src}/core/types.d.ts +12 -0
  7. package/dist/src/engines/FFmpegEngine.d.ts +7 -0
  8. package/dist/{types/engines/FFmpegEngine.d.ts → src/engines/GStreamerEngine.d.ts} +2 -1
  9. package/dist/src/index.d.ts +12 -0
  10. package/dist/{types → src}/processor.d.ts +5 -5
  11. package/dist/{types → src}/storage/FileSystemStorage.d.ts +1 -1
  12. package/dist/{types → src}/streaming/StreamManager.d.ts +1 -1
  13. package/dist/tests/video-processor.test.d.ts +1 -0
  14. package/docs/API.md +109 -0
  15. package/docs/HLS.md +54 -0
  16. package/docs/README.md +172 -169
  17. package/docs/caching.md +62 -0
  18. package/docs/engines.md +62 -58
  19. package/docs/storage.md +57 -0
  20. package/examples/full-demo/backend/.env +6 -0
  21. package/examples/full-demo/backend/package-lock.json +1783 -0
  22. package/examples/full-demo/backend/package.json +22 -0
  23. package/examples/full-demo/backend/src/routes/video.js +92 -0
  24. package/examples/full-demo/backend/src/server.js +43 -0
  25. package/examples/full-demo/backend/src/services/videoProcessor.js +85 -0
  26. package/examples/full-demo/frontend/index.html +13 -0
  27. package/examples/full-demo/frontend/package-lock.json +5791 -0
  28. package/examples/full-demo/frontend/package.json +32 -0
  29. package/examples/full-demo/frontend/postcss.config.js +6 -0
  30. package/examples/full-demo/frontend/src/App.jsx +113 -0
  31. package/examples/full-demo/frontend/src/components/ProcessingStatus.jsx +71 -0
  32. package/examples/full-demo/frontend/src/components/VideoPlayer.jsx +87 -0
  33. package/examples/full-demo/frontend/src/components/VideoUploader.jsx +62 -0
  34. package/examples/full-demo/frontend/src/index.css +3 -0
  35. package/examples/full-demo/frontend/src/main.jsx +10 -0
  36. package/examples/full-demo/frontend/src/services/api.js +30 -0
  37. package/examples/full-demo/frontend/tailwind.config.js +10 -0
  38. package/examples/full-demo/frontend/vite.config.js +16 -0
  39. package/examples/simple-usage/README.md +31 -0
  40. package/examples/simple-usage/index.ts +68 -0
  41. package/examples/simple-usage/package-lock.json +589 -0
  42. package/examples/simple-usage/package.json +15 -0
  43. package/package.json +64 -48
  44. package/rollup.config.js +3 -1
  45. package/src/bandwidth.ts +1 -1
  46. package/src/core/VideoEngine.ts +29 -4
  47. package/src/core/events.ts +9 -1
  48. package/src/core/types.ts +38 -3
  49. package/src/engines/FFmpegEngine.ts +172 -31
  50. package/src/engines/GStreamerEngine.ts +24 -1
  51. package/src/index.ts +1 -1
  52. package/src/processor.ts +115 -31
  53. package/src/storage/FileSystemStorage.ts +1 -3
  54. package/src/storage/StorageProvider.ts +7 -2
  55. package/src/streaming/StreamManager.ts +3 -4
  56. package/tests/video-processor.test.ts +32 -12
  57. package/tsconfig.json +19 -5
  58. package/tsconfig.test.json +17 -4
  59. package/dist/types/index.d.ts +0 -10
  60. package/dist/{types → src}/bandwidth.d.ts +0 -0
  61. package/dist/{types → src}/cache/ExternalCache.d.ts +0 -0
  62. package/dist/{types → src}/cache/LRU.d.ts +2 -2
  63. /package/dist/{types → src}/cache/cacheStrategy.d.ts +0 -0
  64. /package/dist/{types → src}/cache/internalCache.d.ts +0 -0
  65. /package/dist/{types → src}/core/events.d.ts +0 -0
  66. /package/dist/{types → src}/storage/StorageProvider.d.ts +0 -0
package/src/processor.ts CHANGED
@@ -1,15 +1,18 @@
1
-
2
- // processor.ts
1
+ // src/processor.ts
3
2
  import { VideoEngine } from './core/VideoEngine';
4
3
  import { EventEmitter } from 'events';
5
4
  import { StorageProvider } from './storage/StorageProvider';
6
5
  import { StreamManager } from './streaming/StreamManager';
7
- import { VideoConfig, VideoManifest } from './core/types';
8
- import { join } from 'path';
6
+ import { VideoConfig, VideoManifest, VideoChunk, ProcessingOptions } from './core/types';
7
+ import { join, dirname } from 'path';
9
8
  import { promises as fs } from 'fs';
10
9
  import { Readable } from 'stream';
11
10
  import { VideoEvent } from './core/events';
12
11
 
12
+ /**
13
+ * Main orchestrator for video processing.
14
+ * Handles chunking, transcoding (via engines), and manifest generation.
15
+ */
13
16
  export class VideoProcessor extends EventEmitter {
14
17
  private engine: VideoEngine;
15
18
  private streamManager: StreamManager;
@@ -26,49 +29,125 @@ export class VideoProcessor extends EventEmitter {
26
29
  this.config = config;
27
30
  }
28
31
 
29
- async processVideo(inputPath: string): Promise<VideoManifest> {
32
+ /**
33
+ * Processes an input video file into multiple quality levels and chunks.
34
+ * Generates HLS playlists and a JSON manifest.
35
+ *
36
+ * @param inputPath - Absolute path to the source video file
37
+ * @param options - Optional metadata and processing instructions
38
+ * @returns Promise resolving to the generated VideoManifest
39
+ * @throws Error if processing fails at any stage
40
+ */
41
+ async processVideo(
42
+ inputPath: string,
43
+ options?: ProcessingOptions
44
+ ): Promise<VideoManifest> {
30
45
  const videoId = await this.generateVideoId(inputPath);
31
46
  const manifest: VideoManifest = {
32
- videoId,
33
- qualities: this.config.defaultQualities,
34
- chunks: []
47
+ videoId,
48
+ qualities: this.config.defaultQualities,
49
+ chunks: [],
50
+ metadata: {
51
+ title: options?.title,
52
+ description: options?.overallDescription,
53
+ createdAt: new Date().toISOString()
54
+ }
35
55
  };
36
56
 
57
+ // Create base directory structure
58
+ const baseDir = join(this.config.cacheDir, videoId);
59
+ const screenshotDir = join(baseDir, 'screenshots');
60
+ await fs.mkdir(baseDir, { recursive: true });
61
+ await fs.mkdir(screenshotDir, { recursive: true });
62
+
37
63
  const duration = await this.engine.getDuration(inputPath);
38
64
  const chunks = Math.ceil(duration / this.config.chunkSize);
39
65
 
40
66
  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;
67
+ // Create quality-specific directory
68
+ const qualityDir = join(baseDir, `${quality.height}p`);
69
+ await fs.mkdir(qualityDir, { recursive: true });
70
+
71
+ 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';
72
+
73
+ for (let i = 0; i < chunks; i++) {
74
+ const chunkPath = this.getChunkPath(videoId, quality.height, i);
75
+ const screenshotPath = this.getScreenshotPath(videoId, i);
76
+
77
+ try {
78
+ // Process chunk
79
+ await this.engine.processChunk(
80
+ inputPath,
81
+ chunkPath,
82
+ i * this.config.chunkSize,
83
+ quality
84
+ );
85
+
86
+ // Extract screenshot (only once per chunk number, e.g., for the first quality)
87
+ if (quality.height === this.config.defaultQualities[0].height) {
88
+ await this.engine.extractScreenshot(
89
+ inputPath,
90
+ screenshotPath,
91
+ i * this.config.chunkSize + 1 // 1 second into the chunk
92
+ );
93
+ }
94
+
95
+ const chunk: VideoChunk = {
96
+ quality: quality.height,
97
+ number: i,
98
+ path: chunkPath,
99
+ screenshotPath: screenshotPath,
100
+ description: options?.descriptions?.[i]
101
+ };
102
+
103
+ manifest.chunks.push(chunk);
104
+
105
+ // Add to M3U8
106
+ m3u8Content += `#EXTINF:${this.config.chunkSize}.0,\nchunk_${i}.mp4\n`;
107
+
108
+ this.emit(VideoEvent.CHUNK_PROCESSED, { quality, chunkNumber: i });
109
+ } catch (error) {
110
+ this.emit(VideoEvent.ERROR, error);
111
+ throw error;
112
+ }
62
113
  }
114
+ m3u8Content += '#EXT-X-ENDLIST';
115
+
116
+ // Save M3U8 for this quality
117
+ const m3u8Path = join(qualityDir, 'playlist.m3u8');
118
+ await fs.writeFile(m3u8Path, m3u8Content);
119
+
120
+ this.emit(VideoEvent.QUALITY_PROCESSED, quality);
63
121
  }
64
122
 
65
- this.emit(VideoEvent.QUALITY_PROCESSED, quality);
123
+ // Generate Master M3U8
124
+ if (this.config.defaultQualities.length > 1) {
125
+ let masterM3u8 = '#EXTM3U\n';
126
+ for (const quality of this.config.defaultQualities) {
127
+ masterM3u8 += `#EXT-X-STREAM-INF:BANDWIDTH=${quality.bitrate.replace('k', '000')},RESOLUTION=-1x${quality.height}\n${quality.height}p/playlist.m3u8\n`;
128
+ }
129
+ const masterPath = join(baseDir, 'master.m3u8');
130
+ await fs.writeFile(masterPath, masterM3u8);
66
131
  }
67
132
 
133
+ // Save JSON Manifest
134
+ const manifestPath = join(baseDir, 'manifest.json');
135
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
136
+
68
137
  this.emit(VideoEvent.PROCESSING_COMPLETE, manifest);
69
138
  return manifest;
70
139
  }
71
140
 
141
+ /**
142
+ * Creates a readable stream for a specific video chunk.
143
+ * Supported for on-demand delivery of processed segments.
144
+ *
145
+ * @param videoId - ID of the processed video
146
+ * @param quality - Target quality (height)
147
+ * @param chunkNumber - Sequential index of the chunk
148
+ * @param range - Optional byte range for partial content
149
+ * @returns Promise resolving to a Readable stream
150
+ */
72
151
  async streamChunk(
73
152
  videoId: string,
74
153
  quality: number,
@@ -83,8 +162,13 @@ export class VideoProcessor extends EventEmitter {
83
162
  return join(this.config.cacheDir, videoId, `${quality}p`, `chunk_${chunkNumber}.mp4`);
84
163
  }
85
164
 
165
+ private getScreenshotPath(videoId: string, chunkNumber: number): string {
166
+ return join(this.config.cacheDir, videoId, 'screenshots', `chunk_${chunkNumber}.jpg`);
167
+ }
168
+
86
169
  private async generateVideoId(inputPath: string): Promise<string> {
87
170
  const stats = await fs.stat(inputPath);
88
- return `${inputPath.split('/').pop()?.split('.')[0]}_${stats.mtimeMs}`;
171
+ const fileName = inputPath.split(/[\\/]/).pop()?.split('.')[0];
172
+ return `${fileName}_${Math.floor(stats.mtimeMs)}`;
89
173
  }
90
174
  }
@@ -1,6 +1,4 @@
1
-
2
-
3
- // storage/FileSystemStorage.ts
1
+ // src/storage/FileSystemStorage.ts
4
2
  import { promises as fs } from 'fs';
5
3
  import { StorageProvider } from './StorageProvider';
6
4
 
@@ -1,7 +1,12 @@
1
-
2
- // storage/StorageProvider.ts
1
+ // src/storage/StorageProvider.ts
2
+ /**
3
+ * Interface for storage backends (e.g., Local File System, S3, Cloud Storage).
4
+ */
3
5
  export abstract class StorageProvider {
6
+ /** Saves a video chunk to storage */
4
7
  abstract saveChunk(chunkPath: string, data: Buffer): Promise<void>;
8
+ /** Retrieves a video chunk from storage */
5
9
  abstract getChunk(chunkPath: string): Promise<Buffer>;
10
+ /** Deletes a video chunk from storage */
6
11
  abstract deleteChunk(chunkPath: string): Promise<void>;
7
12
  }
@@ -1,5 +1,4 @@
1
-
2
- // streaming/StreamManager.ts
1
+ // src/streaming/StreamManager.ts
3
2
  import { Readable } from 'stream';
4
3
  import { StorageProvider } from '../storage/StorageProvider';
5
4
 
@@ -15,9 +14,9 @@ export class StreamManager {
15
14
  const stream = new Readable();
16
15
 
17
16
  if (range) {
18
- stream.push(data.slice(range.start, range.end + 1));
17
+ stream.push(data.slice(range.start, range.end + 1));
19
18
  } else {
20
- stream.push(data);
19
+ stream.push(data);
21
20
  }
22
21
 
23
22
  stream.push(null);
@@ -12,6 +12,13 @@ import { VideoEvent } from '../src/core/events';
12
12
 
13
13
  // Mock implementation of a custom engine to test modularity
14
14
  class CustomVideoEngine extends VideoEngine {
15
+ private storage: StorageProvider;
16
+
17
+ constructor(storage: StorageProvider) {
18
+ super();
19
+ this.storage = storage;
20
+ }
21
+
15
22
  async processChunk(
16
23
  inputPath: string,
17
24
  outputPath: string,
@@ -19,22 +26,31 @@ class CustomVideoEngine extends VideoEngine {
19
26
  quality: QualityLevel
20
27
  ): Promise<void> {
21
28
  // Create the directory path for the chunk if it doesn't exist
22
- const chunkDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
23
29
  await fs.mkdir(dirname(outputPath), { recursive: true });
24
30
 
25
31
  const chunkData = Buffer.from('test'); // Mock chunk data
26
32
 
27
- // Mock implementation
33
+ // Mock implementation - write to file system
28
34
  await fs.writeFile(outputPath, chunkData);
29
35
 
30
- // Save to storage
31
- const storage = new CustomStorageProvider();
32
- await storage.saveChunk(outputPath, chunkData);
36
+ // Also save to the shared storage provider
37
+ await this.storage.saveChunk(outputPath, chunkData);
33
38
  }
34
39
 
35
40
  async getDuration(inputPath: string): Promise<number> {
36
41
  return 30; // Mock duration
37
42
  }
43
+
44
+ async extractScreenshot(
45
+ inputPath: string,
46
+ outputPath: string,
47
+ time: number
48
+ ): Promise<void> {
49
+ // Create the directory path for the screenshot if it doesn't exist
50
+ await fs.mkdir(dirname(outputPath), { recursive: true });
51
+ // Mock implementation - write an empty file to simulate screenshot
52
+ await fs.writeFile(outputPath, Buffer.from('mock-screenshot'));
53
+ }
38
54
  }
39
55
 
40
56
  // Mock implementation of a custom storage provider
@@ -84,11 +100,15 @@ describe('Video Processing Framework', () => {
84
100
  beforeEach(async () => {
85
101
  // Create test cache directory
86
102
  await fs.mkdir(testConfig.cacheDir, { recursive: true });
103
+ // Create a mock test video file so fs.stat() works in generateVideoId
104
+ await fs.writeFile('test-video.mp4', Buffer.from('mock-video-data'));
87
105
  });
88
106
 
89
107
  afterEach(async () => {
90
108
  // Cleanup test cache directory
91
109
  await fs.rm(testConfig.cacheDir, { recursive: true, force: true });
110
+ // Cleanup mock test video file
111
+ await fs.rm('test-video.mp4', { force: true });
92
112
  });
93
113
 
94
114
  describe('Modular Engine System', () => {
@@ -100,8 +120,8 @@ describe('Video Processing Framework', () => {
100
120
  });
101
121
 
102
122
  it('should work with CustomVideoEngine', async () => {
103
- const engine = new CustomVideoEngine();
104
123
  const storage = new FileSystemStorage();
124
+ const engine = new CustomVideoEngine(storage);
105
125
  const processor = new VideoProcessor(engine, storage, testConfig);
106
126
  expect(processor).to.be.instanceOf(VideoProcessor);
107
127
  });
@@ -109,15 +129,15 @@ describe('Video Processing Framework', () => {
109
129
 
110
130
  describe('Abstract Storage Providers', () => {
111
131
  it('should work with FileSystemStorage', async () => {
112
- const engine = new CustomVideoEngine();
113
132
  const storage = new FileSystemStorage();
133
+ const engine = new CustomVideoEngine(storage);
114
134
  const processor = new VideoProcessor(engine, storage, testConfig);
115
135
  expect(processor).to.be.instanceOf(VideoProcessor);
116
136
  });
117
137
 
118
138
  it('should work with CustomStorageProvider', async () => {
119
- const engine = new CustomVideoEngine();
120
139
  const storage = new CustomStorageProvider();
140
+ const engine = new CustomVideoEngine(storage);
121
141
  const processor = new VideoProcessor(engine, storage, testConfig);
122
142
  expect(processor).to.be.instanceOf(VideoProcessor);
123
143
  });
@@ -125,8 +145,8 @@ describe('Video Processing Framework', () => {
125
145
 
126
146
  describe('Stream Management', () => {
127
147
  it('should create readable stream for chunk', async () => {
128
- const engine = new CustomVideoEngine();
129
148
  const storage = new CustomStorageProvider();
149
+ const engine = new CustomVideoEngine(storage);
130
150
  const processor = new VideoProcessor(engine, storage, testConfig);
131
151
 
132
152
  // Mock a video file for testing
@@ -174,8 +194,8 @@ describe('Video Processing Framework', () => {
174
194
  });
175
195
 
176
196
  it('should support range requests', async () => {
177
- const engine = new CustomVideoEngine();
178
197
  const storage = new CustomStorageProvider();
198
+ const engine = new CustomVideoEngine(storage);
179
199
  const processor = new VideoProcessor(engine, storage, testConfig);
180
200
 
181
201
  const manifest = await processor.processVideo('test-video.mp4');
@@ -191,8 +211,8 @@ describe('Video Processing Framework', () => {
191
211
 
192
212
  describe('Event System', () => {
193
213
  it('should emit events during processing', async () => {
194
- const engine = new CustomVideoEngine();
195
214
  const storage = new CustomStorageProvider();
215
+ const engine = new CustomVideoEngine(storage);
196
216
  const processor = new VideoProcessor(engine, storage, testConfig);
197
217
 
198
218
  const events: string[] = [];
@@ -211,8 +231,8 @@ describe('Video Processing Framework', () => {
211
231
 
212
232
  describe('Configurable Quality Levels', () => {
213
233
  it('should process video in configured quality levels', async () => {
214
- const engine = new CustomVideoEngine();
215
234
  const storage = new CustomStorageProvider();
235
+ const engine = new CustomVideoEngine(storage);
216
236
  const processor = new VideoProcessor(engine, storage, testConfig);
217
237
 
218
238
  const manifest = await processor.processVideo('test-video.mp4');
package/tsconfig.json CHANGED
@@ -2,15 +2,29 @@
2
2
  "compilerOptions": {
3
3
  "target": "ES2020",
4
4
  "module": "ESNext",
5
- "moduleResolution": "node",
5
+ "moduleResolution": "bundler",
6
6
  "esModuleInterop": true,
7
+ "allowSyntheticDefaultImports": true,
7
8
  "strict": true,
8
9
  "skipLibCheck": true,
9
10
  "resolveJsonModule": true,
10
11
  "outDir": "./dist",
11
- "types": ["node", "mocha", "chai"],
12
- "lib": ["dom", "es2015"]
12
+ "types": [
13
+ "node",
14
+ "mocha",
15
+ "chai"
16
+ ],
17
+ "lib": [
18
+ "dom",
19
+ "es2015"
20
+ ]
13
21
  },
14
- "include": ["src/**/*", "tests/**/*"],
15
- "exclude": ["node_modules", "dist"]
22
+ "include": [
23
+ "src/**/*",
24
+ "tests/**/*"
25
+ ],
26
+ "exclude": [
27
+ "node_modules",
28
+ "dist"
29
+ ]
16
30
  }
@@ -8,9 +8,22 @@
8
8
  "skipLibCheck": true,
9
9
  "resolveJsonModule": true,
10
10
  "outDir": "./dist",
11
- "types": ["node", "mocha", "chai"],
12
- "lib": ["dom", "es2015"]
11
+ "types": [
12
+ "node",
13
+ "mocha",
14
+ "chai"
15
+ ],
16
+ "lib": [
17
+ "dom",
18
+ "es2015"
19
+ ]
13
20
  },
14
- "include": ["src/**/*", "tests/**/*"],
15
- "exclude": ["node_modules", "dist"]
21
+ "include": [
22
+ "src/**/*",
23
+ "tests/**/*"
24
+ ],
25
+ "exclude": [
26
+ "node_modules",
27
+ "dist"
28
+ ]
16
29
  }
@@ -1,10 +0,0 @@
1
- export * from './core/types';
2
- export * from './core/events';
3
- export * from './core/VideoEngine';
4
- export * from './engines/FFmpegEngine';
5
- export * from './storage/StorageProvider';
6
- export * from './storage/FileSystemStorage';
7
- export * from './cache/cacheStrategy';
8
- export * from './cache/internalCache';
9
- export * from './cache/ExternalCache';
10
- export * from './processor';
File without changes
File without changes
@@ -1,8 +1,8 @@
1
1
  export declare class LRU<T> {
2
- private cache;
3
2
  private maxSize;
3
+ private cache;
4
4
  constructor(maxSize: number);
5
- get(key: string): T | undefined;
6
5
  set(key: string, value: T): void;
6
+ get(key: string): T | undefined;
7
7
  clear(): void;
8
8
  }
File without changes
File without changes
File without changes