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 +50 -6
- package/android/src/main/AndroidManifest.xml +7 -4
- package/android/src/main/java/com/atomic/videocompressor/NativeVideoCompressorPlugin.java +95 -40
- package/android/src/main/java/com/atomic/videocompressor/VideoCompressionService.java +272 -55
- package/dist/docs.json +46 -0
- package/dist/esm/definitions.d.ts +30 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +5 -0
- package/dist/esm/web.js +7 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +7 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +7 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/NativeVideoCompressorPlugin/NativeVideoCompressorPlugin.swift +9 -1
- package/package.json +1 -1
|
@@ -3,109 +3,326 @@ package com.atomic.videocompressor;
|
|
|
3
3
|
import android.app.Notification;
|
|
4
4
|
import android.app.NotificationChannel;
|
|
5
5
|
import android.app.NotificationManager;
|
|
6
|
+
import android.app.PendingIntent;
|
|
6
7
|
import android.app.Service;
|
|
7
8
|
import android.content.Intent;
|
|
8
9
|
import android.content.pm.ServiceInfo;
|
|
9
10
|
import android.os.Build;
|
|
11
|
+
import android.os.Handler;
|
|
10
12
|
import android.os.IBinder;
|
|
13
|
+
import android.os.Looper;
|
|
14
|
+
import android.os.SystemClock;
|
|
11
15
|
import android.util.Log;
|
|
12
16
|
import androidx.annotation.Nullable;
|
|
13
17
|
import androidx.core.app.NotificationCompat;
|
|
18
|
+
import androidx.core.app.ServiceCompat;
|
|
19
|
+
import java.util.LinkedHashMap;
|
|
20
|
+
import java.util.Map;
|
|
14
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Giữ tiến trình nén + upload sống khi user rời app.
|
|
24
|
+
*
|
|
25
|
+
* Mọi thứ trong đây phải đáp ứng được 5 tiêu chí foreground service của Google
|
|
26
|
+
* Play, vì bản thiếu chúng (1.1.0) đã bị từ chối ở version code 29:
|
|
27
|
+
*
|
|
28
|
+
* - user perceptible -> notification có progress thật, cập nhật theo % nén
|
|
29
|
+
* và theo pha (nén / tải lên), chứ không phải một dòng chữ tĩnh
|
|
30
|
+
* - stoppable by user -> action "Huỷ" ngay trên notification, vì khi user đã
|
|
31
|
+
* rời app thì UI trong app không còn với tới được
|
|
32
|
+
* - runs only as long as necessary -> watchdog tự tắt khi không còn ai báo
|
|
33
|
+
* tiến trình, để một lần JS chết giữa đường không để lại FGS chạy vô hạn
|
|
34
|
+
*/
|
|
15
35
|
public class VideoCompressionService extends Service {
|
|
16
36
|
|
|
17
37
|
private static final String TAG = "VideoCompressionService";
|
|
18
38
|
private static final String CHANNEL_ID = "VideoCompressionChannel";
|
|
19
39
|
private static final int NOTIFICATION_ID = 1001;
|
|
20
40
|
|
|
41
|
+
static final String ACTION_START = "com.atomic.videocompressor.START";
|
|
42
|
+
static final String ACTION_UPLOAD = "com.atomic.videocompressor.UPLOAD";
|
|
43
|
+
static final String ACTION_FINISH = "com.atomic.videocompressor.FINISH";
|
|
44
|
+
/** Do chính user bấm từ notification. */
|
|
45
|
+
static final String ACTION_CANCEL = "com.atomic.videocompressor.CANCEL";
|
|
46
|
+
|
|
47
|
+
static final String EXTRA_JOB_ID = "jobId";
|
|
48
|
+
static final String EXTRA_PERCENT = "percent";
|
|
49
|
+
|
|
21
50
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
51
|
+
* Không nhận được update nào trong khoảng này thì coi như phía gọi đã chết
|
|
52
|
+
* (WebView bị kill, hoặc một exception thoát ra ngoài nhánh finishUpload) và
|
|
53
|
+
* tự dừng. Không có nó, `jobs` không bao giờ rỗng và service chạy mãi với
|
|
54
|
+
* một notification treo - đúng thứ khiến Play đánh trượt tiêu chí "runs only
|
|
55
|
+
* for as long as necessary".
|
|
24
56
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
57
|
+
* Chọn 15 phút: dài hơn mọi khoảng lặng hợp lệ (upload một video đã nén trên
|
|
58
|
+
* mạng chậm, hoặc pha nén chưa kịp bắn progress đầu tiên), ngắn hơn nhiều so
|
|
59
|
+
* với mức mà user kịp nhận ra có gì đó treo.
|
|
60
|
+
*/
|
|
61
|
+
private static final long WATCHDOG_MS = 15 * 60 * 1000L;
|
|
62
|
+
|
|
63
|
+
/** Đừng vẽ lại notification mỗi tick progress của LightCompressor. */
|
|
64
|
+
private static final long MIN_REDRAW_INTERVAL_MS = 500L;
|
|
65
|
+
|
|
66
|
+
private enum Phase { COMPRESSING, UPLOADING }
|
|
67
|
+
|
|
68
|
+
private static final class Job {
|
|
69
|
+
Phase phase = Phase.COMPRESSING;
|
|
70
|
+
int percent = 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** LinkedHashMap: notification một job hiển thị đúng job đã bắt đầu trước. */
|
|
74
|
+
private final Map<String, Job> jobs = new LinkedHashMap<>();
|
|
75
|
+
|
|
76
|
+
private final Handler handler = new Handler(Looper.getMainLooper());
|
|
77
|
+
private final Runnable watchdog = this::onWatchdogFired;
|
|
78
|
+
|
|
79
|
+
private long lastRedrawUptime = 0;
|
|
80
|
+
private boolean inForeground = false;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Instance đang sống, để plugin đẩy % vào mà không phải gửi một Intent cho
|
|
84
|
+
* mỗi tick.
|
|
30
85
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
86
|
+
* volatile vì được đọc từ thread nền: LightCompressor gọi onProgress thẳng
|
|
87
|
+
* từ coroutine encoder (Dispatchers.Default), không hop về main - đã kiểm
|
|
88
|
+
* bằng bytecode, onProgressChanged gọi listener trực tiếp không qua
|
|
89
|
+
* withContext. Thiếu volatile thì thread đó có thể đọc phải một tham chiếu
|
|
90
|
+
* cũ của service đã destroy.
|
|
34
91
|
*/
|
|
35
|
-
|
|
92
|
+
@Nullable
|
|
93
|
+
private static volatile VideoCompressionService instance = null;
|
|
36
94
|
|
|
37
95
|
@Override
|
|
38
96
|
public void onCreate() {
|
|
39
97
|
super.onCreate();
|
|
40
|
-
|
|
98
|
+
instance = this;
|
|
99
|
+
NotificationChannel channel = new NotificationChannel(
|
|
100
|
+
CHANNEL_ID, "Xử lý và tải video lên", NotificationManager.IMPORTANCE_LOW
|
|
101
|
+
);
|
|
102
|
+
channel.setDescription("Tiến trình nén và tải video lên");
|
|
103
|
+
NotificationManager manager = getSystemService(NotificationManager.class);
|
|
104
|
+
if (manager != null) manager.createNotificationChannel(channel);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Override
|
|
108
|
+
public void onDestroy() {
|
|
109
|
+
handler.removeCallbacks(watchdog);
|
|
110
|
+
if (instance == this) instance = null;
|
|
111
|
+
super.onDestroy();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Cập nhật tiến độ nén. Gọi được từ thread bất kỳ.
|
|
116
|
+
*
|
|
117
|
+
* No-op khi service chưa/không còn sống: progress là thông tin trang trí,
|
|
118
|
+
* không đáng để dựng lại một foreground service đã tắt.
|
|
119
|
+
*/
|
|
120
|
+
static void publishProgress(String jobId, int percent) {
|
|
121
|
+
VideoCompressionService service = instance;
|
|
122
|
+
if (service == null) return;
|
|
123
|
+
// Chuyển về main thread: lời gọi tới đây nằm trên thread encoder của
|
|
124
|
+
// LightCompressor, còn jobs/handler/notification đều chỉ được chạm từ
|
|
125
|
+
// main. Không post thì LinkedHashMap bị sửa đồng thời với onStartCommand.
|
|
126
|
+
service.handler.post(() -> service.applyProgress(jobId, percent));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private void applyProgress(String jobId, int percent) {
|
|
130
|
+
Job job = jobs.get(jobId);
|
|
131
|
+
if (job == null) return;
|
|
132
|
+
|
|
133
|
+
int clamped = Math.max(0, Math.min(100, percent));
|
|
134
|
+
// Mỗi update đều gia hạn watchdog, kể cả khi bỏ qua việc vẽ lại:
|
|
135
|
+
// throttle chỉ để tiết kiệm vẽ, không phải dấu hiệu job đã chết.
|
|
136
|
+
armWatchdog();
|
|
137
|
+
if (job.percent == clamped) return;
|
|
138
|
+
job.percent = clamped;
|
|
139
|
+
|
|
140
|
+
long now = SystemClock.uptimeMillis();
|
|
141
|
+
if (now - lastRedrawUptime < MIN_REDRAW_INTERVAL_MS && clamped < 100) return;
|
|
142
|
+
lastRedrawUptime = now;
|
|
143
|
+
render();
|
|
41
144
|
}
|
|
42
145
|
|
|
43
146
|
@Override
|
|
44
147
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
45
|
-
|
|
148
|
+
if (intent == null) {
|
|
149
|
+
// START_NOT_STICKY nên chỉ xảy ra khi hệ thống restart service;
|
|
150
|
+
// không có job nào để phục hồi, dừng cho sạch.
|
|
151
|
+
stopEverything();
|
|
152
|
+
return START_NOT_STICKY;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
String action = intent.getAction();
|
|
46
156
|
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
157
|
+
if (ACTION_CANCEL.equals(action)) {
|
|
158
|
+
// Notification là thứ duy nhất user còn thấy khi đã rời app, nên
|
|
159
|
+
// nút này huỷ tất cả: không có UI nào ở đây để chọn từng job.
|
|
160
|
+
NativeVideoCompressorPlugin.cancelAllFromNotification();
|
|
161
|
+
stopEverything();
|
|
162
|
+
return START_NOT_STICKY;
|
|
50
163
|
}
|
|
51
164
|
|
|
165
|
+
String jobId = intent.getStringExtra(EXTRA_JOB_ID);
|
|
166
|
+
if (jobId == null) return START_NOT_STICKY;
|
|
167
|
+
|
|
168
|
+
if (ACTION_START.equals(action)) {
|
|
169
|
+
jobs.put(jobId, new Job());
|
|
170
|
+
} else if (ACTION_UPLOAD.equals(action)) {
|
|
171
|
+
Job job = jobs.get(jobId);
|
|
172
|
+
if (job != null) {
|
|
173
|
+
job.phase = Phase.UPLOADING;
|
|
174
|
+
// Pha upload không có % nào bắn về, nên thanh progress chuyển
|
|
175
|
+
// sang indeterminate thay vì đứng im ở con số của pha nén.
|
|
176
|
+
job.percent = -1;
|
|
177
|
+
}
|
|
178
|
+
} else if (ACTION_FINISH.equals(action)) {
|
|
179
|
+
jobs.remove(jobId);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (jobs.isEmpty()) {
|
|
183
|
+
stopEverything();
|
|
184
|
+
return START_NOT_STICKY;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
armWatchdog();
|
|
188
|
+
lastRedrawUptime = SystemClock.uptimeMillis();
|
|
189
|
+
render();
|
|
52
190
|
return START_NOT_STICKY;
|
|
53
191
|
}
|
|
54
192
|
|
|
55
|
-
|
|
193
|
+
/**
|
|
194
|
+
* startForeground() phải được gọi trong mọi nhánh của onStartCommand đến
|
|
195
|
+
* được từ startForegroundService(), kể cả nhánh dừng ngay: nếu không,
|
|
196
|
+
* Android giết cả app bằng ForegroundServiceDidNotStartInTimeException.
|
|
197
|
+
*/
|
|
198
|
+
private void render() {
|
|
199
|
+
Job shown = jobs.values().iterator().next();
|
|
200
|
+
int running = jobs.size();
|
|
201
|
+
boolean indeterminate = shown.percent < 0;
|
|
202
|
+
|
|
203
|
+
String title;
|
|
204
|
+
if (running > 1) {
|
|
205
|
+
title = "Đang xử lý " + running + " video";
|
|
206
|
+
} else {
|
|
207
|
+
title = shown.phase == Phase.UPLOADING ? "Đang tải video lên" : "Đang nén video";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Hai pha nói hai điều khác nhau, vì chúng chạy ở hai nơi khác nhau.
|
|
211
|
+
//
|
|
212
|
+
// Nén chạy ở native trong chính service này nên vẫn chạy khi app
|
|
213
|
+
// xuống nền. Upload thì chạy bằng JS trong webview, nên nó đứng lại
|
|
214
|
+
// và chỉ tiếp tục khi app trở lại foreground - đo được: nén xong là
|
|
215
|
+
// mọi hoạt động mạng dừng hẳn cho tới khi watchdog tự dừng service
|
|
216
|
+
// sau 15 phút. Vì vậy pha upload phải nói rõ việc người dùng cần làm.
|
|
217
|
+
String text;
|
|
218
|
+
if (shown.phase == Phase.UPLOADING) {
|
|
219
|
+
text = indeterminate
|
|
220
|
+
? "Mở lại ứng dụng để hoàn tất tải lên"
|
|
221
|
+
: shown.percent + "% - mở lại ứng dụng để hoàn tất tải lên";
|
|
222
|
+
} else {
|
|
223
|
+
text = indeterminate
|
|
224
|
+
? "Đang nén video"
|
|
225
|
+
: shown.percent + "% - đang nén";
|
|
226
|
+
}
|
|
227
|
+
|
|
56
228
|
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
|
|
57
|
-
.setContentTitle(
|
|
58
|
-
.setContentText(
|
|
59
|
-
.setSmallIcon(
|
|
229
|
+
.setContentTitle(title)
|
|
230
|
+
.setContentText(text)
|
|
231
|
+
.setSmallIcon(resolveSmallIcon())
|
|
232
|
+
.setProgress(100, indeterminate ? 0 : shown.percent, indeterminate)
|
|
233
|
+
.setOngoing(true)
|
|
234
|
+
// Không có nó, mỗi lần vẽ lại progress là một lần notification
|
|
235
|
+
// nhảy lên đầu danh sách và rung máy.
|
|
236
|
+
.setOnlyAlertOnce(true)
|
|
60
237
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
238
|
+
.setContentIntent(buildOpenAppIntent())
|
|
239
|
+
.addAction(0, "Huỷ", buildCancelIntent())
|
|
61
240
|
.build();
|
|
62
241
|
|
|
63
242
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
}
|
|
243
|
+
ServiceCompat.startForeground(
|
|
244
|
+
this, NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
|
245
|
+
);
|
|
246
|
+
inForeground = true;
|
|
77
247
|
} catch (Exception e) {
|
|
78
|
-
// Nuốt
|
|
79
|
-
//
|
|
80
|
-
Log.e(TAG, "startForeground thất bại -
|
|
248
|
+
// Nuốt để không giết app, nhưng log: service chạy nền âm thầm thì
|
|
249
|
+
// nén vẫn bị huỷ ngay khi user rời app và không ai biết tại sao.
|
|
250
|
+
Log.e(TAG, "startForeground thất bại - tiến trình có thể bị huỷ khi rời app", e);
|
|
81
251
|
}
|
|
82
252
|
}
|
|
83
253
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Icon brand của app nếu có drawable `ic_stat_video_compress`, nếu không thì
|
|
256
|
+
* icon hệ thống. Notification mang icon hệ thống trông như của Android chứ
|
|
257
|
+
* không phải của app - trên video demo gửi Play, đó là một điểm trừ.
|
|
258
|
+
*/
|
|
259
|
+
private int resolveSmallIcon() {
|
|
260
|
+
int id = getResources().getIdentifier("ic_stat_video_compress", "drawable", getPackageName());
|
|
261
|
+
return id != 0 ? id : android.R.drawable.ic_popup_sync;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private PendingIntent buildCancelIntent() {
|
|
265
|
+
Intent intent = new Intent(this, VideoCompressionService.class).setAction(ACTION_CANCEL);
|
|
266
|
+
return PendingIntent.getService(
|
|
267
|
+
this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
|
268
|
+
);
|
|
90
269
|
}
|
|
91
270
|
|
|
271
|
+
/** Bấm vào notification thì quay lại app, chứ không phải không có gì xảy ra. */
|
|
92
272
|
@Nullable
|
|
273
|
+
private PendingIntent buildOpenAppIntent() {
|
|
274
|
+
Intent launch = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
|
275
|
+
if (launch == null) return null;
|
|
276
|
+
launch.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
|
277
|
+
return PendingIntent.getActivity(
|
|
278
|
+
this, 2, launch, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private void armWatchdog() {
|
|
283
|
+
handler.removeCallbacks(watchdog);
|
|
284
|
+
handler.postDelayed(watchdog, WATCHDOG_MS);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private void onWatchdogFired() {
|
|
288
|
+
Log.w(TAG, "Không có tiến trình nào trong " + (WATCHDOG_MS / 60000) + " phút - tự dừng");
|
|
289
|
+
NativeVideoCompressorPlugin.cancelAllFromNotification();
|
|
290
|
+
stopEverything();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Android 15+ giới hạn dataSync tổng 6 giờ/24 giờ rồi gọi onTimeout. Phải
|
|
295
|
+
* dừng trong vài giây, nếu không app ăn ANR. destini-web đặt minSdk 35 nên
|
|
296
|
+
* mọi thiết bị đều ở trong diện này.
|
|
297
|
+
*/
|
|
93
298
|
@Override
|
|
94
|
-
public
|
|
95
|
-
|
|
299
|
+
public void onTimeout(int startId, int fgsType) {
|
|
300
|
+
Log.w(TAG, "Hệ thống hết hạn foreground service (type " + fgsType + ") - dừng ngay");
|
|
301
|
+
NativeVideoCompressorPlugin.cancelAllFromNotification();
|
|
302
|
+
stopEverything();
|
|
96
303
|
}
|
|
97
304
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
305
|
+
/** Overload của API 34, giữ lại cho thiết bị chạy Android 14. */
|
|
306
|
+
@Override
|
|
307
|
+
public void onTimeout(int startId) {
|
|
308
|
+
Log.w(TAG, "Hệ thống hết hạn foreground service - dừng ngay");
|
|
309
|
+
NativeVideoCompressorPlugin.cancelAllFromNotification();
|
|
310
|
+
stopEverything();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private void stopEverything() {
|
|
314
|
+
handler.removeCallbacks(watchdog);
|
|
315
|
+
jobs.clear();
|
|
316
|
+
if (inForeground) {
|
|
317
|
+
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
|
318
|
+
inForeground = false;
|
|
109
319
|
}
|
|
320
|
+
stopSelf();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@Nullable
|
|
324
|
+
@Override
|
|
325
|
+
public IBinder onBind(Intent intent) {
|
|
326
|
+
return null;
|
|
110
327
|
}
|
|
111
328
|
}
|
package/dist/docs.json
CHANGED
|
@@ -53,6 +53,22 @@
|
|
|
53
53
|
"complexTypes": [],
|
|
54
54
|
"slug": "initialize"
|
|
55
55
|
},
|
|
56
|
+
{
|
|
57
|
+
"name": "finishUpload",
|
|
58
|
+
"signature": "(options: { jobId: string; }) => Promise<void>",
|
|
59
|
+
"parameters": [
|
|
60
|
+
{
|
|
61
|
+
"name": "options",
|
|
62
|
+
"docs": "",
|
|
63
|
+
"type": "{ jobId: string; }"
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"returns": "Promise<void>",
|
|
67
|
+
"tags": [],
|
|
68
|
+
"docs": "Báo rằng phần upload đi kèm `holdForUpload` đã xong (thành công hay thất\nbại đều phải gọi) để Android hạ foreground service xuống.\n\nNo-op trên web và iOS.",
|
|
69
|
+
"complexTypes": [],
|
|
70
|
+
"slug": "finishupload"
|
|
71
|
+
},
|
|
56
72
|
{
|
|
57
73
|
"name": "addListener",
|
|
58
74
|
"signature": "(eventName: 'onProgress', listenerFunc: (info: { status: string; percent?: number; }) => void) => Promise<PluginListenerHandle>",
|
|
@@ -75,6 +91,29 @@
|
|
|
75
91
|
"PluginListenerHandle"
|
|
76
92
|
],
|
|
77
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-"
|
|
78
117
|
}
|
|
79
118
|
],
|
|
80
119
|
"properties": []
|
|
@@ -137,6 +176,13 @@
|
|
|
137
176
|
"docs": "Names this compress so `cancel({ jobId })` can target it.\n\nEvery platform compresses one video at a time, so cancellation is global\nby nature. Two independent callers in the same app (say a comment queue\nand a notes queue) therefore need a way to say *which* job they mean —\nwithout one, a cancel from either kills whatever the other is running.",
|
|
138
177
|
"complexTypes": [],
|
|
139
178
|
"type": "string | undefined"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"name": "holdForUpload",
|
|
182
|
+
"tags": [],
|
|
183
|
+
"docs": "Android: giữ foreground service sống sau khi nén xong, để phần upload do\nJS chạy tiếp theo không bị hệ thống giết khi user rời app.\n\nBật cờ này là một lời hứa: caller PHẢI gọi {@link NativeVideoCompressorPlugin.finishUpload}\nkhi upload kết thúc, kể cả ở nhánh lỗi. Không gọi thì service chỉ còn\nwatchdog dọn hộ sau 15 phút, và trong 15 phút đó user nhìn thấy một\nnotification nói rằng app đang làm việc trong khi nó không làm gì cả.\n\nKhông có tác dụng trên web và iOS.",
|
|
184
|
+
"complexTypes": [],
|
|
185
|
+
"type": "boolean | undefined"
|
|
140
186
|
}
|
|
141
187
|
]
|
|
142
188
|
},
|
|
@@ -18,6 +18,18 @@ export interface CompressOptions {
|
|
|
18
18
|
* without one, a cancel from either kills whatever the other is running.
|
|
19
19
|
*/
|
|
20
20
|
jobId?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Android: giữ foreground service sống sau khi nén xong, để phần upload do
|
|
23
|
+
* JS chạy tiếp theo không bị hệ thống giết khi user rời app.
|
|
24
|
+
*
|
|
25
|
+
* Bật cờ này là một lời hứa: caller PHẢI gọi {@link NativeVideoCompressorPlugin.finishUpload}
|
|
26
|
+
* khi upload kết thúc, kể cả ở nhánh lỗi. Không gọi thì service chỉ còn
|
|
27
|
+
* watchdog dọn hộ sau 15 phút, và trong 15 phút đó user nhìn thấy một
|
|
28
|
+
* notification nói rằng app đang làm việc trong khi nó không làm gì cả.
|
|
29
|
+
*
|
|
30
|
+
* Không có tác dụng trên web và iOS.
|
|
31
|
+
*/
|
|
32
|
+
holdForUpload?: boolean;
|
|
21
33
|
}
|
|
22
34
|
export interface CompressResult {
|
|
23
35
|
success: boolean;
|
|
@@ -49,8 +61,26 @@ export interface NativeVideoCompressorPlugin {
|
|
|
49
61
|
success: boolean;
|
|
50
62
|
message?: string;
|
|
51
63
|
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Báo rằng phần upload đi kèm `holdForUpload` đã xong (thành công hay thất
|
|
66
|
+
* bại đều phải gọi) để Android hạ foreground service xuống.
|
|
67
|
+
*
|
|
68
|
+
* No-op trên web và iOS.
|
|
69
|
+
*/
|
|
70
|
+
finishUpload(options: {
|
|
71
|
+
jobId: string;
|
|
72
|
+
}): Promise<void>;
|
|
52
73
|
addListener(eventName: 'onProgress', listenerFunc: (info: {
|
|
53
74
|
status: string;
|
|
54
75
|
percent?: number;
|
|
55
76
|
}) => void): Promise<PluginListenerHandle>;
|
|
77
|
+
/**
|
|
78
|
+
* Android: user đã bấm "Huỷ" trên notification của foreground service.
|
|
79
|
+
*
|
|
80
|
+
* Pha nén được plugin tự huỷ trước khi event này bắn, nhưng pha upload chạy
|
|
81
|
+
* trong JS nên không ai dừng hộ được - caller phải tự abort request đang
|
|
82
|
+
* chạy. Bỏ qua event này nghĩa là nút Huỷ không thật sự huỷ, và Play đánh
|
|
83
|
+
* trượt tiêu chí "can be terminated or stopped by the user".
|
|
84
|
+
*/
|
|
85
|
+
addListener(eventName: 'onCancelRequested', listenerFunc: () => void): Promise<PluginListenerHandle>;
|
|
56
86
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n/**\n * `code` on the error `compressVideo` rejects with when `cancel()` stopped it.\n * Callers should treat it as \"the user asked for this\" — no error toast, no\n * retry prompt — and tell it apart from a genuine compression failure.\n */\nexport const CANCELLED = 'CANCELLED';\n\nexport interface CompressOptions {\n sourcePath: string;\n destPath?: string;\n quality?: 'VERY_HIGH' | 'HIGH' | 'MEDIUM' | 'LOW' | 'VERY_LOW' | '360P';\n /**\n * Names this compress so `cancel({ jobId })` can target it.\n *\n * Every platform compresses one video at a time, so cancellation is global\n * by nature. Two independent callers in the same app (say a comment queue\n * and a notes queue) therefore need a way to say *which* job they mean —\n * without one, a cancel from either kills whatever the other is running.\n */\n jobId?: string;\n}\n\nexport interface CompressResult {\n success: boolean;\n destPath: string;\n}\n\nexport interface CancelOptions {\n /**\n * Only cancel if this is the job currently running. Omit to cancel whatever\n * is running, whoever started it.\n */\n jobId?: string;\n}\n\nexport interface CancelResult {\n /** False when nothing was running, or when a different job was. */\n cancelled: boolean;\n}\n\nexport interface NativeVideoCompressorPlugin {\n compressVideo(options: CompressOptions): Promise<CompressResult>;\n\n /**\n * Stop the compress that is running now. `compressVideo` then rejects with\n * code {@link CANCELLED} and its partial output is deleted.\n *\n * This is the only way to stop one: the work is synchronous WASM inside a\n * web worker (web) or an AVFoundation / LightCompressor pass on its own\n * thread (native), and none of them watch an AbortSignal.\n */\n cancel(options?: CancelOptions): Promise<CancelResult>;\n\n initialize(): Promise<{ success: boolean; message?: string }>;\n\n // Đã sửa lại chuẩn cho Capacitor mới nhất (chỉ trả về Promise)\n addListener(\n eventName: 'onProgress',\n listenerFunc: (info: { status: string; percent?: number }) => void,\n ): Promise<PluginListenerHandle>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n/**\n * `code` on the error `compressVideo` rejects with when `cancel()` stopped it.\n * Callers should treat it as \"the user asked for this\" — no error toast, no\n * retry prompt — and tell it apart from a genuine compression failure.\n */\nexport const CANCELLED = 'CANCELLED';\n\nexport interface CompressOptions {\n sourcePath: string;\n destPath?: string;\n quality?: 'VERY_HIGH' | 'HIGH' | 'MEDIUM' | 'LOW' | 'VERY_LOW' | '360P';\n /**\n * Names this compress so `cancel({ jobId })` can target it.\n *\n * Every platform compresses one video at a time, so cancellation is global\n * by nature. Two independent callers in the same app (say a comment queue\n * and a notes queue) therefore need a way to say *which* job they mean —\n * without one, a cancel from either kills whatever the other is running.\n */\n jobId?: string;\n\n /**\n * Android: giữ foreground service sống sau khi nén xong, để phần upload do\n * JS chạy tiếp theo không bị hệ thống giết khi user rời app.\n *\n * Bật cờ này là một lời hứa: caller PHẢI gọi {@link NativeVideoCompressorPlugin.finishUpload}\n * khi upload kết thúc, kể cả ở nhánh lỗi. Không gọi thì service chỉ còn\n * watchdog dọn hộ sau 15 phút, và trong 15 phút đó user nhìn thấy một\n * notification nói rằng app đang làm việc trong khi nó không làm gì cả.\n *\n * Không có tác dụng trên web và iOS.\n */\n holdForUpload?: boolean;\n}\n\nexport interface CompressResult {\n success: boolean;\n destPath: string;\n}\n\nexport interface CancelOptions {\n /**\n * Only cancel if this is the job currently running. Omit to cancel whatever\n * is running, whoever started it.\n */\n jobId?: string;\n}\n\nexport interface CancelResult {\n /** False when nothing was running, or when a different job was. */\n cancelled: boolean;\n}\n\nexport interface NativeVideoCompressorPlugin {\n compressVideo(options: CompressOptions): Promise<CompressResult>;\n\n /**\n * Stop the compress that is running now. `compressVideo` then rejects with\n * code {@link CANCELLED} and its partial output is deleted.\n *\n * This is the only way to stop one: the work is synchronous WASM inside a\n * web worker (web) or an AVFoundation / LightCompressor pass on its own\n * thread (native), and none of them watch an AbortSignal.\n */\n cancel(options?: CancelOptions): Promise<CancelResult>;\n\n initialize(): Promise<{ success: boolean; message?: string }>;\n\n /**\n * Báo rằng phần upload đi kèm `holdForUpload` đã xong (thành công hay thất\n * bại đều phải gọi) để Android hạ foreground service xuống.\n *\n * No-op trên web và iOS.\n */\n finishUpload(options: { jobId: string }): Promise<void>;\n\n // Đã sửa lại chuẩn cho Capacitor mới nhất (chỉ trả về Promise)\n addListener(\n eventName: 'onProgress',\n listenerFunc: (info: { status: string; percent?: number }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Android: user đã bấm \"Huỷ\" trên notification của foreground service.\n *\n * Pha nén được plugin tự huỷ trước khi event này bắn, nhưng pha upload chạy\n * trong JS nên không ai dừng hộ được - caller phải tự abort request đang\n * chạy. Bỏ qua event này nghĩa là nút Huỷ không thật sự huỷ, và Play đánh\n * trượt tiêu chí \"can be terminated or stopped by the user\".\n */\n addListener(\n eventName: 'onCancelRequested',\n listenerFunc: () => void,\n ): Promise<PluginListenerHandle>;\n}\n"]}
|
package/dist/esm/web.d.ts
CHANGED
|
@@ -24,6 +24,11 @@ export declare class NativeVideoCompressorWeb extends WebPlugin implements Nativ
|
|
|
24
24
|
* stop it — its cost is the loaded core, which is why the core URLs are
|
|
25
25
|
* cached above.
|
|
26
26
|
*/
|
|
27
|
+
/**
|
|
28
|
+
* Chỉ Android mới có foreground service để mà hạ xuống. Hiện diện ở đây để
|
|
29
|
+
* caller gọi thẳng, không phải rẽ nhánh theo platform ở mọi call site.
|
|
30
|
+
*/
|
|
31
|
+
finishUpload(): Promise<void>;
|
|
27
32
|
cancel(options?: CancelOptions): Promise<CancelResult>;
|
|
28
33
|
private fetchSourceBlob;
|
|
29
34
|
private canUseWebCodecs;
|
package/dist/esm/web.js
CHANGED
|
@@ -134,6 +134,13 @@ export class NativeVideoCompressorWeb extends WebPlugin {
|
|
|
134
134
|
* stop it — its cost is the loaded core, which is why the core URLs are
|
|
135
135
|
* cached above.
|
|
136
136
|
*/
|
|
137
|
+
/**
|
|
138
|
+
* Chỉ Android mới có foreground service để mà hạ xuống. Hiện diện ở đây để
|
|
139
|
+
* caller gọi thẳng, không phải rẽ nhánh theo platform ở mọi call site.
|
|
140
|
+
*/
|
|
141
|
+
async finishUpload() {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
137
144
|
async cancel(options) {
|
|
138
145
|
var _a;
|
|
139
146
|
if (!this.currentJobId)
|