mes-engine 0.0.2 → 1.0.0
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 +94 -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 +592 -0
- package/examples/simple-usage/package.json +15 -0
- package/package.json +69 -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 +173 -32
- package/src/engines/GStreamerEngine.ts +24 -1
- package/src/index.ts +13 -13
- package/src/processor.ts +119 -35
- package/src/storage/FileSystemStorage.ts +2 -4
- package/src/storage/StorageProvider.ts +7 -2
- package/src/streaming/StreamManager.ts +4 -5
- 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/package.json
CHANGED
|
@@ -1,48 +1,69 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mes-engine",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"mes
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"framework"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"rollup": "^
|
|
42
|
-
"rollup
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "mes-engine",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A powerful and flexible video processing framework for Node.js with support for multiple processing engines, adaptive streaming, and intelligent caching.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mes",
|
|
8
|
+
"mes-engine",
|
|
9
|
+
"video",
|
|
10
|
+
"processing",
|
|
11
|
+
"video-framework",
|
|
12
|
+
"framework"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Bumho Nisubire",
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "ts-mocha -p tsconfig.test.json tests/**/*.test.ts --timeout 10000",
|
|
23
|
+
"build": "rollup -c rollup.config.js",
|
|
24
|
+
"demo": "ts-node examples/demo.ts"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"cors": "^2.8.5",
|
|
28
|
+
"express": "^5.2.1",
|
|
29
|
+
"fluent-ffmpeg": "^2.1.3",
|
|
30
|
+
"hls.js": "^1.6.15",
|
|
31
|
+
"lucide-react": "^0.562.0",
|
|
32
|
+
"morgan": "^1.10.1",
|
|
33
|
+
"multer": "^2.0.2",
|
|
34
|
+
"node-fetch": "^2.7.0",
|
|
35
|
+
"react": "^19.2.3",
|
|
36
|
+
"react-dom": "^19.2.3",
|
|
37
|
+
"tslib": "^2.8.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@rollup/plugin-commonjs": "^28.0.2",
|
|
41
|
+
"@rollup/plugin-json": "^6.1.0",
|
|
42
|
+
"@rollup/plugin-node-resolve": "^16.0.0",
|
|
43
|
+
"@rollup/plugin-typescript": "^12.1.2",
|
|
44
|
+
"@types/chai": "^4.3.11",
|
|
45
|
+
"@types/cors": "^2.8.19",
|
|
46
|
+
"@types/express": "^5.0.6",
|
|
47
|
+
"@types/fluent-ffmpeg": "^2.1.28",
|
|
48
|
+
"@types/jest": "^29.5.14",
|
|
49
|
+
"@types/mocha": "^10.0.6",
|
|
50
|
+
"@types/morgan": "^1.9.10",
|
|
51
|
+
"@types/multer": "^2.0.0",
|
|
52
|
+
"@types/node": "^22.10.5",
|
|
53
|
+
"@types/node-fetch": "^2.6.12",
|
|
54
|
+
"@types/react": "^19.2.7",
|
|
55
|
+
"@types/react-dom": "^19.2.3",
|
|
56
|
+
"@vitejs/plugin-react": "^5.1.2",
|
|
57
|
+
"chai": "^4.3.10",
|
|
58
|
+
"concurrently": "^9.2.1",
|
|
59
|
+
"jest": "^29.7.0",
|
|
60
|
+
"mocha": "^10.8.2",
|
|
61
|
+
"rollup": "^4.30.1",
|
|
62
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
63
|
+
"ts-jest": "^29.2.5",
|
|
64
|
+
"ts-mocha": "^10.0.0",
|
|
65
|
+
"ts-node": "^10.9.2",
|
|
66
|
+
"typescript": "^5.3.3",
|
|
67
|
+
"vite": "^7.3.1"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/rollup.config.js
CHANGED
|
@@ -2,6 +2,7 @@ import typescript from '@rollup/plugin-typescript';
|
|
|
2
2
|
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
|
3
3
|
import commonjs from '@rollup/plugin-commonjs';
|
|
4
4
|
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
|
|
5
|
+
import json from '@rollup/plugin-json';
|
|
5
6
|
|
|
6
7
|
export default {
|
|
7
8
|
input: 'src/index.ts', // Entry point
|
|
@@ -13,6 +14,7 @@ export default {
|
|
|
13
14
|
},
|
|
14
15
|
],
|
|
15
16
|
plugins: [
|
|
17
|
+
json(), // Handle JSON imports
|
|
16
18
|
peerDepsExternal(), // Externalize peer dependencies
|
|
17
19
|
nodeResolve(), // Resolve dependencies from node_modules
|
|
18
20
|
commonjs(), // Convert CommonJS to ES6
|
|
@@ -21,4 +23,4 @@ export default {
|
|
|
21
23
|
external: [
|
|
22
24
|
'node-fetch', // Prevent bundling node-fetch
|
|
23
25
|
],
|
|
24
|
-
};
|
|
26
|
+
};
|
package/src/bandwidth.ts
CHANGED
package/src/core/VideoEngine.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
// core/VideoEngine.ts
|
|
1
|
+
// src/core/VideoEngine.ts
|
|
3
2
|
import { EventEmitter } from 'events';
|
|
4
|
-
import {
|
|
5
|
-
import { VideoEvent } from './events';
|
|
3
|
+
import { QualityLevel } from './types.js';
|
|
6
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Base class for video processing engines (e.g., FFmpeg, GStreamer).
|
|
7
|
+
*/
|
|
7
8
|
export abstract class VideoEngine extends EventEmitter {
|
|
9
|
+
/**
|
|
10
|
+
* Extracts and transcodes a chunk of video.
|
|
11
|
+
* @param inputPath - Path to source video
|
|
12
|
+
* @param outputPath - Destination for the chunk
|
|
13
|
+
* @param startTime - Start offset in seconds
|
|
14
|
+
* @param quality - Target quality level
|
|
15
|
+
*/
|
|
8
16
|
abstract processChunk(
|
|
9
17
|
inputPath: string,
|
|
10
18
|
outputPath: string,
|
|
@@ -12,5 +20,22 @@ export abstract class VideoEngine extends EventEmitter {
|
|
|
12
20
|
quality: QualityLevel
|
|
13
21
|
): Promise<void>;
|
|
14
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Extracts a frame from the video as an image.
|
|
25
|
+
* @param inputPath - Path to source video
|
|
26
|
+
* @param outputPath - Destination for the image
|
|
27
|
+
* @param time - Time offset in seconds
|
|
28
|
+
*/
|
|
29
|
+
abstract extractScreenshot(
|
|
30
|
+
inputPath: string,
|
|
31
|
+
outputPath: string,
|
|
32
|
+
time: number
|
|
33
|
+
): Promise<void>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Gets the total duration of the video.
|
|
37
|
+
* @param inputPath - Path to source video
|
|
38
|
+
* @returns Promise resolving to duration in seconds
|
|
39
|
+
*/
|
|
15
40
|
abstract getDuration(inputPath: string): Promise<number>;
|
|
16
41
|
}
|
package/src/core/events.ts
CHANGED
package/src/core/types.ts
CHANGED
|
@@ -1,26 +1,61 @@
|
|
|
1
|
+
// src/core/types.ts
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
/** Configuration for video processing engine */
|
|
4
4
|
export interface VideoConfig {
|
|
5
|
+
/** Size of each video chunk in seconds */
|
|
5
6
|
chunkSize: number;
|
|
7
|
+
/** Directory for temporary cache files */
|
|
6
8
|
cacheDir: string;
|
|
9
|
+
/** Maximum cache size in bytes */
|
|
7
10
|
maxCacheSize: number;
|
|
11
|
+
/** Default quality levels for processing */
|
|
8
12
|
defaultQualities: QualityLevel[];
|
|
9
13
|
}
|
|
10
14
|
|
|
15
|
+
/** Definition of a video quality level */
|
|
11
16
|
export interface QualityLevel {
|
|
17
|
+
/** Vertical resolution (e.g., 720, 1080) */
|
|
12
18
|
height: number;
|
|
19
|
+
/** Target bitrate (e.g., "2500k") */
|
|
13
20
|
bitrate: string;
|
|
14
21
|
}
|
|
15
22
|
|
|
23
|
+
/** Options for a specific processing job */
|
|
24
|
+
export interface ProcessingOptions {
|
|
25
|
+
/** Title of the video */
|
|
26
|
+
title?: string;
|
|
27
|
+
/** Overall description of the video content */
|
|
28
|
+
overallDescription?: string;
|
|
29
|
+
/** Descriptions for individual chunks indexed by chunk number */
|
|
30
|
+
descriptions?: Record<number, string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Represents a processed video chunk */
|
|
16
34
|
export interface VideoChunk {
|
|
35
|
+
/** Height of the quality level */
|
|
17
36
|
quality: number;
|
|
37
|
+
/** Sequential index of the chunk */
|
|
18
38
|
number: number;
|
|
39
|
+
/** Local path to the processed file */
|
|
19
40
|
path: string;
|
|
41
|
+
/** Optional path to a screenshot for this chunk */
|
|
42
|
+
screenshotPath?: string;
|
|
43
|
+
/** Optional localized description */
|
|
44
|
+
description?: string;
|
|
20
45
|
}
|
|
21
46
|
|
|
47
|
+
/** Full manifest for a processed video */
|
|
22
48
|
export interface VideoManifest {
|
|
49
|
+
/** Unique identifier for the video */
|
|
23
50
|
videoId: string;
|
|
51
|
+
/** Available quality levels */
|
|
24
52
|
qualities: QualityLevel[];
|
|
53
|
+
/** List of all processed chunks */
|
|
25
54
|
chunks: VideoChunk[];
|
|
26
|
-
|
|
55
|
+
/** Core metadata */
|
|
56
|
+
metadata: {
|
|
57
|
+
title?: string;
|
|
58
|
+
description?: string;
|
|
59
|
+
createdAt: string;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -1,51 +1,192 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// src/engines/FFmpegEngine.ts
|
|
2
|
+
import { VideoEngine } from '../core/VideoEngine.js';
|
|
3
|
+
import { QualityLevel } from '../core/types.js';
|
|
3
4
|
import { spawn } from 'child_process';
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { dirname } from 'path';
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* FFmpeg implementation of the VideoEngine.
|
|
10
|
+
* Requires `ffmpeg` and `ffprobe` to be installed on the system path.
|
|
11
|
+
*/
|
|
7
12
|
export class FFmpegEngine extends VideoEngine {
|
|
13
|
+
/**
|
|
14
|
+
* Processes a chunk of video using FFmpeg.
|
|
15
|
+
*
|
|
16
|
+
* @param inputPath - The path to the input video file.
|
|
17
|
+
* @param outputPath - The path where the processed video chunk will be saved.
|
|
18
|
+
* @param startTime - The start time (in seconds) of the chunk to process.
|
|
19
|
+
* @param quality - The desired quality level for the output video.
|
|
20
|
+
* @returns A Promise that resolves when the chunk is processed, or rejects on error.
|
|
21
|
+
*/
|
|
8
22
|
async processChunk(
|
|
9
23
|
inputPath: string,
|
|
10
24
|
outputPath: string,
|
|
11
25
|
startTime: number,
|
|
12
26
|
quality: QualityLevel
|
|
13
27
|
): Promise<void> {
|
|
28
|
+
// Ensure output directory exists
|
|
29
|
+
await fs.promises.mkdir(dirname(outputPath), { recursive: true });
|
|
30
|
+
|
|
14
31
|
return new Promise((resolve, reject) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
const args = [
|
|
33
|
+
'-i', inputPath,
|
|
34
|
+
'-ss', startTime.toString(),
|
|
35
|
+
'-t', '10',
|
|
36
|
+
// Force dimensions divisible by 2 for H.264 encoding
|
|
37
|
+
// The scale filter with -2 rounds to the nearest even number
|
|
38
|
+
'-vf', `scale=-2:${quality.height}`,
|
|
39
|
+
'-c:v', 'libx264',
|
|
40
|
+
'-b:v', quality.bitrate,
|
|
41
|
+
'-c:a', 'aac',
|
|
42
|
+
'-b:a', '128k',
|
|
43
|
+
'-preset', 'fast',
|
|
44
|
+
// Use yuv420p for maximum compatibility
|
|
45
|
+
'-pix_fmt', 'yuv420p',
|
|
46
|
+
'-y',
|
|
47
|
+
outputPath
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const ffmpegProcess = spawn('ffmpeg', args);
|
|
51
|
+
|
|
52
|
+
let stderr = '';
|
|
53
|
+
|
|
54
|
+
ffmpegProcess.stderr.on('data', (data) => {
|
|
55
|
+
stderr += data.toString();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
ffmpegProcess.on('error', (err) => {
|
|
59
|
+
reject(new Error(`Failed to spawn FFmpeg: ${err.message}`));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
ffmpegProcess.on('close', (code) => {
|
|
63
|
+
if (code === 0) {
|
|
64
|
+
resolve();
|
|
65
|
+
} else {
|
|
66
|
+
console.error('FFmpeg stderr:', stderr);
|
|
67
|
+
reject(new Error(`FFmpeg error: ${code}\nCommand: ffmpeg ${args.join(' ')}\nStderr: ${stderr}`));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
31
70
|
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async extractScreenshot(
|
|
74
|
+
inputPath: string,
|
|
75
|
+
outputPath: string,
|
|
76
|
+
time: number
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
// Ensure output directory exists
|
|
79
|
+
await fs.promises.mkdir(dirname(outputPath), { recursive: true });
|
|
80
|
+
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const args = [
|
|
83
|
+
'-ss', time.toString(),
|
|
84
|
+
'-i', inputPath,
|
|
85
|
+
'-vframes', '1',
|
|
86
|
+
'-q:v', '2',
|
|
87
|
+
'-y',
|
|
88
|
+
outputPath
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const ffmpegProcess = spawn('ffmpeg', args);
|
|
92
|
+
|
|
93
|
+
let stderr = '';
|
|
94
|
+
|
|
95
|
+
ffmpegProcess.stderr.on('data', (data) => {
|
|
96
|
+
stderr += data.toString();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
ffmpegProcess.on('error', (err) => {
|
|
100
|
+
reject(new Error(`Failed to spawn FFmpeg for screenshot: ${err.message}`));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
ffmpegProcess.on('close', (code) => {
|
|
104
|
+
if (code === 0) {
|
|
105
|
+
resolve();
|
|
106
|
+
} else {
|
|
107
|
+
console.error('FFmpeg screenshot stderr:', stderr);
|
|
108
|
+
reject(new Error(`FFmpeg screenshot error: ${code}\nCommand: ffmpeg ${args.join(' ')}\nStderr: ${stderr}`));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
32
111
|
});
|
|
33
112
|
}
|
|
34
113
|
|
|
35
114
|
async getDuration(inputPath: string): Promise<number> {
|
|
115
|
+
// Use ffprobe for more reliable duration detection
|
|
36
116
|
return new Promise((resolve, reject) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
117
|
+
const ffprobeProcess = spawn('ffprobe', [
|
|
118
|
+
'-v', 'error',
|
|
119
|
+
'-show_entries', 'format=duration',
|
|
120
|
+
'-of', 'default=noprint_wrappers=1:nokey=1',
|
|
121
|
+
inputPath
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
let stdout = '';
|
|
125
|
+
let stderr = '';
|
|
126
|
+
|
|
127
|
+
ffprobeProcess.stdout.on('data', (data) => {
|
|
128
|
+
stdout += data.toString();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
ffprobeProcess.stderr.on('data', (data) => {
|
|
132
|
+
stderr += data.toString();
|
|
133
|
+
});
|
|
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
|
+
|
|
143
|
+
ffprobeProcess.on('close', (code) => {
|
|
144
|
+
if (code === 0) {
|
|
145
|
+
const duration = parseFloat(stdout.trim());
|
|
146
|
+
if (!isNaN(duration)) {
|
|
147
|
+
resolve(duration);
|
|
148
|
+
} else {
|
|
149
|
+
reject(new Error('Could not parse duration'));
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
// Fallback to buffer parsing
|
|
153
|
+
this.getDurationFromBuffer(inputPath)
|
|
154
|
+
.then(resolve)
|
|
155
|
+
.catch(reject);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
49
158
|
});
|
|
50
159
|
}
|
|
160
|
+
|
|
161
|
+
private async getDurationFromBuffer(inputPath: string): Promise<number> {
|
|
162
|
+
const buffer = await fs.promises.readFile(inputPath);
|
|
163
|
+
|
|
164
|
+
// Parse MP4 moov atom for duration
|
|
165
|
+
if (inputPath.endsWith('.mp4')) {
|
|
166
|
+
return this.parseMp4Duration(buffer);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// For other formats, extract from file metadata
|
|
170
|
+
return this.parseMediaDuration(buffer);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private parseMp4Duration(buffer: Buffer): number {
|
|
174
|
+
const moovStart = buffer.indexOf(Buffer.from('moov'));
|
|
175
|
+
if (moovStart === -1) return 0;
|
|
176
|
+
|
|
177
|
+
const mvhdStart = buffer.indexOf(Buffer.from('mvhd'), moovStart);
|
|
178
|
+
if (mvhdStart === -1) return 0;
|
|
179
|
+
|
|
180
|
+
const timeScale = buffer.readUInt32BE(mvhdStart + 12);
|
|
181
|
+
const duration = buffer.readUInt32BE(mvhdStart + 16);
|
|
182
|
+
|
|
183
|
+
return duration / timeScale;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private parseMediaDuration(buffer: Buffer): number {
|
|
187
|
+
// Look for duration metadata in file headers
|
|
188
|
+
const durationStr = buffer.toString('utf8', 0, Math.min(1000, buffer.length));
|
|
189
|
+
const match = durationStr.match(/duration["\s:]+(\d+\.?\d*)/i);
|
|
190
|
+
return match ? parseFloat(match[1]) : 0;
|
|
191
|
+
}
|
|
51
192
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// engines/GStreamerEngine.ts
|
|
1
|
+
// src/engines/GStreamerEngine.ts
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import { VideoEngine } from '../core/VideoEngine';
|
|
4
4
|
import { QualityLevel } from '../core/types';
|
|
@@ -27,6 +27,29 @@ export class GStreamerEngine extends VideoEngine {
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
async extractScreenshot(
|
|
31
|
+
inputPath: string,
|
|
32
|
+
outputPath: string,
|
|
33
|
+
time: number
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const gst = spawn('gst-launch-1.0', [
|
|
37
|
+
'filesrc', `location=${inputPath}`,
|
|
38
|
+
'!', 'decodebin',
|
|
39
|
+
'!', 'videoconvert',
|
|
40
|
+
'!', 'videorate',
|
|
41
|
+
'!', `video/x-raw,framerate=1/1`,
|
|
42
|
+
'!', 'videocut', `starting-time=${time * 1000000000}`,
|
|
43
|
+
'!', 'jpegenc',
|
|
44
|
+
'!', 'filesink', `location=${outputPath}`
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
gst.on('close', code => {
|
|
48
|
+
code === 0 ? resolve() : reject(new Error(`GStreamer screenshot error: ${code}`));
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
async getDuration(inputPath: string): Promise<number> {
|
|
31
54
|
return new Promise((resolve, reject) => {
|
|
32
55
|
const gst = spawn('gst-launch-1.0', [
|
package/src/index.ts
CHANGED
|
@@ -1,13 +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';
|
|
1
|
+
// src/index.ts
|
|
2
|
+
export * from './core/types.js';
|
|
3
|
+
export * from './core/events.js';
|
|
4
|
+
export * from './core/VideoEngine.js';
|
|
5
|
+
export * from './engines/FFmpegEngine.js';
|
|
6
|
+
export * from './streaming/StreamManager.js';
|
|
7
|
+
export * from './engines/GStreamerEngine.js';
|
|
8
|
+
export * from './storage/StorageProvider.js';
|
|
9
|
+
export * from './storage/FileSystemStorage.js';
|
|
10
|
+
export * from './cache/cacheStrategy.js';
|
|
11
|
+
export * from './cache/internalCache.js';
|
|
12
|
+
export * from './cache/ExternalCache.js';
|
|
13
|
+
export * from './processor.js';
|