capacitor-native-video-compressor 1.1.0 → 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 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
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
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
- /** Every terminal callback ends here, so neither is ever left running. */
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().stopService(serviceIntent);
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
- startForeground(NOTIFICATION_ID, notification);
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
- e.printStackTrace();
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
- return START_NOT_STICKY;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-native-video-compressor",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Native video compressor using LightCompressor and AVFoundation",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",