mes-engine 0.0.3 → 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.
Files changed (66) hide show
  1. package/.mocharc.json +7 -0
  2. package/README.md +94 -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 +592 -0
  42. package/examples/simple-usage/package.json +15 -0
  43. package/package.json +69 -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 +173 -32
  50. package/src/engines/GStreamerEngine.ts +24 -1
  51. package/src/index.ts +13 -13
  52. package/src/processor.ts +119 -35
  53. package/src/storage/FileSystemStorage.ts +2 -4
  54. package/src/storage/StorageProvider.ts +7 -2
  55. package/src/streaming/StreamManager.ts +4 -5
  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
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "mes-engine-demo-frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "axios": "^1.6.2",
14
+ "lucide-react": "^0.294.0",
15
+ "react": "^18.2.0",
16
+ "react-dom": "^18.2.0",
17
+ "socket.io-client": "^4.7.2"
18
+ },
19
+ "devDependencies": {
20
+ "@types/react": "^18.2.37",
21
+ "@types/react-dom": "^18.2.15",
22
+ "@vitejs/plugin-react": "^4.2.0",
23
+ "autoprefixer": "^10.4.16",
24
+ "eslint": "^8.53.0",
25
+ "eslint-plugin-react": "^7.33.2",
26
+ "eslint-plugin-react-hooks": "^4.6.0",
27
+ "eslint-plugin-react-refresh": "^0.4.4",
28
+ "postcss": "^8.4.31",
29
+ "tailwindcss": "^3.3.5",
30
+ "vite": "^5.0.0"
31
+ }
32
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -0,0 +1,113 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { io } from 'socket.io-client';
3
+ import { VideoUploader } from './components/VideoUploader';
4
+ import { ProcessingStatus } from './components/ProcessingStatus';
5
+ import { VideoPlayer } from './components/VideoPlayer';
6
+ import { uploadVideo } from './services/api';
7
+
8
+ const socket = io('http://localhost:3001');
9
+
10
+ function App() {
11
+ const [status, setStatus] = useState('idle');
12
+ const [uploadProgress, setUploadProgress] = useState(0);
13
+ const [logs, setLogs] = useState([]);
14
+ const [manifest, setManifest] = useState(null);
15
+ const [selectedQuality, setSelectedQuality] = useState(720);
16
+
17
+ useEffect(() => {
18
+ socket.on('connect', () => {
19
+ console.log('Connected to server');
20
+ });
21
+
22
+ socket.on('processing:started', (data) => {
23
+ setStatus('processing');
24
+ addLog('Processing started');
25
+ });
26
+
27
+ socket.on('processing:progress', (progress) => {
28
+ addLog(`Processing: ${progress.stage} - ${progress.percent}%`);
29
+ });
30
+
31
+ socket.on('processing:complete', (data) => {
32
+ setStatus('complete');
33
+ setManifest(data.manifest);
34
+ addLog('Processing complete!');
35
+ });
36
+
37
+ socket.on('processing:error', (data) => {
38
+ setStatus('error');
39
+ addLog(`Error: ${data.error}`, 'error');
40
+ });
41
+
42
+ return () => {
43
+ socket.off('connect');
44
+ socket.off('processing:started');
45
+ socket.off('processing:progress');
46
+ socket.off('processing:complete');
47
+ socket.off('processing:error');
48
+ };
49
+ }, []);
50
+
51
+ const addLog = (message, type = 'info') => {
52
+ setLogs(prev => [...prev, {
53
+ message,
54
+ type,
55
+ timestamp: Date.now()
56
+ }]);
57
+ };
58
+
59
+ const handleVideoUpload = async (file) => {
60
+ try {
61
+ setStatus('uploading');
62
+ setUploadProgress(0);
63
+ setLogs([]);
64
+
65
+ addLog(`Uploading ${file.name}...`);
66
+
67
+ await uploadVideo(file, socket.id, setUploadProgress);
68
+
69
+ addLog('Upload complete, processing started...');
70
+ } catch (error) {
71
+ setStatus('error');
72
+ addLog(`Upload failed: ${error.message}`, 'error');
73
+ }
74
+ };
75
+
76
+ return (
77
+ <div className="min-h-screen bg-gradient-to-br from-purple-50 to-blue-50 py-12 px-4">
78
+ <div className="max-w-4xl mx-auto">
79
+ <div className="text-center mb-12">
80
+ <h1 className="text-5xl font-bold text-gray-900 mb-4">
81
+ MES-Engine Demo
82
+ </h1>
83
+ <p className="text-xl text-gray-600">
84
+ Multi-Engine Video Processing Framework
85
+ </p>
86
+ </div>
87
+
88
+ {status === 'idle' && (
89
+ <VideoUploader
90
+ onUpload={handleVideoUpload}
91
+ disabled={status !== 'idle'}
92
+ />
93
+ )}
94
+
95
+ <ProcessingStatus
96
+ status={status}
97
+ progress={uploadProgress}
98
+ logs={logs}
99
+ />
100
+
101
+ {manifest && status === 'complete' && (
102
+ <VideoPlayer
103
+ manifest={manifest}
104
+ quality={selectedQuality}
105
+ onQualityChange={setSelectedQuality}
106
+ />
107
+ )}
108
+ </div>
109
+ </div>
110
+ );
111
+ }
112
+
113
+ export default App;
@@ -0,0 +1,71 @@
1
+ import React from 'react';
2
+ import { Loader, CheckCircle, AlertCircle } from 'lucide-react';
3
+
4
+ export const ProcessingStatus = ({ status, progress, logs }) => {
5
+ const getStatusIcon = () => {
6
+ switch (status) {
7
+ case 'processing':
8
+ return <Loader className="w-8 h-8 animate-spin text-purple-500" />;
9
+ case 'complete':
10
+ return <CheckCircle className="w-8 h-8 text-green-500" />;
11
+ case 'error':
12
+ return <AlertCircle className="w-8 h-8 text-red-500" />;
13
+ default:
14
+ return null;
15
+ }
16
+ };
17
+
18
+ const getStatusText = () => {
19
+ switch (status) {
20
+ case 'uploading':
21
+ return 'Uploading video...';
22
+ case 'processing':
23
+ return 'Processing video...';
24
+ case 'complete':
25
+ return 'Processing complete!';
26
+ case 'error':
27
+ return 'Processing failed';
28
+ default:
29
+ return '';
30
+ }
31
+ };
32
+
33
+ if (!status || status === 'idle') return null;
34
+
35
+ return (
36
+ <div className="bg-white rounded-xl shadow-lg p-6 mb-6">
37
+ <div className="flex items-center mb-4">
38
+ {getStatusIcon()}
39
+ <h3 className="text-xl font-semibold ml-3">{getStatusText()}</h3>
40
+ </div>
41
+
42
+ {progress !== null && status === 'uploading' && (
43
+ <div className="mb-4">
44
+ <div className="flex justify-between mb-1">
45
+ <span className="text-sm font-medium text-gray-700">Upload Progress</span>
46
+ <span className="text-sm font-medium text-gray-700">{progress}%</span>
47
+ </div>
48
+ <div className="w-full bg-gray-200 rounded-full h-2.5">
49
+ <div
50
+ className="bg-purple-600 h-2.5 rounded-full transition-all"
51
+ style={{ width: `${progress}%` }}
52
+ ></div>
53
+ </div>
54
+ </div>
55
+ )}
56
+
57
+ {logs && logs.length > 0 && (
58
+ <div className="mt-4">
59
+ <h4 className="text-sm font-semibold mb-2 text-gray-700">Processing Logs</h4>
60
+ <div className="bg-gray-900 rounded-lg p-4 max-h-48 overflow-y-auto font-mono text-xs">
61
+ {logs.map((log, index) => (
62
+ <div key={index} className="text-green-400 mb-1">
63
+ [{new Date(log.timestamp).toLocaleTimeString()}] {log.message}
64
+ </div>
65
+ ))}
66
+ </div>
67
+ </div>
68
+ )}
69
+ </div>
70
+ );
71
+ };
@@ -0,0 +1,87 @@
1
+ import React, { useRef, useEffect, useState } from 'react';
2
+ import { Play, Pause, Volume2, VolumeX } from 'lucide-react';
3
+
4
+ export const VideoPlayer = ({ manifest, quality, onQualityChange }) => {
5
+ const videoRef = useRef(null);
6
+ const [playing, setPlaying] = useState(false);
7
+ const [muted, setMuted] = useState(false);
8
+ const [currentChunk, setCurrentChunk] = useState(0);
9
+
10
+ const togglePlay = () => {
11
+ if (videoRef.current) {
12
+ if (playing) {
13
+ videoRef.current.pause();
14
+ } else {
15
+ videoRef.current.play();
16
+ }
17
+ setPlaying(!playing);
18
+ }
19
+ };
20
+
21
+ const toggleMute = () => {
22
+ if (videoRef.current) {
23
+ videoRef.current.muted = !muted;
24
+ setMuted(!muted);
25
+ }
26
+ };
27
+
28
+ return (
29
+ <div className="bg-white rounded-xl shadow-lg overflow-hidden">
30
+ <div className="relative bg-black">
31
+ <video
32
+ ref={videoRef}
33
+ className="w-full"
34
+ src={`http://localhost:3001/api/video/stream/${manifest.videoId}/${quality}/${currentChunk}`}
35
+ onEnded={() => {
36
+ if (currentChunk < manifest.chunks.length / manifest.qualities.length - 1) {
37
+ setCurrentChunk(currentChunk + 1);
38
+ }
39
+ }}
40
+ />
41
+
42
+ <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-4">
43
+ <div className="flex items-center justify-between text-white">
44
+ <div className="flex items-center space-x-4">
45
+ <button onClick={togglePlay} className="hover:scale-110 transition-transform">
46
+ {playing ? <Pause className="w-6 h-6" /> : <Play className="w-6 h-6" />}
47
+ </button>
48
+ <button onClick={toggleMute} className="hover:scale-110 transition-transform">
49
+ {muted ? <VolumeX className="w-6 h-6" /> : <Volume2 className="w-6 h-6" />}
50
+ </button>
51
+ <span className="text-sm">
52
+ Chunk {currentChunk + 1}
53
+ </span>
54
+ </div>
55
+
56
+ <div className="flex items-center space-x-2">
57
+ <span className="text-sm mr-2">Quality:</span>
58
+ {manifest.qualities.map((q) => (
59
+ <button
60
+ key={q.height}
61
+ onClick={() => onQualityChange(q.height)}
62
+ className={`px-3 py-1 rounded text-sm font-semibold transition-all ${
63
+ quality === q.height
64
+ ? 'bg-purple-600'
65
+ : 'bg-gray-700 hover:bg-gray-600'
66
+ }`}
67
+ >
68
+ {q.height}p
69
+ </button>
70
+ ))}
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <div className="p-4 bg-gray-50">
77
+ <h4 className="font-semibold mb-2">Video Information</h4>
78
+ <div className="grid grid-cols-2 gap-2 text-sm text-gray-600">
79
+ <div>ID: {manifest.videoId}</div>
80
+ <div>Created: {new Date(manifest.metadata.createdAt).toLocaleString()}</div>
81
+ <div>Qualities: {manifest.qualities.length}</div>
82
+ <div>Chunks: {manifest.chunks.length}</div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ );
87
+ };
@@ -0,0 +1,62 @@
1
+ import React, { useState } from 'react';
2
+ import { Upload } from 'lucide-react';
3
+
4
+ export const VideoUploader = ({ onUpload, disabled }) => {
5
+ const [dragActive, setDragActive] = useState(false);
6
+
7
+ const handleDrag = (e) => {
8
+ e.preventDefault();
9
+ e.stopPropagation();
10
+ if (e.type === "dragenter" || e.type === "dragover") {
11
+ setDragActive(true);
12
+ } else if (e.type === "dragleave") {
13
+ setDragActive(false);
14
+ }
15
+ };
16
+
17
+ const handleDrop = (e) => {
18
+ e.preventDefault();
19
+ e.stopPropagation();
20
+ setDragActive(false);
21
+
22
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
23
+ onUpload(e.dataTransfer.files[0]);
24
+ }
25
+ };
26
+
27
+ const handleChange = (e) => {
28
+ if (e.target.files && e.target.files[0]) {
29
+ onUpload(e.target.files[0]);
30
+ }
31
+ };
32
+
33
+ return (
34
+ <div
35
+ className={`relative border-2 border-dashed rounded-xl p-12 text-center transition-all ${
36
+ dragActive ? 'border-purple-500 bg-purple-50' : 'border-gray-300 hover:border-purple-400'
37
+ } ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
38
+ onDragEnter={handleDrag}
39
+ onDragLeave={handleDrag}
40
+ onDragOver={handleDrag}
41
+ onDrop={handleDrop}
42
+ >
43
+ <input
44
+ type="file"
45
+ className="hidden"
46
+ id="video-upload"
47
+ accept="video/*"
48
+ onChange={handleChange}
49
+ disabled={disabled}
50
+ />
51
+ <label htmlFor="video-upload" className={disabled ? '' : 'cursor-pointer'}>
52
+ <Upload className="w-16 h-16 mx-auto mb-4 text-purple-500" />
53
+ <p className="text-lg font-semibold text-gray-700 mb-2">
54
+ Click to upload or drag and drop
55
+ </p>
56
+ <p className="text-sm text-gray-500">
57
+ MP4, MOV, AVI, MKV (MAX. 500MB)
58
+ </p>
59
+ </label>
60
+ </div>
61
+ );
62
+ };
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
@@ -0,0 +1,10 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.jsx'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
@@ -0,0 +1,30 @@
1
+ import axios from 'axios';
2
+
3
+ const API_BASE_URL = 'http://localhost:3001/api';
4
+
5
+ export const uploadVideo = async (file, socketId, onProgress) => {
6
+ const formData = new FormData();
7
+ formData.append('video', file);
8
+ formData.append('socketId', socketId);
9
+
10
+ return axios.post(`${API_BASE_URL}/video/upload`, formData, {
11
+ headers: {
12
+ 'Content-Type': 'multipart/form-data',
13
+ },
14
+ onUploadProgress: (progressEvent) => {
15
+ const percentCompleted = Math.round(
16
+ (progressEvent.loaded * 100) / progressEvent.total
17
+ );
18
+ onProgress(percentCompleted);
19
+ },
20
+ });
21
+ };
22
+
23
+ export const getManifest = async (videoId) => {
24
+ const response = await axios.get(`${API_BASE_URL}/video/manifest/${videoId}`);
25
+ return response.data;
26
+ };
27
+
28
+ export const getStreamUrl = (videoId, quality, chunk) => {
29
+ return `${API_BASE_URL}/video/stream/${videoId}/${quality}/${chunk}`;
30
+ };
@@ -0,0 +1,10 @@
1
+ export default {
2
+ content: [
3
+ "./index.html",
4
+ "./src/**/*.{js,ts,jsx,tsx}",
5
+ ],
6
+ theme: {
7
+ extend: {},
8
+ },
9
+ plugins: [],
10
+ }
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vitejs.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ server: {
8
+ port: 5173,
9
+ proxy: {
10
+ '/api': {
11
+ target: 'http://localhost:3001',
12
+ changeOrigin: true,
13
+ },
14
+ }
15
+ }
16
+ })
@@ -0,0 +1,31 @@
1
+ # Simple Usage Example
2
+
3
+ This example demonstrates the basic application of the `mes-engine` package to process a video into adaptive streaming chunks (HLS).
4
+
5
+ ## Prerequisites
6
+
7
+ - [Node.js](https://nodejs.org/) (v16+)
8
+ - [FFmpeg](https://ffmpeg.org/) installed and available in your PATH.
9
+
10
+ ## Setup
11
+
12
+ 1. **Install dependencies:**
13
+ ```bash
14
+ npm install
15
+ ```
16
+
17
+ 2. **Add an input video:**
18
+ Place a video file named `input.mp4` in this directory (`examples/simple-usage/`).
19
+
20
+ 3. **Run the demo:**
21
+ ```bash
22
+ npm start
23
+ ```
24
+
25
+ ## What it does
26
+
27
+ - Initializes a `FileSystemStorage` to save output files in the `output/` directory.
28
+ - Uses `FFmpegEngine` for video transcoding.
29
+ - Configures `VideoProcessor` to generate two quality levels (720p and 1080p).
30
+ - Generates an HLS manifest (`.m3u8`) for adaptive streaming.
31
+ - Logs processing progress and the final manifest JSON to the console.
@@ -0,0 +1,68 @@
1
+ import {
2
+ VideoProcessor,
3
+ FFmpegEngine,
4
+ FileSystemStorage,
5
+ VideoManifest
6
+ } from '../../src/index.js';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ // Setup basic paths
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ async function runSimpleDemo() {
15
+ console.log('--- MES Engine Simple Demo ---');
16
+
17
+ // 1. Initialize the storage provider
18
+ // This is where processed chunks and manifests will be saved.
19
+ // In this version, FileSystemStorage doesn't take constructor arguments.
20
+ const storage = new FileSystemStorage();
21
+
22
+ // 2. Initialize the video engine
23
+ const engine = new FFmpegEngine();
24
+
25
+ // 3. Initialize the VideoProcessor
26
+ // The VideoProcessor requires: engine, storage, and a VideoConfig object.
27
+ const processor = new VideoProcessor(
28
+ engine,
29
+ storage,
30
+ {
31
+ chunkSize: 10, // 10 second chunks
32
+ cacheDir: path.join(__dirname, 'output'),
33
+ maxCacheSize: 1024 * 1024 * 1024, // 1GB
34
+ defaultQualities: [
35
+ { height: 720, bitrate: '2500k' },
36
+ { height: 1080, bitrate: '5000k' }
37
+ ]
38
+ }
39
+ );
40
+
41
+ // 4. Set up event listeners for progress
42
+ // Note: The actual events are defined in src/core/events.ts
43
+ processor.on('chunk_processed', (event) => {
44
+ console.log(`Chunk ${event.chunkNumber} completed at quality ${event.quality.height}p`);
45
+ });
46
+
47
+ // 5. Start processing a video
48
+ const inputPath = path.join(__dirname, 'input.mp4');
49
+
50
+ console.log(`Starting processing for: ${inputPath}`);
51
+
52
+ try {
53
+ // The actual method is processVideo, not process
54
+ const manifest: VideoManifest = await processor.processVideo(inputPath, {
55
+ title: 'Demo Video',
56
+ overallDescription: 'A simple demonstration of mes-engine'
57
+ });
58
+
59
+ console.log('Processing completed successfully!');
60
+ console.log('Manifest:', JSON.stringify(manifest, null, 2));
61
+ // Note: VideoManifest from src/core/types.ts doesn't have an 'hls' property yet
62
+ // based on the current file content.
63
+ } catch (error) {
64
+ console.error('Processing failed:', error);
65
+ }
66
+ }
67
+
68
+ runSimpleDemo().catch(console.error);