capacitor-native-video-compressor 1.2.0 → 2.1.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 +53 -33
- package/android/build.gradle +6 -4
- package/android/src/main/AndroidManifest.xml +8 -17
- package/android/src/main/java/com/atomic/videocompressor/NativeVideoCompressor.java +134 -4
- package/android/src/main/java/com/atomic/videocompressor/NativeVideoCompressorPlugin.java +385 -122
- package/android/src/main/java/com/atomic/videocompressor/VideoCompressionWorker.java +134 -0
- package/dist/docs.json +24 -24
- package/dist/esm/definitions.d.ts +52 -9
- package/dist/esm/definitions.js +14 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/opfs-output.d.ts +7 -0
- package/dist/esm/opfs-output.js +94 -0
- package/dist/esm/opfs-output.js.map +1 -0
- package/dist/esm/web.d.ts +21 -3
- package/dist/esm/web.js +194 -129
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +306 -128
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +307 -129
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/NativeVideoCompressorPlugin/NativeVideoCompressor.swift +34 -0
- package/ios/Sources/NativeVideoCompressorPlugin/NativeVideoCompressorPlugin.swift +118 -79
- package/ios/Tests/NativeVideoCompressorPluginTests/NativeVideoCompressorTests.swift +44 -0
- package/package.json +9 -4
- package/android/src/main/java/com/atomic/videocompressor/VideoCompressionService.java +0 -310
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
package com.atomic.videocompressor;
|
|
2
2
|
|
|
3
|
-
import android.
|
|
3
|
+
import android.media.MediaMetadataRetriever;
|
|
4
4
|
import android.net.Uri;
|
|
5
5
|
import android.util.Log;
|
|
6
6
|
import androidx.annotation.NonNull;
|
|
7
7
|
import androidx.annotation.Nullable;
|
|
8
|
+
import androidx.work.Data;
|
|
9
|
+
import androidx.work.ExistingWorkPolicy;
|
|
10
|
+
import androidx.work.OneTimeWorkRequest;
|
|
11
|
+
import androidx.work.WorkManager;
|
|
8
12
|
import com.abedelazizshe.lightcompressorlibrary.CompressionListener;
|
|
9
13
|
import com.abedelazizshe.lightcompressorlibrary.VideoCompressor;
|
|
10
14
|
import com.abedelazizshe.lightcompressorlibrary.VideoQuality;
|
|
@@ -16,13 +20,19 @@ import com.getcapacitor.PluginCall;
|
|
|
16
20
|
import com.getcapacitor.PluginMethod;
|
|
17
21
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
18
22
|
import java.io.File;
|
|
19
|
-
import java.lang.ref.WeakReference;
|
|
20
23
|
import java.util.Arrays;
|
|
21
24
|
import java.util.Collections;
|
|
25
|
+
import java.util.HashSet;
|
|
26
|
+
import java.util.Set;
|
|
27
|
+
import java.util.concurrent.CountDownLatch;
|
|
28
|
+
import java.util.concurrent.TimeUnit;
|
|
29
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
22
30
|
|
|
23
31
|
@CapacitorPlugin(name = "NativeVideoCompressor")
|
|
24
32
|
public class NativeVideoCompressorPlugin extends Plugin {
|
|
25
33
|
|
|
34
|
+
private static final String TAG = "NativeVideoCompressor";
|
|
35
|
+
|
|
26
36
|
/**
|
|
27
37
|
* Matches the `CANCELLED` constant the JS side exports, so one
|
|
28
38
|
* `error.code` check covers web, iOS and Android alike.
|
|
@@ -31,68 +41,133 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
31
41
|
|
|
32
42
|
private static final String STORAGE_SUBFOLDER = "compressed_videos";
|
|
33
43
|
|
|
44
|
+
/** Gắn lên mọi work của plugin để dọn được job mồ côi của phiên WebView trước. */
|
|
45
|
+
private static final String WORK_TAG = "native-video-compressor";
|
|
46
|
+
|
|
47
|
+
/** Thứ tự khớp NativeVideoCompressor.SOURCE_FACTORS. */
|
|
48
|
+
private static final VideoQuality[] QUALITY_BY_SOURCE_FACTOR = {
|
|
49
|
+
VideoQuality.VERY_LOW,
|
|
50
|
+
VideoQuality.LOW,
|
|
51
|
+
VideoQuality.MEDIUM,
|
|
52
|
+
VideoQuality.HIGH,
|
|
53
|
+
VideoQuality.VERY_HIGH
|
|
54
|
+
};
|
|
55
|
+
|
|
34
56
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* đây là giữ luôn cả WebView sau khi Activity đã chết.
|
|
57
|
+
* Trần cho pha giữ-chờ-upload. Ngắn hơn mốc 10 phút mà WorkManager tự cắt,
|
|
58
|
+
* để một caller chết giữa chừng không chiếm quota job tới tận phút cuối.
|
|
38
59
|
*/
|
|
39
|
-
private static
|
|
60
|
+
private static final long HOLD_FOR_UPLOAD_TIMEOUT_MINUTES = 8;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Mốc process này nạp plugin - trước mọi lần nén của nó. Bản sao nguồn cũ
|
|
64
|
+
* hơn mốc này là của một process đã chết.
|
|
65
|
+
*/
|
|
66
|
+
private static final long PROCESS_START_MS = System.currentTimeMillis();
|
|
40
67
|
|
|
41
68
|
/** The job compressVideo is running right now, for cancel({ jobId }). */
|
|
42
69
|
private String currentJobId = null;
|
|
43
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Nothing to do natively; LightCompressor needs no warm-up. Answered
|
|
73
|
+
* rather than left unimplemented so a caller can await it unconditionally
|
|
74
|
+
* (the web build does need it, and shares the call site).
|
|
75
|
+
*/
|
|
76
|
+
@PluginMethod
|
|
77
|
+
public void initialize(PluginCall call) {
|
|
78
|
+
JSObject ret = new JSObject();
|
|
79
|
+
ret.put("success", true);
|
|
80
|
+
ret.put("message", "No initialization needed on Android");
|
|
81
|
+
call.resolve(ret);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Bridge mới (Activity dựng lại): mọi job còn chạy thuộc phiên cũ. */
|
|
44
85
|
@Override
|
|
45
86
|
public void load() {
|
|
46
|
-
|
|
47
|
-
active = new WeakReference<>(this);
|
|
87
|
+
cancelOrphans(PROCESS_START_MS);
|
|
48
88
|
}
|
|
49
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Bridge gọi hàm này ở onPageStarted (Bridge.reset), tức mỗi lần WebView
|
|
92
|
+
* tải lại trang - load() thì không chạy lại. JS cũ đã chết nên không còn
|
|
93
|
+
* ai nhận kết quả các job đang chạy.
|
|
94
|
+
*
|
|
95
|
+
* Mốc dọn là "bây giờ": JS mới chưa chạy nên mọi bản sao nguồn đều mồ
|
|
96
|
+
* côi, kể cả bản một job huỷ giữa pha chép vẫn đang ghi tiếp sau claim().
|
|
97
|
+
*/
|
|
50
98
|
@Override
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
99
|
+
public void removeAllListeners() {
|
|
100
|
+
super.removeAllListeners();
|
|
101
|
+
cancelOrphans(System.currentTimeMillis());
|
|
54
102
|
}
|
|
55
103
|
|
|
56
104
|
/**
|
|
57
|
-
*
|
|
105
|
+
* Dừng job trong process trước, đồng bộ, rồi mới huỷ work. Huỷ work là bất
|
|
106
|
+
* đồng bộ: onStopped tới muộn sẽ gọi VideoCompressor.cancel() - vốn global
|
|
107
|
+
* - và giết luôn job đầu tiên của phiên mới. Dừng trước thì onStopped gặp
|
|
108
|
+
* job đã stopped và không làm gì.
|
|
58
109
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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".
|
|
110
|
+
* ponytail: cờ isRunning của LightCompressor là global; nếu phiên mới gọi
|
|
111
|
+
* start trước khi vòng nén cũ kịp đọc cờ (mỗi ~100 µs) thì job cũ chạy
|
|
112
|
+
* tiếp. JS boot mất cả giây nên gần như không xảy ra; cần chặt hơn thì
|
|
113
|
+
* phải bỏ LightCompressor.
|
|
65
114
|
*/
|
|
66
|
-
|
|
115
|
+
private void cancelOrphans(long copiesOlderThan) {
|
|
116
|
+
VideoCompressionWorker.stopAll();
|
|
67
117
|
try {
|
|
68
|
-
|
|
118
|
+
WorkManager.getInstance(getContext()).cancelAllWorkByTag(WORK_TAG);
|
|
69
119
|
} catch (Exception e) {
|
|
70
|
-
Log.w(
|
|
120
|
+
Log.w(TAG, "Could not cancel orphaned work", e);
|
|
121
|
+
}
|
|
122
|
+
// Process bị kill giữa chừng thì không job nào kịp dọn bản sao của nó.
|
|
123
|
+
for (File copy : sourceCopies()) {
|
|
124
|
+
if (NativeVideoCompressor.isStaleSourceCopy(copy.getName(), copy.lastModified(), copiesOlderThan)) {
|
|
125
|
+
deleteQuietly(copy);
|
|
126
|
+
}
|
|
71
127
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** File thường ở gốc dataDir mang tên đúng 13 chữ số - xem NativeVideoCompressor.isSourceCopyName. */
|
|
131
|
+
private File[] sourceCopies() {
|
|
132
|
+
try {
|
|
133
|
+
File[] files = new File(getContext().getApplicationInfo().dataDir).listFiles(
|
|
134
|
+
(File f) -> f.isFile() && NativeVideoCompressor.isSourceCopyName(f.getName())
|
|
135
|
+
);
|
|
136
|
+
if (files != null) return files;
|
|
137
|
+
} catch (Exception e) {
|
|
138
|
+
Log.w(TAG, "Could not list source copies", e);
|
|
76
139
|
}
|
|
77
|
-
|
|
140
|
+
return new File[0];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private Set<String> sourceCopyNames() {
|
|
144
|
+
Set<String> names = new HashSet<>();
|
|
145
|
+
for (File copy : sourceCopies()) names.add(copy.getName());
|
|
146
|
+
return names;
|
|
78
147
|
}
|
|
79
148
|
|
|
80
149
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
150
|
+
* Xoá bản sao xuất hiện sau `before`. Nếu job khác đang chạy song song mà
|
|
151
|
+
* bản sao của nó bị xoá theo cũng vô hại: LightCompressor 1.3.2 chỉ
|
|
152
|
+
* null-check đường dẫn đó, còn nén thì đọc thẳng từ Uri gốc.
|
|
84
153
|
*/
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
154
|
+
private void deleteSourceCopiesSince(Set<String> before) {
|
|
155
|
+
for (File copy : sourceCopies()) {
|
|
156
|
+
if (!before.contains(copy.getName())) deleteQuietly(copy);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private void deleteQuietly(File file) {
|
|
161
|
+
try {
|
|
162
|
+
if (!file.delete()) Log.w(TAG, "Could not delete " + file);
|
|
163
|
+
} catch (Exception e) {
|
|
164
|
+
Log.w(TAG, "Could not delete " + file, e);
|
|
165
|
+
}
|
|
91
166
|
}
|
|
92
167
|
|
|
93
168
|
/**
|
|
94
|
-
* Stop the running compress.
|
|
95
|
-
*
|
|
169
|
+
* Stop the running compress. The job is stopped directly and answers
|
|
170
|
+
* CANCELLED itself; LightCompressor's own callbacks after that are no-ops.
|
|
96
171
|
*/
|
|
97
172
|
@PluginMethod
|
|
98
173
|
public void cancel(PluginCall call) {
|
|
@@ -104,12 +179,17 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
104
179
|
// running job could finish and a new one start in the gap — and
|
|
105
180
|
// VideoCompressor.cancel() being global, it would kill the newcomer.
|
|
106
181
|
// That is exactly what checking the jobId is meant to prevent.
|
|
107
|
-
//
|
|
108
|
-
// the lock across
|
|
182
|
+
// Both calls are non-blocking (one cancels a coroutine job, the other
|
|
183
|
+
// returns an Operation), so holding the lock across them costs nothing.
|
|
109
184
|
synchronized (this) {
|
|
110
185
|
cancelled = currentJobId != null && (requested == null || requested.equals(currentJobId));
|
|
111
186
|
if (cancelled) {
|
|
112
|
-
|
|
187
|
+
String jobId = currentJobId;
|
|
188
|
+
// Dừng job trực tiếp chứ không trông vào onStopped: work còn
|
|
189
|
+
// ENQUEUED thì huỷ xong WorkManager không hề dựng Worker, nên
|
|
190
|
+
// onStopped không bao giờ bắn và PluginCall sẽ treo.
|
|
191
|
+
if (!VideoCompressionWorker.stop(jobId)) VideoCompressor.cancel();
|
|
192
|
+
cancelWork(jobId);
|
|
113
193
|
}
|
|
114
194
|
}
|
|
115
195
|
|
|
@@ -117,6 +197,21 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
117
197
|
call.resolve(ret);
|
|
118
198
|
}
|
|
119
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Caller báo đã xong việc với kết quả nén - thường là upload xong - nên
|
|
202
|
+
* worker đang giữ process sống được phép kết thúc.
|
|
203
|
+
*
|
|
204
|
+
* 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
|
|
205
|
+
* 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ữ.
|
|
206
|
+
*/
|
|
207
|
+
@PluginMethod
|
|
208
|
+
public void finishUpload(PluginCall call) {
|
|
209
|
+
String jobId = call.getString("jobId");
|
|
210
|
+
JSObject ret = new JSObject();
|
|
211
|
+
ret.put("released", jobId != null && VideoCompressionWorker.release(jobId));
|
|
212
|
+
call.resolve(ret);
|
|
213
|
+
}
|
|
214
|
+
|
|
120
215
|
@PluginMethod
|
|
121
216
|
public void compressVideo(PluginCall call) {
|
|
122
217
|
String sourcePath = call.getString("sourcePath");
|
|
@@ -127,9 +222,6 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
127
222
|
}
|
|
128
223
|
|
|
129
224
|
String jobId = call.getString("jobId", "default");
|
|
130
|
-
synchronized (this) {
|
|
131
|
-
currentJobId = jobId;
|
|
132
|
-
}
|
|
133
225
|
|
|
134
226
|
// 1. Chuẩn bị Uri và Tên file xuất ra
|
|
135
227
|
Uri srcUri;
|
|
@@ -145,75 +237,232 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
145
237
|
|
|
146
238
|
// Đọc quality từ Javascript (Mặc định là MEDIUM)
|
|
147
239
|
String qualityString = call.getString("quality", "MEDIUM");
|
|
240
|
+
NativeVideoCompressor.Target target = NativeVideoCompressor.target(qualityString);
|
|
148
241
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
242
|
+
// Kích thước + bitrate theo cùng bậc thang với iOS/web. Đọc metadata
|
|
243
|
+
// hỏng thì để null, LightCompressor tự chọn như trước.
|
|
244
|
+
int[] size = null;
|
|
245
|
+
NativeVideoCompressor.BitratePlan bitrate = null;
|
|
246
|
+
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
|
247
|
+
try {
|
|
248
|
+
retriever.setDataSource(getContext(), srcUri);
|
|
249
|
+
int width = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
|
|
250
|
+
int height = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
|
|
251
|
+
size = NativeVideoCompressor.outputSize(width, height, target.shortSide);
|
|
252
|
+
bitrate = NativeVideoCompressor.bitrate(
|
|
253
|
+
target.bitrate,
|
|
254
|
+
Long.parseLong(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE))
|
|
255
|
+
);
|
|
256
|
+
} catch (Exception e) {
|
|
257
|
+
// Containers without a bitrate tag (MKV, WebM, screen recordings) still keep the tier's size.
|
|
258
|
+
Log.w(TAG, "Could not read video metadata, using LightCompressor defaults for the rest", e);
|
|
259
|
+
} finally {
|
|
260
|
+
// release() khai báo IOException từ API 29; không dùng
|
|
261
|
+
// try-with-resources vì minSdk 24 chưa có AutoCloseable.
|
|
262
|
+
try {
|
|
263
|
+
retriever.release();
|
|
264
|
+
} catch (Exception ignored) {}
|
|
162
265
|
}
|
|
163
266
|
|
|
267
|
+
// Với bitrate tính theo nguồn, quality chỉ còn chọn hệ số nhân của
|
|
268
|
+
// LightCompressor; với Mbps tường minh hoặc metadata hỏng thì giữ MEDIUM.
|
|
269
|
+
VideoQuality videoQuality =
|
|
270
|
+
bitrate != null && bitrate.mbps == null ? QUALITY_BY_SOURCE_FACTOR[bitrate.sourceFactorIndex] : VideoQuality.MEDIUM;
|
|
271
|
+
|
|
164
272
|
Log.d(
|
|
165
|
-
|
|
166
|
-
"
|
|
273
|
+
TAG,
|
|
274
|
+
"Compress " +
|
|
275
|
+
qualityString +
|
|
276
|
+
": size=" +
|
|
277
|
+
(size == null ? "default" : size[0] + "x" + size[1]) +
|
|
278
|
+
", mbps=" +
|
|
279
|
+
(bitrate == null ? "default" : bitrate.mbps) +
|
|
280
|
+
", quality=" +
|
|
281
|
+
videoQuality
|
|
167
282
|
);
|
|
168
283
|
|
|
169
284
|
// 2. Cấu hình thông số nén (Dành riêng cho LightCompressor 1.3.2)
|
|
170
|
-
//
|
|
285
|
+
// Thứ tự tham số theo Configuration.kt: height ĐỨNG TRƯỚC width.
|
|
171
286
|
Configuration configuration = new Configuration(
|
|
172
287
|
videoQuality, // Chất lượng nén
|
|
173
288
|
false, // tắt tính năng giới hạn Bitrate tối thiểu của file gốc
|
|
174
|
-
|
|
289
|
+
bitrate == null ? null : bitrate.mbps, // videoBitrateInMbps
|
|
175
290
|
false, // disableAudio
|
|
176
291
|
false, // keepOriginalResolution
|
|
177
|
-
null, //
|
|
178
|
-
null, //
|
|
292
|
+
size == null ? null : Double.valueOf(size[1]), // videoHeight
|
|
293
|
+
size == null ? null : Double.valueOf(size[0]), // videoWidth
|
|
179
294
|
Arrays.asList(fileName) // videoNames
|
|
180
295
|
);
|
|
181
296
|
|
|
182
297
|
// 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)
|
|
183
298
|
AppSpecificStorageConfiguration storageConfig = new AppSpecificStorageConfiguration(STORAGE_SUBFOLDER);
|
|
184
299
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
300
|
+
synchronized (this) {
|
|
301
|
+
currentJobId = jobId;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 4. Giao pha nén cho WorkManager. Không nén thẳng ở đây: một thread
|
|
305
|
+
// thường bị đóng băng cùng process ngay khi user rời app, còn process
|
|
306
|
+
// đang chạy worker thì không. Đây là phương án Google Play chỉ đích
|
|
307
|
+
// danh khi từ chối foreground service ở version code 32.
|
|
308
|
+
boolean holdForUpload = Boolean.TRUE.equals(call.getBoolean("holdForUpload", false));
|
|
309
|
+
CompressionJob job = new CompressionJob(jobId, srcUri, fileName, configuration, storageConfig, call, holdForUpload);
|
|
310
|
+
VideoCompressionWorker.register(jobId, job);
|
|
311
|
+
|
|
312
|
+
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(VideoCompressionWorker.class)
|
|
313
|
+
.setInputData(new Data.Builder().putString(VideoCompressionWorker.KEY_JOB_ID, jobId).build())
|
|
314
|
+
.addTag(WORK_TAG)
|
|
315
|
+
.build();
|
|
316
|
+
|
|
189
317
|
try {
|
|
190
|
-
|
|
191
|
-
getContext().startForegroundService(serviceIntent);
|
|
192
|
-
} else {
|
|
193
|
-
getContext().startService(serviceIntent);
|
|
194
|
-
}
|
|
318
|
+
WorkManager.getInstance(getContext()).enqueueUniqueWork(workName(jobId), ExistingWorkPolicy.REPLACE, request);
|
|
195
319
|
} catch (Exception e) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
320
|
+
VideoCompressionWorker.unregister(jobId);
|
|
321
|
+
synchronized (this) {
|
|
322
|
+
currentJobId = null;
|
|
323
|
+
}
|
|
324
|
+
call.reject("Cannot schedule video compression", e);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Một lần nén, chạy bên trong {@link VideoCompressionWorker}.
|
|
330
|
+
*
|
|
331
|
+
* Không phải static: nó cần notifyListeners và getContext của plugin đang
|
|
332
|
+
* sống để bắn progress và trả kết quả về đúng PluginCall.
|
|
333
|
+
*/
|
|
334
|
+
private final class CompressionJob implements VideoCompressionWorker.Job {
|
|
335
|
+
|
|
336
|
+
private final String jobId;
|
|
337
|
+
private final Uri srcUri;
|
|
338
|
+
private final String fileName;
|
|
339
|
+
private final Configuration configuration;
|
|
340
|
+
private final AppSpecificStorageConfiguration storageConfig;
|
|
341
|
+
private final PluginCall call;
|
|
342
|
+
private final boolean holdForUpload;
|
|
343
|
+
|
|
344
|
+
/** Mở khi pha nén kết thúc, để worker biết lúc nào được trả về. */
|
|
345
|
+
private final CountDownLatch done = new CountDownLatch(1);
|
|
346
|
+
|
|
347
|
+
/** Mở khi caller gọi finishUpload - chỉ dùng khi holdForUpload. */
|
|
348
|
+
private final CountDownLatch released = new CountDownLatch(1);
|
|
349
|
+
|
|
350
|
+
/** 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. */
|
|
351
|
+
private final AtomicBoolean delivered = new AtomicBoolean(false);
|
|
352
|
+
|
|
353
|
+
/** True khi nén xong có kết quả dùng được, tức là có cái để upload. */
|
|
354
|
+
private volatile boolean compressed = false;
|
|
355
|
+
|
|
356
|
+
/** Bản sao nguồn có sẵn trước job này; claim() xoá những cái mới hơn. */
|
|
357
|
+
private volatile Set<String> copiesBefore = null;
|
|
358
|
+
|
|
359
|
+
private boolean started = false;
|
|
360
|
+
private boolean stopped = false;
|
|
361
|
+
|
|
362
|
+
CompressionJob(
|
|
363
|
+
String jobId,
|
|
364
|
+
Uri srcUri,
|
|
365
|
+
String fileName,
|
|
366
|
+
Configuration configuration,
|
|
367
|
+
AppSpecificStorageConfiguration storageConfig,
|
|
368
|
+
PluginCall call,
|
|
369
|
+
boolean holdForUpload
|
|
370
|
+
) {
|
|
371
|
+
this.jobId = jobId;
|
|
372
|
+
this.srcUri = srcUri;
|
|
373
|
+
this.fileName = fileName;
|
|
374
|
+
this.configuration = configuration;
|
|
375
|
+
this.storageConfig = storageConfig;
|
|
376
|
+
this.call = call;
|
|
377
|
+
this.holdForUpload = holdForUpload;
|
|
199
378
|
}
|
|
200
379
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
380
|
+
@Override
|
|
381
|
+
public void run() {
|
|
382
|
+
synchronized (this) {
|
|
383
|
+
// stop() đã chạy trước khi worker kịp khởi động: đừng bắt đầu
|
|
384
|
+
// một lần nén mà không còn ai chờ kết quả.
|
|
385
|
+
if (!stopped) {
|
|
386
|
+
copiesBefore = sourceCopyNames();
|
|
387
|
+
VideoCompressor.start(
|
|
388
|
+
getContext(),
|
|
389
|
+
Collections.singletonList(srcUri), // Nhận vào một List chứa đường dẫn
|
|
390
|
+
false, // isStreamable
|
|
391
|
+
null, // sharedStorageConfiguration (Không dùng)
|
|
392
|
+
storageConfig, // Cấu hình lưu trữ
|
|
393
|
+
configuration, // Cấu hình chất lượng
|
|
394
|
+
listener()
|
|
395
|
+
);
|
|
396
|
+
started = true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!started) {
|
|
401
|
+
deliverCancelled();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
done.await();
|
|
407
|
+
} catch (InterruptedException e) {
|
|
408
|
+
Thread.currentThread().interrupt();
|
|
409
|
+
deliverCancelled();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Nén hỏng hoặc bị huỷ thì không có gì để upload - về ngay thay vì
|
|
414
|
+
// giữ process sống chờ một finishUpload không bao giờ tới.
|
|
415
|
+
if (!holdForUpload || !compressed) return;
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
// Có hạn chứ không chờ vô tận: nếu caller chết giữa pha upload
|
|
419
|
+
// mà không gọi finishUpload, worker sẽ treo tới khi WorkManager
|
|
420
|
+
// cắt ở phút thứ 10. Về sớm hơn để trả quota job lại cho máy.
|
|
421
|
+
released.await(HOLD_FOR_UPLOAD_TIMEOUT_MINUTES, TimeUnit.MINUTES);
|
|
422
|
+
} catch (InterruptedException e) {
|
|
423
|
+
Thread.currentThread().interrupt();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
@Override
|
|
428
|
+
public void release() {
|
|
429
|
+
released.countDown();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
@Override
|
|
433
|
+
public void stop() {
|
|
434
|
+
boolean wasRunning;
|
|
435
|
+
synchronized (this) {
|
|
436
|
+
if (stopped) return;
|
|
437
|
+
stopped = true;
|
|
438
|
+
wasRunning = started;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Trước mọi thứ khác: worker đang bị cắt, đừng để run() còn ngồi
|
|
442
|
+
// chờ finishUpload nữa.
|
|
443
|
+
released.countDown();
|
|
444
|
+
|
|
445
|
+
// Đã trả kết quả (đang giữ cho upload) thì không còn gì của job này
|
|
446
|
+
// để huỷ - cancel() global lúc đó chỉ giết job khác đang nén.
|
|
447
|
+
if (wasRunning && !delivered.get()) {
|
|
448
|
+
try {
|
|
449
|
+
VideoCompressor.cancel();
|
|
450
|
+
} catch (Exception e) {
|
|
451
|
+
Log.w(TAG, "Could not cancel compression", e);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// Luôn tự kết thúc: coroutine của LightCompressor bị huỷ thường ném
|
|
455
|
+
// CancellationException mà không bắn callback nào (trước onStart,
|
|
456
|
+
// hay lúc processAudio/finishMovie không đọc isRunning). claim()
|
|
457
|
+
// biến onCancelled tới sau thành no-op, còn onSuccess lỡ tới thì
|
|
458
|
+
// tự xoá output.
|
|
459
|
+
deliverCancelled();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private CompressionListener listener() {
|
|
463
|
+
return new CompressionListener() {
|
|
210
464
|
@Override
|
|
211
465
|
public void onProgress(int i, float v) {
|
|
212
|
-
// Notification phải nhúc nhích theo đúng con số mà UI trong
|
|
213
|
-
// app đang hiển thị: một notification đứng im là lý do Play
|
|
214
|
-
// đánh trượt tiêu chí "user perceptible".
|
|
215
|
-
VideoCompressionService.publishProgress(jobId, Math.round(v));
|
|
216
|
-
|
|
217
466
|
// Gửi % tiến độ về Javascript để vẽ thanh Progress
|
|
218
467
|
JSObject ret = new JSObject();
|
|
219
468
|
ret.put("status", "progress");
|
|
@@ -223,13 +472,23 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
223
472
|
|
|
224
473
|
@Override
|
|
225
474
|
public void onFailure(int i, @NonNull String failureMessage) {
|
|
226
|
-
|
|
475
|
+
if (!claim()) return;
|
|
476
|
+
deleteOutput(fileName);
|
|
227
477
|
call.reject("Nén thất bại: " + failureMessage);
|
|
228
478
|
}
|
|
229
479
|
|
|
230
480
|
@Override
|
|
231
481
|
public void onSuccess(int i, long l, @Nullable String s) {
|
|
232
|
-
|
|
482
|
+
// Phải đặt TRƯỚC claim(): claim() mở chốt `done`, và run()
|
|
483
|
+
// đọc `compressed` ngay khi chốt đó mở để quyết định có ở
|
|
484
|
+
// lại chờ upload hay không.
|
|
485
|
+
compressed = s != null;
|
|
486
|
+
if (!claim()) {
|
|
487
|
+
// Đã trả CANCELLED trước đó (stop() tự kết thúc):
|
|
488
|
+
// không ai nhận file này.
|
|
489
|
+
deleteOutput(fileName);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
233
492
|
// Nén xong, trả kết quả đường dẫn file mới về cho JS
|
|
234
493
|
JSObject ret = new JSObject();
|
|
235
494
|
ret.put("success", true);
|
|
@@ -246,56 +505,60 @@ public class NativeVideoCompressorPlugin extends Plugin {
|
|
|
246
505
|
|
|
247
506
|
@Override
|
|
248
507
|
public void onCancelled(int i) {
|
|
249
|
-
|
|
250
|
-
// LightCompressor leaves whatever it had already written
|
|
251
|
-
// behind; without this the cache keeps a partial mp4 per
|
|
252
|
-
// cancel, and nothing ever collects them.
|
|
253
|
-
deleteOutput(fileName);
|
|
254
|
-
// Coded so the caller can tell "the user asked for this"
|
|
255
|
-
// apart from a real failure — same code on every platform.
|
|
256
|
-
call.reject("Đã hủy nén", CANCELLED_CODE);
|
|
508
|
+
deliverCancelled();
|
|
257
509
|
}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private void deliverCancelled() {
|
|
514
|
+
if (!claim()) return;
|
|
515
|
+
// LightCompressor leaves whatever it had already written behind;
|
|
516
|
+
// without this the cache keeps a partial mp4 per cancel, and
|
|
517
|
+
// nothing ever collects them.
|
|
518
|
+
deleteOutput(fileName);
|
|
519
|
+
// Coded so the caller can tell "the user asked for this" apart
|
|
520
|
+
// from a real failure — same code on every platform.
|
|
521
|
+
call.reject("Đã hủy nén", CANCELLED_CODE);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** True đúng một lần, cho nhánh kết thúc đầu tiên tới đích. */
|
|
525
|
+
private boolean claim() {
|
|
526
|
+
if (!delivered.compareAndSet(false, true)) return false;
|
|
527
|
+
Set<String> before = copiesBefore;
|
|
528
|
+
if (before != null) deleteSourceCopiesSince(before);
|
|
529
|
+
synchronized (NativeVideoCompressorPlugin.this) {
|
|
530
|
+
if (jobId.equals(currentJobId)) currentJobId = null;
|
|
258
531
|
}
|
|
259
|
-
|
|
532
|
+
done.countDown();
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
260
535
|
}
|
|
261
536
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
sendToService(VideoCompressionService.ACTION_FINISH, jobId);
|
|
265
|
-
synchronized (this) {
|
|
266
|
-
currentJobId = null;
|
|
267
|
-
}
|
|
537
|
+
private String workName(String jobId) {
|
|
538
|
+
return "video-compression:" + jobId;
|
|
268
539
|
}
|
|
269
540
|
|
|
270
|
-
/**
|
|
271
|
-
|
|
272
|
-
*
|
|
273
|
-
* startService() từ background bị chặn khi app không còn foreground service
|
|
274
|
-
* nào chạy - đúng tình huống xảy ra khi service đã tự tắt bằng watchdog rồi
|
|
275
|
-
* 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
|
|
276
|
-
* một lần nén báo lỗi cho user.
|
|
277
|
-
*/
|
|
278
|
-
private void sendToService(String action, String jobId) {
|
|
541
|
+
/** Best-effort: huỷ một work đã xong là no-op, không đáng để hỏng lời gọi cancel. */
|
|
542
|
+
private void cancelWork(String jobId) {
|
|
279
543
|
try {
|
|
280
|
-
|
|
281
|
-
intent.setAction(action);
|
|
282
|
-
intent.putExtra(VideoCompressionService.EXTRA_JOB_ID, jobId);
|
|
283
|
-
getContext().startService(intent);
|
|
544
|
+
WorkManager.getInstance(getContext()).cancelUniqueWork(workName(jobId));
|
|
284
545
|
} catch (Exception e) {
|
|
285
|
-
Log.w(
|
|
546
|
+
Log.w(TAG, "Could not cancel work for " + jobId, e);
|
|
286
547
|
}
|
|
287
548
|
}
|
|
288
549
|
|
|
289
|
-
/** Best-effort: a partial file left
|
|
550
|
+
/** Best-effort: a partial file left behind is not worth failing over. */
|
|
290
551
|
private void deleteOutput(String fileName) {
|
|
291
552
|
try {
|
|
292
|
-
|
|
553
|
+
// Nơi LightCompressor 1.3.2 ghi với AppSpecificStorageConfiguration:
|
|
554
|
+
// File(context.filesDir, "<subFolder>/<name>").
|
|
555
|
+
File dir = new File(getContext().getFilesDir(), STORAGE_SUBFOLDER);
|
|
293
556
|
File partial = new File(dir, fileName);
|
|
294
557
|
if (partial.exists()) {
|
|
295
558
|
partial.delete();
|
|
296
559
|
}
|
|
297
560
|
} catch (Exception e) {
|
|
298
|
-
Log.w(
|
|
561
|
+
Log.w(TAG, "Could not delete cancelled output", e);
|
|
299
562
|
}
|
|
300
563
|
}
|
|
301
564
|
}
|