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