react-native-video-trim 1.0.24 → 2.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 (49) hide show
  1. package/README.md +55 -9
  2. package/android/src/main/AndroidManifest.xml +15 -0
  3. package/android/src/main/java/com/videotrim/VideoTrimModule.java +191 -63
  4. package/android/src/main/java/com/videotrim/enums/ErrorCode.java +11 -0
  5. package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +6 -1
  6. package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.java +75 -0
  7. package/android/src/main/java/com/videotrim/utils/StorageUtil.java +2 -2
  8. package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +36 -8
  9. package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +588 -308
  10. package/android/src/main/res/drawable/airpodsmax.xml +19 -0
  11. package/android/src/main/res/drawable/chevron_compact_left.xml +15 -0
  12. package/android/src/main/res/drawable/chevron_compact_right.xml +15 -0
  13. package/android/src/main/res/drawable/chevron_right_with_bg.xml +13 -0
  14. package/android/src/main/res/drawable/exclamationmark_triangle_fill.xml +15 -0
  15. package/android/src/main/res/drawable/pause_fill.xml +15 -0
  16. package/android/src/main/res/drawable/play_fill.xml +15 -0
  17. package/android/src/main/res/drawable/rounded_progress_indicator.xml +16 -0
  18. package/android/src/main/res/drawable/rounded_yellow_left_background.xml +8 -0
  19. package/android/src/main/res/drawable/rounded_yellow_right_background.xml +8 -0
  20. package/android/src/main/res/drawable/thumb_container_bg.xml +8 -0
  21. package/android/src/main/res/drawable/yellow_border.xml +9 -0
  22. package/android/src/main/res/layout/video_trimmer_view.xml +194 -75
  23. package/android/src/main/res/values/colors.xml +15 -13
  24. package/android/src/main/res/xml/file_paths.xml +5 -0
  25. package/ios/AssetLoader.swift +99 -0
  26. package/ios/ErrorCode.swift +16 -0
  27. package/ios/VideoTrim.mm +4 -2
  28. package/ios/VideoTrim.swift +405 -168
  29. package/ios/VideoTrimmer.swift +16 -10
  30. package/ios/VideoTrimmerViewController.swift +79 -13
  31. package/lib/commonjs/index.js +20 -57
  32. package/lib/commonjs/index.js.map +1 -1
  33. package/lib/module/index.js +19 -57
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/typescript/index.d.ts +47 -9
  36. package/lib/typescript/index.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/index.tsx +56 -66
  39. package/android/src/main/java/com/videotrim/adapters/VideoTrimmerAdapter.java +0 -54
  40. package/android/src/main/java/com/videotrim/widgets/RangeSeekBarView.java +0 -534
  41. package/android/src/main/java/com/videotrim/widgets/SpacesItemDecoration2.java +0 -33
  42. package/android/src/main/java/com/videotrim/widgets/ZVideoView.java +0 -48
  43. package/android/src/main/java/iknow/android/utils/BuildConfig.java +0 -18
  44. package/android/src/main/java/iknow/android/utils/DateUtil.java +0 -64
  45. package/android/src/main/res/drawable/ic_video_pause_black.png +0 -0
  46. package/android/src/main/res/drawable/ic_video_play_black.png +0 -0
  47. package/android/src/main/res/drawable/ic_video_thumb_handle.png +0 -0
  48. package/android/src/main/res/drawable/icon_seek_bar.png +0 -0
  49. package/android/src/main/res/layout/video_thumb_item_layout.xml +0 -16
@@ -2,15 +2,45 @@ import React
2
2
  import Photos
3
3
  import ffmpegkit
4
4
 
5
+ @available(iOS 13.0, *)
5
6
  @objc(VideoTrim)
