capacitor-native-video-compressor 1.1.3 → 2.0.0
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 +51 -57
- package/android/build.gradle +5 -0
- package/android/src/main/AndroidManifest.xml +8 -14
- package/android/src/main/java/com/atomic/videocompressor/NativeVideoCompressorPlugin.java +217 -116
- package/android/src/main/java/com/atomic/videocompressor/VideoCompressionWorker.java +120 -0
- package/dist/docs.json +15 -38
- package/dist/esm/definitions.d.ts +30 -25
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +3 -5
- package/dist/esm/web.js +6 -7
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +6 -7
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +6 -7
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/NativeVideoCompressorPlugin/NativeVideoCompressorPlugin.swift +0 -8
- package/package.json +1 -1
- package/android/src/main/java/com/atomic/videocompressor/VideoCompressionService.java +0 -328
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
package com.atomic.videocompressor;
|
|
2
2
|
|
|
3
|
-
import android.content.Intent;
|
|
4
3
|
import android.net.Uri;
|
|
5
4
|
import android.util.Log;
|
|
6
5
|
import androidx.annotation.NonNull;
|
|
7
6
|
import androidx.annotation.Nullable;
|
|
7
|
+
import androidx.work.Data;
|
|
8
|
+
import androidx.work.ExistingWorkPolicy;
|
|
9
|
+
import androidx.work.OneTimeWorkRequest;
|
|
10
|
+
import androidx.work.WorkManager;
|
|
8
11
|
import com.abedelazizshe.lightcompressorlibrary.CompressionListener;
|
|
9
12
|
import com.abedelazizshe.lightcompressorlibrary.VideoCompressor;
|
|
10
13
|
import com.abedelazizshe.lightcompressorlibrary.VideoQuality;
|
|
@@ -16,13 +19,17 @@ import com.getcapacitor.PluginCall;
|
|
|
16
19
|
import com.getcapacitor.PluginMethod;
|
|
17
20
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
18
21
|
import java.io.File;
|
|
19
|
-
import java.lang.ref.WeakReference;
|
|
20
22
|
import java.util.Arrays;
|
|
21
23
|
import java.util.Collections;
|
|
24
|
+
import java.util.concurrent.CountDownLatch;
|
|
25
|
+
import java.util.concurrent.TimeUnit;
|
|
26
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
22
27
|
|
|
23
28
|
@CapacitorPlugin(name = "NativeVideoCompressor")
|
|
24
29
|
public class NativeVideoCompressorPlugin extends Plugin {
|
|
25
30
|
|
|
31
|
+
private static final String TAG = "NativeVideoCompressor";
|
|
32
|
+
|
|
26
33
|
/**
|
|
27
34
|
* Matches the `CANCELLED` constant the JS side exports, so one
|
|
28
35
|
* `error.code` check covers web, iOS and Android alike.
|
|
@@ -32,51 +39,14 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
32
39
|
private static final String STORAGE_SUBFOLDER = "compressed_videos";
|
|
33
40
|
|
|
34
41
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* đây là giữ luôn cả WebView sau khi Activity đã chết.
|
|
42
|
+
* Trần cho pha giữ-chờ-upload. Ngắn hơn mốc 10 phút mà WorkManager tự cắt,
|
|
43
|
+
* để một caller chết giữa chừng không chiếm quota job tới tận phút cuối.
|
|
38
44
|
*/
|
|
39
|
-
private static
|
|
45
|
+
private static final long HOLD_FOR_UPLOAD_TIMEOUT_MINUTES = 8;
|
|
40
46
|
|
|
41
47
|
/** The job compressVideo is running right now, for cancel({ jobId }). */
|
|
42
48
|
private String currentJobId = null;
|
|
43
49
|
|
|
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
|
-
|
|
80
50
|
/**
|
|
81
51
|
* Nothing to do natively; LightCompressor needs no warm-up. Answered
|
|
82
52
|
* rather than left unimplemented so a caller can await it unconditionally
|
|
@@ -104,11 +74,15 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
104
74
|
// running job could finish and a new one start in the gap — and
|
|
105
75
|
// VideoCompressor.cancel() being global, it would kill the newcomer.
|
|
106
76
|
// That is exactly what checking the jobId is meant to prevent.
|
|
107
|
-
//
|
|
108
|
-
// the lock across
|
|
77
|
+
// Both calls are non-blocking (one cancels a coroutine job, the other
|
|
78
|
+
// returns an Operation), so holding the lock across them costs nothing.
|
|
109
79
|
synchronized (this) {
|
|
110
80
|
cancelled = currentJobId != null && (requested == null || requested.equals(currentJobId));
|
|
111
81
|
if (cancelled) {
|
|
82
|
+
// Huỷ cả work: pha nén có thể chưa kịp bắt đầu, lúc đó
|
|
83
|
+
// VideoCompressor.cancel() không có gì để huỷ và work vẫn sẽ
|
|
84
|
+
// chạy sau đó. Huỷ work làm onStopped bắn và job tự kết thúc.
|
|
85
|
+
cancelWork(currentJobId);
|
|
112
86
|
VideoCompressor.cancel();
|
|
113
87
|
}
|
|
114
88
|
}
|
|
@@ -117,13 +91,19 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
117
91
|
call.resolve(ret);
|
|
118
92
|
}
|
|
119
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Caller báo đã xong việc với kết quả nén - thường là upload xong - nên
|
|
96
|
+
* worker đang giữ process sống được phép kết thúc.
|
|
97
|
+
*
|
|
98
|
+
* Phải gọi trên MỌI nhánh kết thúc của pha upload, kể cả nhánh lỗi. Bỏ sót
|
|
99
|
+
* một nhánh nghĩa là máy phải nuôi một job rỗng tới khi hết hạn giữ.
|
|
100
|
+
*/
|
|
120
101
|
@PluginMethod
|
|
121
102
|
public void finishUpload(PluginCall call) {
|
|
122
103
|
String jobId = call.getString("jobId");
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
call.resolve();
|
|
104
|
+
JSObject ret = new JSObject();
|
|
105
|
+
ret.put("released", jobId != null && VideoCompressionWorker.release(jobId));
|
|
106
|
+
call.resolve(ret);
|
|
127
107
|
}
|
|
128
108
|
|
|
129
109
|
@PluginMethod
|
|
@@ -136,10 +116,6 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
136
116
|
}
|
|
137
117
|
|
|
138
118
|
String jobId = call.getString("jobId", "default");
|
|
139
|
-
boolean holdForUpload = Boolean.TRUE.equals(call.getBoolean("holdForUpload", false));
|
|
140
|
-
synchronized (this) {
|
|
141
|
-
currentJobId = jobId;
|
|
142
|
-
}
|
|
143
119
|
|
|
144
120
|
// 1. Chuẩn bị Uri và Tên file xuất ra
|
|
145
121
|
Uri srcUri;
|
|
@@ -171,10 +147,7 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
171
147
|
}
|
|
172
148
|
}
|
|
173
149
|
|
|
174
|
-
Log.d(
|
|
175
|
-
"NativeVideoCompressor",
|
|
176
|
-
"====== VIDEO COMPRESSION QUALITY RECEIVED: " + qualityString + "+ video quality: " + videoQuality + " ======"
|
|
177
|
-
);
|
|
150
|
+
Log.d(TAG, "====== VIDEO COMPRESSION QUALITY RECEIVED: " + qualityString + "+ video quality: " + videoQuality + " ======");
|
|
178
151
|
|
|
179
152
|
// 2. Cấu hình thông số nén (Dành riêng cho LightCompressor 1.3.2)
|
|
180
153
|
// Lưu ý: Java phải truyền đủ 9 tham số do thư viện gốc viết bằng Kotlin
|
|
@@ -192,38 +165,165 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
192
165
|
// 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)
|
|
193
166
|
AppSpecificStorageConfiguration storageConfig = new AppSpecificStorageConfiguration(STORAGE_SUBFOLDER);
|
|
194
167
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
168
|
+
synchronized (this) {
|
|
169
|
+
currentJobId = jobId;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 4. Giao pha nén cho WorkManager. Không nén thẳng ở đây: một thread
|
|
173
|
+
// thường bị đóng băng cùng process ngay khi user rời app, còn process
|
|
174
|
+
// đang chạy worker thì không. Đây là phương án Google Play chỉ đích
|
|
175
|
+
// danh khi từ chối foreground service ở version code 32.
|
|
176
|
+
boolean holdForUpload = Boolean.TRUE.equals(call.getBoolean("holdForUpload", false));
|
|
177
|
+
CompressionJob job = new CompressionJob(jobId, srcUri, fileName, configuration, storageConfig, call, holdForUpload);
|
|
178
|
+
VideoCompressionWorker.register(jobId, job);
|
|
179
|
+
|
|
180
|
+
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(VideoCompressionWorker.class)
|
|
181
|
+
.setInputData(new Data.Builder().putString(VideoCompressionWorker.KEY_JOB_ID, jobId).build())
|
|
182
|
+
.build();
|
|
183
|
+
|
|
199
184
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
185
|
+
WorkManager.getInstance(getContext()).enqueueUniqueWork(workName(jobId), ExistingWorkPolicy.REPLACE, request);
|
|
186
|
+
} catch (Exception e) {
|
|
187
|
+
VideoCompressionWorker.unregister(jobId);
|
|
188
|
+
synchronized (this) {
|
|
189
|
+
currentJobId = null;
|
|
190
|
+
}
|
|
191
|
+
call.reject("Cannot schedule video compression", e);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Một lần nén, chạy bên trong {@link VideoCompressionWorker}.
|
|
197
|
+
*
|
|
198
|
+
* Không phải static: nó cần notifyListeners và getContext của plugin đang
|
|
199
|
+
* sống để bắn progress và trả kết quả về đúng PluginCall.
|
|
200
|
+
*/
|
|
201
|
+
private final class CompressionJob implements VideoCompressionWorker.Job {
|
|
202
|
+
|
|
203
|
+
private final String jobId;
|
|
204
|
+
private final Uri srcUri;
|
|
205
|
+
private final String fileName;
|
|
206
|
+
private final Configuration configuration;
|
|
207
|
+
private final AppSpecificStorageConfiguration storageConfig;
|
|
208
|
+
private final PluginCall call;
|
|
209
|
+
private final boolean holdForUpload;
|
|
210
|
+
|
|
211
|
+
/** Mở khi pha nén kết thúc, để worker biết lúc nào được trả về. */
|
|
212
|
+
private final CountDownLatch done = new CountDownLatch(1);
|
|
213
|
+
|
|
214
|
+
/** Mở khi caller gọi finishUpload - chỉ dùng khi holdForUpload. */
|
|
215
|
+
private final CountDownLatch released = new CountDownLatch(1);
|
|
216
|
+
|
|
217
|
+
/** Chốt một-lần cho mọi nhánh kết thúc; PluginCall chỉ được trả lời một lần. */
|
|
218
|
+
private final AtomicBoolean delivered = new AtomicBoolean(false);
|
|
219
|
+
|
|
220
|
+
/** True khi nén xong có kết quả dùng được, tức là có cái để upload. */
|
|
221
|
+
private volatile boolean compressed = false;
|
|
222
|
+
|
|
223
|
+
private boolean started = false;
|
|
224
|
+
private boolean stopped = false;
|
|
225
|
+
|
|
226
|
+
CompressionJob(
|
|
227
|
+
String jobId,
|
|
228
|
+
Uri srcUri,
|
|
229
|
+
String fileName,
|
|
230
|
+
Configuration configuration,
|
|
231
|
+
AppSpecificStorageConfiguration storageConfig,
|
|
232
|
+
PluginCall call,
|
|
233
|
+
boolean holdForUpload
|
|
234
|
+
) {
|
|
235
|
+
this.jobId = jobId;
|
|
236
|
+
this.srcUri = srcUri;
|
|
237
|
+
this.fileName = fileName;
|
|
238
|
+
this.configuration = configuration;
|
|
239
|
+
this.storageConfig = storageConfig;
|
|
240
|
+
this.call = call;
|
|
241
|
+
this.holdForUpload = holdForUpload;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@Override
|
|
245
|
+
public void run() {
|
|
246
|
+
synchronized (this) {
|
|
247
|
+
// stop() đã chạy trước khi worker kịp khởi động: đừng bắt đầu
|
|
248
|
+
// một lần nén mà không còn ai chờ kết quả.
|
|
249
|
+
if (!stopped) {
|
|
250
|
+
VideoCompressor.start(
|
|
251
|
+
getContext(),
|
|
252
|
+
Collections.singletonList(srcUri), // Nhận vào một List chứa đường dẫn
|
|
253
|
+
false, // isStreamable
|
|
254
|
+
null, // sharedStorageConfiguration (Không dùng)
|
|
255
|
+
storageConfig, // Cấu hình lưu trữ
|
|
256
|
+
configuration, // Cấu hình chất lượng
|
|
257
|
+
listener()
|
|
258
|
+
);
|
|
259
|
+
started = true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!started) {
|
|
264
|
+
deliverCancelled();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
done.await();
|
|
270
|
+
} catch (InterruptedException e) {
|
|
271
|
+
Thread.currentThread().interrupt();
|
|
272
|
+
deliverCancelled();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Nén hỏng hoặc bị huỷ thì không có gì để upload - về ngay thay vì
|
|
277
|
+
// giữ process sống chờ một finishUpload không bao giờ tới.
|
|
278
|
+
if (!holdForUpload || !compressed) return;
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
// Có hạn chứ không chờ vô tận: nếu caller chết giữa pha upload
|
|
282
|
+
// mà không gọi finishUpload, worker sẽ treo tới khi WorkManager
|
|
283
|
+
// cắt ở phút thứ 10. Về sớm hơn để trả quota job lại cho máy.
|
|
284
|
+
released.await(HOLD_FOR_UPLOAD_TIMEOUT_MINUTES, TimeUnit.MINUTES);
|
|
285
|
+
} catch (InterruptedException e) {
|
|
286
|
+
Thread.currentThread().interrupt();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@Override
|
|
291
|
+
public void release() {
|
|
292
|
+
released.countDown();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
@Override
|
|
296
|
+
public void stop() {
|
|
297
|
+
boolean wasRunning;
|
|
298
|
+
synchronized (this) {
|
|
299
|
+
if (stopped) return;
|
|
300
|
+
stopped = true;
|
|
301
|
+
wasRunning = started;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Trước mọi thứ khác: worker đang bị cắt, đừng để run() còn ngồi
|
|
305
|
+
// chờ finishUpload nữa.
|
|
306
|
+
released.countDown();
|
|
307
|
+
|
|
308
|
+
if (wasRunning) {
|
|
309
|
+
// onCancelled của LightCompressor sẽ mở chốt; không tự gọi
|
|
310
|
+
// deliverCancelled ở đây để nhánh dọn file tạm vẫn chạy.
|
|
311
|
+
try {
|
|
312
|
+
VideoCompressor.cancel();
|
|
313
|
+
} catch (Exception e) {
|
|
314
|
+
Log.w(TAG, "Could not cancel compression", e);
|
|
315
|
+
deliverCancelled();
|
|
316
|
+
}
|
|
202
317
|
} else {
|
|
203
|
-
|
|
318
|
+
// Chưa bắt đầu thì không có callback nào bắn, phải tự kết thúc.
|
|
319
|
+
deliverCancelled();
|
|
204
320
|
}
|
|
205
|
-
} catch (Exception e) {
|
|
206
|
-
synchronized (this) { currentJobId = null; }
|
|
207
|
-
call.reject("Cannot start foreground video processing", e);
|
|
208
|
-
return;
|
|
209
321
|
}
|
|
210
322
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
getContext(),
|
|
214
|
-
Collections.singletonList(srcUri), // Nhận vào một List chứa đường dẫn
|
|
215
|
-
false, // isStreamable
|
|
216
|
-
null, // sharedStorageConfiguration (Không dùng)
|
|
217
|
-
storageConfig, // Cấu hình lưu trữ
|
|
218
|
-
configuration, // Cấu hình chất lượng
|
|
219
|
-
new CompressionListener() {
|
|
323
|
+
private CompressionListener listener() {
|
|
324
|
+
return new CompressionListener() {
|
|
220
325
|
@Override
|
|
221
326
|
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
|
-
|
|
227
327
|
// Gửi % tiến độ về Javascript để vẽ thanh Progress
|
|
228
328
|
JSObject ret = new JSObject();
|
|
229
329
|
ret.put("status", "progress");
|
|
@@ -233,13 +333,17 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
233
333
|
|
|
234
334
|
@Override
|
|
235
335
|
public void onFailure(int i, @NonNull String failureMessage) {
|
|
236
|
-
|
|
336
|
+
if (!claim()) return;
|
|
237
337
|
call.reject("Nén thất bại: " + failureMessage);
|
|
238
338
|
}
|
|
239
339
|
|
|
240
340
|
@Override
|
|
241
341
|
public void onSuccess(int i, long l, @Nullable String s) {
|
|
242
|
-
|
|
342
|
+
// Phải đặt TRƯỚC claim(): claim() mở chốt `done`, và run()
|
|
343
|
+
// đọc `compressed` ngay khi chốt đó mở để quyết định có ở
|
|
344
|
+
// lại chờ upload hay không.
|
|
345
|
+
compressed = s != null;
|
|
346
|
+
if (!claim()) return;
|
|
243
347
|
// Nén xong, trả kết quả đường dẫn file mới về cho JS
|
|
244
348
|
JSObject ret = new JSObject();
|
|
245
349
|
ret.put("success", true);
|
|
@@ -256,46 +360,43 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
256
360
|
|
|
257
361
|
@Override
|
|
258
362
|
public void onCancelled(int i) {
|
|
259
|
-
|
|
260
|
-
// LightCompressor leaves whatever it had already written
|
|
261
|
-
// behind; without this the cache keeps a partial mp4 per
|
|
262
|
-
// cancel, and nothing ever collects them.
|
|
263
|
-
deleteOutput(fileName);
|
|
264
|
-
// Coded so the caller can tell "the user asked for this"
|
|
265
|
-
// apart from a real failure — same code on every platform.
|
|
266
|
-
call.reject("Đã hủy nén", CANCELLED_CODE);
|
|
363
|
+
deliverCancelled();
|
|
267
364
|
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private void deliverCancelled() {
|
|
369
|
+
if (!claim()) return;
|
|
370
|
+
// LightCompressor leaves whatever it had already written behind;
|
|
371
|
+
// without this the cache keeps a partial mp4 per cancel, and
|
|
372
|
+
// nothing ever collects them.
|
|
373
|
+
deleteOutput(fileName);
|
|
374
|
+
// Coded so the caller can tell "the user asked for this" apart
|
|
375
|
+
// from a real failure — same code on every platform.
|
|
376
|
+
call.reject("Đã hủy nén", CANCELLED_CODE);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** True đúng một lần, cho nhánh kết thúc đầu tiên tới đích. */
|
|
380
|
+
private boolean claim() {
|
|
381
|
+
if (!delivered.compareAndSet(false, true)) return false;
|
|
382
|
+
synchronized (NativeVideoCompressorPlugin.this) {
|
|
383
|
+
if (jobId.equals(currentJobId)) currentJobId = null;
|
|
268
384
|
}
|
|
269
|
-
|
|
385
|
+
done.countDown();
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
270
388
|
}
|
|
271
389
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
sendToService(
|
|
275
|
-
holdForUpload ? VideoCompressionService.ACTION_UPLOAD : VideoCompressionService.ACTION_FINISH,
|
|
276
|
-
jobId
|
|
277
|
-
);
|
|
278
|
-
synchronized (this) {
|
|
279
|
-
currentJobId = null;
|
|
280
|
-
}
|
|
390
|
+
private String workName(String jobId) {
|
|
391
|
+
return "video-compression:" + jobId;
|
|
281
392
|
}
|
|
282
393
|
|
|
283
|
-
/**
|
|
284
|
-
|
|
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) {
|
|
394
|
+
/** Best-effort: huỷ một work đã xong là no-op, không đáng để hỏng lời gọi cancel. */
|
|
395
|
+
private void cancelWork(String jobId) {
|
|
292
396
|
try {
|
|
293
|
-
|
|
294
|
-
intent.setAction(action);
|
|
295
|
-
intent.putExtra(VideoCompressionService.EXTRA_JOB_ID, jobId);
|
|
296
|
-
getContext().startService(intent);
|
|
397
|
+
WorkManager.getInstance(getContext()).cancelUniqueWork(workName(jobId));
|
|
297
398
|
} catch (Exception e) {
|
|
298
|
-
Log.w(
|
|
399
|
+
Log.w(TAG, "Could not cancel work for " + jobId, e);
|
|
299
400
|
}
|
|
300
401
|
}
|
|
301
402
|
|
|
@@ -308,7 +409,7 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
308
409
|
partial.delete();
|
|
309
410
|
}
|
|
310
411
|
} catch (Exception e) {
|
|
311
|
-
Log.w(
|
|
412
|
+
Log.w(TAG, "Could not delete cancelled output", e);
|
|
312
413
|
}
|
|
313
414
|
}
|
|
314
415
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
package com.atomic.videocompressor;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import androidx.annotation.NonNull;
|
|
5
|
+
import androidx.annotation.Nullable;
|
|
6
|
+
import androidx.work.Worker;
|
|
7
|
+
import androidx.work.WorkerParameters;
|
|
8
|
+
import java.util.Map;
|
|
9
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Giữ pha nén chạy được khi user rời app, không cần foreground service.
|
|
13
|
+
*
|
|
14
|
+
* Android 14+ đóng băng process ở trạng thái cached, nên nén bằng một thread
|
|
15
|
+
* thường sẽ đứng lại ngay khi user chuyển app. Foreground service từng là cách
|
|
16
|
+
* giữ nó sống, nhưng Google Play từ chối version code 32: use case này được xếp
|
|
17
|
+
* vào data sync + local processing, không đủ điều kiện cho
|
|
18
|
+
* FOREGROUND_SERVICE_MEDIA_PROCESSING, và họ chỉ đích danh WorkManager làm
|
|
19
|
+
* phương án thay thế. Process đang chạy một worker thì không bị đóng băng.
|
|
20
|
+
*
|
|
21
|
+
* Worker chỉ là vỏ bọc: việc nén thật do plugin đăng ký qua {@link #register}
|
|
22
|
+
* ngay trước khi enqueue, vì chỉ plugin mới giữ được PluginCall để trả kết quả.
|
|
23
|
+
*
|
|
24
|
+
* Với `holdForUpload`, worker ở lại sau khi nén xong cho tới khi caller gọi
|
|
25
|
+
* `finishUpload`. Pha upload chạy bằng JS trong webview, và khoảnh khắc worker
|
|
26
|
+
* kết thúc là khoảnh khắc process thành cached rồi bị đóng băng - đo trên
|
|
27
|
+
* Android 16: upload chết đúng ~5 giây sau khi worker trả về. Giữ worker sống
|
|
28
|
+
* xuyên pha upload là cách duy nhất còn lại để nó sống sót, vì foreground
|
|
29
|
+
* service đã bị Play cấm cho use case này.
|
|
30
|
+
*
|
|
31
|
+
* Giới hạn phải biết: WorkManager dừng worker sau 10 phút, tính chung cả nén
|
|
32
|
+
* lẫn upload. Quá mức đó {@link #onStopped()} huỷ việc đang chạy và caller
|
|
33
|
+
* nhận lỗi CANCELLED - đổi lại lấy được nhiều thời gian hơn thì phải có
|
|
34
|
+
* foreground service, thứ không còn được phép ở đây.
|
|
35
|
+
*/
|
|
36
|
+
public class VideoCompressionWorker extends Worker {
|
|
37
|
+
|
|
38
|
+
static final String KEY_JOB_ID = "jobId";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Việc nén do plugin đăng ký, tra theo jobId.
|
|
42
|
+
*
|
|
43
|
+
* Static vì worker được WorkManager dựng mới, không nhận được tham chiếu
|
|
44
|
+
* nào từ phía plugin. Map chỉ có nghĩa trong phạm vi một process: process
|
|
45
|
+
* chết là WebView chết, không còn ai nhận kết quả nén nữa.
|
|
46
|
+
*/
|
|
47
|
+
private static final Map<String, Job> pending = new ConcurrentHashMap<>();
|
|
48
|
+
|
|
49
|
+
interface Job {
|
|
50
|
+
/**
|
|
51
|
+
* Chạy pha nén, và với holdForUpload thì ở lại tới khi caller gọi
|
|
52
|
+
* finishUpload. Chỉ trả về khi không còn gì phải giữ process sống.
|
|
53
|
+
*/
|
|
54
|
+
void run();
|
|
55
|
+
|
|
56
|
+
/** WorkManager đòi dừng - hết 10 phút, hoặc work bị cancel. */
|
|
57
|
+
void stop();
|
|
58
|
+
|
|
59
|
+
/** Caller báo đã upload xong; worker được phép kết thúc. */
|
|
60
|
+
void release();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static void register(String jobId, Job job) {
|
|
64
|
+
pending.put(jobId, job);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static void unregister(String jobId) {
|
|
68
|
+
pending.remove(jobId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Thả worker đang giữ process sống cho pha upload của jobId.
|
|
73
|
+
*
|
|
74
|
+
* False khi không còn job nào mang id đó - worker đã kết thúc từ trước,
|
|
75
|
+
* không có gì để thả. Đó là trạng thái bình thường, không phải lỗi.
|
|
76
|
+
*/
|
|
77
|
+
static boolean release(String jobId) {
|
|
78
|
+
Job job = pending.get(jobId);
|
|
79
|
+
if (job == null) return false;
|
|
80
|
+
job.release();
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Nullable
|
|
85
|
+
private volatile Job running = null;
|
|
86
|
+
|
|
87
|
+
public VideoCompressionWorker(@NonNull Context context, @NonNull WorkerParameters params) {
|
|
88
|
+
super(context, params);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@NonNull
|
|
92
|
+
@Override
|
|
93
|
+
public Result doWork() {
|
|
94
|
+
String jobId = getInputData().getString(KEY_JOB_ID);
|
|
95
|
+
if (jobId == null) return Result.success();
|
|
96
|
+
|
|
97
|
+
Job job = pending.get(jobId);
|
|
98
|
+
// Không còn job: process đã bị kill và WorkManager dựng lại worker ở
|
|
99
|
+
// process mới. WebView đã chết cùng nó nên không còn ai nhận kết quả -
|
|
100
|
+
// chạy lại pha nén chỉ tốn pin. Success chứ không retry, để WorkManager
|
|
101
|
+
// bỏ hẳn work này đi.
|
|
102
|
+
if (job == null) return Result.success();
|
|
103
|
+
|
|
104
|
+
running = job;
|
|
105
|
+
try {
|
|
106
|
+
job.run();
|
|
107
|
+
} finally {
|
|
108
|
+
running = null;
|
|
109
|
+
pending.remove(jobId);
|
|
110
|
+
}
|
|
111
|
+
return Result.success();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Override
|
|
115
|
+
public void onStopped() {
|
|
116
|
+
Job job = running;
|
|
117
|
+
if (job != null) job.stop();
|
|
118
|
+
super.onStopped();
|
|
119
|
+
}
|
|
120
|
+
}
|
package/dist/docs.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"returns": "Promise<CompressResult>",
|
|
19
19
|
"tags": [],
|
|
20
|
-
"docs": "",
|
|
20
|
+
"docs": "Compress one video and resolve with the path of the result.\n\nAndroid: the work runs inside a WorkManager worker, which keeps the\nprocess out of the frozen cached state while the user is away — there is\nno foreground service and no notification, because Google Play classes\nthis use case as data sync plus local processing and rejects\n`FOREGROUND_SERVICE_MEDIA_PROCESSING` for it. WorkManager stops a worker\nafter 10 minutes, so a compress that runs longer rejects with\n{@link CANCELLED}.",
|
|
21
21
|
"complexTypes": [
|
|
22
22
|
"CompressResult",
|
|
23
23
|
"CompressOptions"
|
|
@@ -43,19 +43,9 @@
|
|
|
43
43
|
],
|
|
44
44
|
"slug": "cancel"
|
|
45
45
|
},
|
|
46
|
-
{
|
|
47
|
-
"name": "initialize",
|
|
48
|
-
"signature": "() => Promise<{ success: boolean; message?: string; }>",
|
|
49
|
-
"parameters": [],
|
|
50
|
-
"returns": "Promise<{ success: boolean; message?: string | undefined; }>",
|
|
51
|
-
"tags": [],
|
|
52
|
-
"docs": "",
|
|
53
|
-
"complexTypes": [],
|
|
54
|
-
"slug": "initialize"
|
|
55
|
-
},
|
|
56
46
|
{
|
|
57
47
|
"name": "finishUpload",
|
|
58
|
-
"signature": "(options: { jobId: string; }) => Promise<
|
|
48
|
+
"signature": "(options: { jobId: string; }) => Promise<{ released: boolean; }>",
|
|
59
49
|
"parameters": [
|
|
60
50
|
{
|
|
61
51
|
"name": "options",
|
|
@@ -63,12 +53,22 @@
|
|
|
63
53
|
"type": "{ jobId: string; }"
|
|
64
54
|
}
|
|
65
55
|
],
|
|
66
|
-
"returns": "Promise<
|
|
56
|
+
"returns": "Promise<{ released: boolean; }>",
|
|
67
57
|
"tags": [],
|
|
68
|
-
"docs": "
|
|
58
|
+
"docs": "Release the worker that `holdForUpload` kept alive for this job.\n\nCall it on every terminal branch of the upload, success or failure. A\nmissed call leaves the device feeding an idle job until the hold expires.\n`released` is false when no job by that id was still being held, which is\nthe normal outcome of a late or duplicate call, not an error.",
|
|
69
59
|
"complexTypes": [],
|
|
70
60
|
"slug": "finishupload"
|
|
71
61
|
},
|
|
62
|
+
{
|
|
63
|
+
"name": "initialize",
|
|
64
|
+
"signature": "() => Promise<{ success: boolean; message?: string; }>",
|
|
65
|
+
"parameters": [],
|
|
66
|
+
"returns": "Promise<{ success: boolean; message?: string | undefined; }>",
|
|
67
|
+
"tags": [],
|
|
68
|
+
"docs": "",
|
|
69
|
+
"complexTypes": [],
|
|
70
|
+
"slug": "initialize"
|
|
71
|
+
},
|
|
72
72
|
{
|
|
73
73
|
"name": "addListener",
|
|
74
74
|
"signature": "(eventName: 'onProgress', listenerFunc: (info: { status: string; percent?: number; }) => void) => Promise<PluginListenerHandle>",
|
|
@@ -91,29 +91,6 @@
|
|
|
91
91
|
"PluginListenerHandle"
|
|
92
92
|
],
|
|
93
93
|
"slug": "addlisteneronprogress-"
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
"name": "addListener",
|
|
97
|
-
"signature": "(eventName: 'onCancelRequested', listenerFunc: () => void) => Promise<PluginListenerHandle>",
|
|
98
|
-
"parameters": [
|
|
99
|
-
{
|
|
100
|
-
"name": "eventName",
|
|
101
|
-
"docs": "",
|
|
102
|
-
"type": "'onCancelRequested'"
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
"name": "listenerFunc",
|
|
106
|
-
"docs": "",
|
|
107
|
-
"type": "() => void"
|
|
108
|
-
}
|
|
109
|
-
],
|
|
110
|
-
"returns": "Promise<PluginListenerHandle>",
|
|
111
|
-
"tags": [],
|
|
112
|
-
"docs": "Android: user đã bấm \"Huỷ\" trên notification của foreground service.\n\nPha nén được plugin tự huỷ trước khi event này bắn, nhưng pha upload chạy\ntrong JS nên không ai dừng hộ được - caller phải tự abort request đang\nchạy. Bỏ qua event này nghĩa là nút Huỷ không thật sự huỷ, và Play đánh\ntrượt tiêu chí \"can be terminated or stopped by the user\".",
|
|
113
|
-
"complexTypes": [
|
|
114
|
-
"PluginListenerHandle"
|
|
115
|
-
],
|
|
116
|
-
"slug": "addlisteneroncancelrequested-"
|
|
117
94
|
}
|
|
118
95
|
],
|
|
119
96
|
"properties": []
|
|
@@ -180,7 +157,7 @@
|
|
|
180
157
|
{
|
|
181
158
|
"name": "holdForUpload",
|
|
182
159
|
"tags": [],
|
|
183
|
-
"docs": "Android:
|
|
160
|
+
"docs": "Android only: keep the background worker alive after the compress ends,\nuntil `finishUpload({ jobId })` is called.\n\nThe upload runs as JavaScript in the webview, and the moment the worker\nreturns the process becomes cached and is frozen — measured on Android 16,\nan in-flight upload dies about five seconds later. Set this whenever the\ncompressed file is about to be uploaded, and always pair it with a\n`finishUpload` in a `finally`.",
|
|
184
161
|
"complexTypes": [],
|
|
185
162
|
"type": "boolean | undefined"
|
|
186
163
|
}
|