capacitor-native-video-compressor 1.0.4 → 1.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 +88 -5
- package/android/src/main/java/com/atomic/videocompressor/NativeVideoCompressorPlugin.java +88 -5
- package/dist/docs.json +58 -0
- package/dist/esm/definitions.d.ts +35 -0
- package/dist/esm/definitions.js +6 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +23 -2
- package/dist/esm/web.js +328 -138
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +334 -137
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +335 -139
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/NativeVideoCompressorPlugin/NativeVideoCompressorPlugin.swift +112 -8
- package/package.json +3 -2
|
@@ -8,18 +8,69 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
8
8
|
|
|
9
9
|
public let identifier = "NativeVideoCompressorPlugin"
|
|
10
10
|
public let jsName = "NativeVideoCompressor"
|
|
11
|
+
// Every method callable from JS must be listed here. `initialize` was
|
|
12
|
+
// missing, so calling it threw "not implemented" on device — harmless
|
|
13
|
+
// (the web build needs it, native doesn't) but it buried a real signal in
|
|
14
|
+
// a console error every single compress.
|
|
11
15
|
public let pluginMethods:[CAPPluginMethod] = [
|
|
12
|
-
CAPPluginMethod(name: "compressVideo", returnType: CAPPluginReturnPromise)
|
|
16
|
+
CAPPluginMethod(name: "compressVideo", returnType: CAPPluginReturnPromise),
|
|
17
|
+
CAPPluginMethod(name: "cancel", returnType: CAPPluginReturnPromise),
|
|
18
|
+
CAPPluginMethod(name: "initialize", returnType: CAPPluginReturnPromise)
|
|
13
19
|
]
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
|
|
21
|
+
/// The job `compressVideo` is running right now, for `cancel(jobId:)`.
|
|
22
|
+
private var currentJobId: String?
|
|
23
|
+
private var isCancelled = false
|
|
24
|
+
/// Held only so `cancel` can stop it; the compress owns it otherwise.
|
|
25
|
+
private var currentReader: AVAssetReader?
|
|
26
|
+
/// Guards the three fields above — `cancel` arrives on the main thread
|
|
27
|
+
/// while the reader is being driven from the compression queues.
|
|
28
|
+
private let stateLock = NSLock()
|
|
29
|
+
|
|
30
|
+
/// Nothing to do natively; AVFoundation needs no warm-up. Answered rather
|
|
31
|
+
/// than left unimplemented so a caller can await it unconditionally.
|
|
32
|
+
@objc func initialize(_ call: CAPPluginCall) {
|
|
33
|
+
call.resolve(["success": true, "message": "No initialization needed on iOS"])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stop the running compress.
|
|
38
|
+
*
|
|
39
|
+
* `cancelReading()` is all it takes: the frame loops below already check
|
|
40
|
+
* `reader.status == .reading` every pass, so a cancelled reader sends them
|
|
41
|
+
* down the `else` branch — `markAsFinished()`, `group.leave()` — and the
|
|
42
|
+
* existing completion handler runs. No new exit path, no polling.
|
|
43
|
+
*/
|
|
44
|
+
@objc func cancel(_ call: CAPPluginCall) {
|
|
45
|
+
stateLock.lock()
|
|
46
|
+
let running = currentJobId
|
|
47
|
+
let requested = call.getString("jobId")
|
|
48
|
+
// A cancel for a job that isn't the one running would otherwise kill
|
|
49
|
+
// an unrelated caller's compress — there is only ever one.
|
|
50
|
+
guard let running = running, requested == nil || requested == running else {
|
|
51
|
+
stateLock.unlock()
|
|
52
|
+
call.resolve(["cancelled": false])
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
isCancelled = true
|
|
56
|
+
let reader = currentReader
|
|
57
|
+
stateLock.unlock()
|
|
58
|
+
|
|
59
|
+
reader?.cancelReading()
|
|
60
|
+
call.resolve(["cancelled": true])
|
|
61
|
+
}
|
|
16
62
|
|
|
17
63
|
@objc func compressVideo(_ call: CAPPluginCall) {
|
|
18
64
|
guard let sourcePath = call.getString("sourcePath") else {
|
|
19
65
|
call.reject("Missing sourcePath")
|
|
20
66
|
return
|
|
21
67
|
}
|
|
22
|
-
|
|
68
|
+
|
|
69
|
+
stateLock.lock()
|
|
70
|
+
currentJobId = call.getString("jobId") ?? "default"
|
|
71
|
+
isCancelled = false
|
|
72
|
+
stateLock.unlock()
|
|
73
|
+
|
|
23
74
|
let videoURL = sourcePath.hasPrefix("file://") ? URL(string: sourcePath)! : URL(fileURLWithPath: sourcePath)
|
|
24
75
|
let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("compressed_\(UUID().uuidString).mp4")
|
|
25
76
|
|
|
@@ -52,7 +103,6 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
52
103
|
let asset = AVAsset(url: videoURL)
|
|
53
104
|
|
|
54
105
|
self.notifyListeners("onProgress", data:["status": "started"])
|
|
55
|
-
self.isCompressing = true
|
|
56
106
|
|
|
57
107
|
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
|
|
58
108
|
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "VideoCompression") {
|
|
@@ -83,6 +133,7 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
83
133
|
backgroundTask: UIBackgroundTaskIdentifier
|
|
84
134
|
) {
|
|
85
135
|
guard let videoTrack = asset.tracks(withMediaType: .video).first else {
|
|
136
|
+
clearJobState()
|
|
86
137
|
call.reject("Không tìm thấy track video")
|
|
87
138
|
return
|
|
88
139
|
}
|
|
@@ -92,6 +143,18 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
92
143
|
let reader = try AVAssetReader(asset: asset)
|
|
93
144
|
let writer = try AVAssetWriter(url: outputURL, fileType: .mp4)
|
|
94
145
|
writer.shouldOptimizeForNetworkUse = true // Tối ưu cho Web/Network
|
|
146
|
+
|
|
147
|
+
stateLock.lock()
|
|
148
|
+
currentReader = reader
|
|
149
|
+
// A cancel that landed between compressVideo and here has nothing
|
|
150
|
+
// to call cancelReading() on, so honour it before any work starts.
|
|
151
|
+
let cancelledEarly = isCancelled
|
|
152
|
+
stateLock.unlock()
|
|
153
|
+
|
|
154
|
+
if cancelledEarly {
|
|
155
|
+
finishCancelled(outputURL: outputURL, call: call, backgroundTask: backgroundTask)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
95
158
|
|
|
96
159
|
// --- XỬ LÝ KÍCH THƯỚC VÀ CHIỀU XOAY (ORIENTATION) ---
|
|
97
160
|
let naturalSize = videoTrack.naturalSize
|
|
@@ -212,11 +275,24 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
212
275
|
|
|
213
276
|
// --- KẾT THÚC ---
|
|
214
277
|
group.notify(queue: .main) {
|
|
215
|
-
self.isCompressing = false
|
|
216
278
|
if backgroundTask != .invalid {
|
|
217
279
|
UIApplication.shared.endBackgroundTask(backgroundTask)
|
|
218
280
|
}
|
|
219
|
-
|
|
281
|
+
|
|
282
|
+
self.stateLock.lock()
|
|
283
|
+
let wasCancelled = self.isCancelled
|
|
284
|
+
self.currentReader = nil
|
|
285
|
+
self.currentJobId = nil
|
|
286
|
+
self.isCancelled = false
|
|
287
|
+
self.stateLock.unlock()
|
|
288
|
+
|
|
289
|
+
if wasCancelled {
|
|
290
|
+
writer.cancelWriting()
|
|
291
|
+
try? FileManager.default.removeItem(at: outputURL)
|
|
292
|
+
call.reject("Đã hủy nén", NativeVideoCompressorPlugin.cancelledCode)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
220
296
|
if reader.status == .completed {
|
|
221
297
|
writer.finishWriting {
|
|
222
298
|
DispatchQueue.main.async {
|
|
@@ -232,12 +308,40 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
232
308
|
}
|
|
233
309
|
} else {
|
|
234
310
|
writer.cancelWriting()
|
|
311
|
+
try? FileManager.default.removeItem(at: outputURL)
|
|
235
312
|
call.reject("Lỗi khi đọc file: \(reader.error?.localizedDescription ?? "Unknown")")
|
|
236
313
|
}
|
|
237
314
|
}
|
|
238
|
-
|
|
315
|
+
|
|
239
316
|
} catch {
|
|
317
|
+
clearJobState()
|
|
240
318
|
call.reject("Lỗi khởi tạo bộ nén: \(error.localizedDescription)")
|
|
241
319
|
}
|
|
242
320
|
}
|
|
321
|
+
|
|
322
|
+
/// Cancelled before the reader started — nothing to stop, just clean up.
|
|
323
|
+
private func finishCancelled(
|
|
324
|
+
outputURL: URL,
|
|
325
|
+
call: CAPPluginCall,
|
|
326
|
+
backgroundTask: UIBackgroundTaskIdentifier
|
|
327
|
+
) {
|
|
328
|
+
if backgroundTask != .invalid {
|
|
329
|
+
UIApplication.shared.endBackgroundTask(backgroundTask)
|
|
330
|
+
}
|
|
331
|
+
clearJobState()
|
|
332
|
+
try? FileManager.default.removeItem(at: outputURL)
|
|
333
|
+
call.reject("Đã hủy nén", NativeVideoCompressorPlugin.cancelledCode)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private func clearJobState() {
|
|
337
|
+
stateLock.lock()
|
|
338
|
+
currentReader = nil
|
|
339
|
+
currentJobId = nil
|
|
340
|
+
isCancelled = false
|
|
341
|
+
stateLock.unlock()
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/// Matches the `CANCELLED` constant the JS side exports, so one
|
|
345
|
+
/// `error.code` check covers web, iOS and Android alike.
|
|
346
|
+
private static let cancelledCode = "CANCELLED"
|
|
243
347
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capacitor-native-video-compressor",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Native video compressor using LightCompressor and AVFoundation",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
82
|
"@ffmpeg/ffmpeg": "^0.12.15",
|
|
83
|
-
"@ffmpeg/util": "^0.12.2"
|
|
83
|
+
"@ffmpeg/util": "^0.12.2",
|
|
84
|
+
"mediabunny": "^1.55.1"
|
|
84
85
|
}
|
|
85
86
|
}
|