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.
@@ -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
- private var isCompressing = false
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.4",
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
  }