capacitor-native-video-compressor 1.0.5 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/android/src/main/AndroidManifest.xml +4 -1
- package/android/src/main/java/com/atomic/videocompressor/NativeVideoCompressorPlugin.java +41 -11
- package/android/src/main/java/com/atomic/videocompressor/VideoCompressionService.java +53 -5
- package/dist/esm/web.d.ts +19 -13
- package/dist/esm/web.js +264 -186
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +263 -185
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +264 -187
- package/dist/plugin.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -35,6 +35,36 @@ If you are reading videos from external storage, ensure you have the appropriate
|
|
|
35
35
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
#### JitPack repository (required)
|
|
39
|
+
|
|
40
|
+
The Android implementation depends on `LightCompressor`, which is published on JitPack.
|
|
41
|
+
A plugin's own `repositories` block does not propagate to the consuming app, so you must
|
|
42
|
+
add JitPack to your app's `android/build.gradle` — otherwise the build fails with
|
|
43
|
+
`Could not find com.github.AbedElazizShe:LightCompressor`:
|
|
44
|
+
|
|
45
|
+
```gradle
|
|
46
|
+
allprojects {
|
|
47
|
+
repositories {
|
|
48
|
+
google()
|
|
49
|
+
mavenCentral()
|
|
50
|
+
maven { url 'https://jitpack.io' }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### compileSdk 35+ (required)
|
|
56
|
+
|
|
57
|
+
The compression foreground service declares `android:foregroundServiceType="mediaProcessing|dataSync"`.
|
|
58
|
+
`mediaProcessing` exists only from **API 35 (Android 15)**, so your app must build with
|
|
59
|
+
`compileSdkVersion` 35 or higher.
|
|
60
|
+
|
|
61
|
+
At runtime the service picks the type by API level: `mediaProcessing` on API 35+,
|
|
62
|
+
`dataSync` on API 29-34. Both types and both permissions are declared, so the plugin
|
|
63
|
+
runs correctly on Android 10 through 15+. Passing only `mediaProcessing` on API 34
|
|
64
|
+
makes the system read the type as unknown and `startForeground()` throws
|
|
65
|
+
`InvalidForegroundServiceTypeException` - the service never reaches the foreground
|
|
66
|
+
and compression is killed as soon as the user leaves the app.
|
|
67
|
+
|
|
38
68
|
## Web Configuration (Required)
|
|
39
69
|
|
|
40
70
|
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:
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
2
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
3
|
+
<!-- mediaProcessing chỉ tồn tại từ API 35. Khai kèm dataSync để máy API 34
|
|
4
|
+
trở xuống vẫn có một type hợp lệ - service chọn type theo SDK_INT lúc chạy. -->
|
|
5
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING" />
|
|
3
6
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
|
4
7
|
|
|
5
8
|
<application>
|
|
6
9
|
<service
|
|
7
10
|
android:name="com.atomic.videocompressor.VideoCompressionService"
|
|
8
|
-
android:foregroundServiceType="dataSync"
|
|
11
|
+
android:foregroundServiceType="mediaProcessing|dataSync"
|
|
9
12
|
android:exported="false" />
|
|
10
13
|
</application>
|
|
11
14
|
</manifest>
|
|
@@ -140,15 +140,7 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
140
140
|
|
|
141
141
|
// Khởi động Foreground Service để giữ app luôn sống khi chạy ngầm
|
|
142
142
|
Intent serviceIntent = new Intent(getContext(), VideoCompressionService.class);
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
getContext().startForegroundService(serviceIntent);
|
|
146
|
-
} catch (Exception e) {
|
|
147
|
-
getContext().startService(serviceIntent);
|
|
148
|
-
}
|
|
149
|
-
} else {
|
|
150
|
-
getContext().startService(serviceIntent);
|
|
151
|
-
}
|
|
143
|
+
startServiceCompat(serviceIntent);
|
|
152
144
|
|
|
153
145
|
// 4. Bắt đầu nén (Đã cập nhật List Uri và int index)
|
|
154
146
|
VideoCompressor.start(
|
|
@@ -206,14 +198,52 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
206
198
|
);
|
|
207
199
|
}
|
|
208
200
|
|
|
209
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Every terminal callback ends here, so neither is ever left running.
|
|
203
|
+
*
|
|
204
|
+
* Dừng bằng một command gửi vào service chứ không phải stopService(): hai lệnh
|
|
205
|
+
* start/stop đi qua ActivityManager không bảo đảm thứ tự, nên một lần nén lỗi
|
|
206
|
+
* trong ~90ms (video hỏng) sẽ dừng service trước khi nó kịp startForeground()
|
|
207
|
+
* và Android giết cả app bằng ForegroundServiceDidNotStartInTimeException.
|
|
208
|
+
* Command thì dispatch tuần tự trên main thread của service nên luôn đúng thứ tự.
|
|
209
|
+
*/
|
|
210
210
|
private void finishJob(Intent serviceIntent) {
|
|
211
|
-
getContext().
|
|
211
|
+
Intent stopIntent = new Intent(getContext(), VideoCompressionService.class);
|
|
212
|
+
stopIntent.setAction(VideoCompressionService.ACTION_STOP);
|
|
213
|
+
if (!startServiceCompat(stopIntent)) {
|
|
214
|
+
// Không gửi được command (hệ thống chặn start) thì service cũng không
|
|
215
|
+
// còn sống để mà treo - dọn bằng cách cũ cho chắc.
|
|
216
|
+
try {
|
|
217
|
+
getContext().stopService(serviceIntent);
|
|
218
|
+
} catch (Exception e) {
|
|
219
|
+
Log.w("NativeVideoCompressor", "Could not stop compression service", e);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
212
222
|
synchronized (this) {
|
|
213
223
|
currentJobId = null;
|
|
214
224
|
}
|
|
215
225
|
}
|
|
216
226
|
|
|
227
|
+
/** @return false khi hệ thống từ chối cả hai cách khởi động service. */
|
|
228
|
+
private boolean startServiceCompat(Intent intent) {
|
|
229
|
+
try {
|
|
230
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
|
231
|
+
getContext().startForegroundService(intent);
|
|
232
|
+
} else {
|
|
233
|
+
getContext().startService(intent);
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
} catch (Exception e) {
|
|
237
|
+
try {
|
|
238
|
+
getContext().startService(intent);
|
|
239
|
+
return true;
|
|
240
|
+
} catch (Exception inner) {
|
|
241
|
+
Log.w("NativeVideoCompressor", "Could not start compression service", inner);
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
217
247
|
/** Best-effort: a partial file left in the cache is not worth failing over. */
|
|
218
248
|
private void deleteOutput(String fileName) {
|
|
219
249
|
try {
|
|
@@ -5,16 +5,35 @@ import android.app.NotificationChannel;
|
|
|
5
5
|
import android.app.NotificationManager;
|
|
6
6
|
import android.app.Service;
|
|
7
7
|
import android.content.Intent;
|
|
8
|
+
import android.content.pm.ServiceInfo;
|
|
8
9
|
import android.os.Build;
|
|
9
10
|
import android.os.IBinder;
|
|
11
|
+
import android.util.Log;
|
|
10
12
|
import androidx.annotation.Nullable;
|
|
11
13
|
import androidx.core.app.NotificationCompat;
|
|
12
14
|
|
|
13
15
|
public class VideoCompressionService extends Service {
|
|
14
16
|
|
|
17
|
+
private static final String TAG = "VideoCompressionService";
|
|
15
18
|
private static final String CHANNEL_ID = "VideoCompressionChannel";
|
|
16
19
|
private static final int NOTIFICATION_ID = 1001;
|
|
17
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Lệnh kết thúc, do plugin gửi qua chính hàng đợi command của service thay vì
|
|
23
|
+
* gọi stopService() từ ngoài.
|
|
24
|
+
*
|
|
25
|
+
* stopService() và startForegroundService() là hai lệnh độc lập đi qua
|
|
26
|
+
* ActivityManager, nên không có gì bảo đảm thứ tự: nén lỗi trong vòng ~90ms
|
|
27
|
+
* (video hỏng) thì lệnh stop tới trước khi onStartCommand kịp chạy, AMS thấy
|
|
28
|
+
* "Bringing down service while still waiting for start foreground" và ném
|
|
29
|
+
* ForegroundServiceDidNotStartInTimeException - chết cả app.
|
|
30
|
+
*
|
|
31
|
+
* Command của service thì được dispatch tuần tự trên main thread, nên STOP
|
|
32
|
+
* luôn chạy sau START. Mỗi onStartCommand đều gọi startForeground() trước,
|
|
33
|
+
* kể cả nhánh STOP, để mọi startForegroundService() đang treo đều được đáp ứng.
|
|
34
|
+
*/
|
|
35
|
+
static final String ACTION_STOP = "com.atomic.videocompressor.action.STOP";
|
|
36
|
+
|
|
18
37
|
@Override
|
|
19
38
|
public void onCreate() {
|
|
20
39
|
super.onCreate();
|
|
@@ -23,6 +42,17 @@ public class VideoCompressionService extends Service {
|
|
|
23
42
|
|
|
24
43
|
@Override
|
|
25
44
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
45
|
+
enterForeground();
|
|
46
|
+
|
|
47
|
+
if (intent != null && ACTION_STOP.equals(intent.getAction())) {
|
|
48
|
+
stopForegroundCompat();
|
|
49
|
+
stopSelf();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return START_NOT_STICKY;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private void enterForeground() {
|
|
26
56
|
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
|
|
27
57
|
.setContentTitle("Đang nén video")
|
|
28
58
|
.setContentText("Tiến trình đang chạy ngầm...")
|
|
@@ -30,15 +60,33 @@ public class VideoCompressionService extends Service {
|
|
|
30
60
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
31
61
|
.build();
|
|
32
62
|
|
|
33
|
-
// Check for Android 14+ specific foreground service typing requirements if compiled against API 34+
|
|
34
|
-
// In this case, we rely on the manifest declaration for dataSync.
|
|
35
63
|
try {
|
|
36
|
-
|
|
64
|
+
// Google Play đòi mediaProcessing cho tác vụ nén video, nhưng type đó chỉ có
|
|
65
|
+
// từ API 35. Trên API 34 hệ thống đọc manifest ra "type unknown",
|
|
66
|
+
// startForeground() ném InvalidForegroundServiceTypeException và service
|
|
67
|
+
// không bao giờ vào foreground - nén bị huỷ ngay khi user rời app. Nên chọn
|
|
68
|
+
// type lúc chạy; AndroidManifest khai cả hai nên type nào cũng hợp lệ.
|
|
69
|
+
if (Build.VERSION.SDK_INT >= 35) {
|
|
70
|
+
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING);
|
|
71
|
+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
72
|
+
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
|
73
|
+
} else {
|
|
74
|
+
// Trước API 29 chưa có khái niệm foreground service type.
|
|
75
|
+
startForeground(NOTIFICATION_ID, notification);
|
|
76
|
+
}
|
|
37
77
|
} catch (Exception e) {
|
|
38
|
-
|
|
78
|
+
// Nuốt lỗi ở đây để không giết app, nhưng phải log ra TAG riêng: nếu không,
|
|
79
|
+
// service chạy nền âm thầm và không ai biết cho tới khi user báo nén hỏng.
|
|
80
|
+
Log.e(TAG, "startForeground thất bại - service sẽ chạy nền, nén có thể bị huỷ", e);
|
|
39
81
|
}
|
|
82
|
+
}
|
|
40
83
|
|
|
41
|
-
|
|
84
|
+
private void stopForegroundCompat() {
|
|
85
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
86
|
+
stopForeground(STOP_FOREGROUND_REMOVE);
|
|
87
|
+
} else {
|
|
88
|
+
stopForeground(true);
|
|
89
|
+
}
|
|
42
90
|
}
|
|
43
91
|
|
|
44
92
|
@Nullable
|
package/dist/esm/web.d.ts
CHANGED
|
@@ -3,25 +3,31 @@ import type { NativeVideoCompressorPlugin, CompressOptions, CompressResult, Canc
|
|
|
3
3
|
export declare class NativeVideoCompressorWeb extends WebPlugin implements NativeVideoCompressorPlugin {
|
|
4
4
|
private ffmpeg;
|
|
5
5
|
private isLoaded;
|
|
6
|
+
/** The mediabunny conversion running now, so `cancel()` can abort it. */
|
|
7
|
+
private activeConversion;
|
|
6
8
|
/** The job `compressVideo` is running right now, for `cancel({ jobId })`. */
|
|
7
9
|
private currentJobId;
|
|
8
10
|
private cancelRequested;
|
|
9
|
-
private loadFFmpeg;
|
|
10
|
-
/**
|
|
11
|
-
* Kill the worker outright — there is no gentler option.
|
|
12
|
-
*
|
|
13
|
-
* `ffmpeg.exec()` runs *synchronously* inside the worker (see
|
|
14
|
-
* @ffmpeg/ffmpeg's worker.js: `ffmpeg.exec(...args)` is not awaited), so the
|
|
15
|
-
* worker never reaches a message poll while encoding. The AbortSignal that
|
|
16
|
-
* `exec()` accepts only rejects the main-thread promise; the encode keeps
|
|
17
|
-
* burning CPU behind it. `worker.terminate()` is what actually stops it.
|
|
18
|
-
*
|
|
19
|
-
* The cost is the loaded core, which is why the core URLs are cached above.
|
|
20
|
-
*/
|
|
21
|
-
cancel(options?: CancelOptions): Promise<CancelResult>;
|
|
22
11
|
initialize(): Promise<{
|
|
23
12
|
success: boolean;
|
|
24
13
|
message?: string;
|
|
25
14
|
}>;
|
|
26
15
|
compressVideo(options: CompressOptions): Promise<CompressResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Kill whichever engine is mid-compress. Only one is ever active at a time,
|
|
18
|
+
* so acting on both is safe.
|
|
19
|
+
*
|
|
20
|
+
* WebCodecs: `conversion.cancel()` makes the in-flight `execute()` reject
|
|
21
|
+
* with `ConversionCanceledError`, surfaced as a `CancelledError` below.
|
|
22
|
+
* ffmpeg: `exec()` runs synchronously inside the worker and never reaches a
|
|
23
|
+
* message poll while encoding, so `worker.terminate()` is the only way to
|
|
24
|
+
* stop it — its cost is the loaded core, which is why the core URLs are
|
|
25
|
+
* cached above.
|
|
26
|
+
*/
|
|
27
|
+
cancel(options?: CancelOptions): Promise<CancelResult>;
|
|
28
|
+
private fetchSourceBlob;
|
|
29
|
+
private canUseWebCodecs;
|
|
30
|
+
private compressWithWebCodecs;
|
|
31
|
+
private loadFFmpeg;
|
|
32
|
+
private compressWithFfmpeg;
|
|
27
33
|
}
|