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/.mocharc.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extension": [
3
+ "ts"
4
+ ],
5
+ "spec": "tests/**/*.test.ts",
6
+ "timeout": 10000
7
+ }
package/README.md CHANGED
@@ -1,86 +1,94 @@
1
- # mes-engine
2
-
3
- A powerful and flexible video processing framework for Node.js with support for multiple processing engines, adaptive streaming, and intelligent caching.
4
-
5
- [![npm version](https://badge.fury.io/js/mes-engine.svg)](https://badge.fury.io/js/mes-engine)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
-
8
- ## Features
9
-
10
- - 🎥 Multiple video processing engines (FFmpeg, GStreamer)
11
- - 🔄 Adaptive streaming with quality switching
12
- - 📦 Chunk-based processing for efficient streaming
13
- - 💾 Flexible storage providers
14
- - 🚀 Built-in caching strategies (internal/external)
15
- - 📡 Event-driven architecture
16
- - 🔌 Extensible plugin system
17
-
18
- ## Quick Start
19
-
20
- ### Installation
21
-
22
- ```bash
23
- npm install mes-engine
24
-
25
- # Install required engine
26
- npm install ffmpeg-static # For FFmpeg engine
27
- # or
28
- npm install gstreamer # For GStreamer engine
29
- ```
30
-
31
- ### Basic Usage
32
-
33
- ```typescript
34
- import {
35
- VideoProcessor,
36
- FFmpegEngine,
37
- FileSystemStorage,
38
- InternalCache
39
- } from 'mes-engine';
40
-
41
- // Initialize processor
42
- const processor = new VideoProcessor({
43
- engine: new FFmpegEngine(),
44
- storage: new FileSystemStorage(),
45
- cache: new InternalCache({
46
- maxSize: 1024 * 1024 * 100, // 100MB
47
- ttl: 3600,
48
- preloadNextChunk: true
49
- })
50
- });
51
-
52
- // Process video
53
- const manifest = await processor.processVideo('input.mp4');
54
-
55
- // Stream video chunk
56
- const stream = await processor.streamChunk(
57
- manifest.videoId,
58
- 720, // quality
59
- 0 // chunk number
60
- );
61
- ```
62
-
63
- ## Documentation
64
-
65
- Full documentation is available in the [docs directory](./docs).
66
-
67
- ### Key Topics:
68
- - [Getting Started](./docs/getting-started.md)
69
- - [Video Engines](./docs/engines.md)
70
- - [Storage Providers](./docs/storage.md)
71
- - [Caching Strategies](./docs/caching.md)
72
- - [API Reference](./docs/api.md)
73
-
74
- ## Supported Engines
75
-
76
- - **FFmpegEngine**: Full-featured video processing using FFmpeg
77
- - **GStreamerEngine**: High-performance processing using GStreamer
78
- - **Custom Engines**: Create your own by extending `VideoEngine`
79
-
80
- ## Contributing
81
-
82
- Contributions are welcome! Please see our [Contributing Guide](./CONTRIBUTING.md) for details.
83
-
84
- ## License
85
-
1
+ # mes-engine
2
+
3
+ A powerful and flexible video processing framework for Node.js with support for multiple processing engines, adaptive streaming, and intelligent caching.
4
+
5
+ [![npm version](https://badge.fury.io/js/mes-engine.svg)](https://badge.fury.io/js/mes-engine)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ - 🎥 **Multiple Video Engines**: FFmpeg and GStreamer support.
11
+ - 🔄 **Adaptive Streaming**: Automatic HLS (`.m3u8`) manifest generation with master and quality playlists.
12
+ - 📦 **Chunk-based Processing**: Parallelizable video chunking for efficient delivery.
13
+ - 📸 **Screenshot Extraction**: Automatic thumbnail generation for every video chunk.
14
+ - 💾 **Flexible Storage**: Abstract storage layer supporting local filesystem and easily extensible to S3/Cloud storage.
15
+ - 🚀 **Intelligent Caching**: Built-in strategies for preloading and caching video segments.
16
+ - 📡 **Event-driven**: Comprehensive event system for monitoring processing progress.
17
+
18
+ ## Quick Start
19
+
20
+ ### Installation
21
+
22
+ ```bash
23
+ npm install mes-engine
24
+ ```
25
+
26
+ ### Basic Usage
27
+
28
+ ```typescript
29
+ import {
30
+ VideoProcessor,
31
+ FFmpegEngine,
32
+ FileSystemStorage
33
+ } from 'mes-engine';
34
+
35
+ // 1. Initialize Storage and Engine
36
+ const storage = new FileSystemStorage();
37
+ const engine = new FFmpegEngine();
38
+
39
+ // 2. Setup Configuration
40
+ const config = {
41
+ chunkSize: 10, // seconds
42
+ cacheDir: './output',
43
+ maxCacheSize: 1024 * 1024 * 1024, // 1GB
44
+ defaultQualities: [
45
+ { height: 720, bitrate: '2500k' },
46
+ { height: 1080, bitrate: '5000k' }
47
+ ]
48
+ };
49
+
50
+ // 3. Initialize Processor
51
+ const processor = new VideoProcessor(engine, storage, config);
52
+
53
+ // 4. Listen for Progress
54
+ processor.on('chunk_processed', (event) => {
55
+ console.log(`Processed ${event.quality.height}p chunk ${event.chunkNumber}`);
56
+ });
57
+
58
+ // 5. Process Video
59
+ const manifest = await processor.processVideo('input.mp4', {
60
+ title: 'My Awesome Video',
61
+ overallDescription: 'A high-quality adaptive stream.'
62
+ });
63
+
64
+ console.log('Master Playlist:', manifest.hls?.masterPlaylist);
65
+ ```
66
+
67
+ ### Examples
68
+ - **[Simple Usage](./examples/simple-usage)**: Minimal setup to get started quickly.
69
+ - **[Full Demo](./examples/full-demo)**: A complete React + Node.js application.
70
+
71
+ ## Documentation
72
+
73
+ Full documentation is available in the [docs directory](./docs).
74
+
75
+ ### Key Topics:
76
+ - [Adaptive Streaming (HLS)](https://github.com/Bum-Ho12/mes-engine/blob/main/docs/HLS.md)
77
+ - [Video Engines](https://github.com/Bum-Ho12/mes-engine/blob/main/docs/engines.md)
78
+ - [Storage Providers](https://github.com/Bum-Ho12/mes-engine/blob/main/docs/storage.md)
79
+ - [Caching Strategies](https://github.com/Bum-Ho12/mes-engine/blob/main/docs/caching.md)
80
+ - [API Reference](https://github.com/Bum-Ho12/mes-engine/blob/main/docs/API.md)
81
+
82
+ ## Supported Engines
83
+
84
+ - **FFmpegEngine**: Full-featured video processing using FFmpeg
85
+ - **GStreamerEngine**: High-performance processing using GStreamer
86
+ - **Custom Engines**: Create your own by extending `VideoEngine`
87
+
88
+ ## Contributing
89
+
90
+ Contributions are welcome! Please see our [Contributing Guide](./CONTRIBUTING.md) for details.
91
+
92
+ ## License
93
+
86
94
  MIT © [Bumho Nisubire]
package/dist/index.js CHANGED
@@ -1,10 +1,19 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { spawn } from 'child_process';
3
+ import fs, { promises } from 'fs';
4
+ import { dirname, join } from 'path';
3
5
  import { Readable } from 'stream';
4
- import { promises } from 'fs';
5
6
  import fetch from 'node-fetch';
6
- import { join } from 'path';
7
7
 
8
+ // src/core/events.ts
9
+ /**
10
+ * Options:
11
+ *
12
+ * - CHUNK_PROCESSED
13
+ * - QUALITY_PROCESSED
14
+ * - PROCESSING_COMPLETE
15
+ * - ERROR
16
+ */
8
17
  var VideoEvent;
9
18
  (function (VideoEvent) {
10
19
  VideoEvent["CHUNK_PROCESSED"] = "chunkProcessed";
@@ -13,50 +22,171 @@ var VideoEvent;
13
22
  VideoEvent["ERROR"] = "error";
14
23
  })(VideoEvent || (VideoEvent = {}));
15
24
 
16
- // core/VideoEngine.ts
25
+ // src/core/VideoEngine.ts
26
+ /**
27
+ * Base class for video processing engines (e.g., FFmpeg, GStreamer).
28
+ */
17
29
  class VideoEngine extends EventEmitter {
18
30
  }
19
31
 
20
- // engines/FFmpegEngine.ts
32
+ // src/engines/FFmpegEngine.ts
33
+ /**
34
+ * FFmpeg implementation of the VideoEngine.
35
+ * Requires `ffmpeg` and `ffprobe` to be installed on the system path.
36
+ */
21
37
  class FFmpegEngine extends VideoEngine {
38
+ /**
39
+ * Processes a chunk of video using FFmpeg.
40
+ *
41
+ * @param inputPath - The path to the input video file.
42
+ * @param outputPath - The path where the processed video chunk will be saved.
43
+ * @param startTime - The start time (in seconds) of the chunk to process.
44
+ * @param quality - The desired quality level for the output video.
45
+ * @returns A Promise that resolves when the chunk is processed, or rejects on error.
46
+ */
22
47
  async processChunk(inputPath, outputPath, startTime, quality) {
48
+ // Ensure output directory exists
49
+ await fs.promises.mkdir(dirname(outputPath), { recursive: true });
23
50
  return new Promise((resolve, reject) => {
24
- const ffmpeg = spawn('ffmpeg', [
51
+ const args = [
25
52
  '-i', inputPath,
26
53
  '-ss', startTime.toString(),
27
54
  '-t', '10',
28
- '-vf', `scale=-1:${quality.height}`,
55
+ // Force dimensions divisible by 2 for H.264 encoding
56
+ // The scale filter with -2 rounds to the nearest even number
57
+ '-vf', `scale=-2:${quality.height}`,
29
58
  '-c:v', 'libx264',
30
59
  '-b:v', quality.bitrate,
31
60
  '-c:a', 'aac',
32
61
  '-b:a', '128k',
33
62
  '-preset', 'fast',
63
+ // Use yuv420p for maximum compatibility
64
+ '-pix_fmt', 'yuv420p',
34
65
  '-y',
35
66
  outputPath
36
- ]);
37
- ffmpeg.on('close', code => {
38
- code === 0 ? resolve() : reject(new Error(`FFmpeg error: ${code}`));
67
+ ];
68
+ const ffmpegProcess = spawn('ffmpeg', args);
69
+ let stderr = '';
70
+ ffmpegProcess.stderr.on('data', (data) => {
71
+ stderr += data.toString();
72
+ });
73
+ ffmpegProcess.on('error', (err) => {
74
+ reject(new Error(`Failed to spawn FFmpeg: ${err.message}`));
75
+ });
76
+ ffmpegProcess.on('close', (code) => {
77
+ if (code === 0) {
78
+ resolve();
79
+ }
80
+ else {
81
+ console.error('FFmpeg stderr:', stderr);
82
+ reject(new Error(`FFmpeg error: ${code}\nCommand: ffmpeg ${args.join(' ')}\nStderr: ${stderr}`));
83
+ }
84
+ });
85
+ });
86
+ }
87
+ async extractScreenshot(inputPath, outputPath, time) {
88
+ // Ensure output directory exists
89
+ await fs.promises.mkdir(dirname(outputPath), { recursive: true });
90
+ return new Promise((resolve, reject) => {
91
+ const args = [
92
+ '-ss', time.toString(),
93
+ '-i', inputPath,
94
+ '-vframes', '1',
95
+ '-q:v', '2',
96
+ '-y',
97
+ outputPath
98
+ ];
99
+ const ffmpegProcess = spawn('ffmpeg', args);
100
+ let stderr = '';
101
+ ffmpegProcess.stderr.on('data', (data) => {
102
+ stderr += data.toString();
103
+ });
104
+ ffmpegProcess.on('error', (err) => {
105
+ reject(new Error(`Failed to spawn FFmpeg for screenshot: ${err.message}`));
106
+ });
107
+ ffmpegProcess.on('close', (code) => {
108
+ if (code === 0) {
109
+ resolve();
110
+ }
111
+ else {
112
+ console.error('FFmpeg screenshot stderr:', stderr);
113
+ reject(new Error(`FFmpeg screenshot error: ${code}\nCommand: ffmpeg ${args.join(' ')}\nStderr: ${stderr}`));
114
+ }
39
115
  });
40
116
  });
41
117
  }
42
118
  async getDuration(inputPath) {
119
+ // Use ffprobe for more reliable duration detection
43
120
  return new Promise((resolve, reject) => {
44
- const ffprobe = spawn('ffprobe', [
121
+ const ffprobeProcess = spawn('ffprobe', [
45
122
  '-v', 'error',
46
123
  '-show_entries', 'format=duration',
47
124
  '-of', 'default=noprint_wrappers=1:nokey=1',
48
125
  inputPath
49
126
  ]);
50
- let output = '';
51
- ffprobe.stdout.on('data', data => output += data);
52
- ffprobe.on('close', code => {
53
- code === 0 ? resolve(parseFloat(output)) : reject(new Error(`FFprobe error: ${code}`));
127
+ let stdout = '';
128
+ let stderr = '';
129
+ ffprobeProcess.stdout.on('data', (data) => {
130
+ stdout += data.toString();
131
+ });
132
+ ffprobeProcess.stderr.on('data', (data) => {
133
+ stderr += data.toString();
134
+ });
135
+ ffprobeProcess.on('error', (err) => {
136
+ // Fallback to buffer parsing if ffprobe not available
137
+ console.warn('ffprobe not available, falling back to buffer parsing');
138
+ this.getDurationFromBuffer(inputPath)
139
+ .then(resolve)
140
+ .catch(reject);
141
+ });
142
+ ffprobeProcess.on('close', (code) => {
143
+ if (code === 0) {
144
+ const duration = parseFloat(stdout.trim());
145
+ if (!isNaN(duration)) {
146
+ resolve(duration);
147
+ }
148
+ else {
149
+ reject(new Error('Could not parse duration'));
150
+ }
151
+ }
152
+ else {
153
+ // Fallback to buffer parsing
154
+ this.getDurationFromBuffer(inputPath)
155
+ .then(resolve)
156
+ .catch(reject);
157
+ }
54
158
  });
55
159
  });
56
160
  }
161
+ async getDurationFromBuffer(inputPath) {
162
+ const buffer = await fs.promises.readFile(inputPath);
163
+ // Parse MP4 moov atom for duration
164
+ if (inputPath.endsWith('.mp4')) {
165
+ return this.parseMp4Duration(buffer);
166
+ }
167
+ // For other formats, extract from file metadata
168
+ return this.parseMediaDuration(buffer);
169
+ }
170
+ parseMp4Duration(buffer) {
171
+ const moovStart = buffer.indexOf(Buffer.from('moov'));
172
+ if (moovStart === -1)
173
+ return 0;
174
+ const mvhdStart = buffer.indexOf(Buffer.from('mvhd'), moovStart);
175
+ if (mvhdStart === -1)
176
+ return 0;
177
+ const timeScale = buffer.readUInt32BE(mvhdStart + 12);
178
+ const duration = buffer.readUInt32BE(mvhdStart + 16);
179
+ return duration / timeScale;
180
+ }
181
+ parseMediaDuration(buffer) {
182
+ // Look for duration metadata in file headers
183
+ const durationStr = buffer.toString('utf8', 0, Math.min(1000, buffer.length));
184
+ const match = durationStr.match(/duration["\s:]+(\d+\.?\d*)/i);
185
+ return match ? parseFloat(match[1]) : 0;
186
+ }
57
187
  }
58
188
 
59
- // streaming/StreamManager.ts
189
+ // src/streaming/StreamManager.ts
60
190
  class StreamManager {
61
191
  constructor(storage) {
62
192
  this.storage = storage;
@@ -75,7 +205,7 @@ class StreamManager {
75
205
  }
76
206
  }
77
207
 
78
- // engines/GStreamerEngine.ts
208
+ // src/engines/GStreamerEngine.ts
79
209
  class GStreamerEngine extends VideoEngine {
80
210
  async processChunk(inputPath, outputPath, startTime, quality) {
81
211
  return new Promise((resolve, reject) => {
@@ -93,6 +223,23 @@ class GStreamerEngine extends VideoEngine {
93
223
  });
94
224
  });
95
225
  }
226
+ async extractScreenshot(inputPath, outputPath, time) {
227
+ return new Promise((resolve, reject) => {
228
+ const gst = spawn('gst-launch-1.0', [
229
+ 'filesrc', `location=${inputPath}`,
230
+ '!', 'decodebin',
231
+ '!', 'videoconvert',
232
+ '!', 'videorate',
233
+ '!', `video/x-raw,framerate=1/1`,
234
+ '!', 'videocut', `starting-time=${time * 1000000000}`,
235
+ '!', 'jpegenc',
236
+ '!', 'filesink', `location=${outputPath}`
237
+ ]);
238
+ gst.on('close', code => {
239
+ code === 0 ? resolve() : reject(new Error(`GStreamer screenshot error: ${code}`));
240
+ });
241
+ });
242
+ }
96
243
  async getDuration(inputPath) {
97
244
  return new Promise((resolve, reject) => {
98
245
  const gst = spawn('gst-launch-1.0', [
@@ -109,11 +256,14 @@ class GStreamerEngine extends VideoEngine {
109
256
  }
110
257
  }
111
258
 
112
- // storage/StorageProvider.ts
259
+ // src/storage/StorageProvider.ts
260
+ /**
261
+ * Interface for storage backends (e.g., Local File System, S3, Cloud Storage).
262
+ */
113
263
  class StorageProvider {
114
264
  }
115
265
 
116
- // storage/FileSystemStorage.ts
266
+ // src/storage/FileSystemStorage.ts
117
267
  class FileSystemStorage extends StorageProvider {
118
268
  async saveChunk(chunkPath, data) {
119
269
  await promises.writeFile(chunkPath, data);
@@ -247,6 +397,10 @@ class ExternalCache extends CacheStrategy {
247
397
  }
248
398
  }
249
399
 
400
+ /**
401
+ * Main orchestrator for video processing.
402
+ * Handles chunking, transcoding (via engines), and manifest generation.
403
+ */
250
404
  class VideoProcessor extends EventEmitter {
251
405
  constructor(engine, storage, config) {
252
406
  super();
@@ -254,25 +408,60 @@ class VideoProcessor extends EventEmitter {
254
408
  this.streamManager = new StreamManager(storage);
255
409
  this.config = config;
256
410
  }
257
- async processVideo(inputPath) {
411
+ /**
412
+ * Processes an input video file into multiple quality levels and chunks.
413
+ * Generates HLS playlists and a JSON manifest.
414
+ *
415
+ * @param inputPath - Absolute path to the source video file
416
+ * @param options - Optional metadata and processing instructions
417
+ * @returns Promise resolving to the generated VideoManifest
418
+ * @throws Error if processing fails at any stage
419
+ */
420
+ async processVideo(inputPath, options) {
258
421
  const videoId = await this.generateVideoId(inputPath);
259
422
  const manifest = {
260
423
  videoId,
261
424
  qualities: this.config.defaultQualities,
262
- chunks: []
425
+ chunks: [],
426
+ metadata: {
427
+ title: options?.title,
428
+ description: options?.overallDescription,
429
+ createdAt: new Date().toISOString()
430
+ }
263
431
  };
432
+ // Create base directory structure
433
+ const baseDir = join(this.config.cacheDir, videoId);
434
+ const screenshotDir = join(baseDir, 'screenshots');
435
+ await promises.mkdir(baseDir, { recursive: true });
436
+ await promises.mkdir(screenshotDir, { recursive: true });
264
437
  const duration = await this.engine.getDuration(inputPath);
265
438
  const chunks = Math.ceil(duration / this.config.chunkSize);
266
439
  for (const quality of this.config.defaultQualities) {
440
+ // Create quality-specific directory
441
+ const qualityDir = join(baseDir, `${quality.height}p`);
442
+ await promises.mkdir(qualityDir, { recursive: true });
443
+ 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';
267
444
  for (let i = 0; i < chunks; i++) {
268
445
  const chunkPath = this.getChunkPath(videoId, quality.height, i);
446
+ const screenshotPath = this.getScreenshotPath(videoId, i);
269
447
  try {
448
+ // Process chunk
270
449
  await this.engine.processChunk(inputPath, chunkPath, i * this.config.chunkSize, quality);
271
- manifest.chunks.push({
450
+ // Extract screenshot (only once per chunk number, e.g., for the first quality)
451
+ if (quality.height === this.config.defaultQualities[0].height) {
452
+ await this.engine.extractScreenshot(inputPath, screenshotPath, i * this.config.chunkSize + 1 // 1 second into the chunk
453
+ );
454
+ }
455
+ const chunk = {
272
456
  quality: quality.height,
273
457
  number: i,
274
- path: chunkPath
275
- });
458
+ path: chunkPath,
459
+ screenshotPath: screenshotPath,
460
+ description: options?.descriptions?.[i]
461
+ };
462
+ manifest.chunks.push(chunk);
463
+ // Add to M3U8
464
+ m3u8Content += `#EXTINF:${this.config.chunkSize}.0,\nchunk_${i}.mp4\n`;
276
465
  this.emit(VideoEvent.CHUNK_PROCESSED, { quality, chunkNumber: i });
277
466
  }
278
467
  catch (error) {
@@ -280,11 +469,37 @@ class VideoProcessor extends EventEmitter {
280
469
  throw error;
281
470
  }
282
471
  }
472
+ m3u8Content += '#EXT-X-ENDLIST';
473
+ // Save M3U8 for this quality
474
+ const m3u8Path = join(qualityDir, 'playlist.m3u8');
475
+ await promises.writeFile(m3u8Path, m3u8Content);
283
476
  this.emit(VideoEvent.QUALITY_PROCESSED, quality);
284
477
  }
478
+ // Generate Master M3U8
479
+ if (this.config.defaultQualities.length > 1) {
480
+ let masterM3u8 = '#EXTM3U\n';
481
+ for (const quality of this.config.defaultQualities) {
482
+ masterM3u8 += `#EXT-X-STREAM-INF:BANDWIDTH=${quality.bitrate.replace('k', '000')},RESOLUTION=-1x${quality.height}\n${quality.height}p/playlist.m3u8\n`;
483
+ }
484
+ const masterPath = join(baseDir, 'master.m3u8');
485
+ await promises.writeFile(masterPath, masterM3u8);
486
+ }
487
+ // Save JSON Manifest
488
+ const manifestPath = join(baseDir, 'manifest.json');
489
+ await promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
285
490
  this.emit(VideoEvent.PROCESSING_COMPLETE, manifest);
286
491
  return manifest;
287
492
  }
493
+ /**
494
+ * Creates a readable stream for a specific video chunk.
495
+ * Supported for on-demand delivery of processed segments.
496
+ *
497
+ * @param videoId - ID of the processed video
498
+ * @param quality - Target quality (height)
499
+ * @param chunkNumber - Sequential index of the chunk
500
+ * @param range - Optional byte range for partial content
501
+ * @returns Promise resolving to a Readable stream
502
+ */
288
503
  async streamChunk(videoId, quality, chunkNumber, range) {
289
504
  const chunkPath = this.getChunkPath(videoId, quality, chunkNumber);
290
505
  return this.streamManager.createStream(chunkPath, range);
@@ -292,9 +507,13 @@ class VideoProcessor extends EventEmitter {
292
507
  getChunkPath(videoId, quality, chunkNumber) {
293
508
  return join(this.config.cacheDir, videoId, `${quality}p`, `chunk_${chunkNumber}.mp4`);
294
509
  }
510
+ getScreenshotPath(videoId, chunkNumber) {
511
+ return join(this.config.cacheDir, videoId, 'screenshots', `chunk_${chunkNumber}.jpg`);
512
+ }
295
513
  async generateVideoId(inputPath) {
296
514
  const stats = await promises.stat(inputPath);
297
- return `${inputPath.split('/').pop()?.split('.')[0]}_${stats.mtimeMs}`;
515
+ const fileName = inputPath.split(/[\\/]/).pop()?.split('.')[0];
516
+ return `${fileName}_${Math.floor(stats.mtimeMs)}`;
298
517
  }
299
518
  }
300
519