capacitor-native-video-compressor 0.0.1 → 1.0.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # capacitor-native-video-compressor
2
2
 
3
- Native video compressor using LightCompressor and AVFoundation
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&lt;{ success: boolean; message?: string; }&gt;</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,24 +1,23 @@
1
1
  package com.atomic.videocompressor;
2
2
 
3
+ import android.content.Intent;
3
4
  import android.net.Uri;
4
- import java.util.Collections;
5
+ import android.util.Log;
6
+ import androidx.annotation.NonNull;
7
+ import androidx.annotation.Nullable;
5
8
  import com.abedelazizshe.lightcompressorlibrary.CompressionListener;
6
9
  import com.abedelazizshe.lightcompressorlibrary.VideoCompressor;
7
10
  import com.abedelazizshe.lightcompressorlibrary.VideoQuality;
8
- import com.abedelazizshe.lightcompressorlibrary.config.Configuration;
9
11
  import com.abedelazizshe.lightcompressorlibrary.config.AppSpecificStorageConfiguration;
12
+ import com.abedelazizshe.lightcompressorlibrary.config.Configuration;
10
13
  import com.getcapacitor.JSObject;
11
14
  import com.getcapacitor.Plugin;
12
15
  import com.getcapacitor.PluginCall;
13
16
  import com.getcapacitor.PluginMethod;
14
17
  import com.getcapacitor.annotation.CapacitorPlugin;
15
-
16
- import androidx.annotation.NonNull;
17
- import androidx.annotation.Nullable;
18
-
19
18
  import java.io.File;
20
19
  import java.util.Arrays;
21
- import android.content.Intent;
20
+ import java.util.Collections;
22
21
 
23
22
  @CapacitorPlugin(name = "NativeVideoCompressor")