6
- class VideoTrim: RCTEventEmitter {
7
+ class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDelegate {
7
8
  private let FILE_PREFIX = "trimmedVideo"
8
9
  private var hasListeners = false
9
10
  private var isShowing = false
10
11
 
11
- private var saveToPhoto = true
12
+ private var saveToPhoto = false
12
13
  private var removeAfterSavedToPhoto = false
14
+ private var removeAfterFailedToSavePhoto = false
15
+ private var removeAfterSavedToDocuments = false
16
+ private var removeAfterFailedToSaveDocuments = false
17
+ private var removeAfterShared = false
18
+ private var removeAfterFailedToShare = false
19
+
13
20
  private var trimmingText = "Trimming video..."
21
+ private var enableCancelDialog = true
22
+ private var cancelDialogTitle = "Warning!"
23
+ private var cancelDialogMessage = "Are you sure want to cancel?"
24
+ private var cancelDialogCancelText = "Close"
25
+ private var cancelDialogConfirmText = "Proceed"
26
+ private var enableSaveDialog = true
27
+ private var saveDialogTitle = "Confirmation!"
28
+ private var saveDialogMessage = "Are you sure want to save?"
29
+ private var saveDialogCancelText = "Close"
30
+ private var saveDialogConfirmText = "Proceed"
31
+ private var fullScreenModalIOS = false
32
+ private var maxDuration: Int?
33
+ private var minDuration: Int?
34
+ private var cancelButtonText = "Cancel"
35
+ private var saveButtonText = "Save"
36
+ private var vc: VideoTrimmerViewController?
37
+ private var isVideoType = true
38
+ private var outputExt = "mp4"
39
+ private var openDocumentsOnFinish = false
40
+ private var openShareSheetOnFinish = false
41
+ private var outputFile: URL?
42
+ private var enableHapticFeedback = true
43
+
14
44
 
15
45
  @objc
16
46
  static override func requiresMainQueueSetup() -> Bool {
@@ -29,150 +59,152 @@ class VideoTrim: RCTEventEmitter {
29
59
  hasListeners = false
30
60
  }
31
61
 
32
- @objc(isValidVideo:withResolver:withRejecter:)
33
- func isValidVideo(uri: String, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
34
- if let destPath = copyFileToDocumentDir(uri: uri) {
35
- resolve(UIVideoEditorController.canEditVideo(atPath: destPath.path))
36
- let _ = deleteFile(url: destPath) // remove the file we just copied to document directory
37
- } else {
38
- resolve(false)
39
- }
40
- }
41
-
42
62
  @objc(showEditor:withConfig:)
43
63
  func showEditor(uri: String, config: NSDictionary){
44
64
  if isShowing {
45
65
  return
46
66
  }
47
-
48
- saveToPhoto = config["saveToPhoto"] as? Bool ?? true
67
+ saveToPhoto = config["saveToPhoto"] as? Bool ?? false
68
+
49
69
  removeAfterSavedToPhoto = config["removeAfterSavedToPhoto"] as? Bool ?? false
70
+ removeAfterFailedToSavePhoto = config["removeAfterFailedToSavePhoto"] as? Bool ?? false
71
+ removeAfterSavedToDocuments = config["removeAfterSavedToDocuments"] as? Bool ?? false
72
+ removeAfterFailedToSaveDocuments = config["removeAfterFailedToSaveDocuments"] as? Bool ?? false
73
+ removeAfterShared = config["removeAfterShared"] as? Bool ?? false
74
+ removeAfterFailedToShare = config["removeAfterFailedToShare"] as? Bool ?? false
50
75
 
51
- let enableCancelDialog = config["enableCancelDialog"] as? Bool ?? true
52
- let cancelDialogTitle = config["cancelDialogTitle"] as? String ?? "Warning!"
53
- let cancelDialogMessage = config["cancelDialogMessage"] as? String ?? "Are you sure want to cancel?"
54
- let cancelDialogCancelText = config["cancelDialogCancelText"] as? String ?? "Close"
55
- let cancelDialogConfirmText = config["cancelDialogConfirmText"] as? String ?? "Proceed"
56
-
57
- let enableSaveDialog = config["enableSaveDialog"] as? Bool ?? true
58
- let saveDialogTitle = config["saveDialogTitle"] as? String ?? "Confirmation!"
59
- let saveDialogMessage = config["saveDialogMessage"] as? String ?? "Are you sure want to save?"
60
- let saveDialogCancelText = config["saveDialogCancelText"] as? String ?? "Close"
61
- let saveDialogConfirmText = config["saveDialogConfirmText"] as? String ?? "Proceed"
62
- trimmingText = config["trimmingText"] as? String ?? "Trimming video..."
63
- let fullScreenModalIOS = config["fullScreenModalIOS"] as? Bool ?? false
76
+ enableCancelDialog = config["enableCancelDialog"] as? Bool ?? true
77
+ cancelDialogTitle = config["cancelDialogTitle"] as? String ?? "Warning!"
78
+ cancelDialogMessage = config["cancelDialogMessage"] as? String ?? "Are you sure want to cancel?"
79
+ cancelDialogCancelText = config["cancelDialogCancelText"] as? String ?? "Close"
80
+ cancelDialogConfirmText = config["cancelDialogConfirmText"] as? String ?? "Proceed"
64
81
 
65
- if let destPath = copyFileToDocumentDir(uri: uri) {
66
- if UIVideoEditorController.canEditVideo(atPath: destPath.path) {
67
- DispatchQueue.main.async {
68
- if #available(iOS 13.0, *) {
69
- let vc = VideoTrimmerViewController()
70
- vc.asset = AVURLAsset(url: destPath, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])
71
-
72
- if let maxDuration = config["maxDuration"] as? Int {
73
- vc.maximumDuration = maxDuration
74
- }
75
-
76
- if let minDuration = config["minDuration"] as? Int {
77
- vc.minimumDuration = minDuration
78
- }
79
-
80
- if let cancelBtnText = config["cancelButtonText"] as? String, !cancelBtnText.isEmpty {
81
- vc.cancelBtnText = cancelBtnText
82
- }
83
-
84
- if let saveButtonText = config["saveButtonText"] as? String, !saveButtonText.isEmpty {
85
- vc.saveButtonText = saveButtonText
86
- }
87
-
88
- vc.cancelBtnClicked = {
89
- if !enableCancelDialog {
90
- let _ = self.deleteFile(url: destPath) // remove the file we just copied to document directory
91
- self.emitEventToJS("onCancelTrimming", eventData: nil)
92
-
93
- vc.dismiss(animated: true, completion: {
94
- self.emitEventToJS("onHide", eventData: nil)
95
- self.isShowing = false
96
- })
97
- return
98
- }
99
-
100
- // Create Alert
101
- let dialogMessage = UIAlertController(title: cancelDialogTitle, message: cancelDialogMessage, preferredStyle: .alert)
102
-
103
- // Create OK button with action handler
104
- let ok = UIAlertAction(title: cancelDialogConfirmText, style: .destructive, handler: { (action) -> Void in
105
- let _ = self.deleteFile(url: destPath) // remove the file we just copied to document directory
106
- self.emitEventToJS("onCancelTrimming", eventData: nil)
107
-
108
- vc.dismiss(animated: true, completion: {
109
- self.emitEventToJS("onHide", eventData: nil)
110
- self.isShowing = false
111
- })
112
- })
113
-
114
- // Create Cancel button with action handlder
115
- let cancel = UIAlertAction(title: cancelDialogCancelText, style: .cancel)
116
-
117
- //Add OK and Cancel button to an Alert object
118
- dialogMessage.addAction(ok)
119
- dialogMessage.addAction(cancel)
120
-
121
- // Present alert message to user
122
- if let root = RCTPresentedViewController() {
123
- root.present(dialogMessage, animated: true, completion: nil)
124
- }
125
- }
126
-
127
- vc.saveBtnClicked = {(selectedRange: CMTimeRange) in
128
- if !enableSaveDialog {
129
- self.trim(viewController: vc,inputFile: destPath, videoDuration: vc.asset.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
130
- return
131
- }
132
-
133
- // Create Alert
134
- let dialogMessage = UIAlertController(title: saveDialogTitle, message: saveDialogMessage, preferredStyle: .alert)
135
-
136
- // Create OK button with action handler
137
- let ok = UIAlertAction(title: saveDialogConfirmText, style: .default, handler: { (action) -> Void in
138
- self.trim(viewController: vc,inputFile: destPath, videoDuration: vc.asset.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
139
- })
140
-
141
- // Create Cancel button with action handlder
142
- let cancel = UIAlertAction(title: saveDialogCancelText, style: .cancel)
82
+ enableSaveDialog = config["enableSaveDialog"] as? Bool ?? true
83
+ saveDialogTitle = config["saveDialogTitle"] as? String ?? "Confirmation!"
84
+ saveDialogMessage = config["saveDialogMessage"] as? String ?? "Are you sure want to save?"
85
+ saveDialogCancelText = config["saveDialogCancelText"] as? String ?? "Close"
86
+ saveDialogConfirmText = config["saveDialogConfirmText"] as? String ?? "Proceed"
87
+ trimmingText = config["trimmingText"] as? String ?? "Trimming video..."
88
+ fullScreenModalIOS = config["fullScreenModalIOS"] as? Bool ?? false
89
+ isVideoType = (config["type"] as? String ?? "video") == "video"
90
+ outputExt = config["outputExt"] as? String ?? "mp4"
91
+ openDocumentsOnFinish = config["openDocumentsOnFinish"] as? Bool ?? false
92
+ openShareSheetOnFinish = config["openShareSheetOnFinish"] as? Bool ?? false
93
+ enableHapticFeedback = config["enableHapticFeedback"] as? Bool ?? true
143
94
 
144
- //Add OK and Cancel button to an Alert object
145
- dialogMessage.addAction(ok)
146
- dialogMessage.addAction(cancel)
95
+ if let maxDuration = config["maxDuration"] as? Int {
96
+ self.maxDuration = maxDuration
97
+ }
98
+
99
+ if let minDuration = config["minDuration"] as? Int {
100
+ self.minDuration = minDuration
101
+ }
102
+
103
+ if let cancelBtnText = config["cancelButtonText"] as? String, !cancelBtnText.isEmpty {
104
+ self.cancelButtonText = cancelBtnText
105
+ }
106
+
107
+ if let saveButtonText = config["saveButtonText"] as? String, !saveButtonText.isEmpty {
108
+ self.saveButtonText = saveButtonText
109
+ }
110
+
111
+ let destPath = URL(string: uri)
112
+ guard let destPath = destPath else { return }
113
+ let assetLoader = AssetLoader()
114
+ assetLoader.delegate = self
115
+ assetLoader.loadAsset(url: destPath, isVideoType: isVideoType)
116
+
117
+ DispatchQueue.main.async {
118
+ self.vc = VideoTrimmerViewController()
119
+
120
+ guard let vc = self.vc else { return }
121
+
122
+ vc.maximumDuration = self.maxDuration
123
+ vc.minimumDuration = self.minDuration
124
+ vc.cancelBtnText = self.cancelButtonText
125
+ vc.saveButtonText = self.saveButtonText
126
+ vc.isVideoType = self.isVideoType
127
+ vc.enableHapticFeedback = self.enableHapticFeedback
147
128
 
148
- // Present alert message to user
149
- if let root = RCTPresentedViewController() {
150
- root.present(dialogMessage, animated: true, completion: nil)
151
- }
152
- }
153
-
154
- vc.isModalInPresentation = true // prevent modal closed by swipe down
155
-
156
- if fullScreenModalIOS {
157
- vc.modalPresentationStyle = .fullScreen
158
- }
159
-
160
- if let root = RCTPresentedViewController() {
161
- root.present(vc, animated: true, completion: {
162
- self.emitEventToJS("onShow", eventData: nil)
163
- self.isShowing = true
164
- })
165
- }
166
- }
129
+
130
+ vc.cancelBtnClicked = {
131
+ if !self.enableCancelDialog {
132
+ self.emitEventToJS("onCancelTrimming", eventData: nil)
133
+
134
+ vc.dismiss(animated: true, completion: {
135
+ self.emitEventToJS("onHide", eventData: nil)
136
+ self.isShowing = false
137
+ })
138
+ return
139
+ }
140
+
141
+ // Create Alert
142
+ let dialogMessage = UIAlertController(title: self.cancelDialogTitle, message: self.cancelDialogMessage, preferredStyle: .alert)
143
+
144
+ // Create OK button with action handler
145
+ let ok = UIAlertAction(title: self.cancelDialogConfirmText, style: .destructive, handler: { (action) -> Void in
146
+ self.emitEventToJS("onCancelTrimming", eventData: nil)
147
+
148
+ vc.dismiss(animated: true, completion: {
149
+ self.emitEventToJS("onHide", eventData: nil)
150
+ self.isShowing = false
151
+ })
152
+ })
153
+
154
+ // Create Cancel button with action handlder
155
+ let cancel = UIAlertAction(title: self.cancelDialogCancelText, style: .cancel)
156
+
157
+ //Add OK and Cancel button to an Alert object
158
+ dialogMessage.addAction(ok)
159
+ dialogMessage.addAction(cancel)
160
+
161
+ // Present alert message to user
162
+ if let root = RCTPresentedViewController() {
163
+ root.present(dialogMessage, animated: true, completion: nil)
167
164
  }
168
- } else {
169
- let eventPayload: [String: Any] = ["message": "File is not a valid video"]
170
- self.emitEventToJS("onError", eventData: eventPayload)
171
165
  }
172
- } else {
173
- let eventPayload: [String: Any] = ["message": "File is invalid"]
174
- self.emitEventToJS("onError", eventData: eventPayload)
166
+
167
+ vc.saveBtnClicked = {(selectedRange: CMTimeRange) in
168
+ if !self.enableSaveDialog {
169
+ self.trim(viewController: vc,inputFile: destPath, videoDuration: self.vc!.asset!.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
170
+ return
171
+ }
172
+
173
+ // Create Alert
174
+ let dialogMessage = UIAlertController(title: self.saveDialogTitle, message: self.saveDialogMessage, preferredStyle: .alert)
175
+
176
+ // Create OK button with action handler
177
+ let ok = UIAlertAction(title: self.saveDialogConfirmText, style: .default, handler: { (action) -> Void in
178
+ self.trim(viewController: vc,inputFile: destPath, videoDuration: vc.asset!.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
179
+ })
180
+
181
+ // Create Cancel button with action handlder
182
+ let cancel = UIAlertAction(title: self.saveDialogCancelText, style: .cancel)
183
+
184
+ //Add OK and Cancel button to an Alert object
185
+ dialogMessage.addAction(ok)
186
+ dialogMessage.addAction(cancel)
187
+
188
+ // Present alert message to user
189
+ if let root = RCTPresentedViewController() {
190
+ root.present(dialogMessage, animated: true, completion: nil)
191
+ }
192
+ }
193
+
194
+ vc.isModalInPresentation = true // prevent modal closed by swipe down
195
+
196
+ if self.fullScreenModalIOS {
197
+ vc.modalPresentationStyle = .fullScreen
198
+ }
199
+
200
+ if let root = RCTPresentedViewController() {
201
+ root.present(vc, animated: true, completion: {
202
+ self.emitEventToJS("onShow", eventData: nil)
203
+ self.isShowing = true
204
+ })
205
+ }
175
206
  }
207
+
176
208
  }
177
209
 
178
210
  private func copyFileToDocumentDir(uri: String) -> URL? {
@@ -270,11 +302,11 @@ class VideoTrim: RCTEventEmitter {
270
302
  }
271
303
  }
272
304
 
273
- @available(iOS 13.0, *)
274
305
  private func trim(viewController: VideoTrimmerViewController, inputFile: URL, videoDuration: Double, startTime: Double, endTime: Double) {
275
306
  let timestamp = Int(Date().timeIntervalSince1970)
276
- let outputName = "\(FILE_PREFIX)_\(timestamp).mp4" // use mp4 to prevent any issue with ffmpeg about file extension
277
- let outputFile = "\(inputFile.deletingLastPathComponent().absoluteURL)\(outputName)"
307
+ let outputName = "\(FILE_PREFIX)_\(timestamp).\(outputExt)"
308
+ let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
309
+ outputFile = documentsDirectory.appendingPathComponent(outputName)
278
310
 
279
311
  let formatter = DateFormatter()
280
312
  formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"
@@ -284,20 +316,20 @@ class VideoTrim: RCTEventEmitter {
284
316
  self.emitEventToJS("onStartTrimming", eventData: nil)
285
317
 
286
318
  // Create Alert
287
- let dialogMessage = UIAlertController(title: trimmingText, message: nil, preferredStyle: .alert)
288
-
319
+ let progressDialog = UIAlertController(title: trimmingText, message: nil, preferredStyle: .alert)
320
+
289
321
  // Present alert message to user
290
322
  let progressView = UIProgressView(frame: .zero)
291
323
  progressView.tintColor = .systemBlue
292
324
  if let root = RCTPresentedViewController() {
293
- root.present(dialogMessage, animated: true, completion: {
294
- dialogMessage.view.addSubview(progressView)
325
+ root.present(progressDialog, animated: true, completion: {
326
+ progressDialog.view.addSubview(progressView)
295
327
 
296
328
  progressView.translatesAutoresizingMaskIntoConstraints = false
297
329
  NSLayoutConstraint.activate([
298
- progressView.leadingAnchor.constraint(equalTo: dialogMessage.view.leadingAnchor, constant: 8),
299
- progressView.trailingAnchor.constraint(equalTo: dialogMessage.view.trailingAnchor, constant: -8),
300
- progressView.bottomAnchor.constraint(equalTo: dialogMessage.view.bottomAnchor, constant: -8)
330
+ progressView.leadingAnchor.constraint(equalTo: progressDialog.view.leadingAnchor, constant: 8),
331
+ progressView.trailingAnchor.constraint(equalTo: progressDialog.view.trailingAnchor, constant: -8),
332
+ progressView.bottomAnchor.constraint(equalTo: progressDialog.view.bottomAnchor, constant: -8)
301
333
  ])
302
334
  })
303
335
  }
@@ -313,70 +345,275 @@ class VideoTrim: RCTEventEmitter {
313
345
  "copy",
314
346
  "-metadata",
315
347
  "creation_time=\(dateTime)",
316
- outputFile
348
+ outputFile!.absoluteString
317
349
  ]
318
350
 
351
+ print("Command: ", cmds.joined(separator: " "))
352
+
353
+ let eventPayload: [String: Any] = [
354
+ "command": cmds.joined(separator: " ")
355
+ ]
356
+ self.emitEventToJS("onLog", eventData: eventPayload)
357
+
319
358
  FFmpegKit.execute(withArgumentsAsync: cmds, withCompleteCallback: { session in
320
- let _ = self.deleteFile(url: inputFile) // remove the file we just copied to document directory
359
+ DispatchQueue.main.async {
360
+ progressDialog.dismiss(animated: true)
361
+ }
321
362
 
322
363
  let state = session?.getState()
323
364
  let returnCode = session?.getReturnCode()
324
-
365
+
325
366
  if ReturnCode.isSuccess(returnCode) {
326
- let eventPayload: [String: Any] = ["outputPath": outputFile, "startTime": startTime, "endTime": endTime, "duration": videoDuration]
367
+ let eventPayload: [String: Any] = ["outputPath": self.outputFile!.absoluteString, "startTime": (startTime * 1000).rounded(), "endTime": (endTime * 1000).rounded(), "duration": (videoDuration * 1000).rounded()]
327
368
  self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
328
369
 
329
- if (self.saveToPhoto) {
370
+ if (self.saveToPhoto && self.isVideoType) {
330
371
  PHPhotoLibrary.requestAuthorization { status in
331
372
  guard status == .authorized else {
332
- let eventPayload: [String: Any] = ["message": "Permission to access Photo Library is not granted"]
333
- self.emitEventToJS("onError", eventData: eventPayload)
373
+ self.onError(message: "Permission to access Photo Library is not granted", code: .noPhotoPermission)
334
374
  return
335
375
  }
336
376
 
337
377
  PHPhotoLibrary.shared().performChanges({
338
- let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(string: outputFile)!)
378
+ let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: self.outputFile!)
339
379
  request?.creationDate = Date()
340
380
  }) { success, error in
341
381
  if success {
342
382
  print("Edited video saved to Photo Library successfully.")
343
383
 
344
384
  if self.removeAfterSavedToPhoto {
345
- let _ = self.deleteFile(url: URL(string: outputFile)!)
385
+ let _ = self.deleteFile(url: self.outputFile!)
346
386
  }
347
387
  } else {
348
- let eventPayload: [String: Any] = ["message": "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")"]
349
- self.emitEventToJS("onError", eventData: eventPayload)
388
+ self.onError(message: "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")", code: .failToSaveToPhoto)
389
+ if self.removeAfterFailedToSavePhoto {
390
+ let _ = self.deleteFile(url: self.outputFile!)
391
+ }
350
392
  }
351
393
  }
352
394
  }
395
+ } else if self.openDocumentsOnFinish {
396
+ self.saveFileToFilesApp(fileURL: self.outputFile!)
397
+
398
+ // must return otherwise editor will close
399
+ return
400
+ } else if self.openShareSheetOnFinish {
401
+ self.shareFile(fileURL: self.outputFile!)
402
+
403
+ // must return otherwise editor will close
404
+ return
353
405
  }
354
406
  } else {
355
407
  // CANCEL + FAILURE
356
- let eventPayload: [String: Any] = ["message": "Command failed with state \(String(describing: FFmpegKitConfig.sessionState(toString: state ?? .failed))) and rc \(String(describing: returnCode)).\(String(describing: session?.getFailStackTrace()))"]
357
- self.emitEventToJS("onError", eventData: eventPayload)
408
+ self.onError(message: "Command failed with state \(String(describing: FFmpegKitConfig.sessionState(toString: state ?? .failed))) and rc \(String(describing: returnCode)).\(String(describing: session?.getFailStackTrace()))", code: .trimmingFailed)
358
409
  }
359
410
 
360
- // some how in case we trim a very short video the view controller is still visible after first .dismiss call
361
- // even the file is successfully saved
362
- // that's why we need a small delay here to ensure vc will be dismissed
363
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
364
- dialogMessage.dismiss(animated: false)
365
- viewController.dismiss(animated: true, completion: {
366
- self.emitEventToJS("onHide", eventData: nil)
367
- self.isShowing = false
368
- })
369
- }
411
+ self.closeEditor()
370
412
  }, withLogCallback: { log in
371
- print("FFmpeg process started with log " + (log?.getMessage() ?? ""));
413
+ guard let log = log else { return }
414
+
415
+ print("FFmpeg process started with log " + (log.getMessage()));
416
+
417
+ let eventPayload: [String: Any] = [
418
+ "level": log.getLevel(),
419
+ "message": log.getMessage() ?? "",
420
+ "sessionId": log.getSessionId(),
421
+ ]
422
+ self.emitEventToJS("onLog", eventData: eventPayload)
423
+
372
424
  }, withStatisticsCallback: { statistics in
373
- let timeInMilliseconds = statistics?.getTime() ?? 0;
425
+ guard let statistics = statistics else { return }
426
+
427
+ let timeInMilliseconds = statistics.getTime()
374
428
  if timeInMilliseconds > 0 {
375
429
  let completePercentage = timeInMilliseconds / (videoDuration * 1000); // from 0 -> 1
376
430
  DispatchQueue.main.async {
377
431
  progressView.setProgress(Float(completePercentage), animated: true)
378
432
  }
379
433
  }
434
+
435
+ let eventPayload: [String: Any] = [
436
+ "sessionId": statistics.getSessionId(),
437
+ "videoFrameNumber": statistics.getVideoFrameNumber(),
438
+ "videoFps": statistics.getVideoFps(),
439
+ "videoQuality": statistics.getVideoQuality(),
440
+ "size": statistics.getSize(),
441
+ "time": statistics.getTime(),
442
+ "bitrate": statistics.getBitrate(),
443
+ "speed": statistics.getSpeed()
444
+ ]
445
+ self.emitEventToJS("onStatistics", eventData: eventPayload)
380
446
  })
381
447
  }
448
+
449
+ func assetLoader(_ loader: AssetLoader, didFailWithError error: any Error, forKey key: String) {
450
+ let message = "Failed to load \(key): \(error.localizedDescription)"
451
+ print(message)
452
+
453
+ self.onError(message: message, code: .failToLoadVideo)
454
+ vc?.onAssetFailToLoad()
455
+ }
456
+
457
+ func assetLoaderDidSucceed(_ loader: AssetLoader) {
458
+ print("Asset loaded successfully")
459
+
460
+ vc?.asset = loader.asset
461
+ }
462
+
463
+
464
+
465
+ private func saveFileToFilesApp(fileURL: URL) {
466
+ DispatchQueue.main.async {
467
+ let documentPicker = UIDocumentPickerViewController(url: fileURL, in: .exportToService)
468
+ documentPicker.delegate = self
469
+ documentPicker.modalPresentationStyle = .formSheet
470
+ if let root = RCTPresentedViewController() {
471
+ root.present(documentPicker, animated: true, completion: nil)
472
+ }
473
+ }
474
+ }
475
+
476
+ private func shareFile(fileURL: URL) {
477
+ DispatchQueue.main.async {
478
+ // Create an instance of UIActivityViewController
479
+ let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
480
+
481
+ activityViewController.completionWithItemsHandler = { activityType, completed, returnedItems, error in
482
+
483
+ if let error = error {
484
+ let message = "Sharing error: \(error.localizedDescription)"
485
+ print(message)
486
+ self.onError(message: message, code: .failToShare)
487
+
488
+ if self.removeAfterFailedToShare {
489
+ let _ = self.deleteFile(url: self.outputFile!)
490
+ }
491
+ return
492
+ }
493
+
494
+ if completed {
495
+ print("User completed the sharing activity")
496
+ if self.removeAfterShared {
497
+ let _ = self.deleteFile(url: self.outputFile!)
498
+ }
499
+ } else {
500
+ print("User cancelled or failed to complete the sharing activity")
501
+ if self.removeAfterFailedToShare {
502
+ let _ = self.deleteFile(url: self.outputFile!)
503
+ }
504
+ }
505
+
506
+ self.closeEditor()
507
+
508
+ }
509
+
510
+ // Present the share sheet
511
+ if let root = RCTPresentedViewController() {
512
+ root.present(activityViewController, animated: true, completion: nil)
513
+ }
514
+ }
515
+
516
+ }
517
+
518
+ func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
519
+ if removeAfterSavedToDocuments {
520
+ let _ = deleteFile(url: outputFile!)
521
+ }
522
+ closeEditor()
523
+ }
524
+
525
+ func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
526
+ if removeAfterFailedToSaveDocuments {
527
+ let _ = deleteFile(url: outputFile!)
528
+ }
529
+ closeEditor()
530
+ }
531
+
532
+ @objc(closeEditor:withRejecter:)
533
+ func closeEditor(resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
534
+ closeEditor()
535
+ resolve(true)
536
+ }
537
+
538
+ private func closeEditor() {
539
+ guard let vc = vc else { return }
540
+ // some how in case we trim a very short video the view controller is still visible after first .dismiss call
541
+ // even the file is successfully saved
542
+ // that's why we need a small delay here to ensure vc will be dismissed
543
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
544
+ vc.dismiss(animated: true, completion: {
545
+ self.emitEventToJS("onHide", eventData: nil)
546
+ self.isShowing = false
547
+ })
548
+ }
549
+ }
550
+
551
+ @objc(isValidFile:withResolver:withRejecter:)
552
+ func isValidFile(uri: String, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
553
+ let fileURL = URL(string: uri)!
554
+ checkFileValidity(url: fileURL) { isValid, fileType, duration in
555
+ if isValid {
556
+ print("Valid \(fileType) file with duration: \(duration) milliseconds")
557
+ } else {
558
+ print("Invalid file")
559
+ }
560
+
561
+ let payload: [String: Any] = [
562
+ "isValid": isValid,
563
+ "fileType": fileType,
564
+ "duration": duration
565
+ ]
566
+ resolve(payload)
567
+ }
568
+
569
+ }
570
+
571
+ private func onError(message: String, code: ErrorCode) {
572
+ let eventPayload: [String: String] = [
573
+ "message": message,
574
+ "errorCode": code.rawValue
575
+ ]
576
+ self.emitEventToJS("onError", eventData: eventPayload)
577
+ }
578
+
579
+ private func checkFileValidity(url: URL, completion: @escaping (Bool, String, Double) -> Void) {
580
+ let asset = AVAsset(url: url)
581
+
582
+ // Load the duration and tracks asynchronously
583
+ asset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) {
584
+ var error: NSError? = nil
585
+
586
+ // Check if the duration and tracks are loaded
587
+ let durationStatus = asset.statusOfValue(forKey: "duration", error: &error)
588
+ let tracksStatus = asset.statusOfValue(forKey: "tracks", error: &error)
589
+
590
+ // Ensure both properties are loaded successfully
591
+ guard durationStatus == .loaded, tracksStatus == .loaded, error == nil else {
592
+ DispatchQueue.main.async {
593
+ completion(false, "unknown", -1)
594
+ }
595
+ return
596
+ }
597
+
598
+ // Check if the asset contains any video or audio tracks
599
+ let videoTracks = asset.tracks(withMediaType: .video)
600
+ let audioTracks = asset.tracks(withMediaType: .audio)
601
+
602
+ let isValid = !videoTracks.isEmpty || !audioTracks.isEmpty
603
+ let fileType: String
604
+ if !videoTracks.isEmpty {
605
+ fileType = "video"
606
+ } else if !audioTracks.isEmpty {
607
+ fileType = "audio"
608
+ } else {
609
+ fileType = "unknown"
610
+ }
611
+
612
+ let duration = CMTimeGetSeconds(asset.duration) * 1000
613
+
614
+ DispatchQueue.main.async {
615
+ completion(isValid, fileType, isValid ? duration.rounded() : -1)
616
+ }
617
+ }
618
+ }
382
619
  }