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