capacitor-native-video-compressor 1.0.2 → 1.0.4
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 +153 -6
- package/android/src/main/java/com/atomic/videocompressor/NativeVideoCompressorPlugin.java +66 -67
- package/android/src/main/java/com/atomic/videocompressor/VideoCompressionService.java +8 -9
- package/dist/docs.json +11 -1
- package/dist/esm/definitions.d.ts +5 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +7 -0
- package/dist/esm/web.js +160 -8
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +160 -8
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +160 -10
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/NativeVideoCompressorPlugin/NativeVideoCompressorPlugin.swift +188 -73
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# capacitor-native-video-compressor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Capacitor plugin to compress videos natively on iOS (using AVFoundation), Android (using LightCompressor), and Web (using FFmpeg.wasm).
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -9,11 +9,147 @@ npm install capacitor-native-video-compressor
|
|
|
9
9
|
npx cap sync
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
## Supported Platforms
|
|
13
|
+
|
|
14
|
+
- **iOS**: Uses `AVAssetExportSession` (Hardware accelerated).
|
|
15
|
+
- **Android**: Uses `LightCompressor` (MediaCodec hardware accelerated).
|
|
16
|
+
- **Web**: Uses `@ffmpeg/ffmpeg` (WebAssembly).
|
|
17
|
+
|
|
18
|
+
## Native Configuration
|
|
19
|
+
|
|
20
|
+
### iOS
|
|
21
|
+
|
|
22
|
+
Remember to add the following to your `Info.plist` if you are picking videos from the Photo Library before compressing them:
|
|
23
|
+
|
|
24
|
+
```xml
|
|
25
|
+
<key>NSPhotoLibraryUsageDescription</key>
|
|
26
|
+
<string>We need access to your photo library to select videos for compression.</string>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Android
|
|
30
|
+
|
|
31
|
+
If you are reading videos from external storage, ensure you have the appropriate permissions in your `AndroidManifest.xml`:
|
|
32
|
+
|
|
33
|
+
```xml
|
|
34
|
+
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
|
35
|
+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Web Configuration (Required)
|
|
39
|
+
|
|
40
|
+
To use video compression on the Web platform, `FFmpeg.wasm` requires `SharedArrayBuffer` to function properly. You must configure your web server/framework to serve these specific HTTP headers:
|
|
41
|
+
|
|
42
|
+
```http
|
|
43
|
+
Cross-Origin-Embedder-Policy: credentialless
|
|
44
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Example: Nuxt 3 / Vite
|
|
48
|
+
|
|
49
|
+
If you are using Vite or Nuxt, update your configuration to include these headers and avoid optimizing the FFmpeg packages:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// nuxt.config.ts or vite.config.ts
|
|
53
|
+
export default defineNuxtConfig({
|
|
54
|
+
routeRules: {
|
|
55
|
+
'/**': {
|
|
56
|
+
headers: {
|
|
57
|
+
'Cross-Origin-Embedder-Policy': 'credentialless',
|
|
58
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
vite: {
|
|
63
|
+
server: {
|
|
64
|
+
headers: {
|
|
65
|
+
'Cross-Origin-Embedder-Policy': 'credentialless',
|
|
66
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
optimizeDeps: {
|
|
70
|
+
exclude: ['@ffmpeg/ffmpeg', '@ffmpeg/util', 'capacitor-native-video-compressor'],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
### 1. Initialization (Web Only)
|
|
79
|
+
|
|
80
|
+
Before compressing videos on the Web platform, you **must** initialize the FFmpeg core. This step is safely ignored on Native platforms (iOS/Android), so calling it cross-platform is perfectly fine.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { NativeVideoCompressor } from 'capacitor-native-video-compressor';
|
|
84
|
+
import { Capacitor } from '@capacitor/core';
|
|
85
|
+
|
|
86
|
+
const initCompressor = async () => {
|
|
87
|
+
if (Capacitor.getPlatform() === 'web') {
|
|
88
|
+
const result = await NativeVideoCompressor.initialize();
|
|
89
|
+
if (!result.success) {
|
|
90
|
+
console.error('Failed to load FFmpeg core:', result.message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 2. Compression & Progress Subscription
|
|
97
|
+
|
|
98
|
+
Subscribe to the `onProgress` event to get updates on the compression process, and then call `compressVideo()`.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { NativeVideoCompressor } from 'capacitor-native-video-compressor';
|
|
102
|
+
|
|
103
|
+
// 1. Add progress listener
|
|
104
|
+
const listener = await NativeVideoCompressor.addListener('onProgress', (info) => {
|
|
105
|
+
// info.status can be: 'started', 'loading_core', 'progress'
|
|
106
|
+
if (info.status === 'progress') {
|
|
107
|
+
console.log(`Compression Progress: ${info.percent}%`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 2. Start Compression
|
|
112
|
+
try {
|
|
113
|
+
const result = await NativeVideoCompressor.compressVideo({
|
|
114
|
+
sourcePath: '/path/to/original/video.mp4', // Web: blob URL or file path
|
|
115
|
+
quality: 'MEDIUM', // 'VERY_HIGH' | 'HIGH' | 'MEDIUM' | 'LOW' | 'VERY_LOW'
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
console.log('Compression successful! Saved at:', result.destPath);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('Compression failed:', error);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 3. Remove listener when done
|
|
124
|
+
listener.remove();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Quality Presets
|
|
128
|
+
|
|
129
|
+
The `quality` parameter determines the output resolution and compression ratio. Depending on the original aspect ratio, the video will be scaled to match the following heights (or widths, preserving aspect ratio):
|
|
130
|
+
|
|
131
|
+
| Preset | Target Resolution | Best For |
|
|
132
|
+
| ----------- | ----------------- | ----------------------------------------- |
|
|
133
|
+
| `VERY_HIGH` | ~ 1080p | Preserving original high quality |
|
|
134
|
+
| `HIGH` | ~ 720p | Good balance for social media sharing |
|
|
135
|
+
| `MEDIUM` | ~ 540p | Efficient storage and decent quality |
|
|
136
|
+
| `LOW` | ~ 480p | Standard low-bandwidth usage |
|
|
137
|
+
| `360P` | ~ 360p | Very small files |
|
|
138
|
+
| `VERY_LOW` | ~ 240p | Extreme compression, noticeably pixelated |
|
|
139
|
+
|
|
140
|
+
> **Note on Web Platform**: On Web, lower qualities also aggressively reduce the Audio Bitrate (down to 48kbps for VERY_LOW) to maximize space savings. On Native, audio maintains steady quality.
|
|
141
|
+
|
|
142
|
+
### Platform Differences for `sourcePath`
|
|
143
|
+
|
|
144
|
+
- **iOS/Android**: `sourcePath` must be a physical device file path (e.g., `file:///var/mobile/...` or `content://...`). Do not pass paths obtained from the Capacitor `WebView` (like `http://localhost/...`).
|
|
145
|
+
- **Web**: `sourcePath` can be a Blob URL (e.g., `blob:http://localhost:3000/...`), a base64 Data URI, or a direct link to a file if CORS allows.
|
|
146
|
+
|
|
12
147
|
## API
|
|
13
148
|
|
|
14
149
|
<docgen-index>
|
|
15
150
|
|
|
16
151
|
* [`compressVideo(...)`](#compressvideo)
|
|
152
|
+
* [`initialize()`](#initialize)
|
|
17
153
|
* [`addListener('onProgress', ...)`](#addlisteneronprogress-)
|
|
18
154
|
* [Interfaces](#interfaces)
|
|
19
155
|
|
|
@@ -37,6 +173,17 @@ compressVideo(options: CompressOptions) => Promise<CompressResult>
|
|
|
37
173
|
--------------------
|
|
38
174
|
|
|
39
175
|
|
|
176
|
+
### initialize()
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
initialize() => Promise<{ success: boolean; message?: string; }>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Returns:** <code>Promise<{ success: boolean; message?: string; }></code>
|
|
183
|
+
|
|
184
|
+
--------------------
|
|
185
|
+
|
|
186
|
+
|
|
40
187
|
### addListener('onProgress', ...)
|
|
41
188
|
|
|
42
189
|
```typescript
|
|
@@ -66,11 +213,11 @@ addListener(eventName: 'onProgress', listenerFunc: (info: { status: string; perc
|
|
|
66
213
|
|
|
67
214
|
#### CompressOptions
|
|
68
215
|
|
|
69
|
-
| Prop | Type
|
|
70
|
-
| ---------------- |
|
|
71
|
-
| **`sourcePath`** | <code>string</code>
|
|
72
|
-
| **`destPath`** | <code>string</code>
|
|
73
|
-
| **`quality`** | <code>'VERY_HIGH' \| 'HIGH' \| 'MEDIUM' \| 'LOW' \| 'VERY_LOW'</code> |
|
|
216
|
+
| Prop | Type |
|
|
217
|
+
| ---------------- | ------------------------------------------------------------------------------- |
|
|
218
|
+
| **`sourcePath`** | <code>string</code> |
|
|
219
|
+
| **`destPath`** | <code>string</code> |
|
|
220
|
+
| **`quality`** | <code>'VERY_HIGH' \| 'HIGH' \| 'MEDIUM' \| 'LOW' \| 'VERY_LOW' \| '360P'</code> |
|
|
74
221
|
|
|
75
222
|
|
|
76
223
|
#### PluginListenerHandle
|
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
package com.atomic.videocompressor;
|
|
2
2
|
|
|
3
|
-
import android.
|
|
4
|
-
|
|
3
|
+
import android.content.Intent;
|
|
5
4
|
import android.net.Uri;
|
|
6
|
-
import
|
|
5
|
+
import android.util.Log;
|
|
6
|
+
import androidx.annotation.NonNull;
|
|
7
|
+
import androidx.annotation.Nullable;
|
|
7
8
|
import com.abedelazizshe.lightcompressorlibrary.CompressionListener;
|
|
8
9
|
import com.abedelazizshe.lightcompressorlibrary.VideoCompressor;
|
|
9
10
|
import com.abedelazizshe.lightcompressorlibrary.VideoQuality;
|
|
10
|
-
import com.abedelazizshe.lightcompressorlibrary.config.Configuration;
|
|
11
11
|
import com.abedelazizshe.lightcompressorlibrary.config.AppSpecificStorageConfiguration;
|
|
12
|
+
import com.abedelazizshe.lightcompressorlibrary.config.Configuration;
|
|
12
13
|
import com.getcapacitor.JSObject;
|
|
13
14
|
import com.getcapacitor.Plugin;
|
|
14
15
|
import com.getcapacitor.PluginCall;
|
|
15
16
|
import com.getcapacitor.PluginMethod;
|
|
16
17
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
17
|
-
|
|
18
|
-
import androidx.annotation.NonNull;
|
|
19
|
-
import androidx.annotation.Nullable;
|
|
20
|
-
|
|
21
18
|
import java.io.File;
|
|
22
19
|
import java.util.Arrays;
|
|
23
|
-
import
|
|
20
|
+
import java.util.Collections;
|
|
24
21
|
|
|
25
22
|
@CapacitorPlugin(name = "NativeVideoCompressor")
|
|
26
23
|
public class NativeVideoCompressorPlugin extends Plugin {
|
|
@@ -28,7 +25,7 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
28
25
|
@PluginMethod
|
|
29
26
|
public void compressVideo(PluginCall call) {
|
|
30
27
|
String sourcePath = call.getString("sourcePath");
|
|
31
|
-
|
|
28
|
+
|
|
32
29
|
if (sourcePath == null) {
|
|
33
30
|
call.reject("Missing sourcePath");
|
|
34
31
|
return;
|
|
@@ -37,7 +34,7 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
37
34
|
// 1. Chuẩn bị Uri và Tên file xuất ra
|
|
38
35
|
Uri srcUri;
|
|
39
36
|
String fileName = "compressed_" + System.currentTimeMillis() + ".mp4";
|
|
40
|
-
|
|
37
|
+
|
|
41
38
|
if (sourcePath.startsWith("content://")) {
|
|
42
39
|
srcUri = Uri.parse(sourcePath);
|
|
43
40
|
} else if (sourcePath.startsWith("file://")) {
|
|
@@ -48,7 +45,7 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
48
45
|
|
|
49
46
|
// Đọc quality từ Javascript (Mặc định là MEDIUM)
|
|
50
47
|
String qualityString = call.getString("quality", "MEDIUM");
|
|
51
|
-
|
|
48
|
+
|
|
52
49
|
VideoQuality videoQuality = VideoQuality.MEDIUM;
|
|
53
50
|
if (qualityString != null) {
|
|
54
51
|
if (qualityString.equals("360P")) {
|
|
@@ -64,19 +61,22 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
64
61
|
}
|
|
65
62
|
}
|
|
66
63
|
|
|
67
|
-
|
|
64
|
+
Log.d(
|
|
65
|
+
"NativeVideoCompressor",
|
|
66
|
+
"====== VIDEO COMPRESSION QUALITY RECEIVED: " + qualityString + "+ video quality: " + videoQuality + " ======"
|
|
67
|
+
);
|
|
68
68
|
|
|
69
69
|
// 2. Cấu hình thông số nén (Dành riêng cho LightCompressor 1.3.2)
|
|
70
70
|
// Lưu ý: Java phải truyền đủ 9 tham số do thư viện gốc viết bằng Kotlin
|
|
71
71
|
Configuration configuration = new Configuration(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
videoQuality, // Chất lượng nén
|
|
73
|
+
false, // tắt tính năng giới hạn Bitrate tối thiểu của file gốc
|
|
74
|
+
null, // videoBitrateInMbps (Ép buộc giá trị Bitrate để đảm bảo file luôn nhẹ đi)
|
|
75
|
+
false, // disableAudio
|
|
76
|
+
false, // keepOriginalResolution
|
|
77
|
+
null, // videoWidth
|
|
78
|
+
null, // videoHeight
|
|
79
|
+
Arrays.asList(fileName) // videoNames
|
|
80
80
|
);
|
|
81
81
|
|
|
82
82
|
// 3. Cấu hình nơi lưu (Lưu an toàn vào thư mục nội bộ của App)
|
|
@@ -96,52 +96,51 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
96
96
|
|
|
97
97
|
// 4. Bắt đầu nén (Đã cập nhật List Uri và int index)
|
|
98
98
|
VideoCompressor.start(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
call.reject("Nén thất bại: " + failureMessage);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
@Override
|
|
123
|
-
public void onSuccess(int i, long l, @Nullable String s) {
|
|
124
|
-
getContext().stopService(serviceIntent);
|
|
125
|
-
// Nén xong, trả kết quả đường dẫn file mới về cho JS
|
|
126
|
-
JSObject ret = new JSObject();
|
|
127
|
-
ret.put("success", true);
|
|
128
|
-
ret.put("destPath", s);
|
|
129
|
-
call.resolve(ret);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
@Override
|
|
133
|
-
public void onStart(int i) {
|
|
134
|
-
JSObject ret = new JSObject();
|
|
135
|
-
ret.put("status", "started");
|
|
136
|
-
notifyListeners("onProgress", ret);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
@Override
|
|
140
|
-
public void onCancelled(int i) {
|
|
141
|
-
getContext().stopService(serviceIntent);
|
|
142
|
-
call.reject("Đã hủy nén");
|
|
143
|
-
}
|
|
99
|
+
getContext(),
|
|
100
|
+
Collections.singletonList(srcUri), // Nhận vào một List chứa đường dẫn
|
|
101
|
+
false, // isStreamable
|
|
102
|
+
null, // sharedStorageConfiguration (Không dùng)
|
|
103
|
+
storageConfig, // Cấu hình lưu trữ
|
|
104
|
+
configuration, // Cấu hình chất lượng
|
|
105
|
+
new CompressionListener() {
|
|
106
|
+
@Override
|
|
107
|
+
public void onProgress(int i, float v) {
|
|
108
|
+
// Gửi % tiến độ về Javascript để vẽ thanh Progress
|
|
109
|
+
JSObject ret = new JSObject();
|
|
110
|
+
ret.put("status", "progress");
|
|
111
|
+
ret.put("percent", v);
|
|
112
|
+
notifyListeners("onProgress", ret);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@Override
|
|
116
|
+
public void onFailure(int i, @NonNull String failureMessage) {
|
|
117
|
+
getContext().stopService(serviceIntent);
|
|
118
|
+
call.reject("Nén thất bại: " + failureMessage);
|
|
144
119
|
}
|
|
120
|
+
|
|
121
|
+
@Override
|
|
122
|
+
public void onSuccess(int i, long l, @Nullable String s) {
|
|
123
|
+
getContext().stopService(serviceIntent);
|
|
124
|
+
// Nén xong, trả kết quả đường dẫn file mới về cho JS
|
|
125
|
+
JSObject ret = new JSObject();
|
|
126
|
+
ret.put("success", true);
|
|
127
|
+
ret.put("destPath", s);
|
|
128
|
+
call.resolve(ret);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@Override
|
|
132
|
+
public void onStart(int i) {
|
|
133
|
+
JSObject ret = new JSObject();
|
|
134
|
+
ret.put("status", "started");
|
|
135
|
+
notifyListeners("onProgress", ret);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@Override
|
|
139
|
+
public void onCancelled(int i) {
|
|
140
|
+
getContext().stopService(serviceIntent);
|
|
141
|
+
call.reject("Đã hủy nén");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
145
144
|
);
|
|
146
145
|
}
|
|
147
|
-
}
|
|
146
|
+
}
|
|
@@ -7,7 +7,6 @@ import android.app.Service;
|
|
|
7
7
|
import android.content.Intent;
|
|
8
8
|
import android.os.Build;
|
|
9
9
|
import android.os.IBinder;
|
|
10
|
-
|
|
11
10
|
import androidx.annotation.Nullable;
|
|
12
11
|
import androidx.core.app.NotificationCompat;
|
|
13
12
|
|
|
@@ -25,11 +24,11 @@ public class VideoCompressionService extends Service {
|
|
|
25
24
|
@Override
|
|
26
25
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
27
26
|
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
.setContentTitle("Đang nén video")
|
|
28
|
+
.setContentText("Tiến trình đang chạy ngầm...")
|
|
29
|
+
.setSmallIcon(android.R.drawable.ic_popup_sync)
|
|
30
|
+
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
31
|
+
.build();
|
|
33
32
|
|
|
34
33
|
// Check for Android 14+ specific foreground service typing requirements if compiled against API 34+
|
|
35
34
|
// In this case, we rely on the manifest declaration for dataSync.
|
|
@@ -51,9 +50,9 @@ public class VideoCompressionService extends Service {
|
|
|
51
50
|
private void createNotificationChannel() {
|
|
52
51
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
53
52
|
NotificationChannel serviceChannel = new NotificationChannel(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
CHANNEL_ID,
|
|
54
|
+
"Video Compression Service Channel",
|
|
55
|
+
NotificationManager.IMPORTANCE_LOW
|
|
57
56
|
);
|
|
58
57
|
NotificationManager manager = getSystemService(NotificationManager.class);
|
|
59
58
|
if (manager != null) {
|
package/dist/docs.json
CHANGED
|
@@ -24,6 +24,16 @@
|
|
|
24
24
|
],
|
|
25
25
|
"slug": "compressvideo"
|
|
26
26
|
},
|
|
27
|
+
{
|
|
28
|
+
"name": "initialize",
|
|
29
|
+
"signature": "() => Promise<{ success: boolean; message?: string; }>",
|
|
30
|
+
"parameters": [],
|
|
31
|
+
"returns": "Promise<{ success: boolean; message?: string | undefined; }>",
|
|
32
|
+
"tags": [],
|
|
33
|
+
"docs": "",
|
|
34
|
+
"complexTypes": [],
|
|
35
|
+
"slug": "initialize"
|
|
36
|
+
},
|
|
27
37
|
{
|
|
28
38
|
"name": "addListener",
|
|
29
39
|
"signature": "(eventName: 'onProgress', listenerFunc: (info: { status: string; percent?: number; }) => void) => Promise<PluginListenerHandle>",
|
|
@@ -100,7 +110,7 @@
|
|
|
100
110
|
"tags": [],
|
|
101
111
|
"docs": "",
|
|
102
112
|
"complexTypes": [],
|
|
103
|
-
"type": "'VERY_HIGH' | 'HIGH' | 'MEDIUM' | 'LOW' | 'VERY_LOW' | undefined"
|
|
113
|
+
"type": "'VERY_HIGH' | 'HIGH' | 'MEDIUM' | 'LOW' | 'VERY_LOW' | '360P' | undefined"
|
|
104
114
|
}
|
|
105
115
|
]
|
|
106
116
|
},
|
|
@@ -2,7 +2,7 @@ import type { PluginListenerHandle } from '@capacitor/core';
|
|
|
2
2
|
export interface CompressOptions {
|
|
3
3
|
sourcePath: string;
|
|
4
4
|
destPath?: string;
|
|
5
|
-
quality?: 'VERY_HIGH' | 'HIGH' | 'MEDIUM' | 'LOW' | 'VERY_LOW';
|
|
5
|
+
quality?: 'VERY_HIGH' | 'HIGH' | 'MEDIUM' | 'LOW' | 'VERY_LOW' | '360P';
|
|
6
6
|
}
|
|
7
7
|
export interface CompressResult {
|
|
8
8
|
success: boolean;
|
|
@@ -10,6 +10,10 @@ export interface CompressResult {
|
|
|
10
10
|
}
|
|
11
11
|
export interface NativeVideoCompressorPlugin {
|
|
12
12
|
compressVideo(options: CompressOptions): Promise<CompressResult>;
|
|
13
|
+
initialize(): Promise<{
|
|
14
|
+
success: boolean;
|
|
15
|
+
message?: string;
|
|
16
|
+
}>;
|
|
13
17
|
addListener(eventName: 'onProgress', listenerFunc: (info: {
|
|
14
18
|
status: string;
|
|
15
19
|
percent?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\nexport interface CompressOptions {\n sourcePath: string;\n destPath?: string;\n quality?: 'VERY_HIGH' | 'HIGH' | 'MEDIUM' | 'LOW' | 'VERY_LOW';\n}\n\nexport interface CompressResult {\n success: boolean;\n destPath: string;\n}\n\nexport interface NativeVideoCompressorPlugin {\n compressVideo(options: CompressOptions): Promise<CompressResult>;\n\n // Đã sửa lại chuẩn cho Capacitor mới nhất (chỉ trả về Promise)\n addListener(\n eventName: 'onProgress',\n listenerFunc: (info: { status: string; percent?: number }) => void,\n ): Promise<PluginListenerHandle>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\nexport interface CompressOptions {\n sourcePath: string;\n destPath?: string;\n quality?: 'VERY_HIGH' | 'HIGH' | 'MEDIUM' | 'LOW' | 'VERY_LOW' | '360P';\n}\n\nexport interface CompressResult {\n success: boolean;\n destPath: string;\n}\n\nexport interface NativeVideoCompressorPlugin {\n compressVideo(options: CompressOptions): Promise<CompressResult>;\n\n initialize(): Promise<{ success: boolean; message?: string }>;\n\n // Đã sửa lại chuẩn cho Capacitor mới nhất (chỉ trả về Promise)\n addListener(\n eventName: 'onProgress',\n listenerFunc: (info: { status: string; percent?: number }) => void,\n ): Promise<PluginListenerHandle>;\n}\n"]}
|
package/dist/esm/web.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { WebPlugin } from '@capacitor/core';
|
|
2
2
|
import type { NativeVideoCompressorPlugin, CompressOptions, CompressResult } from './definitions';
|
|
3
3
|
export declare class NativeVideoCompressorWeb extends WebPlugin implements NativeVideoCompressorPlugin {
|
|
4
|
+
private ffmpeg;
|
|
5
|
+
private isLoaded;
|
|
6
|
+
private loadFFmpeg;
|
|
7
|
+
initialize(): Promise<{
|
|
8
|
+
success: boolean;
|
|
9
|
+
message?: string;
|
|
10
|
+
}>;
|
|
4
11
|
compressVideo(options: CompressOptions): Promise<CompressResult>;
|
|
5
12
|
}
|
package/dist/esm/web.js
CHANGED
|
@@ -1,14 +1,166 @@
|
|
|
1
1
|
import { WebPlugin } from '@capacitor/core';
|
|
2
|
+
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
|
3
|
+
import { fetchFile, toBlobURL } from '@ffmpeg/util';
|
|
2
4
|
export class NativeVideoCompressorWeb extends WebPlugin {
|
|
5
|
+
constructor() {
|
|
6
|
+
super(...arguments);
|
|
7
|
+
this.ffmpeg = null;
|
|
8
|
+
this.isLoaded = false;
|
|
9
|
+
}
|
|
10
|
+
async loadFFmpeg() {
|
|
11
|
+
if (this.isLoaded && this.ffmpeg)
|
|
12
|
+
return;
|
|
13
|
+
this.notifyListeners('onProgress', { status: 'loading_core', percent: 0 });
|
|
14
|
+
try {
|
|
15
|
+
this.ffmpeg = new FFmpeg();
|
|
16
|
+
// Listen to FFmpeg progress and map it to capacitor plugin events
|
|
17
|
+
this.ffmpeg.on('progress', ({ progress }) => {
|
|
18
|
+
// progress is a fraction between 0 and 1
|
|
19
|
+
this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });
|
|
20
|
+
});
|
|
21
|
+
this.ffmpeg.on('log', ({ message }) => {
|
|
22
|
+
console.log('[FFmpeg Log]', message);
|
|
23
|
+
});
|
|
24
|
+
// Use standard single-threaded version for best compatibility across browsers
|
|
25
|
+
// without requiring complex SharedArrayBuffer headers configuration
|
|
26
|
+
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';
|
|
27
|
+
console.log('Starting to load FFmpeg core from:', baseURL);
|
|
28
|
+
await this.ffmpeg.load({
|
|
29
|
+
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
|
|
30
|
+
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
|
|
31
|
+
});
|
|
32
|
+
this.isLoaded = true;
|
|
33
|
+
console.log('FFmpeg core loaded successfully');
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
console.error('Failed to load FFmpeg:', e);
|
|
37
|
+
throw e;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async initialize() {
|
|
41
|
+
try {
|
|
42
|
+
if (this.isLoaded) {
|
|
43
|
+
return { success: true, message: 'Already initialized' };
|
|
44
|
+
}
|
|
45
|
+
await this.loadFFmpeg();
|
|
46
|
+
return { success: true, message: 'Initialization successful' };
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
return { success: false, message: e.message || 'Initialization failed' };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
3
52
|
async compressVideo(options) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
53
|
+
try {
|
|
54
|
+
if (!this.isLoaded) {
|
|
55
|
+
await this.loadFFmpeg();
|
|
56
|
+
}
|
|
57
|
+
if (!this.ffmpeg) {
|
|
58
|
+
throw new Error('FFmpeg failed to load');
|
|
59
|
+
}
|
|
60
|
+
this.notifyListeners('onProgress', { status: 'started', percent: 0 });
|
|
61
|
+
const { sourcePath, quality = 'MEDIUM' } = options;
|
|
62
|
+
const inputFileName = 'input.mp4';
|
|
63
|
+
const outputFileName = 'output.mp4';
|
|
64
|
+
// Map quality to CRF, Scale, and Audio Bitrate
|
|
65
|
+
let crf = '28';
|
|
66
|
+
let scale = ''; // default keep resolution
|
|
67
|
+
let audioBitrate = '128k';
|
|
68
|
+
switch (quality) {
|
|
69
|
+
case 'VERY_HIGH': // 1080p
|
|
70
|
+
crf = '23';
|
|
71
|
+
scale = '-vf scale=-2:1080';
|
|
72
|
+
audioBitrate = '128k';
|
|
73
|
+
break;
|
|
74
|
+
case 'HIGH': // 720p
|
|
75
|
+
crf = '28';
|
|
76
|
+
scale = '-vf scale=-2:720';
|
|
77
|
+
audioBitrate = '128k';
|
|
78
|
+
break;
|
|
79
|
+
case 'MEDIUM': // 540p
|
|
80
|
+
crf = '30';
|
|
81
|
+
scale = '-vf scale=-2:540';
|
|
82
|
+
audioBitrate = '96k';
|
|
83
|
+
break;
|
|
84
|
+
case 'LOW': // 480p
|
|
85
|
+
crf = '32';
|
|
86
|
+
scale = '-vf scale=-2:480';
|
|
87
|
+
audioBitrate = '96k';
|
|
88
|
+
break;
|
|
89
|
+
case '360P': // 360p
|
|
90
|
+
crf = '28';
|
|
91
|
+
scale = '-vf scale=-2:360';
|
|
92
|
+
audioBitrate = '64k';
|
|
93
|
+
break;
|
|
94
|
+
case 'VERY_LOW': // 240p
|
|
95
|
+
crf = '35';
|
|
96
|
+
scale = '-vf scale=-2:240';
|
|
97
|
+
audioBitrate = '48k';
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
console.log('[FFmpeg] Fetching source file...', sourcePath);
|
|
101
|
+
let inputData;
|
|
102
|
+
try {
|
|
103
|
+
inputData = await fetchFile(sourcePath);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error('[FFmpeg] Error fetching source file:', err);
|
|
107
|
+
throw new Error('Could not fetch source video file');
|
|
108
|
+
}
|
|
109
|
+
console.log('[FFmpeg] Writing file to FS...', inputData.byteLength, 'bytes');
|
|
110
|
+
try {
|
|
111
|
+
await this.ffmpeg.writeFile(inputFileName, inputData);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
console.error('[FFmpeg] Error writing to FS:', err);
|
|
115
|
+
throw new Error('Could not write to FFmpeg FS');
|
|
116
|
+
}
|
|
117
|
+
// Construct FFmpeg command array
|
|
118
|
+
const args = ['-i', inputFileName];
|
|
119
|
+
// Video codec and compression parameters (superfast provides much better size than ultrafast)
|
|
120
|
+
args.push('-c:v', 'libx264', '-preset', 'superfast', '-crf', crf);
|
|
121
|
+
// Resize if needed (split by space)
|
|
122
|
+
if (scale) {
|
|
123
|
+
args.push(...scale.split(' '));
|
|
124
|
+
}
|
|
125
|
+
// Audio re-encoding to save space
|
|
126
|
+
args.push('-c:a', 'aac', '-b:a', audioBitrate);
|
|
127
|
+
// Output
|
|
128
|
+
args.push(outputFileName);
|
|
129
|
+
console.log('[FFmpeg] Executing command:', args.join(' '));
|
|
130
|
+
try {
|
|
131
|
+
const retCode = await this.ffmpeg.exec(args);
|
|
132
|
+
console.log('[FFmpeg] Exec returned code:', retCode);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
console.error('[FFmpeg] Error executing command:', err);
|
|
136
|
+
throw new Error('FFmpeg execution failed');
|
|
137
|
+
}
|
|
138
|
+
console.log('[FFmpeg] Reading output file...');
|
|
139
|
+
let data;
|
|
140
|
+
try {
|
|
141
|
+
data = await this.ffmpeg.readFile(outputFileName);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
console.error('[FFmpeg] Error reading output file:', err);
|
|
145
|
+
throw new Error('Could not read FFmpeg output');
|
|
146
|
+
}
|
|
147
|
+
// Free memory
|
|
148
|
+
await this.ffmpeg.deleteFile(inputFileName);
|
|
149
|
+
await this.ffmpeg.deleteFile(outputFileName);
|
|
150
|
+
console.log('[FFmpeg] Creating Blob URL from output...', data.byteLength, 'bytes');
|
|
151
|
+
// Create a blob URL for the resulting file
|
|
152
|
+
const blob = new Blob([data], { type: 'video/mp4' });
|
|
153
|
+
const destPath = URL.createObjectURL(blob);
|
|
154
|
+
console.log('[FFmpeg] Compression complete, returning path:', destPath);
|
|
155
|
+
return {
|
|
156
|
+
success: true,
|
|
157
|
+
destPath: destPath,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
console.error('[FFmpeg] Compression process failed:', error);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
12
164
|
}
|
|
13
165
|
}
|
|
14
166
|
//# sourceMappingURL=web.js.map
|