react-native-video-trim 3.0.10 → 4.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.
Files changed (92) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +80 -63
  3. package/VideoTrim.podspec +24 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +82 -49
  6. package/android/gradle.properties +7 -5
  7. package/android/src/main/AndroidManifest.xml +4 -2
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/videotrim/VideoTrim.kt +646 -0
  10. package/android/src/main/java/com/margelo/nitro/videotrim/VideoTrimPackage.kt +22 -0
  11. package/android/src/main/java/com/{videotrim → margelo/nitro/videotrim}/enums/ErrorCode.java +1 -1
  12. package/android/src/main/java/com/{videotrim → margelo/nitro/videotrim}/interfaces/IVideoTrimmerView.java +1 -1
  13. package/android/src/main/java/com/{videotrim → margelo/nitro/videotrim}/interfaces/VideoTrimListener.java +6 -5
  14. package/android/src/main/java/com/{videotrim → margelo/nitro/videotrim}/utils/MediaMetadataUtil.java +1 -1
  15. package/android/src/main/java/com/{videotrim → margelo/nitro/videotrim}/utils/StorageUtil.java +3 -1
  16. package/android/src/main/java/com/{videotrim → margelo/nitro/videotrim}/utils/VideoTrimmerUtil.java +51 -41
  17. package/android/src/main/java/com/{videotrim → margelo/nitro/videotrim}/widgets/VideoTrimmerView.java +45 -69
  18. package/ios/AssetLoader.swift +2 -2
  19. package/ios/ErrorCode.swift +2 -2
  20. package/ios/ProgressAlertController.swift +2 -2
  21. package/ios/VideoTrim.swift +52 -835
  22. package/ios/VideoTrimImpl.swift +957 -0
  23. package/ios/VideoTrimmer.swift +2 -3
  24. package/ios/VideoTrimmerThumb.swift +33 -26
  25. package/ios/VideoTrimmerViewController.swift +47 -28
  26. package/lib/module/VideoTrim.nitro.js +4 -0
  27. package/lib/module/VideoTrim.nitro.js.map +1 -0
  28. package/lib/module/index.js +98 -22
  29. package/lib/module/index.js.map +1 -1
  30. package/lib/module/package.json +1 -0
  31. package/lib/typescript/package.json +1 -0
  32. package/lib/typescript/{index.d.ts → src/VideoTrim.nitro.d.ts} +125 -134
  33. package/lib/typescript/src/VideoTrim.nitro.d.ts.map +1 -0
  34. package/lib/typescript/src/index.d.ts +49 -0
  35. package/lib/typescript/src/index.d.ts.map +1 -0
  36. package/nitrogen/generated/android/c++/JEditorConfig.hpp +237 -0
  37. package/nitrogen/generated/android/c++/JFileValidationResult.hpp +61 -0
  38. package/nitrogen/generated/android/c++/JFunc_void.hpp +74 -0
  39. package/nitrogen/generated/android/c++/JFunc_void_std__string_std__unordered_map_std__string__std__string_.hpp +89 -0
  40. package/nitrogen/generated/android/c++/JHybridVideoTrimSpec.cpp +151 -0
  41. package/nitrogen/generated/android/c++/JHybridVideoTrimSpec.hpp +68 -0
  42. package/nitrogen/generated/android/c++/JTrimOptions.hpp +109 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/EditorConfig.kt +72 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/FileValidationResult.kt +28 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/Func_void.kt +80 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/Func_void_std__string_std__unordered_map_std__string__std__string_.kt +80 -0
  47. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/HybridVideoTrimSpec.kt +86 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/TrimOptions.kt +40 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/videotrimOnLoad.kt +35 -0
  50. package/nitrogen/generated/android/videotrim+autolinking.cmake +78 -0
  51. package/nitrogen/generated/android/videotrim+autolinking.gradle +27 -0
  52. package/nitrogen/generated/android/videotrimOnLoad.cpp +50 -0
  53. package/nitrogen/generated/android/videotrimOnLoad.hpp +25 -0
  54. package/nitrogen/generated/ios/VideoTrim+autolinking.rb +60 -0
  55. package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Bridge.cpp +96 -0
  56. package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Bridge.hpp +374 -0
  57. package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Umbrella.hpp +56 -0
  58. package/nitrogen/generated/ios/VideoTrimAutolinking.mm +33 -0
  59. package/nitrogen/generated/ios/VideoTrimAutolinking.swift +25 -0
  60. package/nitrogen/generated/ios/c++/HybridVideoTrimSpecSwift.cpp +11 -0
  61. package/nitrogen/generated/ios/c++/HybridVideoTrimSpecSwift.hpp +127 -0
  62. package/nitrogen/generated/ios/swift/EditorConfig.swift +541 -0
  63. package/nitrogen/generated/ios/swift/FileValidationResult.swift +57 -0
  64. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  65. package/nitrogen/generated/ios/swift/Func_void_FileValidationResult.swift +46 -0
  66. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  67. package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
  68. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  69. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  70. package/nitrogen/generated/ios/swift/Func_void_std__string_std__unordered_map_std__string__std__string_.swift +54 -0
  71. package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +46 -0
  72. package/nitrogen/generated/ios/swift/HybridVideoTrimSpec.swift +54 -0
  73. package/nitrogen/generated/ios/swift/HybridVideoTrimSpec_cxx.swift +241 -0
  74. package/nitrogen/generated/ios/swift/TrimOptions.swift +189 -0
  75. package/nitrogen/generated/shared/c++/EditorConfig.hpp +253 -0
  76. package/nitrogen/generated/shared/c++/FileValidationResult.hpp +77 -0
  77. package/nitrogen/generated/shared/c++/HybridVideoTrimSpec.cpp +27 -0
  78. package/nitrogen/generated/shared/c++/HybridVideoTrimSpec.hpp +80 -0
  79. package/nitrogen/generated/shared/c++/TrimOptions.hpp +125 -0
  80. package/package.json +75 -71
  81. package/src/VideoTrim.nitro.ts +263 -0
  82. package/src/index.tsx +120 -257
  83. package/android/src/main/AndroidManifestDeprecated.xml +0 -3
  84. package/android/src/main/java/com/videotrim/VideoTrimModule.java +0 -603
  85. package/android/src/main/java/com/videotrim/VideoTrimPackage.java +0 -28
  86. package/ios/VideoTrim-Bridging-Header.h +0 -2
  87. package/ios/VideoTrim.mm +0 -17
  88. package/ios/VideoTrim.xcodeproj/project.pbxproj +0 -283
  89. package/lib/commonjs/index.js +0 -87
  90. package/lib/commonjs/index.js.map +0 -1
  91. package/lib/typescript/index.d.ts.map +0 -1
  92. package/react-native-video-trim.podspec +0 -43
