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.
- package/.mocharc.json +7 -0
- package/README.md +93 -85
- package/dist/index.js +243 -24
- package/dist/index.js.map +1 -1
- package/dist/{types → src}/core/VideoEngine.d.ts +2 -1
- package/dist/{types → src}/core/types.d.ts +12 -0
- package/dist/src/engines/FFmpegEngine.d.ts +7 -0
- package/dist/{types/engines/FFmpegEngine.d.ts → src/engines/GStreamerEngine.d.ts} +2 -1
- package/dist/src/index.d.ts +12 -0
- package/dist/{types → src}/processor.d.ts +5 -5
- package/dist/{types → src}/storage/FileSystemStorage.d.ts +1 -1
- package/dist/{types → src}/streaming/StreamManager.d.ts +1 -1
- package/dist/tests/video-processor.test.d.ts +1 -0
- package/docs/API.md +109 -0
- package/docs/HLS.md +54 -0
- package/docs/README.md +172 -169
- package/docs/caching.md +62 -0
- package/docs/engines.md +62 -58
- package/docs/storage.md +57 -0
- package/examples/full-demo/backend/.env +6 -0
- package/examples/full-demo/backend/package-lock.json +1783 -0
- package/examples/full-demo/backend/package.json +22 -0
- package/examples/full-demo/backend/src/routes/video.js +92 -0
- package/examples/full-demo/backend/src/server.js +43 -0
- package/examples/full-demo/backend/src/services/videoProcessor.js +85 -0
- package/examples/full-demo/frontend/index.html +13 -0
- package/examples/full-demo/frontend/package-lock.json +5791 -0
- package/examples/full-demo/frontend/package.json +32 -0
- package/examples/full-demo/frontend/postcss.config.js +6 -0
- package/examples/full-demo/frontend/src/App.jsx +113 -0
- package/examples/full-demo/frontend/src/components/ProcessingStatus.jsx +71 -0
- package/examples/full-demo/frontend/src/components/VideoPlayer.jsx +87 -0
- package/examples/full-demo/frontend/src/components/VideoUploader.jsx +62 -0
- package/examples/full-demo/frontend/src/index.css +3 -0
- package/examples/full-demo/frontend/src/main.jsx +10 -0
- package/examples/full-demo/frontend/src/services/api.js +30 -0
- package/examples/full-demo/frontend/tailwind.config.js +10 -0
- package/examples/full-demo/frontend/vite.config.js +16 -0
- package/examples/simple-usage/README.md +31 -0
- package/examples/simple-usage/index.ts +68 -0
- package/examples/simple-usage/package-lock.json +589 -0
- package/examples/simple-usage/package.json +15 -0
- package/package.json +64 -48
- package/rollup.config.js +3 -1
- package/src/bandwidth.ts +1 -1
- package/src/core/VideoEngine.ts +29 -4
- package/src/core/events.ts +9 -1
- package/src/core/types.ts +38 -3
- package/src/engines/FFmpegEngine.ts +172 -31
- package/src/engines/GStreamerEngine.ts +24 -1
- package/src/index.ts +1 -1
- package/src/processor.ts +115 -31
- package/src/storage/FileSystemStorage.ts +1 -3
- package/src/storage/StorageProvider.ts +7 -2
- package/src/streaming/StreamManager.ts +3 -4
- package/tests/video-processor.test.ts +32 -12
- package/tsconfig.json +19 -5
- package/tsconfig.test.json +17 -4
- package/dist/types/index.d.ts +0 -10
- package/dist/{types → src}/bandwidth.d.ts +0 -0
- package/dist/{types → src}/cache/ExternalCache.d.ts +0 -0
- package/dist/{types → src}/cache/LRU.d.ts +2 -2
- /package/dist/{types → src}/cache/cacheStrategy.d.ts +0 -0
- /package/dist/{types → src}/cache/internalCache.d.ts +0 -0
- /package/dist/{types → src}/core/events.d.ts +0 -0
- /package/dist/{types → src}/storage/StorageProvider.d.ts +0 -0
package/.mocharc.json
ADDED
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
|
-
[](https://badge.fury.io/js/mes-engine)
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
|
|
8
|
-
## Features
|
|
9
|
-
|
|
10
|
-
- 🎥 Multiple
|
|
11
|
-
- 🔄 Adaptive
|
|
12
|
-
- 📦 Chunk-based
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
## Quick Start
|
|
19
|
-
|
|
20
|
-
### Installation
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm install mes-engine
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
###
|
|
68
|
-
- [
|
|
69
|
-
- [
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
[](https://badge.fury.io/js/mes-engine)
|
|
6
|
+
[](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
|
|
51
|
+
const args = [
|
|
25
52
|
'-i', inputPath,
|
|
26
53
|
'-ss', startTime.toString(),
|
|
27
54
|
'-t', '10',
|
|
28
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
515
|
+
const fileName = inputPath.split(/[\\/]/).pop()?.split('.')[0];
|
|
516
|
+
return `${fileName}_${Math.floor(stats.mtimeMs)}`;
|
|
298
517
|
}
|
|
299
518
|
}
|
|
300
519
|
|