capacitor-native-video-compressor 1.1.1 → 1.1.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
@@ -226,7 +226,9 @@ The `quality` parameter determines the output resolution and compression ratio.
226
226
  * [`compressVideo(...)`](#compressvideo)
227
227
  * [`cancel(...)`](#cancel)
228
228
  * [`initialize()`](#initialize)
229
+ * [`finishUpload(...)`](#finishupload)
229
230
  * [`addListener('onProgress', ...)`](#addlisteneronprogress-)
231
+ * [`addListener('onCancelRequested', ...)`](#addlisteneroncancelrequested-)
230
232
  * [Interfaces](#interfaces)
231
233
 
232
234
  </docgen-index>
@@ -282,6 +284,24 @@ initialize() => Promise<{ success: boolean; message?: string; }>
282
284
  --------------------
283
285
 
284
286
 
287
+ ### finishUpload(...)
288
+
289
+ ```typescript
290
+ finishUpload(options: { jobId: string; }) => Promise<void>
291
+ ```
292
+
293
+ Báo rằng phần upload đi kèm `holdForUpload` đã xong (thành công hay thất
294
+ bại đều phải gọi) để Android hạ foreground service xuống.
295
+
296
+ No-op trên web và iOS.
297
+
298
+ | Param | Type |
299
+ | ------------- | ------------------------------- |
300
+ | **`options`** | <code>{ jobId: string; }</code> |
301
+
302
+ --------------------
303
+
304
+
285
305
  ### addListener('onProgress', ...)
286
306
 
287
307
  ```typescript
@@ -298,6 +318,29 @@ addListener(eventName: 'onProgress', listenerFunc: (info: { status: string; perc
298
318
  --------------------
299
319
 
300
320
 
321
+ ### addListener('onCancelRequested', ...)
322
+
323
+ ```typescript
324
+ addListener(eventName: 'onCancelRequested', listenerFunc: () => void) => Promise<PluginListenerHandle>
325
+ ```
326
+
327
+ Android: user đã bấm "Huỷ" trên notification của foreground service.
328
+
329
+ Pha nén được plugin tự huỷ trước khi event này bắn, nhưng pha upload chạy
330
+ trong JS nên không ai dừng hộ được - caller phải tự abort request đang
331
+ chạy. Bỏ qua event này nghĩa là nút Huỷ không thật sự huỷ, và Play đánh
332
+ trượt tiêu chí "can be terminated or stopped by the user".
333
+
334
+ | Param | Type |
335
+ | ------------------ | -------------------------------- |
336
+ | **`eventName`** | <code>'onCancelRequested'</code> |
337
+ | **`listenerFunc`** | <code>() =&gt; void</code> |
338
+
339
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
340
+
341
+ --------------------
342
+
343
+
301
344
  ### Interfaces
302
345
 
303
346
 
@@ -311,12 +354,13 @@ addListener(eventName: 'onProgress', listenerFunc: (info: { status: string; perc
311
354
 
312
355
  #### CompressOptions
313
356
 
314
- | Prop | Type | Description |
315
- | ---------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
316
- | **`sourcePath`** | <code>string</code> | |
317
- | **`destPath`** | <code>string</code> | |
318
- | **`quality`** | <code>'VERY_HIGH' \| 'HIGH' \| 'MEDIUM' \| 'LOW' \| 'VERY_LOW' \| '360P'</code> | |
319
- | **`jobId`** | <code>string</code> | Names this compress so `cancel({ jobId })` can target it. Every platform compresses one video at a time, so cancellation is global by nature. Two independent callers in the same app (say a comment queue and a notes queue) therefore need a way to say *which* job they mean — without one, a cancel from either kills whatever the other is running. |
357
+ | Prop | Type | Description |
358
+ | ------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
359
+ | **`sourcePath`** | <code>string</code> | |
360
+ | **`destPath`** | <code>string</code> | |
361
+ | **`quality`** | <code>'VERY_HIGH' \| 'HIGH' \| 'MEDIUM' \| 'LOW' \| 'VERY_LOW' \| '360P'</code> | |
362
+ | **`jobId`** | <code>string</code> | Names this compress so `cancel({ jobId })` can target it. Every platform compresses one video at a time, so cancellation is global by nature. Two independent callers in the same app (say a comment queue and a notes queue) therefore need a way to say *which* job they mean — without one, a cancel from either kills whatever the other is running. |
363
+ | **`holdForUpload`** | <code>boolean</code> | Android: giữ foreground service sống sau khi nén xong, để phần upload do JS chạy tiếp theo không bị hệ thống giết khi user rời app. Bật cờ này là một lời hứa: caller PHẢI gọi {@link NativeVideoCompressorPlugin.finishUpload} khi upload kết thúc, kể cả ở nhánh lỗi. Không gọi thì service chỉ còn watchdog dọn hộ sau 15 phút, và trong 15 phút đó user nhìn thấy một notification nói rằng app đang làm việc trong khi nó không làm gì cả. Không có tác dụng trên web và iOS. |
320
364
 
321
365
 
322
366
  #### CancelResult
@@ -1,14 +1,17 @@
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
+ <!-- Chỉ dataSync, không khai mediaProcessing.
4
+ Hai type nghĩa là hai quyền phải biện minh riêng trong Play Console với
5
+ cùng một video demo, và mediaProcessing khó thuyết phục hơn hẳn. dataSync
6
+ phủ được cả hai pha: nén rồi tải lên là một thao tác đồng bộ dữ liệu liên
7
+ tục do user khởi tạo - đúng ví dụ "upload photo to the cloud" mà policy nêu. -->
6
8
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
9
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
7
10
 
8
11
  <application>
9
12
  <service
10
13
  android:name="com.atomic.videocompressor.VideoCompressionService"
11
- android:foregroundServiceType="mediaProcessing|dataSync"
14
+ android:foregroundServiceType="dataSync"
12
15
  android:exported="false" />
13
16
  </application>
14
17
  </manifest>
@@ -16,6 +16,7 @@ import com.getcapacitor.PluginCall;
16
16
  import com.getcapacitor.PluginMethod;
17
17
  import com.getcapacitor.annotation.CapacitorPlugin;
18
18
  import java.io.File;
19
+ import java.lang.ref.WeakReference;
19
20
  import java.util.Arrays;
20
21
  import java.util.Collections;
21
22
 
@@ -30,9 +31,52 @@ public class NativeVideoCompressorPlugin extends Plugin {
30
31
 
31
32
  private static final String STORAGE_SUBFOLDER = "compressed_videos";
32
33
 
34
+ /**
35
+ * Instance đang sống, để service gọi ngược khi user bấm "Huỷ" trên
36
+ * notification. WeakReference vì service sống lâu hơn bridge: giữ cứng ở
37
+ * đây là giữ luôn cả WebView sau khi Activity đã chết.
38
+ */
39
+ private static WeakReference<NativeVideoCompressorPlugin> active = new WeakReference<>(null);
40
+
33
41
  /** The job compressVideo is running right now, for cancel({ jobId }). */
34
42
  private String currentJobId = null;
35
43
 
44
+ @Override
45
+ public void load() {
46
+ super.load();
47
+ active = new WeakReference<>(this);
48
+ }
49
+
50
+ @Override
51
+ protected void handleOnDestroy() {
52
+ if (active.get() == this) active = new WeakReference<>(null);
53
+ super.handleOnDestroy();
54
+ }
55
+
56
+ /**
57
+ * User bấm "Huỷ" trên notification. Phải huỷ được cả hai pha:
58
+ *
59
+ * - pha nén chạy trong LightCompressor (native) -> cancel() tĩnh, gọi được
60
+ * kể cả khi bridge đã chết
61
+ * - pha upload chạy trong JS -> chỉ có thể nhờ JS tự dừng, nên bắn
62
+ * onCancelRequested; không có event này thì bấm Huỷ lúc đang upload chỉ
63
+ * tắt được notification còn request vẫn chạy tiếp, và Play coi như
64
+ * "không dừng được bởi user".
65
+ */
66
+ static void cancelAllFromNotification() {
67
+ try {
68
+ VideoCompressor.cancel();
69
+ } catch (Exception e) {
70
+ Log.w("NativeVideoCompressor", "Could not cancel compression", e);
71
+ }
72
+ NativeVideoCompressorPlugin plugin = active.get();
73
+ if (plugin == null) return;
74
+ synchronized (plugin) {
75
+ plugin.currentJobId = null;
76
+ }
77
+ plugin.notifyListeners("onCancelRequested", new JSObject());
78
+ }
79
+
36
80
  /**
37
81
  * Nothing to do natively; LightCompressor needs no warm-up. Answered
38
82
  * rather than left unimplemented so a caller can await it unconditionally
@@ -73,6 +117,15 @@ public class NativeVideoCompressorPlugin extends Plugin {
73
117
  call.resolve(ret);
74
118
  }
75
119
 
120
+ @PluginMethod
121
+ public void finishUpload(PluginCall call) {
122
+ String jobId = call.getString("jobId");
123
+ if (jobId != null) {
124
+ sendToService(VideoCompressionService.ACTION_FINISH, jobId);
125
+ }
126
+ call.resolve();
127
+ }
128
+
76
129
  @PluginMethod
77
130
  public void compressVideo(PluginCall call) {
78
131
  String sourcePath = call.getString("sourcePath");
@@ -83,6 +136,7 @@ public class NativeVideoCompressorPlugin extends Plugin {
83
136
  }
84
137
 
85
138
  String jobId = call.getString("jobId", "default");
139
+ boolean holdForUpload = Boolean.TRUE.equals(call.getBoolean("holdForUpload", false));
86
140
  synchronized (this) {
87
141
  currentJobId = jobId;
88
142
  }
@@ -140,7 +194,19 @@ public class NativeVideoCompressorPlugin extends Plugin {
140
194
 
141
195
  // Khởi động Foreground Service để giữ app luôn sống khi chạy ngầm
142
196
  Intent serviceIntent = new Intent(getContext(), VideoCompressionService.class);
143
- startServiceCompat(serviceIntent);
197
+ serviceIntent.setAction(VideoCompressionService.ACTION_START);
198
+ serviceIntent.putExtra(VideoCompressionService.EXTRA_JOB_ID, jobId);
199
+ try {
200
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
201
+ getContext().startForegroundService(serviceIntent);
202
+ } else {
203
+ getContext().startService(serviceIntent);
204
+ }
205
+ } catch (Exception e) {
206
+ synchronized (this) { currentJobId = null; }
207
+ call.reject("Cannot start foreground video processing", e);
208
+ return;
209
+ }
144
210
 
145
211
  // 4. Bắt đầu nén (Đã cập nhật List Uri và int index)
146
212
  VideoCompressor.start(
@@ -153,6 +219,11 @@ public class NativeVideoCompressorPlugin extends Plugin {
153
219
  new CompressionListener() {
154
220
  @Override
155
221
  public void onProgress(int i, float v) {
222
+ // Notification phải nhúc nhích theo đúng con số mà UI trong
223
+ // app đang hiển thị: một notification đứng im là lý do Play
224
+ // đánh trượt tiêu chí "user perceptible".
225
+ VideoCompressionService.publishProgress(jobId, Math.round(v));
226
+
156
227
  // Gửi % tiến độ về Javascript để vẽ thanh Progress
157
228
  JSObject ret = new JSObject();
158
229
  ret.put("status", "progress");
@@ -162,13 +233,13 @@ public class NativeVideoCompressorPlugin extends Plugin {
162
233
 
163
234
  @Override
164
235
  public void onFailure(int i, @NonNull String failureMessage) {
165
- finishJob(serviceIntent);
236
+ finishJob(jobId, false);
166
237
  call.reject("Nén thất bại: " + failureMessage);
167
238
  }
168
239
 
169
240
  @Override
170
241
  public void onSuccess(int i, long l, @Nullable String s) {
171
- finishJob(serviceIntent);
242
+ finishJob(jobId, holdForUpload);
172
243
  // Nén xong, trả kết quả đường dẫn file mới về cho JS
173
244
  JSObject ret = new JSObject();
174
245
  ret.put("success", true);
@@ -185,7 +256,7 @@ public class NativeVideoCompressorPlugin extends Plugin {
185
256
 
186
257
  @Override
187
258
  public void onCancelled(int i) {
188
- finishJob(serviceIntent);
259
+ finishJob(jobId, false);
189
260
  // LightCompressor leaves whatever it had already written
190
261
  // behind; without this the cache keeps a partial mp4 per
191
262
  // cancel, and nothing ever collects them.
@@ -198,49 +269,33 @@ public class NativeVideoCompressorPlugin extends Plugin {
198
269
  );
199
270
  }
200
271
 
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
- private void finishJob(Intent 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
- }
272
+ /** Every terminal callback ends here, so neither is ever left running. */
273
+ private void finishJob(String jobId, boolean holdForUpload) {
274
+ sendToService(
275
+ holdForUpload ? VideoCompressionService.ACTION_UPLOAD : VideoCompressionService.ACTION_FINISH,
276
+ jobId
277
+ );
222
278
  synchronized (this) {
223
279
  currentJobId = null;
224
280
  }
225
281
  }
226
282
 
227
- /** @return false khi hệ thống từ chối cả hai cách khởi động service. */
228
- private boolean startServiceCompat(Intent intent) {
283
+ /**
284
+ * Lệnh gửi vào service, nuốt lỗi có chủ đích.
285
+ *
286
+ * startService() từ background bị chặn khi app không còn foreground service
287
+ * nào chạy - đúng tình huống xảy ra khi service đã tự tắt bằng watchdog rồi
288
+ * JS mới gọi finishUpload. Ném ra ở đây sẽ biến một lần dọn dẹp vô hại thành
289
+ * một lần nén báo lỗi cho user.
290
+ */
291
+ private void sendToService(String action, String jobId) {
229
292
  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;
293
+ Intent intent = new Intent(getContext(), VideoCompressionService.class);
294
+ intent.setAction(action);
295
+ intent.putExtra(VideoCompressionService.EXTRA_JOB_ID, jobId);
296
+ getContext().startService(intent);
236
297
  } 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
- }
298
+ Log.w("NativeVideoCompressor", "Could not send " + action + " to service", e);
244
299
  }
245
300
  }
246
301