@honem/native-video-compressor 0.1.0 → 0.2.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/README.md +41 -240
- package/dist/index.js +0 -5
- package/dist/web.js +2 -2
- package/package.json +2 -10
- package/electron/README.md +0 -53
- package/electron/src/index.ts +0 -4
- package/electron/src/video-compressor.ts +0 -202
package/README.md
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
# @honem/native-video-compressor
|
|
2
2
|
|
|
3
|
-
Capacitor plugin for video compression using native platform APIs
|
|
3
|
+
Capacitor plugin for video compression using native platform APIs.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- ✅ **Native Performance**: Uses platform-native APIs for optimal performance
|
|
8
|
-
- ✅ **Multiple Formats**: Supports MP4, MOV, M4V, 3GP, and WebM
|
|
9
|
-
- ✅ **Configurable Quality**: Low, Medium, High, and Custom quality presets
|
|
10
|
-
- ✅ **Progress Tracking**: Real-time compression progress callbacks
|
|
11
|
-
- ✅ **Cross-Platform**: iOS, Android, and Electron support
|
|
5
|
+
- **iOS**: AVFoundation with VideoToolbox hardware acceleration
|
|
6
|
+
- **Android**: MediaCodec with hardware acceleration
|
|
12
7
|
|
|
13
8
|
## Installation
|
|
14
9
|
|
|
@@ -17,267 +12,73 @@ npm install @honem/native-video-compressor
|
|
|
17
12
|
npx cap sync
|
|
18
13
|
```
|
|
19
14
|
|
|
20
|
-
### Electron Setup
|
|
21
|
-
|
|
22
|
-
For Electron apps, you also need to install FFmpeg dependencies:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
npm install fluent-ffmpeg @ffmpeg-installer/ffmpeg
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Then register the plugin in your Electron main process (see [Electron Setup](#electron-setup) below).
|
|
29
|
-
|
|
30
15
|
## Usage
|
|
31
16
|
|
|
32
|
-
### Basic Usage
|
|
33
|
-
|
|
34
17
|
```typescript
|
|
35
18
|
import { VideoCompressor } from '@honem/native-video-compressor';
|
|
36
19
|
|
|
20
|
+
// Compress a video
|
|
37
21
|
const result = await VideoCompressor.compressVideo({
|
|
38
|
-
inputPath: '/path/to/input/video.mp4',
|
|
39
|
-
quality: 'medium',
|
|
40
|
-
// Format will be auto-detected from input file (MOV → MOV, MP4 → MP4)
|
|
41
|
-
// format: 'mp4', // Optional: specify format to override auto-detection
|
|
42
|
-
onProgress: (progress) => {
|
|
43
|
-
console.log(`Compression progress: ${progress}%`);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
console.log(`Compressed video saved to: ${result.outputPath}`);
|
|
48
|
-
console.log(`Compression ratio: ${result.compressionRatio}x`);
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Quality Presets
|
|
52
|
-
|
|
53
|
-
#### Low Quality
|
|
54
|
-
```typescript
|
|
55
|
-
await VideoCompressor.compressVideo({
|
|
56
|
-
inputPath: '/path/to/video.mp4',
|
|
57
|
-
quality: 'low', // 640x480, 500 kbps
|
|
58
|
-
});
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
#### Medium Quality (Default)
|
|
62
|
-
```typescript
|
|
63
|
-
await VideoCompressor.compressVideo({
|
|
64
|
-
inputPath: '/path/to/video.mp4',
|
|
65
|
-
quality: 'medium', // 1280x720, 2 Mbps
|
|
66
|
-
});
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
#### High Quality
|
|
70
|
-
```typescript
|
|
71
|
-
await VideoCompressor.compressVideo({
|
|
72
|
-
inputPath: '/path/to/video.mp4',
|
|
73
|
-
quality: 'high', // Original resolution, 8 Mbps
|
|
74
|
-
});
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
#### Custom Quality
|
|
78
|
-
```typescript
|
|
79
|
-
await VideoCompressor.compressVideo({
|
|
80
|
-
inputPath: '/path/to/video.mp4',
|
|
81
|
-
quality: 'custom',
|
|
82
|
-
bitrate: 4000000, // 4 Mbps
|
|
83
|
-
width: 1920,
|
|
84
|
-
height: 1080,
|
|
85
|
-
format: 'mp4',
|
|
86
|
-
});
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### Progress Tracking
|
|
90
|
-
|
|
91
|
-
**Note**: Progress callbacks (`onProgress`) are currently not supported on iOS and Android due to Capacitor's architecture limitations. Progress tracking would require implementing Capacitor's event system with `notifyListeners()`. For now, progress callbacks only work on Electron.
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
await VideoCompressor.compressVideo({
|
|
95
22
|
inputPath: '/path/to/video.mp4',
|
|
96
23
|
quality: 'medium',
|
|
97
|
-
onProgress: (progress) => {
|
|
98
|
-
// Progress is a number from 0 to 100
|
|
99
|
-
// Currently only works on Electron
|
|
100
|
-
updateProgressBar(progress);
|
|
101
|
-
}
|
|
102
24
|
});
|
|
103
|
-
```
|
|
104
25
|
|
|
105
|
-
|
|
26
|
+
console.log(result.outputPath); // Path to compressed file
|
|
27
|
+
console.log(result.compressionRatio); // e.g., 3.5
|
|
106
28
|
|
|
107
|
-
|
|
108
|
-
await VideoCompressor.
|
|
109
|
-
inputPath: '/path/to/input/video.mp4',
|
|
110
|
-
outputPath: '/path/to/output/compressed.mp4',
|
|
111
|
-
quality: 'medium',
|
|
112
|
-
});
|
|
29
|
+
// Optional: delete temp file immediately after use
|
|
30
|
+
await VideoCompressor.deleteFile({ path: result.outputPath });
|
|
113
31
|
```
|
|
114
32
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
## API Reference
|
|
118
|
-
|
|
119
|
-
### `compressVideo(options: CompressionOptions): Promise<CompressionResult>`
|
|
33
|
+
## API
|
|
120
34
|
|
|
121
|
-
|
|
35
|
+
### compressVideo(options)
|
|
122
36
|
|
|
123
|
-
|
|
37
|
+
| Option | Type | Required | Description |
|
|
38
|
+
|--------|------|----------|-------------|
|
|
39
|
+
| `inputPath` | string | Yes | Path to input video |
|
|
40
|
+
| `outputPath` | string | No | Output path (temp file if omitted) |
|
|
41
|
+
| `quality` | `'low'` \| `'medium'` \| `'high'` \| `'custom'` | No | Default: `'medium'` |
|
|
42
|
+
| `bitrate` | number | No | Bitrate in bps (for `'custom'` quality) |
|
|
43
|
+
| `width` | number | No | Output width (for `'custom'` quality) |
|
|
44
|
+
| `height` | number | No | Output height (for `'custom'` quality) |
|
|
45
|
+
| `format` | `'mp4'` \| `'mov'` \| `'m4v'` \| `'3gp'` \| `'webm'` | No | Default: same as input |
|
|
124
46
|
|
|
125
|
-
|
|
47
|
+
**Returns:**
|
|
126
48
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
- `onProgress` ((progress: number) => void, optional): Callback function for progress updates (0-100)
|
|
49
|
+
| Property | Type | Description |
|
|
50
|
+
|----------|------|-------------|
|
|
51
|
+
| `outputPath` | string | Path to compressed file |
|
|
52
|
+
| `originalSize` | number | Original size in bytes |
|
|
53
|
+
| `compressedSize` | number | Compressed size in bytes |
|
|
54
|
+
| `compressionRatio` | number | originalSize / compressedSize |
|
|
55
|
+
| `duration` | number | Compression time in ms |
|
|
135
56
|
|
|
136
|
-
|
|
57
|
+
### deleteFile(options)
|
|
137
58
|
|
|
138
|
-
|
|
59
|
+
When `outputPath` is not provided, compressed files are saved to the system temp directory which is automatically cleaned up by the OS. Use `deleteFile()` if you want to delete the file immediately after use.
|
|
139
60
|
|
|
140
|
-
|
|
141
|
-
- `originalSize` (number): Original file size in bytes
|
|
142
|
-
- `compressedSize` (number): Compressed file size in bytes
|
|
143
|
-
- `compressionRatio` (number): Compression ratio (originalSize / compressedSize)
|
|
144
|
-
- `duration` (number): Duration of the compression process in milliseconds
|
|
145
|
-
|
|
146
|
-
## Platform Support
|
|
147
|
-
|
|
148
|
-
### iOS
|
|
149
|
-
|
|
150
|
-
- **Framework**: AVFoundation
|
|
151
|
-
- **Supported Formats**: MP4, MOV, M4V
|
|
152
|
-
- **Hardware Acceleration**: Yes (VideoToolbox)
|
|
153
|
-
- **Minimum iOS Version**: iOS 11.0+
|
|
154
|
-
- **Progress Callbacks**: Not supported (Capacitor limitation - would require event listeners)
|
|
155
|
-
|
|
156
|
-
### Android
|
|
157
|
-
|
|
158
|
-
- **API**: MediaCodec, MediaMuxer
|
|
159
|
-
- **Supported Formats**: MP4, 3GP, WebM
|
|
160
|
-
- **Hardware Acceleration**: Yes (MediaCodec)
|
|
161
|
-
- **Minimum Android Version**: API 21 (Android 5.0)+
|
|
162
|
-
- **Progress Callbacks**: Not supported (Capacitor limitation - would require event listeners)
|
|
163
|
-
|
|
164
|
-
### Electron
|
|
165
|
-
|
|
166
|
-
- **Library**: FFmpeg (via fluent-ffmpeg)
|
|
167
|
-
- **Supported Formats**: All formats supported by FFmpeg (MP4, MOV, M4V, 3GP, WebM, etc.)
|
|
168
|
-
- **FFmpeg Source**: Bundled via @ffmpeg-installer/ffmpeg or system FFmpeg
|
|
169
|
-
|
|
170
|
-
#### Electron Setup
|
|
171
|
-
|
|
172
|
-
1. Install dependencies:
|
|
173
|
-
```bash
|
|
174
|
-
npm install fluent-ffmpeg @ffmpeg-installer/ffmpeg
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
2. The plugin works automatically with Capacitor's standard plugin system. No additional registration is required. Simply import and use:
|
|
61
|
+
Deletes a file.
|
|
178
62
|
|
|
179
63
|
```typescript
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// Works automatically in Electron
|
|
183
|
-
const result = await VideoCompressor.compressVideo({
|
|
184
|
-
inputPath: '/path/to/video.mp4',
|
|
185
|
-
quality: 'medium'
|
|
186
|
-
});
|
|
64
|
+
await VideoCompressor.deleteFile({ path: result.outputPath });
|
|
187
65
|
```
|
|
188
66
|
|
|
189
|
-
**Note**: The plugin automatically detects the Electron platform and uses the FFmpeg-based implementation. Make sure FFmpeg is available (via `@ffmpeg-installer/ffmpeg` or system PATH).
|
|
190
|
-
|
|
191
|
-
**Important**: This plugin does not require `@capacitor-community/electron`. It uses Capacitor's standard plugin system with `WebPlugin`, which is compatible with Capacitor 6.0.0+. If you encounter issues with plugin registration in Electron, you may need to manually register it in your Electron main process (see the [Electron README](./electron/README.md) for details).
|
|
192
|
-
|
|
193
|
-
### Web
|
|
194
|
-
|
|
195
|
-
- **Status**: Not supported
|
|
196
|
-
- **Reason**: Video compression requires native APIs or FFmpeg, which are not available in web browsers
|
|
197
|
-
- **Fallback**: Returns an error message directing users to use a native platform
|
|
198
|
-
|
|
199
67
|
## Quality Presets
|
|
200
68
|
|
|
201
|
-
| Preset | Resolution | Bitrate |
|
|
202
|
-
|
|
203
|
-
|
|
|
204
|
-
|
|
|
205
|
-
|
|
|
206
|
-
|
|
|
69
|
+
| Preset | Max Resolution | Bitrate |
|
|
70
|
+
|--------|---------------|---------|
|
|
71
|
+
| `low` | 854x480 | 800 kbps |
|
|
72
|
+
| `medium` | 1280x720 | 1.5 Mbps |
|
|
73
|
+
| `high` | Original | 8 Mbps |
|
|
74
|
+
| `custom` | User-defined | User-defined |
|
|
207
75
|
|
|
208
|
-
##
|
|
76
|
+
## Platform Requirements
|
|
209
77
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
- **
|
|
213
|
-
- **File URLs**: `file:///path/to/video.mp4`
|
|
214
|
-
- **Capacitor file paths**: `capacitor://localhost/_capacitor_file_/path/to/video.mp4`
|
|
215
|
-
- **Relative paths**: Resolved from the platform's default document directory
|
|
216
|
-
|
|
217
|
-
## Error Handling
|
|
218
|
-
|
|
219
|
-
The plugin throws errors for:
|
|
220
|
-
|
|
221
|
-
- Missing input file
|
|
222
|
-
- Invalid file paths
|
|
223
|
-
- Unsupported formats
|
|
224
|
-
- Compression failures
|
|
225
|
-
- Missing FFmpeg (Electron only)
|
|
226
|
-
|
|
227
|
-
Always wrap compression calls in try-catch blocks:
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
try {
|
|
231
|
-
const result = await VideoCompressor.compressVideo({
|
|
232
|
-
inputPath: '/path/to/video.mp4',
|
|
233
|
-
quality: 'medium',
|
|
234
|
-
});
|
|
235
|
-
} catch (error) {
|
|
236
|
-
console.error('Compression failed:', error);
|
|
237
|
-
}
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
## Examples
|
|
241
|
-
|
|
242
|
-
See the [example app](../src/App.tsx) in the main project for a complete React implementation.
|
|
243
|
-
|
|
244
|
-
## Development
|
|
245
|
-
|
|
246
|
-
### Building the Plugin
|
|
247
|
-
|
|
248
|
-
```bash
|
|
249
|
-
cd zoobook-compressor-plugin
|
|
250
|
-
npm install
|
|
251
|
-
npm run build
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
### Project Structure
|
|
255
|
-
|
|
256
|
-
```
|
|
257
|
-
zoobook-compressor-plugin/
|
|
258
|
-
├── src/ # TypeScript source
|
|
259
|
-
│ ├── definitions.ts # Type definitions
|
|
260
|
-
│ ├── index.ts # Plugin registration
|
|
261
|
-
│ └── web.ts # Web implementation (fallback)
|
|
262
|
-
├── ios/ # iOS native code
|
|
263
|
-
│ └── Plugin/
|
|
264
|
-
│ ├── VideoCompressor.swift
|
|
265
|
-
│ ├── VideoCompressorPlugin.swift
|
|
266
|
-
│ └── Plugin.m
|
|
267
|
-
├── android/ # Android native code
|
|
268
|
-
│ └── src/main/java/com/honem/nativevideocompressor/
|
|
269
|
-
│ ├── VideoCompressor.java
|
|
270
|
-
│ └── VideoCompressorPlugin.java
|
|
271
|
-
├── electron/ # Electron implementation
|
|
272
|
-
│ └── src/
|
|
273
|
-
│ └── video-compressor.ts
|
|
274
|
-
└── package.json
|
|
275
|
-
```
|
|
78
|
+
- **iOS**: 15.0+
|
|
79
|
+
- **Android**: API 22+ (Android 5.1)
|
|
80
|
+
- **Web**: Not supported
|
|
276
81
|
|
|
277
82
|
## License
|
|
278
83
|
|
|
279
84
|
MIT
|
|
280
|
-
|
|
281
|
-
## Contributing
|
|
282
|
-
|
|
283
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { registerPlugin } from '@capacitor/core';
|
|
2
|
-
// Register plugin with platform-specific implementations
|
|
3
|
-
// For Electron, the plugin will use the Electron implementation automatically
|
|
4
|
-
// when running in an Electron environment (via Capacitor's plugin bridge)
|
|
5
2
|
const VideoCompressor = registerPlugin('VideoCompressor', {
|
|
6
3
|
web: () => import('./web').then(m => new m.VideoCompressorWeb()),
|
|
7
|
-
// Electron implementation extends WebPlugin and works with Capacitor's standard plugin system
|
|
8
|
-
// It will be automatically used when the plugin is called in an Electron environment
|
|
9
4
|
});
|
|
10
5
|
export * from './definitions';
|
|
11
6
|
export { VideoCompressor };
|
package/dist/web.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { WebPlugin } from '@capacitor/core';
|
|
2
2
|
export class VideoCompressorWeb extends WebPlugin {
|
|
3
3
|
async compressVideo(_options) {
|
|
4
|
-
throw new Error('Video compression is not supported in web browsers. Please use a native platform (iOS
|
|
4
|
+
throw new Error('Video compression is not supported in web browsers. Please use a native platform (iOS or Android).');
|
|
5
5
|
}
|
|
6
6
|
async deleteFile(_options) {
|
|
7
|
-
throw new Error('File deletion is not supported in web browsers. Please use a native platform (iOS
|
|
7
|
+
throw new Error('File deletion is not supported in web browsers. Please use a native platform (iOS or Android).');
|
|
8
8
|
}
|
|
9
9
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@honem/native-video-compressor",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Capacitor plugin for video compression using native platform APIs (iOS AVFoundation, Android MediaCodec
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Capacitor plugin for video compression using native platform APIs (iOS AVFoundation, Android MediaCodec)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist/",
|
|
9
9
|
"ios/",
|
|
10
10
|
"android/",
|
|
11
|
-
"electron/",
|
|
12
11
|
"HonemNativeVideoCompressor.podspec"
|
|
13
12
|
],
|
|
14
13
|
"scripts": {
|
|
@@ -26,8 +25,6 @@
|
|
|
26
25
|
"native",
|
|
27
26
|
"ios",
|
|
28
27
|
"android",
|
|
29
|
-
"electron",
|
|
30
|
-
"ffmpeg",
|
|
31
28
|
"mediacodec",
|
|
32
29
|
"avfoundation"
|
|
33
30
|
],
|
|
@@ -55,10 +52,5 @@
|
|
|
55
52
|
"devDependencies": {
|
|
56
53
|
"@capacitor/core": "^6.2.0",
|
|
57
54
|
"typescript": "~5.9.3"
|
|
58
|
-
},
|
|
59
|
-
"optionalDependencies": {
|
|
60
|
-
"fluent-ffmpeg": "^2.1.2",
|
|
61
|
-
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
62
|
-
"@types/fluent-ffmpeg": "^2.1.24"
|
|
63
55
|
}
|
|
64
56
|
}
|
package/electron/README.md
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# Electron Implementation
|
|
2
|
-
|
|
3
|
-
This directory contains the Electron-specific implementation of the video compressor plugin.
|
|
4
|
-
|
|
5
|
-
## Setup
|
|
6
|
-
|
|
7
|
-
To use this plugin in an Electron app, you need to:
|
|
8
|
-
|
|
9
|
-
1. Install the required dependencies:
|
|
10
|
-
```bash
|
|
11
|
-
npm install fluent-ffmpeg @ffmpeg-installer/ffmpeg
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
2. Register the plugin in your Electron main process:
|
|
15
|
-
|
|
16
|
-
The plugin uses Capacitor's standard plugin registration. In your Electron main process, you can register it manually if needed:
|
|
17
|
-
|
|
18
|
-
```typescript
|
|
19
|
-
import { VideoCompressorElectron } from '@honem/native-video-compressor/electron';
|
|
20
|
-
import { Capacitor } from '@capacitor/core';
|
|
21
|
-
|
|
22
|
-
// Register the Electron implementation
|
|
23
|
-
if (Capacitor.getPlatform() === 'electron') {
|
|
24
|
-
// The plugin will be automatically registered when imported
|
|
25
|
-
// If you need manual registration, you can do:
|
|
26
|
-
const compressor = new VideoCompressorElectron();
|
|
27
|
-
// Register with Capacitor's bridge if needed
|
|
28
|
-
}
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Alternatively, you can use the plugin directly in your renderer process:
|
|
32
|
-
|
|
33
|
-
```typescript
|
|
34
|
-
import { VideoCompressor } from '@honem/native-video-compressor';
|
|
35
|
-
|
|
36
|
-
// The plugin will automatically use the Electron implementation
|
|
37
|
-
// when running in an Electron environment
|
|
38
|
-
const result = await VideoCompressor.compressVideo({
|
|
39
|
-
inputPath: '/path/to/video.mp4',
|
|
40
|
-
quality: 'medium'
|
|
41
|
-
});
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
**Note**: The plugin automatically detects the Electron platform and uses the appropriate implementation. No additional registration is typically required.
|
|
45
|
-
|
|
46
|
-
## FFmpeg
|
|
47
|
-
|
|
48
|
-
The Electron implementation uses FFmpeg for video compression. It will:
|
|
49
|
-
1. First try to use the bundled FFmpeg from `@ffmpeg-installer/ffmpeg`
|
|
50
|
-
2. Fall back to system FFmpeg if available in PATH
|
|
51
|
-
|
|
52
|
-
Make sure FFmpeg is available either via the npm package or installed on the system.
|
|
53
|
-
|
package/electron/src/index.ts
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import type { VideoCompressorPlugin, CompressionOptions, CompressionResult, DeleteFileOptions, DeleteFileResult } from '../../src/definitions';
|
|
2
|
-
import { WebPlugin } from '@capacitor/core';
|
|
3
|
-
import * as ffmpeg from 'fluent-ffmpeg';
|
|
4
|
-
import * as ffmpegInstaller from '@ffmpeg-installer/ffmpeg';
|
|
5
|
-
import * as path from 'path';
|
|
6
|
-
import * as fs from 'fs';
|
|
7
|
-
import * as os from 'os';
|
|
8
|
-
import { promisify } from 'util';
|
|
9
|
-
|
|
10
|
-
const stat = promisify(fs.stat);
|
|
11
|
-
const unlink = promisify(fs.unlink);
|
|
12
|
-
|
|
13
|
-
let ffmpegPath: string | null = null;
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
ffmpegPath = ffmpegInstaller.path;
|
|
17
|
-
ffmpeg.setFfmpegPath(ffmpegPath);
|
|
18
|
-
} catch {
|
|
19
|
-
try {
|
|
20
|
-
ffmpegPath = 'ffmpeg';
|
|
21
|
-
} catch {
|
|
22
|
-
console.warn('FFmpeg not found. Please install FFmpeg or use @ffmpeg-installer/ffmpeg');
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class VideoCompressorElectron extends WebPlugin implements VideoCompressorPlugin {
|
|
27
|
-
constructor() {
|
|
28
|
-
super();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async compressVideo(options: CompressionOptions): Promise<CompressionResult> {
|
|
32
|
-
if (!ffmpegPath) {
|
|
33
|
-
throw new Error('FFmpeg is not available. Please install FFmpeg or use @ffmpeg-installer/ffmpeg');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const startTime = Date.now();
|
|
37
|
-
const inputPath = this.resolveFilePath(options.inputPath);
|
|
38
|
-
const outputPath = options.outputPath
|
|
39
|
-
? this.resolveFilePath(options.outputPath)
|
|
40
|
-
: this.generateTempOutputPath(options.format || 'mp4');
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
await stat(inputPath);
|
|
44
|
-
} catch {
|
|
45
|
-
throw new Error(`Input file not found: ${inputPath}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const inputStats = await stat(inputPath);
|
|
49
|
-
const originalSize = inputStats.size;
|
|
50
|
-
|
|
51
|
-
const { targetBitrate, targetWidth, targetHeight, videoCodec, audioCodec, format } =
|
|
52
|
-
this.getCompressionParams(options);
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
await stat(outputPath);
|
|
56
|
-
await unlink(outputPath);
|
|
57
|
-
} catch {
|
|
58
|
-
// File doesn't exist, which is fine
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return new Promise((resolve, reject) => {
|
|
62
|
-
const command = ffmpeg(inputPath)
|
|
63
|
-
.videoCodec(videoCodec)
|
|
64
|
-
.audioCodec(audioCodec)
|
|
65
|
-
.outputOptions([
|
|
66
|
-
'-preset fast',
|
|
67
|
-
'-movflags +faststart',
|
|
68
|
-
]);
|
|
69
|
-
|
|
70
|
-
if (targetBitrate) {
|
|
71
|
-
command.videoBitrate(targetBitrate);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (targetWidth && targetHeight) {
|
|
75
|
-
command.size(`${targetWidth}x${targetHeight}`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
command.format(format);
|
|
79
|
-
command.output(outputPath);
|
|
80
|
-
|
|
81
|
-
command.on('error', (error) => {
|
|
82
|
-
reject(new Error(`FFmpeg error: ${error.message}`));
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
command.on('end', async () => {
|
|
86
|
-
try {
|
|
87
|
-
const outputStats = await stat(outputPath);
|
|
88
|
-
const compressedSize = outputStats.size;
|
|
89
|
-
const duration = Date.now() - startTime;
|
|
90
|
-
const compressionRatio = originalSize / compressedSize;
|
|
91
|
-
|
|
92
|
-
resolve({
|
|
93
|
-
outputPath,
|
|
94
|
-
originalSize,
|
|
95
|
-
compressedSize,
|
|
96
|
-
compressionRatio,
|
|
97
|
-
duration,
|
|
98
|
-
});
|
|
99
|
-
} catch (error) {
|
|
100
|
-
reject(new Error(`Failed to get output file stats: ${error}`));
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
command.run();
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async deleteFile(options: DeleteFileOptions): Promise<DeleteFileResult> {
|
|
109
|
-
try {
|
|
110
|
-
const filePath = this.resolveFilePath(options.path);
|
|
111
|
-
await stat(filePath);
|
|
112
|
-
await unlink(filePath);
|
|
113
|
-
return { success: true };
|
|
114
|
-
} catch {
|
|
115
|
-
return { success: false };
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private resolveFilePath(filePath: string): string {
|
|
120
|
-
if (filePath.startsWith('file://')) {
|
|
121
|
-
return filePath.replace('file://', '');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (path.isAbsolute(filePath)) {
|
|
125
|
-
return filePath;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return path.resolve(process.cwd(), filePath);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
private generateTempOutputPath(format: string): string {
|
|
132
|
-
const tempDir = os.tmpdir();
|
|
133
|
-
const extension = this.getFileExtension(format);
|
|
134
|
-
const uniqueName = `compressed_${Date.now()}.${extension}`;
|
|
135
|
-
return path.join(tempDir, uniqueName);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private getFileExtension(format: string): string {
|
|
139
|
-
switch (format.toLowerCase()) {
|
|
140
|
-
case 'mov':
|
|
141
|
-
return 'mov';
|
|
142
|
-
case 'm4v':
|
|
143
|
-
return 'm4v';
|
|
144
|
-
case 'webm':
|
|
145
|
-
return 'webm';
|
|
146
|
-
case '3gp':
|
|
147
|
-
return '3gp';
|
|
148
|
-
case 'mp4':
|
|
149
|
-
default:
|
|
150
|
-
return 'mp4';
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
private getCompressionParams(options: CompressionOptions) {
|
|
155
|
-
let targetBitrate: number | undefined;
|
|
156
|
-
let targetWidth: number | undefined;
|
|
157
|
-
let targetHeight: number | undefined;
|
|
158
|
-
let videoCodec = 'libx264';
|
|
159
|
-
let audioCodec = 'aac';
|
|
160
|
-
const format = options.format || 'mp4';
|
|
161
|
-
|
|
162
|
-
switch (options.quality) {
|
|
163
|
-
case 'low':
|
|
164
|
-
targetBitrate = 800;
|
|
165
|
-
targetWidth = 854;
|
|
166
|
-
targetHeight = 480;
|
|
167
|
-
break;
|
|
168
|
-
case 'high':
|
|
169
|
-
targetBitrate = options.bitrate ? options.bitrate / 1000 : 8000;
|
|
170
|
-
targetWidth = options.width;
|
|
171
|
-
targetHeight = options.height;
|
|
172
|
-
break;
|
|
173
|
-
case 'custom':
|
|
174
|
-
targetBitrate = options.bitrate ? options.bitrate / 1000 : 2000;
|
|
175
|
-
targetWidth = options.width;
|
|
176
|
-
targetHeight = options.height;
|
|
177
|
-
break;
|
|
178
|
-
default: // 'medium'
|
|
179
|
-
targetBitrate = 1500;
|
|
180
|
-
targetWidth = 1280;
|
|
181
|
-
targetHeight = 720;
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (format === 'webm') {
|
|
186
|
-
videoCodec = 'libvpx-vp9';
|
|
187
|
-
audioCodec = 'libopus';
|
|
188
|
-
} else if (format === '3gp') {
|
|
189
|
-
videoCodec = 'h263';
|
|
190
|
-
audioCodec = 'amr_nb';
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
targetBitrate,
|
|
195
|
-
targetWidth,
|
|
196
|
-
targetHeight,
|
|
197
|
-
videoCodec,
|
|
198
|
-
audioCodec,
|
|
199
|
-
format,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
}
|