24
23
  public class NativeVideoCompressorPlugin extends Plugin {
@@ -26,7 +25,7 @@ public class NativeVideoCompressorPlugin extends Plugin {
26
25
  @PluginMethod
27
26
  public void compressVideo(PluginCall call) {
28
27
  String sourcePath = call.getString("sourcePath");
29
-
28
+
30
29
  if (sourcePath == null) {
31
30
  call.reject("Missing sourcePath");
32
31
  return;
@@ -35,7 +34,7 @@ public class NativeVideoCompressorPlugin extends Plugin {
35
34
  // 1. Chuẩn bị Uri và Tên file xuất ra
36
35
  Uri srcUri;
37
36
  String fileName = "compressed_" + System.currentTimeMillis() + ".mp4";
38
-
37
+
39
38
  if (sourcePath.startsWith("content://")) {
40
39
  srcUri = Uri.parse(sourcePath);
41
40
  } else if (sourcePath.startsWith("file://")) {
@@ -46,26 +45,38 @@ public class NativeVideoCompressorPlugin extends Plugin {
46
45
 
47
46
  // Đọc quality từ Javascript (Mặc định là MEDIUM)
48
47
  String qualityString = call.getString("quality", "MEDIUM");
48
+
49
49
  VideoQuality videoQuality = VideoQuality.MEDIUM;
50
50
  if (qualityString != null) {
51
- try {
52
- videoQuality = VideoQuality.valueOf(qualityString);
53
- } catch (IllegalArgumentException e) {
54
- videoQuality = VideoQuality.MEDIUM;
51
+ if (qualityString.equals("360P")) {
52
+ videoQuality = VideoQuality.LOW; // Map 360p to LOW quality on Android
53
+ } else if (qualityString.equals("LOW")) {
54
+ videoQuality = VideoQuality.VERY_LOW; // Adjusting others dynamically if needed
55
+ } else {
56
+ try {
57
+ videoQuality = VideoQuality.valueOf(qualityString);
58
+ } catch (IllegalArgumentException e) {
59
+ videoQuality = VideoQuality.MEDIUM;
60
+ }
55
61
  }
56
62
  }
57
63
 
64
+ Log.d(
65
+ "NativeVideoCompressor",
66
+ "====== VIDEO COMPRESSION QUALITY RECEIVED: " + qualityString + "+ video quality: " + videoQuality + " ======"
67
+ );
68
+
58
69
  // 2. Cấu hình thông số nén (Dành riêng cho LightCompressor 1.3.2)
59
70
  // Lưu ý: Java phải truyền đủ 9 tham số do thư viện gốc viết bằng Kotlin
60
71
  Configuration configuration = new Configuration(
61
- videoQuality, // Chất lượng nén
62
- false, // isMinBitrateCheckEnabled
63
- null, // videoBitrateInMbps
64
- false, // disableAudio
65
- false, // keepOriginalResolution
66
- null, // videoWidth
67
- null, // videoHeight
68
- Arrays.asList(fileName) // videoNames
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
69
80
  );
70
81
 
71
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)
@@ -85,52 +96,51 @@ public class NativeVideoCompressorPlugin extends Plugin {
85
96
 
86
97
  // 4. Bắt đầu nén (Đã cập nhật List Uri và int index)
87
98
  VideoCompressor.start(
88
- getContext(),
89
- Collections.singletonList(srcUri), // Nhận vào một List chứa đường dẫn
90
- false, // isStreamable
91
- null, // sharedStorageConfiguration (Không dùng)
92
- storageConfig, // Cấu hình lưu trữ
93
- configuration, // Cấu hình chất lượng
94
- new CompressionListener() {
95
-
96
- @Override
97
- public void onProgress(int i, float v) {
98
- // Gửi % tiến độ về Javascript để vẽ thanh Progress
99
- JSObject ret = new JSObject();
100
- ret.put("status", "progress");
101
- ret.put("percent", v);
102
- notifyListeners("onProgress", ret);
103
- }
104
-
105
- @Override
106
- public void onFailure(int i, @NonNull String failureMessage) {
107
- getContext().stopService(serviceIntent);
108
- call.reject("Nén thất bại: " + failureMessage);
109
- }
110
-
111
- @Override
112
- public void onSuccess(int i, long l, @Nullable String s) {
113
- getContext().stopService(serviceIntent);
114
- // Nén xong, trả kết quả đường dẫn file mới về cho JS
115
- JSObject ret = new JSObject();
116
- ret.put("success", true);
117
- ret.put("destPath", s);
118
- call.resolve(ret);
119
- }
120
-
121
- @Override
122
- public void onStart(int i) {
123
- JSObject ret = new JSObject();
124
- ret.put("status", "started");
125
- notifyListeners("onProgress", ret);
126
- }
127
-
128
- @Override
129
- public void onCancelled(int i) {
130
- getContext().stopService(serviceIntent);
131
- call.reject("Đã hủy nén");
132
- }
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);
133
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
+ }
134
144
  );
135
145
  }
136
- }
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
- .setContentTitle("Đang nén video")
29
- .setContentText("Tiến trình đang chạy ngầm...")
30
- .setSmallIcon(android.R.drawable.ic_popup_sync)
31
- .setPriority(NotificationCompat.PRIORITY_LOW)
32
- .build();
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
- CHANNEL_ID,
55
- "Video Compression Service Channel",
56
- NotificationManager.IMPORTANCE_LOW
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
- console.warn('Tính năng nén video bằng GPU không hỗ trợ trên trình duyệt Web.', options);
5
- // Báo lỗi cho chuẩn Capacitor báo rằng nền tảng này không được hỗ trợ
6
- throw this.unimplemented('Not implemented on web.');
7
- // (Hoặc nếu bạn không muốn văng lỗi trên web, bạn có thể trả về luôn đường dẫn gốc)
8
- // return {
9
- // success: false,
10
- // destPath: options.sourcePath
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
@@ -1 +1 @@
1
- {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,MAAM,OAAO,wBAAyB,SAAQ,SAAS;IACrD,KAAK,CAAC,aAAa,CAAC,OAAwB;QAC1C,OAAO,CAAC,IAAI,CAAC,iEAAiE,EAAE,OAAO,CAAC,CAAC;QAEzF,sEAAsE;QACtE,MAAM,IAAI,CAAC,aAAa,CAAC,yBAAyB,CAAC,CAAC;QAEpD,oFAAoF;QACpF,WAAW;QACX,oBAAoB;QACpB,iCAAiC;QACjC,KAAK;IACP,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\nimport type { NativeVideoCompressorPlugin, CompressOptions, CompressResult } from './definitions';\n\nexport class NativeVideoCompressorWeb extends WebPlugin implements NativeVideoCompressorPlugin {\n async compressVideo(options: CompressOptions): Promise<CompressResult> {\n console.warn('Tính năng nén video bằng GPU không hỗ trợ trên trình duyệt Web.', options);\n\n // Báo lỗi cho chuẩn Capacitor báo rằng nền tảng này không được hỗ trợ\n throw this.unimplemented('Not implemented on web.');\n\n // (Hoặc nếu bạn không muốn văng lỗi trên web, bạn có thể trả về luôn đường dẫn gốc)\n // return {\n // success: false,\n // destPath: options.sourcePath\n // };\n }\n}\n"]}
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"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","sourcesContent":["import { WebPlugin } from '@capacitor/core';\nimport { FFmpeg } from '@ffmpeg/ffmpeg';\nimport { fetchFile, toBlobURL } from '@ffmpeg/util';\n\nimport type { NativeVideoCompressorPlugin, CompressOptions, CompressResult } from './definitions';\n\nexport class NativeVideoCompressorWeb extends WebPlugin implements NativeVideoCompressorPlugin {\n private ffmpeg: FFmpeg | null = null;\n private isLoaded = false;\n\n private async loadFFmpeg() {\n if (this.isLoaded && this.ffmpeg) return;\n\n this.notifyListeners('onProgress', { status: 'loading_core', percent: 0 });\n\n try {\n this.ffmpeg = new FFmpeg();\n\n // Listen to FFmpeg progress and map it to capacitor plugin events\n this.ffmpeg.on('progress', ({ progress }) => {\n // progress is a fraction between 0 and 1\n this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });\n });\n\n this.ffmpeg.on('log', ({ message }) => {\n console.log('[FFmpeg Log]', message);\n });\n\n // Use standard single-threaded version for best compatibility across browsers\n // without requiring complex SharedArrayBuffer headers configuration\n const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';\n\n console.log('Starting to load FFmpeg core from:', baseURL);\n await this.ffmpeg.load({\n coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),\n wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),\n });\n\n this.isLoaded = true;\n console.log('FFmpeg core loaded successfully');\n } catch (e: any) {\n console.error('Failed to load FFmpeg:', e);\n throw e;\n }\n }\n\n async initialize(): Promise<{ success: boolean; message?: string }> {\n try {\n if (this.isLoaded) {\n return { success: true, message: 'Already initialized' };\n }\n await this.loadFFmpeg();\n return { success: true, message: 'Initialization successful' };\n } catch (e: any) {\n return { success: false, message: e.message || 'Initialization failed' };\n }\n }\n\n async compressVideo(options: CompressOptions): Promise<CompressResult> {\n try {\n if (!this.isLoaded) {\n await this.loadFFmpeg();\n }\n\n if (!this.ffmpeg) {\n throw new Error('FFmpeg failed to load');\n }\n\n this.notifyListeners('onProgress', { status: 'started', percent: 0 });\n\n const { sourcePath, quality = 'MEDIUM' } = options;\n const inputFileName = 'input.mp4';\n const outputFileName = 'output.mp4';\n\n // Map quality to CRF, Scale, and Audio Bitrate\n let crf = '28';\n let scale = ''; // default keep resolution\n let audioBitrate = '128k';\n\n switch (quality) {\n case 'VERY_HIGH': // 1080p\n crf = '23';\n scale = '-vf scale=-2:1080';\n audioBitrate = '128k';\n break;\n case 'HIGH': // 720p\n crf = '28';\n scale = '-vf scale=-2:720';\n audioBitrate = '128k';\n break;\n case 'MEDIUM': // 540p\n crf = '30';\n scale = '-vf scale=-2:540';\n audioBitrate = '96k';\n break;\n case 'LOW': // 480p\n crf = '32';\n scale = '-vf scale=-2:480';\n audioBitrate = '96k';\n break;\n case '360P': // 360p\n crf = '28';\n scale = '-vf scale=-2:360';\n audioBitrate = '64k';\n break;\n case 'VERY_LOW': // 240p\n crf = '35';\n scale = '-vf scale=-2:240';\n audioBitrate = '48k';\n break;\n }\n\n console.log('[FFmpeg] Fetching source file...', sourcePath);\n let inputData: Uint8Array;\n try {\n inputData = await fetchFile(sourcePath);\n } catch (err) {\n console.error('[FFmpeg] Error fetching source file:', err);\n throw new Error('Could not fetch source video file');\n }\n\n console.log('[FFmpeg] Writing file to FS...', inputData.byteLength, 'bytes');\n try {\n await this.ffmpeg.writeFile(inputFileName, inputData);\n } catch (err) {\n console.error('[FFmpeg] Error writing to FS:', err);\n throw new Error('Could not write to FFmpeg FS');\n }\n\n // Construct FFmpeg command array\n const args: string[] = ['-i', inputFileName];\n\n // Video codec and compression parameters (superfast provides much better size than ultrafast)\n args.push('-c:v', 'libx264', '-preset', 'superfast', '-crf', crf);\n\n // Resize if needed (split by space)\n if (scale) {\n args.push(...scale.split(' '));\n }\n\n // Audio re-encoding to save space\n args.push('-c:a', 'aac', '-b:a', audioBitrate);\n\n // Output\n args.push(outputFileName);\n\n console.log('[FFmpeg] Executing command:', args.join(' '));\n try {\n const retCode = await this.ffmpeg.exec(args);\n console.log('[FFmpeg] Exec returned code:', retCode);\n } catch (err) {\n console.error('[FFmpeg] Error executing command:', err);\n throw new Error('FFmpeg execution failed');\n }\n\n console.log('[FFmpeg] Reading output file...');\n let data: any;\n try {\n data = await this.ffmpeg.readFile(outputFileName);\n } catch (err) {\n console.error('[FFmpeg] Error reading output file:', err);\n throw new Error('Could not read FFmpeg output');\n }\n\n // Free memory\n await this.ffmpeg.deleteFile(inputFileName);\n await this.ffmpeg.deleteFile(outputFileName);\n\n console.log('[FFmpeg] Creating Blob URL from output...', data.byteLength, 'bytes');\n // Create a blob URL for the resulting file\n const blob = new Blob([data as any], { type: 'video/mp4' });\n const destPath = URL.createObjectURL(blob);\n\n console.log('[FFmpeg] Compression complete, returning path:', destPath);\n return {\n success: true,\n destPath: destPath,\n };\n } catch (error: any) {\n console.error('[FFmpeg] Compression process failed:', error);\n throw error;\n }\n }\n}\n"]}
@@ -1,21 +1,173 @@
1
1
  'use strict';
2
2
 
3
3
  var core = require('@capacitor/core');
4
+ var ffmpeg = require('@ffmpeg/ffmpeg');
5
+ var util = require('@ffmpeg/util');
4
6
 
5
7
  const NativeVideoCompressor = core.registerPlugin('NativeVideoCompressor', {
6
8
  web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.NativeVideoCompressorWeb()),
7
9
  });
8
10
 
9
11
  class NativeVideoCompressorWeb extends core.WebPlugin {
12
+ constructor() {
13
+ super(...arguments);
14
+ this.ffmpeg = null;
15
+ this.isLoaded = false;
16
+ }
17
+ async loadFFmpeg() {
18
+ if (this.isLoaded && this.ffmpeg)
19
+ return;
20
+ this.notifyListeners('onProgress', { status: 'loading_core', percent: 0 });
21
+ try {
22
+ this.ffmpeg = new ffmpeg.FFmpeg();
23
+ // Listen to FFmpeg progress and map it to capacitor plugin events
24
+ this.ffmpeg.on('progress', ({ progress }) => {
25
+ // progress is a fraction between 0 and 1
26
+ this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });
27
+ });
28
+ this.ffmpeg.on('log', ({ message }) => {
29
+ console.log('[FFmpeg Log]', message);
30
+ });
31
+ // Use standard single-threaded version for best compatibility across browsers
32
+ // without requiring complex SharedArrayBuffer headers configuration
33
+ const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';
34
+ console.log('Starting to load FFmpeg core from:', baseURL);
35
+ await this.ffmpeg.load({
36
+ coreURL: await util.toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
37
+ wasmURL: await util.toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
38
+ });
39
+ this.isLoaded = true;
40
+ console.log('FFmpeg core loaded successfully');
41
+ }
42
+ catch (e) {
43
+ console.error('Failed to load FFmpeg:', e);
44
+ throw e;
45
+ }
46
+ }
47
+ async initialize() {
48
+ try {
49
+ if (this.isLoaded) {
50
+ return { success: true, message: 'Already initialized' };
51
+ }
52
+ await this.loadFFmpeg();
53
+ return { success: true, message: 'Initialization successful' };
54
+ }
55
+ catch (e) {
56
+ return { success: false, message: e.message || 'Initialization failed' };
57
+ }
58
+ }
10
59
  async compressVideo(options) {
11
- console.warn('Tính năng nén video bằng GPU không hỗ trợ trên trình duyệt Web.', options);
12
- // Báo lỗi cho chuẩn Capacitor báo rằng nền tảng này không được hỗ trợ
13
- throw this.unimplemented('Not implemented on web.');
14
- // (Hoặc nếu bạn không muốn văng lỗi trên web, bạn có thể trả về luôn đường dẫn gốc)
15
- // return {
16
- // success: false,
17
- // destPath: options.sourcePath
18
- // };
60
+ try {
61
+ if (!this.isLoaded) {
62
+ await this.loadFFmpeg();
63
+ }
64
+ if (!this.ffmpeg) {
65
+ throw new Error('FFmpeg failed to load');
66
+ }
67
+ this.notifyListeners('onProgress', { status: 'started', percent: 0 });
68
+ const { sourcePath, quality = 'MEDIUM' } = options;
69
+ const inputFileName = 'input.mp4';
70
+ const outputFileName = 'output.mp4';
71
+ // Map quality to CRF, Scale, and Audio Bitrate
72
+ let crf = '28';
73
+ let scale = ''; // default keep resolution
74
+ let audioBitrate = '128k';
75
+ switch (quality) {
76
+ case 'VERY_HIGH': // 1080p
77
+ crf = '23';
78
+ scale = '-vf scale=-2:1080';
79
+ audioBitrate = '128k';
80
+ break;
81
+ case 'HIGH': // 720p
82
+ crf = '28';
83
+ scale = '-vf scale=-2:720';
84
+ audioBitrate = '128k';
85
+ break;
86
+ case 'MEDIUM': // 540p
87
+ crf = '30';
88
+ scale = '-vf scale=-2:540';
89
+ audioBitrate = '96k';
90
+ break;
91
+ case 'LOW': // 480p
92
+ crf = '32';
93
+ scale = '-vf scale=-2:480';
94
+ audioBitrate = '96k';
95
+ break;
96
+ case '360P': // 360p
97
+ crf = '28';
98
+ scale = '-vf scale=-2:360';
99
+ audioBitrate = '64k';
100
+ break;
101
+ case 'VERY_LOW': // 240p
102
+ crf = '35';
103
+ scale = '-vf scale=-2:240';
104
+ audioBitrate = '48k';
105
+ break;
106
+ }
107
+ console.log('[FFmpeg] Fetching source file...', sourcePath);
108
+ let inputData;
109
+ try {
110
+ inputData = await util.fetchFile(sourcePath);
111
+ }
112
+ catch (err) {
113
+ console.error('[FFmpeg] Error fetching source file:', err);
114
+ throw new Error('Could not fetch source video file');
115
+ }
116
+ console.log('[FFmpeg] Writing file to FS...', inputData.byteLength, 'bytes');
117
+ try {
118
+ await this.ffmpeg.writeFile(inputFileName, inputData);
119
+ }
120
+ catch (err) {
121
+ console.error('[FFmpeg] Error writing to FS:', err);
122
+ throw new Error('Could not write to FFmpeg FS');
123
+ }
124
+ // Construct FFmpeg command array
125
+ const args = ['-i', inputFileName];
126
+ // Video codec and compression parameters (superfast provides much better size than ultrafast)
127
+ args.push('-c:v', 'libx264', '-preset', 'superfast', '-crf', crf);
128
+ // Resize if needed (split by space)
129
+ if (scale) {
130
+ args.push(...scale.split(' '));
131
+ }
132
+ // Audio re-encoding to save space
133
+ args.push('-c:a', 'aac', '-b:a', audioBitrate);
134
+ // Output
135
+ args.push(outputFileName);
136
+ console.log('[FFmpeg] Executing command:', args.join(' '));
137
+ try {
138
+ const retCode = await this.ffmpeg.exec(args);
139
+ console.log('[FFmpeg] Exec returned code:', retCode);
140
+ }
141
+ catch (err) {
142
+ console.error('[FFmpeg] Error executing command:', err);
143
+ throw new Error('FFmpeg execution failed');
144
+ }
145
+ console.log('[FFmpeg] Reading output file...');
146
+ let data;
147
+ try {
148
+ data = await this.ffmpeg.readFile(outputFileName);
149
+ }
150
+ catch (err) {
151
+ console.error('[FFmpeg] Error reading output file:', err);
152
+ throw new Error('Could not read FFmpeg output');
153
+ }
154
+ // Free memory
155
+ await this.ffmpeg.deleteFile(inputFileName);
156
+ await this.ffmpeg.deleteFile(outputFileName);
157
+ console.log('[FFmpeg] Creating Blob URL from output...', data.byteLength, 'bytes');
158
+ // Create a blob URL for the resulting file
159
+ const blob = new Blob([data], { type: 'video/mp4' });
160
+ const destPath = URL.createObjectURL(blob);
161
+ console.log('[FFmpeg] Compression complete, returning path:', destPath);
162
+ return {
163
+ success: true,
164
+ destPath: destPath,
165
+ };
166
+ }
167
+ catch (error) {
168
+ console.error('[FFmpeg] Compression process failed:', error);
169
+ throw error;
170
+ }
19
171
  }
20
172
  }
21
173
 
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst NativeVideoCompressor = registerPlugin('NativeVideoCompressor', {\n web: () => import('./web').then((m) => new m.NativeVideoCompressorWeb()),\n});\nexport * from './definitions';\nexport { NativeVideoCompressor };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class NativeVideoCompressorWeb extends WebPlugin {\n async compressVideo(options) {\n console.warn('Tính năng nén video bằng GPU không hỗ trợ trên trình duyệt Web.', options);\n // Báo lỗi cho chuẩn Capacitor báo rằng nền tảng này không được hỗ trợ\n throw this.unimplemented('Not implemented on web.');\n // (Hoặc nếu bạn không muốn văng lỗi trên web, bạn có thể trả về luôn đường dẫn gốc)\n // return {\n // success: false,\n // destPath: options.sourcePath\n // };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,qBAAqB,GAAGA,mBAAc,CAAC,uBAAuB,EAAE;AACtE,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,wBAAwB,EAAE,CAAC;AAC5E,CAAC;;ACFM,MAAM,wBAAwB,SAASC,cAAS,CAAC;AACxD,IAAI,MAAM,aAAa,CAAC,OAAO,EAAE;AACjC,QAAQ,OAAO,CAAC,IAAI,CAAC,iEAAiE,EAAE,OAAO,CAAC;AAChG;AACA,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,yBAAyB,CAAC;AAC3D;AACA;AACA;AACA;AACA;AACA,IAAI;AACJ;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst NativeVideoCompressor = registerPlugin('NativeVideoCompressor', {\n web: () => import('./web').then((m) => new m.NativeVideoCompressorWeb()),\n});\nexport * from './definitions';\nexport { NativeVideoCompressor };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nimport { FFmpeg } from '@ffmpeg/ffmpeg';\nimport { fetchFile, toBlobURL } from '@ffmpeg/util';\nexport class NativeVideoCompressorWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.ffmpeg = null;\n this.isLoaded = false;\n }\n async loadFFmpeg() {\n if (this.isLoaded && this.ffmpeg)\n return;\n this.notifyListeners('onProgress', { status: 'loading_core', percent: 0 });\n try {\n this.ffmpeg = new FFmpeg();\n // Listen to FFmpeg progress and map it to capacitor plugin events\n this.ffmpeg.on('progress', ({ progress }) => {\n // progress is a fraction between 0 and 1\n this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });\n });\n this.ffmpeg.on('log', ({ message }) => {\n console.log('[FFmpeg Log]', message);\n });\n // Use standard single-threaded version for best compatibility across browsers\n // without requiring complex SharedArrayBuffer headers configuration\n const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';\n console.log('Starting to load FFmpeg core from:', baseURL);\n await this.ffmpeg.load({\n coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),\n wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),\n });\n this.isLoaded = true;\n console.log('FFmpeg core loaded successfully');\n }\n catch (e) {\n console.error('Failed to load FFmpeg:', e);\n throw e;\n }\n }\n async initialize() {\n try {\n if (this.isLoaded) {\n return { success: true, message: 'Already initialized' };\n }\n await this.loadFFmpeg();\n return { success: true, message: 'Initialization successful' };\n }\n catch (e) {\n return { success: false, message: e.message || 'Initialization failed' };\n }\n }\n async compressVideo(options) {\n try {\n if (!this.isLoaded) {\n await this.loadFFmpeg();\n }\n if (!this.ffmpeg) {\n throw new Error('FFmpeg failed to load');\n }\n this.notifyListeners('onProgress', { status: 'started', percent: 0 });\n const { sourcePath, quality = 'MEDIUM' } = options;\n const inputFileName = 'input.mp4';\n const outputFileName = 'output.mp4';\n // Map quality to CRF, Scale, and Audio Bitrate\n let crf = '28';\n let scale = ''; // default keep resolution\n let audioBitrate = '128k';\n switch (quality) {\n case 'VERY_HIGH': // 1080p\n crf = '23';\n scale = '-vf scale=-2:1080';\n audioBitrate = '128k';\n break;\n case 'HIGH': // 720p\n crf = '28';\n scale = '-vf scale=-2:720';\n audioBitrate = '128k';\n break;\n case 'MEDIUM': // 540p\n crf = '30';\n scale = '-vf scale=-2:540';\n audioBitrate = '96k';\n break;\n case 'LOW': // 480p\n crf = '32';\n scale = '-vf scale=-2:480';\n audioBitrate = '96k';\n break;\n case '360P': // 360p\n crf = '28';\n scale = '-vf scale=-2:360';\n audioBitrate = '64k';\n break;\n case 'VERY_LOW': // 240p\n crf = '35';\n scale = '-vf scale=-2:240';\n audioBitrate = '48k';\n break;\n }\n console.log('[FFmpeg] Fetching source file...', sourcePath);\n let inputData;\n try {\n inputData = await fetchFile(sourcePath);\n }\n catch (err) {\n console.error('[FFmpeg] Error fetching source file:', err);\n throw new Error('Could not fetch source video file');\n }\n console.log('[FFmpeg] Writing file to FS...', inputData.byteLength, 'bytes');\n try {\n await this.ffmpeg.writeFile(inputFileName, inputData);\n }\n catch (err) {\n console.error('[FFmpeg] Error writing to FS:', err);\n throw new Error('Could not write to FFmpeg FS');\n }\n // Construct FFmpeg command array\n const args = ['-i', inputFileName];\n // Video codec and compression parameters (superfast provides much better size than ultrafast)\n args.push('-c:v', 'libx264', '-preset', 'superfast', '-crf', crf);\n // Resize if needed (split by space)\n if (scale) {\n args.push(...scale.split(' '));\n }\n // Audio re-encoding to save space\n args.push('-c:a', 'aac', '-b:a', audioBitrate);\n // Output\n args.push(outputFileName);\n console.log('[FFmpeg] Executing command:', args.join(' '));\n try {\n const retCode = await this.ffmpeg.exec(args);\n console.log('[FFmpeg] Exec returned code:', retCode);\n }\n catch (err) {\n console.error('[FFmpeg] Error executing command:', err);\n throw new Error('FFmpeg execution failed');\n }\n console.log('[FFmpeg] Reading output file...');\n let data;\n try {\n data = await this.ffmpeg.readFile(outputFileName);\n }\n catch (err) {\n console.error('[FFmpeg] Error reading output file:', err);\n throw new Error('Could not read FFmpeg output');\n }\n // Free memory\n await this.ffmpeg.deleteFile(inputFileName);\n await this.ffmpeg.deleteFile(outputFileName);\n console.log('[FFmpeg] Creating Blob URL from output...', data.byteLength, 'bytes');\n // Create a blob URL for the resulting file\n const blob = new Blob([data], { type: 'video/mp4' });\n const destPath = URL.createObjectURL(blob);\n console.log('[FFmpeg] Compression complete, returning path:', destPath);\n return {\n success: true,\n destPath: destPath,\n };\n }\n catch (error) {\n console.error('[FFmpeg] Compression process failed:', error);\n throw error;\n }\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin","FFmpeg","toBlobURL","fetchFile"],"mappings":";;;;;;AACK,MAAC,qBAAqB,GAAGA,mBAAc,CAAC,uBAAuB,EAAE;AACtE,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,wBAAwB,EAAE,CAAC;AAC5E,CAAC;;ACAM,MAAM,wBAAwB,SAASC,cAAS,CAAC;AACxD,IAAI,WAAW,GAAG;AAClB,QAAQ,KAAK,CAAC,GAAG,SAAS,CAAC;AAC3B,QAAQ,IAAI,CAAC,MAAM,GAAG,IAAI;AAC1B,QAAQ,IAAI,CAAC,QAAQ,GAAG,KAAK;AAC7B,IAAI;AACJ,IAAI,MAAM,UAAU,GAAG;AACvB,QAAQ,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM;AACxC,YAAY;AACZ,QAAQ,IAAI,CAAC,eAAe,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;AAClF,QAAQ,IAAI;AACZ,YAAY,IAAI,CAAC,MAAM,GAAG,IAAIC,aAAM,EAAE;AACtC;AACA,YAAY,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK;AACzD;AACA,gBAAgB,IAAI,CAAC,eAAe,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,GAAG,CAAC,EAAE,CAAC;AAC/G,YAAY,CAAC,CAAC;AACd,YAAY,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK;AACnD,gBAAgB,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,OAAO,CAAC;AACpD,YAAY,CAAC,CAAC;AACd;AACA;AACA,YAAY,MAAM,OAAO,GAAG,gDAAgD;AAC5E,YAAY,OAAO,CAAC,GAAG,CAAC,oCAAoC,EAAE,OAAO,CAAC;AACtE,YAAY,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;AACnC,gBAAgB,OAAO,EAAE,MAAMC,cAAS,CAAC,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,EAAE,iBAAiB,CAAC;AACxF,gBAAgB,OAAO,EAAE,MAAMA,cAAS,CAAC,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,EAAE,kBAAkB,CAAC;AAC3F,aAAa,CAAC;AACd,YAAY,IAAI,CAAC,QAAQ,GAAG,IAAI;AAChC,YAAY,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC;AAC1D,QAAQ;AACR,QAAQ,OAAO,CAAC,EAAE;AAClB,YAAY,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,CAAC,CAAC;AACtD,YAAY,MAAM,CAAC;AACnB,QAAQ;AACR,IAAI;AACJ,IAAI,MAAM,UAAU,GAAG;AACvB,QAAQ,IAAI;AACZ,YAAY,IAAI,IAAI,CAAC,QAAQ,EAAE;AAC/B,gBAAgB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,qBAAqB,EAAE;AACxE,YAAY;AACZ,YAAY,MAAM,IAAI,CAAC,UAAU,EAAE;AACnC,YAAY,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,2BAA2B,EAAE;AAC1E,QAAQ;AACR,QAAQ,OAAO,CAAC,EAAE;AAClB,YAAY,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,uBAAuB,EAAE;AACpF,QAAQ;AACR,IAAI;AACJ,IAAI,MAAM,aAAa,CAAC,OAAO,EAAE;AACjC,QAAQ,IAAI;AACZ,YAAY,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE;AAChC,gBAAgB,MAAM,IAAI,CAAC,UAAU,EAAE;AACvC,YAAY;AACZ,YAAY,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;AAC9B,gBAAgB,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC;AACxD,YAAY;AACZ,YAAY,IAAI,CAAC,eAAe,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;AACjF,YAAY,MAAM,EAAE,UAAU,EAAE,OAAO,GAAG,QAAQ,EAAE,GAAG,OAAO;AAC9D,YAAY,MAAM,aAAa,GAAG,WAAW;AAC7C,YAAY,MAAM,cAAc,GAAG,YAAY;AAC/C;AACA,YAAY,IAAI,GAAG,GAAG,IAAI;AAC1B,YAAY,IAAI,KAAK,GAAG,EAAE,CAAC;AAC3B,YAAY,IAAI,YAAY,GAAG,MAAM;AACrC,YAAY,QAAQ,OAAO;AAC3B,gBAAgB,KAAK,WAAW;AAChC,oBAAoB,GAAG,GAAG,IAAI;AAC9B,oBAAoB,KAAK,GAAG,mBAAmB;AAC/C,oBAAoB,YAAY,GAAG,MAAM;AACzC,oBAAoB;AACpB,gBAAgB,KAAK,MAAM;AAC3B,oBAAoB,GAAG,GAAG,IAAI;AAC9B,oBAAoB,KAAK,GAAG,kBAAkB;AAC9C,oBAAoB,YAAY,GAAG,MAAM;AACzC,oBAAoB;AACpB,gBAAgB,KAAK,QAAQ;AAC7B,oBAAoB,GAAG,GAAG,IAAI;AAC9B,oBAAoB,KAAK,GAAG,kBAAkB;AAC9C,oBAAoB,YAAY,GAAG,KAAK;AACxC,oBAAoB;AACpB,gBAAgB,KAAK,KAAK;AAC1B,oBAAoB,GAAG,GAAG,IAAI;AAC9B,oBAAoB,KAAK,GAAG,kBAAkB;AAC9C,oBAAoB,YAAY,GAAG,KAAK;AACxC,oBAAoB;AACpB,gBAAgB,KAAK,MAAM;AAC3B,oBAAoB,GAAG,GAAG,IAAI;AAC9B,oBAAoB,KAAK,GAAG,kBAAkB;AAC9C,oBAAoB,YAAY,GAAG,KAAK;AACxC,oBAAoB;AACpB,gBAAgB,KAAK,UAAU;AAC/B,oBAAoB,GAAG,GAAG,IAAI;AAC9B,oBAAoB,KAAK,GAAG,kBAAkB;AAC9C,oBAAoB,YAAY,GAAG,KAAK;AACxC,oBAAoB;AACpB;AACA,YAAY,OAAO,CAAC,GAAG,CAAC,kCAAkC,EAAE,UAAU,CAAC;AACvE,YAAY,IAAI,SAAS;AACzB,YAAY,IAAI;AAChB,gBAAgB,SAAS,GAAG,MAAMC,cAAS,CAAC,UAAU,CAAC;AACvD,YAAY;AACZ,YAAY,OAAO,GAAG,EAAE;AACxB,gBAAgB,OAAO,CAAC,KAAK,CAAC,sCAAsC,EAAE,GAAG,CAAC;AAC1E,gBAAgB,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC;AACpE,YAAY;AACZ,YAAY,OAAO,CAAC,GAAG,CAAC,gCAAgC,EAAE,SAAS,CAAC,UAAU,EAAE,OAAO,CAAC;AACxF,YAAY,IAAI;AAChB,gBAAgB,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,aAAa,EAAE,SAAS,CAAC;AACrE,YAAY;AACZ,YAAY,OAAO,GAAG,EAAE;AACxB,gBAAgB,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,GAAG,CAAC;AACnE,gBAAgB,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC;AAC/D,YAAY;AACZ;AACA,YAAY,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,aAAa,CAAC;AAC9C;AACA,YAAY,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC;AAC7E;AACA,YAAY,IAAI,KAAK,EAAE;AACvB,gBAAgB,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAC9C,YAAY;AACZ;AACA,YAAY,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC;AAC1D;AACA,YAAY,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC;AACrC,YAAY,OAAO,CAAC,GAAG,CAAC,6BAA6B,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACtE,YAAY,IAAI;AAChB,gBAAgB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;AAC5D,gBAAgB,OAAO,CAAC,GAAG,CAAC,8BAA8B,EAAE,OAAO,CAAC;AACpE,YAAY;AACZ,YAAY,OAAO,GAAG,EAAE;AACxB,gBAAgB,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,GAAG,CAAC;AACvE,gBAAgB,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC;AAC1D,YAAY;AACZ,YAAY,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC;AAC1D,YAAY,IAAI,IAAI;AACpB,YAAY,IAAI;AAChB,gBAAgB,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC;AACjE,YAAY;AACZ,YAAY,OAAO,GAAG,EAAE;AACxB,gBAAgB,OAAO,CAAC,KAAK,CAAC,qCAAqC,EAAE,GAAG,CAAC;AACzE,gBAAgB,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC;AAC/D,YAAY;AACZ;AACA,YAAY,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,aAAa,CAAC;AACvD,YAAY,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC;AACxD,YAAY,OAAO,CAAC,GAAG,CAAC,2CAA2C,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC;AAC9F;AACA,YAAY,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;AAChE,YAAY,MAAM,QAAQ,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC;AACtD,YAAY,OAAO,CAAC,GAAG,CAAC,gDAAgD,EAAE,QAAQ,CAAC;AACnF,YAAY,OAAO;AACnB,gBAAgB,OAAO,EAAE,IAAI;AAC7B,gBAAgB,QAAQ,EAAE,QAAQ;AAClC,aAAa;AACb,QAAQ;AACR,QAAQ,OAAO,KAAK,EAAE;AACtB,YAAY,OAAO,CAAC,KAAK,CAAC,sCAAsC,EAAE,KAAK,CAAC;AACxE,YAAY,MAAM,KAAK;AACvB,QAAQ;AACR,IAAI;AACJ;;;;;;;;;"}
package/dist/plugin.js CHANGED
@@ -1,4 +1,4 @@
1
- var capacitorNativeVideoCompressor = (function (exports, core) {
1
+ var capacitorNativeVideoCompressor = (function (exports, core, ffmpeg, util) {
2
2
  'use strict';
3
3
 
4
4
  const NativeVideoCompressor = core.registerPlugin('NativeVideoCompressor', {
@@ -6,15 +6,165 @@ var capacitorNativeVideoCompressor = (function (exports, core) {
6
6
  });
7
7
 
8
8
  class NativeVideoCompressorWeb extends core.WebPlugin {
9
+ constructor() {
10
+ super(...arguments);
11
+ this.ffmpeg = null;
12
+ this.isLoaded = false;
13
+ }
14
+ async loadFFmpeg() {
15
+ if (this.isLoaded && this.ffmpeg)
16
+ return;
17
+ this.notifyListeners('onProgress', { status: 'loading_core', percent: 0 });
18
+ try {
19
+ this.ffmpeg = new ffmpeg.FFmpeg();
20
+ // Listen to FFmpeg progress and map it to capacitor plugin events
21
+ this.ffmpeg.on('progress', ({ progress }) => {
22
+ // progress is a fraction between 0 and 1
23
+ this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });
24
+ });
25
+ this.ffmpeg.on('log', ({ message }) => {
26
+ console.log('[FFmpeg Log]', message);
27
+ });
28
+ // Use standard single-threaded version for best compatibility across browsers
29
+ // without requiring complex SharedArrayBuffer headers configuration
30
+ const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';
31
+ console.log('Starting to load FFmpeg core from:', baseURL);
32
+ await this.ffmpeg.load({
33
+ coreURL: await util.toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
34
+ wasmURL: await util.toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
35
+ });
36
+ this.isLoaded = true;
37
+ console.log('FFmpeg core loaded successfully');
38
+ }
39
+ catch (e) {
40
+ console.error('Failed to load FFmpeg:', e);
41
+ throw e;
42
+ }
43
+ }
44
+ async initialize() {
45
+ try {
46
+ if (this.isLoaded) {
47
+ return { success: true, message: 'Already initialized' };
48
+ }
49
+ await this.loadFFmpeg();
50
+ return { success: true, message: 'Initialization successful' };
51
+ }
52
+ catch (e) {
53
+ return { success: false, message: e.message || 'Initialization failed' };
54
+ }
55
+ }
9
56
  async compressVideo(options) {
10
- console.warn('Tính năng nén video bằng GPU không hỗ trợ trên trình duyệt Web.', options);
11
- // Báo lỗi cho chuẩn Capacitor báo rằng nền tảng này không được hỗ trợ
12
- throw this.unimplemented('Not implemented on web.');
13
- // (Hoặc nếu bạn không muốn văng lỗi trên web, bạn có thể trả về luôn đường dẫn gốc)
14
- // return {
15
- // success: false,
16
- // destPath: options.sourcePath
17
- // };
57
+ try {
58
+ if (!this.isLoaded) {
59
+ await this.loadFFmpeg();
60
+ }
61
+ if (!this.ffmpeg) {
62
+ throw new Error('FFmpeg failed to load');
63
+ }
64
+ this.notifyListeners('onProgress', { status: 'started', percent: 0 });
65
+ const { sourcePath, quality = 'MEDIUM' } = options;
66
+ const inputFileName = 'input.mp4';
67
+ const outputFileName = 'output.mp4';
68
+ // Map quality to CRF, Scale, and Audio Bitrate
69
+ let crf = '28';
70
+ let scale = ''; // default keep resolution
71
+ let audioBitrate = '128k';
72
+ switch (quality) {
73
+ case 'VERY_HIGH': // 1080p
74
+ crf = '23';
75
+ scale = '-vf scale=-2:1080';
76
+ audioBitrate = '128k';
77
+ break;
78
+ case 'HIGH': // 720p
79
+ crf = '28';
80
+ scale = '-vf scale=-2:720';
81
+ audioBitrate = '128k';
82
+ break;
83
+ case 'MEDIUM': // 540p
84
+ crf = '30';
85
+ scale = '-vf scale=-2:540';
86
+ audioBitrate = '96k';
87
+ break;
88
+ case 'LOW': // 480p
89
+ crf = '32';
90
+ scale = '-vf scale=-2:480';
91
+ audioBitrate = '96k';
92
+ break;
93
+ case '360P': // 360p
94
+ crf = '28';
95
+ scale = '-vf scale=-2:360';
96
+ audioBitrate = '64k';
97
+ break;
98
+ case 'VERY_LOW': // 240p
99
+ crf = '35';
100
+ scale = '-vf scale=-2:240';
101
+ audioBitrate = '48k';
102
+ break;
103
+ }
104
+ console.log('[FFmpeg] Fetching source file...', sourcePath);
105
+ let inputData;
106
+ try {
107
+ inputData = await util.fetchFile(sourcePath);
108
+ }
109
+ catch (err) {
110
+ console.error('[FFmpeg] Error fetching source file:', err);
111
+ throw new Error('Could not fetch source video file');
112
+ }
113
+ console.log('[FFmpeg] Writing file to FS...', inputData.byteLength, 'bytes');
114
+ try {
115
+ await this.ffmpeg.writeFile(inputFileName, inputData);
116
+ }
117
+ catch (err) {
118
+ console.error('[FFmpeg] Error writing to FS:', err);
119
+ throw new Error('Could not write to FFmpeg FS');
120
+ }
121
+ // Construct FFmpeg command array
122
+ const args = ['-i', inputFileName];
123
+ // Video codec and compression parameters (superfast provides much better size than ultrafast)
124
+ args.push('-c:v', 'libx264', '-preset', 'superfast', '-crf', crf);
125
+ // Resize if needed (split by space)
126
+ if (scale) {
127
+ args.push(...scale.split(' '));
128
+ }
129
+ // Audio re-encoding to save space
130
+ args.push('-c:a', 'aac', '-b:a', audioBitrate);
131
+ // Output
132
+ args.push(outputFileName);
133
+ console.log('[FFmpeg] Executing command:', args.join(' '));
134
+ try {
135
+ const retCode = await this.ffmpeg.exec(args);
136
+ console.log('[FFmpeg] Exec returned code:', retCode);
137
+ }
138
+ catch (err) {
139
+ console.error('[FFmpeg] Error executing command:', err);
140
+ throw new Error('FFmpeg execution failed');
141
+ }
142
+ console.log('[FFmpeg] Reading output file...');
143
+ let data;
144
+ try {
145
+ data = await this.ffmpeg.readFile(outputFileName);
146
+ }
147
+ catch (err) {
148
+ console.error('[FFmpeg] Error reading output file:', err);
149
+ throw new Error('Could not read FFmpeg output');
150
+ }
151
+ // Free memory
152
+ await this.ffmpeg.deleteFile(inputFileName);
153
+ await this.ffmpeg.deleteFile(outputFileName);
154
+ console.log('[FFmpeg] Creating Blob URL from output...', data.byteLength, 'bytes');
155
+ // Create a blob URL for the resulting file
156
+ const blob = new Blob([data], { type: 'video/mp4' });
157
+ const destPath = URL.createObjectURL(blob);
158
+ console.log('[FFmpeg] Compression complete, returning path:', destPath);
159
+ return {
160
+ success: true,
161
+ destPath: destPath,
162
+ };
163
+ }
164
+ catch (error) {
165
+ console.error('[FFmpeg] Compression process failed:', error);
166
+ throw error;
167
+ }
18
168
  }
19
169
  }
20
170
 
@@ -27,5 +177,5 @@ var capacitorNativeVideoCompressor = (function (exports, core) {
27
177
 
28
178
  return exports;
29
179
 
30
- })({}, capacitorExports);
180
+ })({}, capacitorExports, ffmpeg, util);
31
181
  //# sourceMappingURL=plugin.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst NativeVideoCompressor = registerPlugin('NativeVideoCompressor', {\n web: () => import('./web').then((m) => new m.NativeVideoCompressorWeb()),\n});\nexport * from './definitions';\nexport { NativeVideoCompressor };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class NativeVideoCompressorWeb extends WebPlugin {\n async compressVideo(options) {\n console.warn('Tính năng nén video bằng GPU không hỗ trợ trên trình duyệt Web.', options);\n // Báo lỗi cho chuẩn Capacitor báo rằng nền tảng này không được hỗ trợ\n throw this.unimplemented('Not implemented on web.');\n // (Hoặc nếu bạn không muốn văng lỗi trên web, bạn có thể trả về luôn đường dẫn gốc)\n // return {\n // success: false,\n // destPath: options.sourcePath\n // };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,qBAAqB,GAAGA,mBAAc,CAAC,uBAAuB,EAAE;IACtE,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,wBAAwB,EAAE,CAAC;IAC5E,CAAC;;ICFM,MAAM,wBAAwB,SAASC,cAAS,CAAC;IACxD,IAAI,MAAM,aAAa,CAAC,OAAO,EAAE;IACjC,QAAQ,OAAO,CAAC,IAAI,CAAC,iEAAiE,EAAE,OAAO,CAAC;IAChG;IACA,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,yBAAyB,CAAC;IAC3D;IACA;IACA;IACA;IACA;IACA,IAAI;IACJ;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst NativeVideoCompressor = registerPlugin('NativeVideoCompressor', {\n web: () => import('./web').then((m) => new m.NativeVideoCompressorWeb()),\n});\nexport * from './definitions';\nexport { NativeVideoCompressor };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nimport { FFmpeg } from '@ffmpeg/ffmpeg';\nimport { fetchFile, toBlobURL } from '@ffmpeg/util';\nexport class NativeVideoCompressorWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.ffmpeg = null;\n this.isLoaded = false;\n }\n async loadFFmpeg() {\n if (this.isLoaded && this.ffmpeg)\n return;\n this.notifyListeners('onProgress', { status: 'loading_core', percent: 0 });\n try {\n this.ffmpeg = new FFmpeg();\n // Listen to FFmpeg progress and map it to capacitor plugin events\n this.ffmpeg.on('progress', ({ progress }) => {\n // progress is a fraction between 0 and 1\n this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });\n });\n this.ffmpeg.on('log', ({ message }) => {\n console.log('[FFmpeg Log]', message);\n });\n // Use standard single-threaded version for best compatibility across browsers\n // without requiring complex SharedArrayBuffer headers configuration\n const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';\n console.log('Starting to load FFmpeg core from:', baseURL);\n await this.ffmpeg.load({\n coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),\n wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),\n });\n this.isLoaded = true;\n console.log('FFmpeg core loaded successfully');\n }\n catch (e) {\n console.error('Failed to load FFmpeg:', e);\n throw e;\n }\n }\n async initialize() {\n try {\n if (this.isLoaded) {\n return { success: true, message: 'Already initialized' };\n }\n await this.loadFFmpeg();\n return { success: true, message: 'Initialization successful' };\n }\n catch (e) {\n return { success: false, message: e.message || 'Initialization failed' };\n }\n }\n async compressVideo(options) {\n try {\n if (!this.isLoaded) {\n await this.loadFFmpeg();\n }\n if (!this.ffmpeg) {\n throw new Error('FFmpeg failed to load');\n }\n this.notifyListeners('onProgress', { status: 'started', percent: 0 });\n const { sourcePath, quality = 'MEDIUM' } = options;\n const inputFileName = 'input.mp4';\n const outputFileName = 'output.mp4';\n // Map quality to CRF, Scale, and Audio Bitrate\n let crf = '28';\n let scale = ''; // default keep resolution\n let audioBitrate = '128k';\n switch (quality) {\n case 'VERY_HIGH': // 1080p\n crf = '23';\n scale = '-vf scale=-2:1080';\n audioBitrate = '128k';\n break;\n case 'HIGH': // 720p\n crf = '28';\n scale = '-vf scale=-2:720';\n audioBitrate = '128k';\n break;\n case 'MEDIUM': // 540p\n crf = '30';\n scale = '-vf scale=-2:540';\n audioBitrate = '96k';\n break;\n case 'LOW': // 480p\n crf = '32';\n scale = '-vf scale=-2:480';\n audioBitrate = '96k';\n break;\n case '360P': // 360p\n crf = '28';\n scale = '-vf scale=-2:360';\n audioBitrate = '64k';\n break;\n case 'VERY_LOW': // 240p\n crf = '35';\n scale = '-vf scale=-2:240';\n audioBitrate = '48k';\n break;\n }\n console.log('[FFmpeg] Fetching source file...', sourcePath);\n let inputData;\n try {\n inputData = await fetchFile(sourcePath);\n }\n catch (err) {\n console.error('[FFmpeg] Error fetching source file:', err);\n throw new Error('Could not fetch source video file');\n }\n console.log('[FFmpeg] Writing file to FS...', inputData.byteLength, 'bytes');\n try {\n await this.ffmpeg.writeFile(inputFileName, inputData);\n }\n catch (err) {\n console.error('[FFmpeg] Error writing to FS:', err);\n throw new Error('Could not write to FFmpeg FS');\n }\n // Construct FFmpeg command array\n const args = ['-i', inputFileName];\n // Video codec and compression parameters (superfast provides much better size than ultrafast)\n args.push('-c:v', 'libx264', '-preset', 'superfast', '-crf', crf);\n // Resize if needed (split by space)\n if (scale) {\n args.push(...scale.split(' '));\n }\n // Audio re-encoding to save space\n args.push('-c:a', 'aac', '-b:a', audioBitrate);\n // Output\n args.push(outputFileName);\n console.log('[FFmpeg] Executing command:', args.join(' '));\n try {\n const retCode = await this.ffmpeg.exec(args);\n console.log('[FFmpeg] Exec returned code:', retCode);\n }\n catch (err) {\n console.error('[FFmpeg] Error executing command:', err);\n throw new Error('FFmpeg execution failed');\n }\n console.log('[FFmpeg] Reading output file...');\n let data;\n try {\n data = await this.ffmpeg.readFile(outputFileName);\n }\n catch (err) {\n console.error('[FFmpeg] Error reading output file:', err);\n throw new Error('Could not read FFmpeg output');\n }\n // Free memory\n await this.ffmpeg.deleteFile(inputFileName);\n await this.ffmpeg.deleteFile(outputFileName);\n console.log('[FFmpeg] Creating Blob URL from output...', data.byteLength, 'bytes');\n // Create a blob URL for the resulting file\n const blob = new Blob([data], { type: 'video/mp4' });\n const destPath = URL.createObjectURL(blob);\n console.log('[FFmpeg] Compression complete, returning path:', destPath);\n return {\n success: true,\n destPath: destPath,\n };\n }\n catch (error) {\n console.error('[FFmpeg] Compression process failed:', error);\n throw error;\n }\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin","FFmpeg","toBlobURL","fetchFile"],"mappings":"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"}
@@ -28,10 +28,33 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
28
28
  // Tạo file đầu ra lưu ở thư mục Temp
29
29
  let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("compressed_\(UUID().uuidString).mp4")
30
30
 
31
+ // Lấy thông số chất lượng truyền từ giao diện (Mặc định là MEDIUM nếu không có)
32
+ let qualityStr = call.getString("quality") ?? "MEDIUM"
33
+ var presetName: String
34
+
35
+ print("====== VIDEO COMPRESSION QUALITY RECEIVED: \(qualityStr) ======")
36
+
37
+ switch qualityStr {
38
+ case "VERY_HIGH":
39
+ presetName = AVAssetExportPresetHighestQuality
40
+ case "HIGH":
41
+ presetName = AVAssetExportPreset1280x720 // 720p cho mức High để nhận thấy sự giảm dung lượng rõ rệt
42
+ case "MEDIUM":
43
+ presetName = AVAssetExportPreset960x540 // 540p cho mức Medium
44
+ case "LOW":
45
+ presetName = AVAssetExportPreset640x480 // 480p cho mức Low
46
+ case "360P" :
47
+ presetName = AVAssetExportPresetMediumQuality // 360p cho mức
48
+ case "VERY_LOW":
49
+ presetName = AVAssetExportPresetLowQuality // 124 cho mức Very Low
50
+ default:
51
+ presetName = AVAssetExportPreset960x540
52
+ }
53
+
31
54
  let asset = AVAsset(url: videoURL)
32
55
 
33
- // 3. Khởi tạo bộ nén phần cứng (Chọn chất lượng MediumQuality hoặc 1280x720)
34
- guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetMediumQuality) else {
56
+ // 3. Khởi tạo bộ nén phần cứng với preset tương ứng
57
+ guard let exportSession = AVAssetExportSession(asset: asset, presetName: presetName) else {
35
58
  call.reject("Không thể khởi tạo bộ nén iOS")
36
59
  return
37
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-native-video-compressor",
3
- "version": "0.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Native video compressor using LightCompressor and AVFoundation",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -77,5 +77,9 @@
77
77
  "android": {
78
78
  "src": "android"
79
79
  }
80
+ },
81
+ "dependencies": {
82
+ "@ffmpeg/ffmpeg": "^0.12.15",
83
+ "@ffmpeg/util": "^0.12.2"
80
84
  }
81
85
  }