@@ -1,850 +1,67 @@
1
- import Photos
2
- import React
1
+ import NitroModules
3
2
  import ffmpegkit
3
+ import Photos
4
4
 
5
- @available(iOS 13.0, *)
6
- @objc(VideoTrim)
7
- class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDelegate
8
- {
9
- private let FILE_PREFIX = "trimmedVideo"
10
- private var hasListeners = false
11
- private var isShowing = false
12
-
13
- private var saveToPhoto = false
14
- private var removeAfterSavedToPhoto = false
15
- private var removeAfterFailedToSavePhoto = false
16
- private var removeAfterSavedToDocuments = false
17
- private var removeAfterFailedToSaveDocuments = false
18
- private var removeAfterShared = false
19
- private var removeAfterFailedToShare = false
20
-
21
- private var trimmingText = "Trimming video..."
22
- private var enableCancelDialog = true
23
- private var cancelDialogTitle = "Warning!"
24
- private var cancelDialogMessage = "Are you sure want to cancel?"
25
- private var cancelDialogCancelText = "Close"
26
- private var cancelDialogConfirmText = "Proceed"
27
- private var enableSaveDialog = true
28
- private var saveDialogTitle = "Confirmation!"
29
- private var saveDialogMessage = "Are you sure want to save?"
30
- private var saveDialogCancelText = "Close"
31
- private var saveDialogConfirmText = "Proceed"
32
- private var fullScreenModalIOS = false
33
- private var cancelButtonText = "Cancel"
34
- private var saveButtonText = "Save"
35
- private var vc: VideoTrimmerViewController?
36
- private var isVideoType = true
37
- private var outputExt = "mp4"
38
- private var openDocumentsOnFinish = false
39
- private var openShareSheetOnFinish = false
40
- private var outputFile: URL?
41
- private var closeWhenFinish = true
42
- private var enableCancelTrimming = true
43
- private var cancelTrimmingButtonText = "Cancel"
44
- private var enableCancelTrimmingDialog = true
45
- private var cancelTrimmingDialogTitle = "Warning!"
46
- private var cancelTrimmingDialogMessage = "Are you sure want to trimming?"
47
- private var cancelTrimmingDialogCancelText = "Close"
48
- private var cancelTrimmingDialogConfirmText = "Proceed"
49
- private var alertOnFailToLoad = true
50
- private var alertOnFailTitle = "Error"
51
- private var alertOnFailMessage =
52
- "Fail to load media. Possibly invalid file or no network connection"
53
- private var alertOnFailCloseText = "Close"
54
-
55
- @objc
56
- static override func requiresMainQueueSetup() -> Bool {
57
- return true
58
- }
59
-
60
- override func supportedEvents() -> [String]! {
61
- return ["VideoTrim"]
62
- }
63
-
64
- override func startObserving() {
65
- hasListeners = true
66
- }
67
-
68
- override func stopObserving() {
69
- hasListeners = false
70
- }
71
-
72
- @objc(showEditor:withConfig:)
73
- func showEditor(uri: String, config: NSDictionary) {
74
- if isShowing {
75
- return
76
- }
77
- saveToPhoto = config["saveToPhoto"] as? Bool ?? false
78
-
79
- removeAfterSavedToPhoto =
80
- config["removeAfterSavedToPhoto"] as? Bool ?? false
81
- removeAfterFailedToSavePhoto =
82
- config["removeAfterFailedToSavePhoto"] as? Bool ?? false
83
- removeAfterSavedToDocuments =
84
- config["removeAfterSavedToDocuments"] as? Bool ?? false
85
- removeAfterFailedToSaveDocuments =
86
- config["removeAfterFailedToSaveDocuments"] as? Bool ?? false
87
- removeAfterShared = config["removeAfterShared"] as? Bool ?? false
88
- removeAfterFailedToShare =
89
- config["removeAfterFailedToShare"] as? Bool ?? false
90
-
91
- enableCancelDialog = config["enableCancelDialog"] as? Bool ?? true
92
- cancelDialogTitle = config["cancelDialogTitle"] as? String ?? "Warning!"
93
- cancelDialogMessage =
94
- config["cancelDialogMessage"] as? String ?? "Are you sure want to cancel?"
95
- cancelDialogCancelText =
96
- config["cancelDialogCancelText"] as? String ?? "Close"
97
- cancelDialogConfirmText =
98
- config["cancelDialogConfirmText"] as? String ?? "Proceed"
99
-
100
- enableSaveDialog = config["enableSaveDialog"] as? Bool ?? true
101
- saveDialogTitle = config["saveDialogTitle"] as? String ?? "Confirmation!"
102
- saveDialogMessage =
103
- config["saveDialogMessage"] as? String ?? "Are you sure want to save?"
104
- saveDialogCancelText = config["saveDialogCancelText"] as? String ?? "Close"
105
- saveDialogConfirmText =
106
- config["saveDialogConfirmText"] as? String ?? "Proceed"
107
- trimmingText = config["trimmingText"] as? String ?? "Trimming video..."
108
- fullScreenModalIOS = config["fullScreenModalIOS"] as? Bool ?? false
109
- isVideoType = (config["type"] as? String ?? "video") == "video"
110
- outputExt = config["outputExt"] as? String ?? "mp4"
111
- openDocumentsOnFinish = config["openDocumentsOnFinish"] as? Bool ?? false
112
- openShareSheetOnFinish = config["openShareSheetOnFinish"] as? Bool ?? false
113
-
114
- closeWhenFinish = config["closeWhenFinish"] as? Bool ?? true
115
- enableCancelTrimming = config["enableCancelTrimming"] as? Bool ?? true
116
- cancelTrimmingButtonText =
117
- config["cancelTrimmingButtonText"] as? String ?? "Cancel"
118
- enableCancelTrimmingDialog =
119
- config["enableCancelTrimmingDialog"] as? Bool ?? true
120
- cancelTrimmingDialogTitle =
121
- config["cancelTrimmingDialogTitle"] as? String ?? "Warning!"
122
- cancelTrimmingDialogMessage =
123
- config["cancelTrimmingDialogMessage"] as? String
124
- ?? "Are you sure want to cancel trimming?"
125
- cancelTrimmingDialogCancelText =
126
- config["cancelTrimmingDialogCancelText"] as? String ?? "Close"
127
- cancelTrimmingDialogConfirmText =
128
- config["cancelTrimmingDialogConfirmText"] as? String ?? "Proceed"
129
- alertOnFailToLoad = config["alertOnFailToLoad"] as? Bool ?? true
130
- alertOnFailTitle = config["alertOnFailTitle"] as? String ?? "Error"
131
- alertOnFailMessage =
132
- config["alertOnFailMessage"] as? String
133
- ?? "Fail to load media. Possibly invalid file or no network connection"
134
- alertOnFailCloseText = config["alertOnFailCloseText"] as? String ?? "Close"
135
-
136
- if let cancelBtnText = config["cancelButtonText"] as? String,
137
- !cancelBtnText.isEmpty
138
- {
139
- self.cancelButtonText = cancelBtnText
140
- }
141
-
142
- if let saveButtonText = config["saveButtonText"] as? String,
143
- !saveButtonText.isEmpty
144
- {
145
- self.saveButtonText = saveButtonText
146
- }
147
-
148
- let destPath = URL(string: uri)
149
- let newPath = renameFile(at: destPath!, newName: "beforeTrim")
150
-
151
- guard let destPath = newPath else { return }
152
-
153
- DispatchQueue.main.async {
154
- self.vc = VideoTrimmerViewController()
155
-
156
- guard let vc = self.vc else { return }
157
-
158
- vc.configure(config: config)
159
-
160
- vc.cancelBtnClicked = {
161
- if !self.enableCancelDialog {
162
- self.emitEventToJS("onCancel", eventData: nil)
163
-
164
- vc.dismiss(
165
- animated: true,
166
- completion: {
167
- self.emitEventToJS("onHide", eventData: nil)
168
- self.isShowing = false
169
- })
170
- return
171
- }
172
-
173
- // Create Alert
174
- let dialogMessage = UIAlertController(
175
- title: self.cancelDialogTitle, message: self.cancelDialogMessage,
176
- preferredStyle: .alert)
177
- dialogMessage.overrideUserInterfaceStyle = .dark
178
-
179
- // Create OK button with action handler
180
- let ok = UIAlertAction(
181
- title: self.cancelDialogConfirmText, style: .destructive,
182
- handler: { (action) -> Void in
183
- self.emitEventToJS("onCancel", eventData: nil)
184
-
185
- vc.dismiss(
186
- animated: true,
187
- completion: {
188
- self.emitEventToJS("onHide", eventData: nil)
189
- self.isShowing = false
190
- })
191
- })
192
-
193
- // Create Cancel button with action handlder
194
- let cancel = UIAlertAction(
195
- title: self.cancelDialogCancelText, style: .cancel)
196
-
197
- //Add OK and Cancel button to an Alert object
198
- dialogMessage.addAction(ok)
199
- dialogMessage.addAction(cancel)
200
-
201
- // Present alert message to user
202
- if let root = RCTPresentedViewController() {
203
- root.present(dialogMessage, animated: true, completion: nil)
204
- }
205
- }
206
-
207
- vc.saveBtnClicked = { (selectedRange: CMTimeRange) in
208
- if !self.enableSaveDialog {
209
- self.trim(
210
- viewController: vc, inputFile: destPath,
211
- videoDuration: self.vc!.asset!.duration.seconds,
212
- startTime: selectedRange.start.seconds,
213
- endTime: selectedRange.end.seconds)
214
- return
215
- }
216
-
217
- // Create Alert
218
- let dialogMessage = UIAlertController(
219
- title: self.saveDialogTitle, message: self.saveDialogMessage,
220
- preferredStyle: .alert)
221
- dialogMessage.overrideUserInterfaceStyle = .dark
222
-
223
- // Create OK button with action handler
224
- let ok = UIAlertAction(
225
- title: self.saveDialogConfirmText, style: .default,
226
- handler: { (action) -> Void in
227
- self.trim(
228
- viewController: vc, inputFile: destPath,
229
- videoDuration: vc.asset!.duration.seconds,
230
- startTime: selectedRange.start.seconds,
231
- endTime: selectedRange.end.seconds)
232
- })
233
-
234
- // Create Cancel button with action handlder
235
- let cancel = UIAlertAction(
236
- title: self.saveDialogCancelText, style: .cancel)
237
-
238
- //Add OK and Cancel button to an Alert object
239
- dialogMessage.addAction(ok)
240
- dialogMessage.addAction(cancel)
241
-
242
- // Present alert message to user
243
- if let root = RCTPresentedViewController() {
244
- root.present(dialogMessage, animated: true, completion: nil)
245
- }
246
- }
247
-
248
- vc.isModalInPresentation = true // prevent modal closed by swipe down
249
-
250
- if self.fullScreenModalIOS {
251
- vc.modalPresentationStyle = .fullScreen
252
- }
253
-
254
- if let root = RCTPresentedViewController() {
255
- root.present(
256
- vc, animated: true,
257
- completion: {
258
- self.emitEventToJS("onShow", eventData: nil)
259
- self.isShowing = true
260
-
261
- // start loading asset after view is finished presenting
262
- // otherwise it may run too fast for local file and autoplay looks weird
263
- let assetLoader = AssetLoader()
264
- assetLoader.delegate = self
265
- assetLoader.loadAsset(url: destPath, isVideoType: self.isVideoType)
266
- })
267
- }
268
- }
269
-
270
- }
271
-
272
- private func copyFileToDocumentDir(uri: String) -> URL? {
273
- if let videoURL = URL(string: uri) {
274
- // Save the video to the document directory
275
- let documentsDirectory = FileManager.default.urls(
276
- for: .documentDirectory, in: .userDomainMask
277
- ).first!
278
- // Extract the file extension from the videoURL
279
- let fileExtension = videoURL.pathExtension
280
-
281
- // Define the filename with the correct file extension
282
- let timestamp = Int(Date().timeIntervalSince1970)
283
- let destinationURL = documentsDirectory.appendingPathComponent(
284
- "\(FILE_PREFIX)_original_\(timestamp).\(fileExtension)")
285
-
286
- do {
287
- try FileManager.default.copyItem(at: videoURL, to: destinationURL)
288
- } catch {
289
- print("Error while copying file to document directory \(error)")
290
- return nil
291
- }
292
-
293
- return destinationURL
294
- } else {
295
- return nil
296
- }
297
- }
298
-
299
- private func emitEventToJS(_ eventName: String, eventData: [String: Any]?) {
300
- if hasListeners {
301
- var modifiedEventData = eventData ?? [:] // If eventData is nil, create an empty dictionary
302
- modifiedEventData["name"] = eventName
303
- sendEvent(withName: "VideoTrim", body: modifiedEventData)
304
- }
305
- }
306
-
307
- @objc(listFiles:withRejecter:)
308
- func listFiles(
309
- resolve: @escaping RCTPromiseResolveBlock,
310
- reject: @escaping RCTPromiseRejectBlock
311
- ) {
312
- let files = listFiles()
313
- resolve(files.map { $0.absoluteString })
314
- }
315
-
316
- @objc(cleanFiles:withRejecter:)
317
- func cleanFiles(
318
- resolve: @escaping RCTPromiseResolveBlock,
319
- reject: @escaping RCTPromiseRejectBlock
320
- ) {
321
- let files = listFiles()
322
- var successCount = 0
323
- for file in files {
324
- let state = deleteFile(url: file)
325
-
326
- if state == 0 {
327
- successCount += 1
328
- }
329
- }
330
-
331
- resolve(successCount)
332
- }
333
-
334
- @objc(deleteFile:withResolver:withRejecter:)
335
- func deleteFile(
336
- uri: String, resolve: @escaping RCTPromiseResolveBlock,
337
- reject: @escaping RCTPromiseRejectBlock
338
- ) {
339
- let state = deleteFile(url: URL(string: uri)!)
340
- resolve(state == 0)
341
- }
342
-
343
- private func listFiles() -> [URL] {
344
- var files: [URL] = []
345
-
346
- let documentsDirectory = FileManager.default.urls(
347
- for: .documentDirectory, in: .userDomainMask
348
- ).first!
349
-
350
- do {
351
- let directoryContents = try FileManager.default.contentsOfDirectory(
352
- at: documentsDirectory, includingPropertiesForKeys: nil)
353
-
354
- for fileURL in directoryContents {
355
- if fileURL.lastPathComponent.starts(with: FILE_PREFIX) {
356
- files.append(fileURL)
357
- }
358
- }
359
- } catch {
360
- print("[listFiles] Error when retrieving files: \(error)")
361
- }
362
-
363
- return files
364
- }
365
-
366
- private func deleteFile(url: URL) -> Int {
367
- do {
368
- if FileManager.default.fileExists(atPath: url.path) {
369
- try FileManager.default.removeItem(at: url)
370
-
371
- return 0
372
- }
373
-
374
- return 1
375
- } catch {
376
- print("[deleteFile] Error deleting files: \(error)")
377
-
378
- return 2
379
- }
380
- }
381
-
382
- private func trim(
383
- viewController: VideoTrimmerViewController, inputFile: URL,
384
- videoDuration: Double, startTime: Double, endTime: Double
385
- ) {
386
- vc?.pausePlayer()
387
-
388
- let timestamp = Int(Date().timeIntervalSince1970)
389
- let outputName = "\(FILE_PREFIX)_\(timestamp).\(outputExt)"
390
- let documentsDirectory = FileManager.default.urls(
391
- for: .documentDirectory, in: .userDomainMask
392
- ).first!
393
- outputFile = documentsDirectory.appendingPathComponent(outputName)
394
-
395
- let formatter = DateFormatter()
396
- formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"
397
- formatter.timeZone = TimeZone(identifier: "UTC")
398
- let dateTime = formatter.string(from: Date())
399
-
400
- emitEventToJS("onStartTrimming", eventData: nil)
401
-
402
- var ffmpegSession: FFmpegSession?
403
- let progressAlert = ProgressAlertController()
404
- progressAlert.modalPresentationStyle = .overFullScreen
405
- progressAlert.modalTransitionStyle = .crossDissolve
406
- progressAlert.setTitle(trimmingText)
407
-
408
- if enableCancelTrimming {
409
- progressAlert.setCancelTitle(cancelTrimmingButtonText)
410
- progressAlert.showCancelBtn()
411
- progressAlert.onDismiss = {
412
- if self.enableCancelTrimmingDialog {
413
- let dialogMessage = UIAlertController(
414
- title: self.cancelTrimmingDialogTitle,
415
- message: self.cancelTrimmingDialogMessage, preferredStyle: .alert)
416
- dialogMessage.overrideUserInterfaceStyle = .dark
417
-
418
- // Create OK button with action handler
419
- let ok = UIAlertAction(
420
- title: self.cancelDialogConfirmText, style: .destructive,
421
- handler: { (action) -> Void in
422
-
423
- if let ffmpegSession = ffmpegSession {
424
- ffmpegSession.cancel()
425
- } else {
426
- self.emitEventToJS("onCancelTrimming", eventData: nil)
427
- }
428
-
429
- progressAlert.dismiss(animated: true)
430
- })
431
-
432
- // Create Cancel button with action handlder
433
- let cancel = UIAlertAction(
434
- title: self.cancelDialogCancelText, style: .cancel)
435
-
436
- //Add OK and Cancel button to an Alert object
437
- dialogMessage.addAction(ok)
438
- dialogMessage.addAction(cancel)
439
-
440
- // Present alert message to user
441
- if let root = RCTPresentedViewController() {
442
- root.present(dialogMessage, animated: true, completion: nil)
443
- }
444
- } else {
445
- if let ffmpegSession = ffmpegSession {
446
- ffmpegSession.cancel()
447
- } else {
448
- self.emitEventToJS("onCancelTrimming", eventData: nil)
449
- }
450
-
451
- progressAlert.dismiss(animated: true)
5
+ class VideoTrim: HybridVideoTrimSpec {
6
+
7
+ private let impl = VideoTrimImpl()
8
+
9
+ public func showEditor(
10
+ filePath: String,
11
+ config: EditorConfig,
12
+ onEvent: @escaping (_ eventName: String, _ payload: Dictionary<String, String>) -> Void
13
+ ) throws {
14
+ impl.showEditor(uri: filePath, editorConfig: config, onEvent: onEvent)
15
+ }
16
+
17
+ func listFiles() throws -> Promise<[String]> {
18
+ return Promise.async {
19
+ // This runs on a separate Thread, and can use `await` syntax!
20
+ let files = self.impl.listFiles().map { $0.absoluteString }
21
+ return files
452
22
  }
453
-
454
- }
455
23
  }
456
-
457
- if let root = RCTPresentedViewController() {
458
- root.present(progressAlert, animated: true, completion: nil)
459
- }
460
-
461
- let cmds = [
462
- "-ss",
463
- "\(startTime * 1000)ms",
464
- "-to",
465
- "\(endTime * 1000)ms",
466
- "-i",
467
- "\(inputFile)",
468
- "-c",
469
- "copy",
470
- "-metadata",
471
- "creation_time=\(dateTime)",
472
- outputFile!.absoluteString,
473
- ]
474
-
475
- print("Command: ", cmds.joined(separator: " "))
476
-
477
- let eventPayload: [String: Any] = [
478
- "command": cmds.joined(separator: " ")
479
- ]
480
- self.emitEventToJS("onLog", eventData: eventPayload)
481
-
482
- ffmpegSession = FFmpegKit.execute(
483
- withArgumentsAsync: cmds,
484
- withCompleteCallback: { session in
485
-
486
- // always hide progressAlert
487
- DispatchQueue.main.async {
488
- progressAlert.dismiss(animated: true)
489
- }
490
-
491
- let state = session?.getState()
492
- let returnCode = session?.getReturnCode()
493
-
494
- if ReturnCode.isSuccess(returnCode) {
495
- let eventPayload: [String: Any] = [
496
- "outputPath": self.outputFile!.absoluteString,
497
- "startTime": (startTime * 1000).rounded(),
498
- "endTime": (endTime * 1000).rounded(),
499
- "duration": (videoDuration * 1000).rounded(),
500
- ]
501
- self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
502
-
503
- if self.saveToPhoto && self.isVideoType {
504
- PHPhotoLibrary.requestAuthorization { status in
505
- guard status == .authorized else {
506
- self.onError(
507
- message: "Permission to access Photo Library is not granted",
508
- code: .noPhotoPermission)
509
- return
510
- }
511
-
512
- PHPhotoLibrary.shared().performChanges({
513
- let request =
514
- PHAssetChangeRequest.creationRequestForAssetFromVideo(
515
- atFileURL: self.outputFile!)
516
- request?.creationDate = Date()
517
- }) { success, error in
518
- if success {
519
- print("Edited video saved to Photo Library successfully.")
520
-
521
- if self.removeAfterSavedToPhoto {
522
- let _ = self.deleteFile(url: self.outputFile!)
523
- }
524
- } else {
525
- self.onError(
526
- message:
527
- "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")",
528
- code: .failToSaveToPhoto)
529
- if self.removeAfterFailedToSavePhoto {
530
- let _ = self.deleteFile(url: self.outputFile!)
531
- }
24
+
25
+ public func cleanFiles() throws -> Promise<Double> {
26
+ return Promise.async {
27
+ // This runs on a separate Thread, and can use `await` syntax!
28
+ let files = self.impl.listFiles()
29
+ var successCount = 0
30
+ for file in files {
31
+ let state = self.impl.deleteFile(url: file)
32
+
33
+ if state == 0 {
34
+ successCount += 1
532
35
  }
533
- }
534
36
  }
535
- } else if self.openDocumentsOnFinish {
536
- self.saveFileToFilesApp(fileURL: self.outputFile!)
537
-
538
- // must return otherwise editor will close
539
- return
540
- } else if self.openShareSheetOnFinish {
541
- self.shareFile(fileURL: self.outputFile!)
542
-
543
- // must return otherwise editor will close
544
- return
545
- }
546
-
547
- if self.closeWhenFinish {
548
- self.closeEditor()
549
- }
550
-
551
- } else if ReturnCode.isCancel(returnCode) {
552
- // CANCEL
553
- self.emitEventToJS("onCancelTrimming", eventData: nil)
554
- } else {
555
- // FAILURE
556
- self.onError(
557
- message:
558
- "Command failed with state \(String(describing: FFmpegKitConfig.sessionState(toString: state ?? .failed))) and rc \(String(describing: returnCode)).\(String(describing: session?.getFailStackTrace()))",
559
- code: .trimmingFailed)
560
- if self.closeWhenFinish {
561
- self.closeEditor()
562
- }
563
- }
564
-
565
- },
566
- withLogCallback: { log in
567
- guard let log = log else { return }
568
-
569
- print("FFmpeg process started with log " + (log.getMessage()))
570
-
571
- let eventPayload: [String: Any] = [
572
- "level": log.getLevel(),
573
- "message": log.getMessage() ?? "",
574
- "sessionId": log.getSessionId(),
575
- ]
576
- self.emitEventToJS("onLog", eventData: eventPayload)
577
-
578
- },
579
- withStatisticsCallback: { statistics in
580
- guard let statistics = statistics else { return }
581
-
582
- let timeInMilliseconds = statistics.getTime()
583
- if timeInMilliseconds > 0 {
584
- let completePercentage = timeInMilliseconds / (videoDuration * 1000) // from 0 -> 1
585
- DispatchQueue.main.async {
586
- progressAlert.setProgress(Float(completePercentage))
587
- }
37
+
38
+ return Double(successCount)
588
39
  }
589
-
590
- let eventPayload: [String: Any] = [
591
- "sessionId": statistics.getSessionId(),
592
- "videoFrameNumber": statistics.getVideoFrameNumber(),
593
- "videoFps": statistics.getVideoFps(),
594
- "videoQuality": statistics.getVideoQuality(),
595
- "size": statistics.getSize(),
596
- "time": statistics.getTime(),
597
- "bitrate": statistics.getBitrate(),
598
- "speed": statistics.getSpeed(),
599
- ]
600
- self.emitEventToJS("onStatistics", eventData: eventPayload)
601
- })
602
- }
603
-
604
- func assetLoader(
605
- _ loader: AssetLoader, didFailWithError error: any Error, forKey key: String
606
- ) {
607
- let message = "Failed to load \(key): \(error.localizedDescription)"
608
- print("Failed to load \(key)", message)
609
-
610
- self.onError(message: message, code: .failToLoadMedia)
611
- vc?.onAssetFailToLoad()
612
-
613
- if alertOnFailToLoad {
614
- let dialogMessage = UIAlertController(
615
- title: alertOnFailTitle, message: alertOnFailMessage,
616
- preferredStyle: .alert)
617
- dialogMessage.overrideUserInterfaceStyle = .dark
618
-
619
- // Create Cancel button with action handlder
620
- let ok = UIAlertAction(title: alertOnFailCloseText, style: .default)
621
-
622
- //Add OK and Cancel button to an Alert object
623
- dialogMessage.addAction(ok)
624
-
625
- // Present alert message to user
626
- if let root = RCTPresentedViewController() {
627
- root.present(dialogMessage, animated: true, completion: nil)
628
- }
629
40
  }
630
- }
631
-
632
- func assetLoaderDidSucceed(_ loader: AssetLoader) {
633
- print("Asset loaded successfully")
634
-
635
- vc?.asset = loader.asset
636
-
637
- let eventPayload: [String: Any] = [
638
- "duration": loader.asset!.duration.seconds * 1000
639
- ]
640
- self.emitEventToJS("onLoad", eventData: eventPayload)
641
- }
642
-
643
- private func saveFileToFilesApp(fileURL: URL) {
644
- DispatchQueue.main.async {
645
- let documentPicker = UIDocumentPickerViewController(
646
- url: fileURL, in: .exportToService)
647
- documentPicker.delegate = self
648
- documentPicker.modalPresentationStyle = .formSheet
649
- if let root = RCTPresentedViewController() {
650
- root.present(documentPicker, animated: true, completion: nil)
651
- }
652
- }
653
- }
654
-
655
- private func shareFile(fileURL: URL) {
656
- DispatchQueue.main.async {
657
- // Create an instance of UIActivityViewController
658
- let activityViewController = UIActivityViewController(
659
- activityItems: [fileURL], applicationActivities: nil)
660
-
661
- activityViewController.completionWithItemsHandler = {
662
- activityType, completed, returnedItems, error in
663
-
664
- if let error = error {
665
- let message = "Sharing error: \(error.localizedDescription)"
666
- print(message)
667
- self.onError(message: message, code: .failToShare)
668
-
669
- if self.removeAfterFailedToShare {
670
- let _ = self.deleteFile(url: self.outputFile!)
671
- }
672
- return
673
- }
674
-
675
- if completed {
676
- print("User completed the sharing activity")
677
- if self.removeAfterShared {
678
- let _ = self.deleteFile(url: self.outputFile!)
679
- }
680
- } else {
681
- print("User cancelled or failed to complete the sharing activity")
682
- if self.removeAfterFailedToShare {
683
- let _ = self.deleteFile(url: self.outputFile!)
684
- }
41
+
42
+ public func deleteFile(filePath: String) throws -> Promise<Bool> {
43
+ return Promise.async {
44
+ // This runs on a separate Thread, and can use `await` syntax!
45
+ let state = self.impl.deleteFile(url: URL(string: filePath)!)
46
+ return state == 0
685
47
  }
686
-
687
- self.closeEditor()
688
-
689
- }
690
-
691
- // Present the share sheet
692
- if let root = RCTPresentedViewController() {
693
- root.present(activityViewController, animated: true, completion: nil)
694
- }
695
- }
696
-
697
- }
698
-
699
- func documentPicker(
700
- _ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]
701
- ) {
702
- if removeAfterSavedToDocuments {
703
- let _ = deleteFile(url: outputFile!)
704
- }
705
- closeEditor()
706
- }
707
-
708
- func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController)
709
- {
710
- if removeAfterFailedToSaveDocuments {
711
- let _ = deleteFile(url: outputFile!)
712
48
  }
713
- closeEditor()
714
- }
715
-
716
- @objc(closeEditor:withRejecter:)
717
- func closeEditor(
718
- resolve: @escaping RCTPromiseResolveBlock,
719
- reject: @escaping RCTPromiseRejectBlock
720
- ) {
721
- closeEditor()
722
- resolve(true)
723
- }
724
-
725
- private func closeEditor() {
726
- guard let vc = vc else { return }
727
- // some how in case we trim a very short video the view controller is still visible after first .dismiss call
728
- // even the file is successfully saved
729
- // that's why we need a small delay here to ensure vc will be dismissed
730
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
731
- vc.dismiss(
732
- animated: true,
733
- completion: {
734
- self.emitEventToJS("onHide", eventData: nil)
735
- self.isShowing = false
736
- })
49
+
50
+ public func closeEditor(onComplete: @escaping () -> Void) throws {
51
+ impl.closeEditor(onComplete)
737
52
  }
738
- }
739
-
740
- @objc(isValidFile:withResolver:withRejecter:)
741
- func isValidFile(
742
- uri: String, resolve: @escaping RCTPromiseResolveBlock,
743
- reject: @escaping RCTPromiseRejectBlock
744
- ) {
745
- let fileURL = URL(string: uri)!
746
- checkFileValidity(url: fileURL) { isValid, fileType, duration in
747
- if isValid {
748
- print("Valid \(fileType) file with duration: \(duration) milliseconds")
749
- } else {
750
- print("Invalid file")
751
- }
752
-
753
- let payload: [String: Any] = [
754
- "isValid": isValid,
755
- "fileType": fileType,
756
- "duration": duration,
757
- ]
758
- resolve(payload)
759
- }
760
-
761
- }
762
-
763
- private func onError(message: String, code: ErrorCode) {
764
- let eventPayload: [String: String] = [
765
- "message": message,
766
- "errorCode": code.rawValue,
767
- ]
768
- self.emitEventToJS("onError", eventData: eventPayload)
769
- }
770
-
771
- private func checkFileValidity(
772
- url: URL, completion: @escaping (Bool, String, Double) -> Void
773
- ) {
774
- let asset = AVAsset(url: url)
775
-
776
- // Load the duration and tracks asynchronously
777
- asset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) {
778
- var error: NSError? = nil
779
-
780
- // Check if the duration and tracks are loaded
781
- let durationStatus = asset.statusOfValue(
782
- forKey: "duration", error: &error)
783
- let tracksStatus = asset.statusOfValue(forKey: "tracks", error: &error)
784
-
785
- // Ensure both properties are loaded successfully
786
- guard durationStatus == .loaded, tracksStatus == .loaded, error == nil
787
- else {
788
- DispatchQueue.main.async {
789
- completion(false, "unknown", -1)
53
+
54
+ public func isValidFile(url: String) throws -> Promise<FileValidationResult> {
55
+ return Promise.async {
56
+ // This runs on a separate Thread, and can use```` `await` syntax!
57
+ return await self.impl.isValidFile(uri: url)
790
58
  }
791
- return
792
- }
793
-
794
- // Check if the asset contains any video or audio tracks
795
- let videoTracks = asset.tracks(withMediaType: .video)
796
- let audioTracks = asset.tracks(withMediaType: .audio)
797
-
798
- let isValid = !videoTracks.isEmpty || !audioTracks.isEmpty
799
- let fileType: String
800
- if !videoTracks.isEmpty {
801
- fileType = "video"
802
- } else if !audioTracks.isEmpty {
803
- fileType = "audio"
804
- } else {
805
- fileType = "unknown"
806
- }
807
-
808
- let duration = CMTimeGetSeconds(asset.duration) * 1000
809
-
810
- DispatchQueue.main.async {
811
- completion(isValid, fileType, isValid ? duration.rounded() : -1)
812
- }
813
59
  }
814
- }
815
-
816
- private func renameFile(at url: URL, newName: String) -> URL? {
817
- let fileManager = FileManager.default
818
-
819
- // Get the directory of the existing file
820
- let directory = url.deletingLastPathComponent()
821
-
822
- // Get the file extension
823
- let fileExtension = url.pathExtension
824
-
825
- // Create the new file URL with the new name and the same extension
826
- let newFileURL = directory.appendingPathComponent(newName)
827
- .appendingPathExtension(fileExtension)
828
-
829
- // Check if a file with the new name already exists
830
- if fileManager.fileExists(atPath: newFileURL.path) {
831
- do {
832
- // If the file exists, remove it first to avoid conflicts
833
- try fileManager.removeItem(at: newFileURL)
834
- } catch {
835
- print("Error removing existing file: \(error)")
836
- return nil
837
- }
838
- }
839
-
840
- do {
841
- // Rename (move) the file
842
- try fileManager.moveItem(at: url, to: newFileURL)
843
- print("File renamed successfully to \(newFileURL.absoluteString)")
844
- return newFileURL
845
- } catch {
846
- print("Error renaming file: \(error)")
847
- return nil
60
+
61
+ func trim(url: String, options: TrimOptions) throws -> NitroModules.Promise<String> {
62
+ return Promise.async {
63
+ // This runs on a separate Thread, and can use```` `await` syntax!
64
+ return try await self.impl.trim(url: url, options: options)
65
+ }
848
66
  }
849
- }
850
67
  }