react-native-video-trim 2.0.0 → 2.2.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 (31) hide show
  1. package/README.md +168 -33
  2. package/android/src/main/AndroidManifest.xml +13 -0
  3. package/android/src/main/java/com/videotrim/VideoTrimModule.java +282 -75
  4. package/android/src/main/java/com/videotrim/enums/ErrorCode.java +10 -0
  5. package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +4 -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 +26 -16
  9. package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +310 -81
  10. package/android/src/main/res/drawable/airpodsmax.xml +19 -0
  11. package/android/src/main/res/drawable/exclamationmark_triangle_fill.xml +15 -0
  12. package/android/src/main/res/drawable/thumb_container_bg.xml +8 -0
  13. package/android/src/main/res/layout/video_trimmer_view.xml +71 -4
  14. package/android/src/main/res/xml/file_paths.xml +5 -0
  15. package/ios/AssetLoader.swift +99 -0
  16. package/ios/ErrorCode.swift +16 -0
  17. package/ios/ProgressAlertController.swift +100 -0
  18. package/ios/VideoTrim.mm +4 -2
  19. package/ios/VideoTrim.swift +472 -177
  20. package/ios/VideoTrimmer.swift +16 -10
  21. package/ios/VideoTrimmerViewController.swift +191 -22
  22. package/lib/commonjs/index.js +25 -55
  23. package/lib/commonjs/index.js.map +1 -1
  24. package/lib/module/index.js +24 -55
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/typescript/index.d.ts +215 -9
  27. package/lib/typescript/index.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/index.tsx +229 -66
  30. package/android/src/main/java/iknow/android/utils/BuildConfig.java +0 -18
  31. package/android/src/main/java/iknow/android/utils/DateUtil.java +0 -64
@@ -2,15 +2,53 @@ 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 cancelButtonText = "Cancel"
33
+ private var saveButtonText = "Save"
34
+ private var vc: VideoTrimmerViewController?
35
+ private var isVideoType = true
36
+ private var outputExt = "mp4"
37
+ private var openDocumentsOnFinish = false
38
+ private var openShareSheetOnFinish = false
39
+ private var outputFile: URL?
40
+ private var closeWhenFinish = true
41
+ private var enableCancelTrimming = true;
42
+ private var cancelTrimmingButtonText = "Cancel";
43
+ private var enableCancelTrimmingDialog = true;
44
+ private var cancelTrimmingDialogTitle = "Warning!";
45
+ private var cancelTrimmingDialogMessage = "Are you sure want to trimming?";
46
+ private var cancelTrimmingDialogCancelText = "Close";
47
+ private var cancelTrimmingDialogConfirmText = "Proceed";
48
+ private var alertOnFailToLoad = true;
49
+ private var alertOnFailTitle = "Error";
50
+ private var alertOnFailMessage = "Fail to load media. Possibly invalid file or no network connection";
51
+ private var alertOnFailCloseText = "Close";
14
52
 
15
53
  @objc
16
54
  static override func requiresMainQueueSetup() -> Bool {
@@ -29,150 +67,155 @@ class VideoTrim: RCTEventEmitter {
29
67
  hasListeners = false
30
68
  }
31
69
 
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
70
  @objc(showEditor:withConfig:)
43
71
  func showEditor(uri: String, config: NSDictionary){
44
72
  if isShowing {
45
73
  return
46
74
  }
47
-
48
- saveToPhoto = config["saveToPhoto"] as? Bool ?? true
75
+ saveToPhoto = config["saveToPhoto"] as? Bool ?? false
76
+
49
77
  removeAfterSavedToPhoto = config["removeAfterSavedToPhoto"] as? Bool ?? false
78
+ removeAfterFailedToSavePhoto = config["removeAfterFailedToSavePhoto"] as? Bool ?? false
79
+ removeAfterSavedToDocuments = config["removeAfterSavedToDocuments"] as? Bool ?? false
80
+ removeAfterFailedToSaveDocuments = config["removeAfterFailedToSaveDocuments"] as? Bool ?? false
81
+ removeAfterShared = config["removeAfterShared"] as? Bool ?? false
82
+ removeAfterFailedToShare = config["removeAfterFailedToShare"] as? Bool ?? false
50
83
 
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"
84
+ enableCancelDialog = config["enableCancelDialog"] as? Bool ?? true
85
+ cancelDialogTitle = config["cancelDialogTitle"] as? String ?? "Warning!"
86
+ cancelDialogMessage = config["cancelDialogMessage"] as? String ?? "Are you sure want to cancel?"
87
+ cancelDialogCancelText = config["cancelDialogCancelText"] as? String ?? "Close"
88
+ cancelDialogConfirmText = config["cancelDialogConfirmText"] as? String ?? "Proceed"
89
+
90
+ enableSaveDialog = config["enableSaveDialog"] as? Bool ?? true
91
+ saveDialogTitle = config["saveDialogTitle"] as? String ?? "Confirmation!"
92
+ saveDialogMessage = config["saveDialogMessage"] as? String ?? "Are you sure want to save?"
93
+ saveDialogCancelText = config["saveDialogCancelText"] as? String ?? "Close"
94
+ saveDialogConfirmText = config["saveDialogConfirmText"] as? String ?? "Proceed"
62
95
  trimmingText = config["trimmingText"] as? String ?? "Trimming video..."
63
- let fullScreenModalIOS = config["fullScreenModalIOS"] as? Bool ?? false
96
+ fullScreenModalIOS = config["fullScreenModalIOS"] as? Bool ?? false
97
+ isVideoType = (config["type"] as? String ?? "video") == "video"
98
+ outputExt = config["outputExt"] as? String ?? "mp4"
99
+ openDocumentsOnFinish = config["openDocumentsOnFinish"] as? Bool ?? false
100
+ openShareSheetOnFinish = config["openShareSheetOnFinish"] as? Bool ?? false
64
101
 
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)
102
+ closeWhenFinish = config["closeWhenFinish"] as? Bool ?? true
103
+ enableCancelTrimming = config["enableCancelTrimming"] as? Bool ?? true
104
+ cancelTrimmingButtonText = config["cancelTrimmingButtonText"] as? String ?? "Cancel"
105
+ enableCancelTrimmingDialog = config["enableCancelTrimmingDialog"] as? Bool ?? true
106
+ cancelTrimmingDialogTitle = config["cancelTrimmingDialogTitle"] as? String ?? "Warning!"
107
+ cancelTrimmingDialogMessage = config["cancelTrimmingDialogMessage"] as? String ?? "Are you sure want to cancel trimming?"
108
+ cancelTrimmingDialogCancelText = config["cancelTrimmingDialogCancelText"] as? String ?? "Close"
109
+ cancelTrimmingDialogConfirmText = config["cancelTrimmingDialogConfirmText"] as? String ?? "Proceed"
110
+ alertOnFailToLoad = config["alertOnFailToLoad"] as? Bool ?? true
111
+ alertOnFailTitle = config["alertOnFailTitle"] as? String ?? "Error"
112
+ alertOnFailMessage = config["alertOnFailMessage"] as? String ?? "Fail to load media. Possibly invalid file or no network connection"
113
+ alertOnFailCloseText = config["alertOnFailCloseText"] as? String ?? "Close"
114
+
115
+ if let cancelBtnText = config["cancelButtonText"] as? String, !cancelBtnText.isEmpty {
116
+ self.cancelButtonText = cancelBtnText
117
+ }
118
+
119
+ if let saveButtonText = config["saveButtonText"] as? String, !saveButtonText.isEmpty {
120
+ self.saveButtonText = saveButtonText
121
+ }
122
+
123
+ let destPath = URL(string: uri)
124
+ guard let destPath = destPath else { return }
125
+
126
+ DispatchQueue.main.async {
127
+ self.vc = VideoTrimmerViewController()
128
+
129
+ guard let vc = self.vc else { return }
130
+
131
+ vc.configure(config: config)
143
132
 
144
- //Add OK and Cancel button to an Alert object
145
- dialogMessage.addAction(ok)
146
- dialogMessage.addAction(cancel)
133
+ vc.cancelBtnClicked = {
134
+ if !self.enableCancelDialog {
135
+ self.emitEventToJS("onCancel", eventData: nil)
136
+
137
+ vc.dismiss(animated: true, completion: {
138
+ self.emitEventToJS("onHide", eventData: nil)
139
+ self.isShowing = false
140
+ })
141
+ return
142
+ }
143
+
144
+ // Create Alert
145
+ let dialogMessage = UIAlertController(title: self.cancelDialogTitle, message: self.cancelDialogMessage, preferredStyle: .alert)
146
+ dialogMessage.overrideUserInterfaceStyle = .dark
147
+
148
+ // Create OK button with action handler
149
+ let ok = UIAlertAction(title: self.cancelDialogConfirmText, style: .destructive, handler: { (action) -> Void in
150
+ self.emitEventToJS("onCancel", eventData: nil)
151
+
152
+ vc.dismiss(animated: true, completion: {
153
+ self.emitEventToJS("onHide", eventData: nil)
154
+ self.isShowing = false
155
+ })
156
+ })
157
+
158
+ // Create Cancel button with action handlder
159
+ let cancel = UIAlertAction(title: self.cancelDialogCancelText, style: .cancel)
160
+
161
+ //Add OK and Cancel button to an Alert object
162
+ dialogMessage.addAction(ok)
163
+ dialogMessage.addAction(cancel)
164
+
165
+ // Present alert message to user
166
+ if let root = RCTPresentedViewController() {
167
+ root.present(dialogMessage, animated: true, completion: nil)
168
+ }
169
+ }
170
+
171
+ vc.saveBtnClicked = {(selectedRange: CMTimeRange) in
172
+ if !self.enableSaveDialog {
173
+ self.trim(viewController: vc,inputFile: destPath, videoDuration: self.vc!.asset!.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
174
+ return
175
+ }
176
+
177
+ // Create Alert
178
+ let dialogMessage = UIAlertController(title: self.saveDialogTitle, message: self.saveDialogMessage, preferredStyle: .alert)
179
+ dialogMessage.overrideUserInterfaceStyle = .dark
147
180
 
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
- }
181
+ // Create OK button with action handler
182
+ let ok = UIAlertAction(title: self.saveDialogConfirmText, style: .default, handler: { (action) -> Void in
183
+ self.trim(viewController: vc,inputFile: destPath, videoDuration: vc.asset!.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
184
+ })
185
+
186
+ // Create Cancel button with action handlder
187
+ let cancel = UIAlertAction(title: self.saveDialogCancelText, style: .cancel)
188
+
189
+ //Add OK and Cancel button to an Alert object
190
+ dialogMessage.addAction(ok)
191
+ dialogMessage.addAction(cancel)
192
+
193
+ // Present alert message to user
194
+ if let root = RCTPresentedViewController() {
195
+ root.present(dialogMessage, animated: true, completion: nil)
167
196
  }
168
- } else {
169
- let eventPayload: [String: Any] = ["message": "File is not a valid video"]
170
- self.emitEventToJS("onError", eventData: eventPayload)
171
197
  }
172
- } else {
173
- let eventPayload: [String: Any] = ["message": "File is invalid"]
174
- self.emitEventToJS("onError", eventData: eventPayload)
198
+
199
+ vc.isModalInPresentation = true // prevent modal closed by swipe down
200
+
201
+ if self.fullScreenModalIOS {
202
+ vc.modalPresentationStyle = .fullScreen
203
+ }
204
+
205
+ if let root = RCTPresentedViewController() {
206
+ root.present(vc, animated: true, completion: {
207
+ self.emitEventToJS("onShow", eventData: nil)
208
+ self.isShowing = true
209
+
210
+ // start loading asset after view is finished presenting
211
+ // otherwise it may run too fast for local file and autoplay looks weird
212
+ let assetLoader = AssetLoader()
213
+ assetLoader.delegate = self
214
+ assetLoader.loadAsset(url: destPath, isVideoType: self.isVideoType)
215
+ })
216
+ }
175
217
  }
218
+
176
219
  }
177
220
 
178
221
  private func copyFileToDocumentDir(uri: String) -> URL? {
@@ -270,36 +313,73 @@ class VideoTrim: RCTEventEmitter {
270
313
  }
271
314
  }
272
315
 
273
- @available(iOS 13.0, *)
274
316
  private func trim(viewController: VideoTrimmerViewController, inputFile: URL, videoDuration: Double, startTime: Double, endTime: Double) {
317
+ vc?.pausePlayer()
318
+
275
319
  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)"
320
+ let outputName = "\(FILE_PREFIX)_\(timestamp).\(outputExt)"
321
+ let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
322
+ outputFile = documentsDirectory.appendingPathComponent(outputName)
278
323
 
279
324
  let formatter = DateFormatter()
280
325
  formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"
281
326
  formatter.timeZone = TimeZone(identifier: "UTC")
282
327
  let dateTime = formatter.string(from: Date())
283
328
 
284
- self.emitEventToJS("onStartTrimming", eventData: nil)
329
+ emitEventToJS("onStartTrimming", eventData: nil)
330
+
331
+ var ffmpegSession: FFmpegSession?
332
+ let progressAlert = ProgressAlertController()
333
+ progressAlert.modalPresentationStyle = .overFullScreen
334
+ progressAlert.modalTransitionStyle = .crossDissolve
335
+ progressAlert.setTitle(trimmingText)
285
336
 
286
- // Create Alert
287
- let dialogMessage = UIAlertController(title: trimmingText, message: nil, preferredStyle: .alert)
337
+ if enableCancelTrimming {
338
+ progressAlert.setCancelTitle(cancelTrimmingButtonText)
339
+ progressAlert.showCancelBtn()
340
+ progressAlert.onDismiss = {
341
+ if self.enableCancelTrimmingDialog {
342
+ let dialogMessage = UIAlertController(title: self.cancelTrimmingDialogTitle, message: self.cancelTrimmingDialogMessage, preferredStyle: .alert)
343
+ dialogMessage.overrideUserInterfaceStyle = .dark
288
344
 
289
- // Present alert message to user
290
- let progressView = UIProgressView(frame: .zero)
291
- progressView.tintColor = .systemBlue
292
- if let root = RCTPresentedViewController() {
293
- root.present(dialogMessage, animated: true, completion: {
294
- dialogMessage.view.addSubview(progressView)
345
+ // Create OK button with action handler
346
+ let ok = UIAlertAction(title: self.cancelDialogConfirmText, style: .destructive, handler: { (action) -> Void in
347
+
348
+ if let ffmpegSession = ffmpegSession {
349
+ ffmpegSession.cancel()
350
+ } else {
351
+ self.emitEventToJS("onCancelTrimming", eventData: nil)
352
+ }
353
+
354
+ progressAlert.dismiss(animated: true)
355
+ })
356
+
357
+ // Create Cancel button with action handlder
358
+ let cancel = UIAlertAction(title: self.cancelDialogCancelText, style: .cancel)
359
+
360
+ //Add OK and Cancel button to an Alert object
361
+ dialogMessage.addAction(ok)
362
+ dialogMessage.addAction(cancel)
363
+
364
+ // Present alert message to user
365
+ if let root = RCTPresentedViewController() {
366
+ root.present(dialogMessage, animated: true, completion: nil)
367
+ }
368
+ } else {
369
+ if let ffmpegSession = ffmpegSession {
370
+ ffmpegSession.cancel()
371
+ } else {
372
+ self.emitEventToJS("onCancelTrimming", eventData: nil)
373
+ }
374
+
375
+ progressAlert.dismiss(animated: true)
376
+ }
295
377
 
296
- progressView.translatesAutoresizingMaskIntoConstraints = false
297
- 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)
301
- ])
302
- })
378
+ }
379
+ }
380
+
381
+ if let root = RCTPresentedViewController() {
382
+ root.present(progressAlert, animated: true, completion: nil)
303
383
  }
304
384
 
305
385
  let cmds = [
@@ -313,60 +393,83 @@ class VideoTrim: RCTEventEmitter {
313
393
  "copy",
314
394
  "-metadata",
315
395
  "creation_time=\(dateTime)",
316
- outputFile
396
+ outputFile!.absoluteString
397
+ ]
398
+
399
+ print("Command: ", cmds.joined(separator: " "))
400
+
401
+ let eventPayload: [String: Any] = [
402
+ "command": cmds.joined(separator: " ")
317
403
  ]
404
+ self.emitEventToJS("onLog", eventData: eventPayload)
318
405
 
319
- FFmpegKit.execute(withArgumentsAsync: cmds, withCompleteCallback: { session in
320
- let _ = self.deleteFile(url: inputFile) // remove the file we just copied to document directory
406
+ ffmpegSession = FFmpegKit.execute(withArgumentsAsync: cmds, withCompleteCallback: { session in
407
+
408
+ // always hide progressAlert
409
+ DispatchQueue.main.async {
410
+ progressAlert.dismiss(animated: true)
411
+ }
321
412
 
322
413
  let state = session?.getState()
323
414
  let returnCode = session?.getReturnCode()
324
415
 
325
416
  if ReturnCode.isSuccess(returnCode) {
326
- let eventPayload: [String: Any] = ["outputPath": outputFile, "startTime": startTime, "endTime": endTime, "duration": videoDuration]
417
+ let eventPayload: [String: Any] = ["outputPath": self.outputFile!.absoluteString, "startTime": (startTime * 1000).rounded(), "endTime": (endTime * 1000).rounded(), "duration": (videoDuration * 1000).rounded()]
327
418
  self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
328
419
 
329
- if (self.saveToPhoto) {
420
+ if (self.saveToPhoto && self.isVideoType) {
330
421
  PHPhotoLibrary.requestAuthorization { status in
331
422
  guard status == .authorized else {
332
- let eventPayload: [String: Any] = ["message": "Permission to access Photo Library is not granted"]
333
- self.emitEventToJS("onError", eventData: eventPayload)
423
+ self.onError(message: "Permission to access Photo Library is not granted", code: .noPhotoPermission)
334
424
  return
335
425
  }
336
426
 
337
427
  PHPhotoLibrary.shared().performChanges({
338
- let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(string: outputFile)!)
428
+ let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: self.outputFile!)
339
429
  request?.creationDate = Date()
340
430
  }) { success, error in
341
431
  if success {
342
432
  print("Edited video saved to Photo Library successfully.")
343
433
 
344
434
  if self.removeAfterSavedToPhoto {
345
- let _ = self.deleteFile(url: URL(string: outputFile)!)
435
+ let _ = self.deleteFile(url: self.outputFile!)
346
436
  }
347
437
  } 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)
438
+ self.onError(message: "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")", code: .failToSaveToPhoto)
439
+ if self.removeAfterFailedToSavePhoto {
440
+ let _ = self.deleteFile(url: self.outputFile!)
441
+ }
350
442
  }
351
443
  }
352
444
  }
445
+ } else if self.openDocumentsOnFinish {
446
+ self.saveFileToFilesApp(fileURL: self.outputFile!)
447
+
448
+ // must return otherwise editor will close
449
+ return
450
+ } else if self.openShareSheetOnFinish {
451
+ self.shareFile(fileURL: self.outputFile!)
452
+
453
+ // must return otherwise editor will close
454
+ return
455
+ }
456
+
457
+ if self.closeWhenFinish {
458
+ self.closeEditor()
353
459
  }
460
+
461
+ } else if ReturnCode.isCancel(returnCode) {
462
+ // CANCEL
463
+ self.emitEventToJS("onCancelTrimming", eventData: nil)
354
464
  } else {
355
- // 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)
465
+ // FAILURE
466
+ 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)
467
+ if self.closeWhenFinish {
468
+ self.closeEditor()
469
+ }
358
470
  }
359
471
 
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
- }
472
+
370
473
  }, withLogCallback: { log in
371
474
  guard let log = log else { return }
372
475
 
@@ -381,12 +484,12 @@ class VideoTrim: RCTEventEmitter {
381
484
 
382
485
  }, withStatisticsCallback: { statistics in
383
486
  guard let statistics = statistics else { return }
384
-
487
+
385
488
  let timeInMilliseconds = statistics.getTime()
386
489
  if timeInMilliseconds > 0 {
387
490
  let completePercentage = timeInMilliseconds / (videoDuration * 1000); // from 0 -> 1
388
491
  DispatchQueue.main.async {
389
- progressView.setProgress(Float(completePercentage), animated: true)
492
+ progressAlert.setProgress(Float(completePercentage))
390
493
  }
391
494
  }
392
495
 
@@ -403,4 +506,196 @@ class VideoTrim: RCTEventEmitter {
403
506
  self.emitEventToJS("onStatistics", eventData: eventPayload)
404
507
  })
405
508
  }
509
+
510
+ func assetLoader(_ loader: AssetLoader, didFailWithError error: any Error, forKey key: String) {
511
+ let message = "Failed to load \(key): \(error.localizedDescription)"
512
+ print(message)
513
+
514
+ self.onError(message: message, code: .failToLoadMedia)
515
+ vc?.onAssetFailToLoad()
516
+
517
+ if alertOnFailToLoad {
518
+ let dialogMessage = UIAlertController(title: alertOnFailTitle, message: alertOnFailMessage, preferredStyle: .alert)
519
+ dialogMessage.overrideUserInterfaceStyle = .dark
520
+
521
+ // Create Cancel button with action handlder
522
+ let ok = UIAlertAction(title: alertOnFailCloseText, style: .default)
523
+
524
+ //Add OK and Cancel button to an Alert object
525
+ dialogMessage.addAction(ok)
526
+
527
+ // Present alert message to user
528
+ if let root = RCTPresentedViewController() {
529
+ root.present(dialogMessage, animated: true, completion: nil)
530
+ }
531
+ }
532
+ }
533
+
534
+ func assetLoaderDidSucceed(_ loader: AssetLoader) {
535
+ print("Asset loaded successfully")
536
+
537
+ vc?.asset = loader.asset
538
+
539
+ let eventPayload: [String: Any] = [
540
+ "duration": loader.asset!.duration.seconds * 1000,
541
+ ]
542
+ self.emitEventToJS("onLoad", eventData: eventPayload)
543
+ }
544
+
545
+
546
+
547
+ private func saveFileToFilesApp(fileURL: URL) {
548
+ DispatchQueue.main.async {
549
+ let documentPicker = UIDocumentPickerViewController(url: fileURL, in: .exportToService)
550
+ documentPicker.delegate = self
551
+ documentPicker.modalPresentationStyle = .formSheet
552
+ if let root = RCTPresentedViewController() {
553
+ root.present(documentPicker, animated: true, completion: nil)
554
+ }
555
+ }
556
+ }
557
+
558
+ private func shareFile(fileURL: URL) {
559
+ DispatchQueue.main.async {
560
+ // Create an instance of UIActivityViewController
561
+ let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
562
+
563
+ activityViewController.completionWithItemsHandler = { activityType, completed, returnedItems, error in
564
+
565
+ if let error = error {
566
+ let message = "Sharing error: \(error.localizedDescription)"
567
+ print(message)
568
+ self.onError(message: message, code: .failToShare)
569
+
570
+ if self.removeAfterFailedToShare {
571
+ let _ = self.deleteFile(url: self.outputFile!)
572
+ }
573
+ return
574
+ }
575
+
576
+ if completed {
577
+ print("User completed the sharing activity")
578
+ if self.removeAfterShared {
579
+ let _ = self.deleteFile(url: self.outputFile!)
580
+ }
581
+ } else {
582
+ print("User cancelled or failed to complete the sharing activity")
583
+ if self.removeAfterFailedToShare {
584
+ let _ = self.deleteFile(url: self.outputFile!)
585
+ }
586
+ }
587
+
588
+ self.closeEditor()
589
+
590
+ }
591
+
592
+ // Present the share sheet
593
+ if let root = RCTPresentedViewController() {
594
+ root.present(activityViewController, animated: true, completion: nil)
595
+ }
596
+ }
597
+
598
+ }
599
+
600
+ func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
601
+ if removeAfterSavedToDocuments {
602
+ let _ = deleteFile(url: outputFile!)
603
+ }
604
+ closeEditor()
605
+ }
606
+
607
+ func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
608
+ if removeAfterFailedToSaveDocuments {
609
+ let _ = deleteFile(url: outputFile!)
610
+ }
611
+ closeEditor()
612
+ }
613
+
614
+ @objc(closeEditor:withRejecter:)
615
+ func closeEditor(resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
616
+ closeEditor()
617
+ resolve(true)
618
+ }
619
+
620
+ private func closeEditor() {
621
+ guard let vc = vc else { return }
622
+ // some how in case we trim a very short video the view controller is still visible after first .dismiss call
623
+ // even the file is successfully saved
624
+ // that's why we need a small delay here to ensure vc will be dismissed
625
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
626
+ vc.dismiss(animated: true, completion: {
627
+ self.emitEventToJS("onHide", eventData: nil)
628
+ self.isShowing = false
629
+ })
630
+ }
631
+ }
632
+
633
+ @objc(isValidFile:withResolver:withRejecter:)
634
+ func isValidFile(uri: String, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
635
+ let fileURL = URL(string: uri)!
636
+ checkFileValidity(url: fileURL) { isValid, fileType, duration in
637
+ if isValid {
638
+ print("Valid \(fileType) file with duration: \(duration) milliseconds")
639
+ } else {
640
+ print("Invalid file")
641
+ }
642
+
643
+ let payload: [String: Any] = [
644
+ "isValid": isValid,
645
+ "fileType": fileType,
646
+ "duration": duration
647
+ ]
648
+ resolve(payload)
649
+ }
650
+
651
+ }
652
+
653
+ private func onError(message: String, code: ErrorCode) {
654
+ let eventPayload: [String: String] = [
655
+ "message": message,
656
+ "errorCode": code.rawValue
657
+ ]
658
+ self.emitEventToJS("onError", eventData: eventPayload)
659
+ }
660
+
661
+ private func checkFileValidity(url: URL, completion: @escaping (Bool, String, Double) -> Void) {
662
+ let asset = AVAsset(url: url)
663
+
664
+ // Load the duration and tracks asynchronously
665
+ asset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) {
666
+ var error: NSError? = nil
667
+
668
+ // Check if the duration and tracks are loaded
669
+ let durationStatus = asset.statusOfValue(forKey: "duration", error: &error)
670
+ let tracksStatus = asset.statusOfValue(forKey: "tracks", error: &error)
671
+
672
+ // Ensure both properties are loaded successfully
673
+ guard durationStatus == .loaded, tracksStatus == .loaded, error == nil else {
674
+ DispatchQueue.main.async {
675
+ completion(false, "unknown", -1)
676
+ }
677
+ return
678
+ }
679
+
680
+ // Check if the asset contains any video or audio tracks
681
+ let videoTracks = asset.tracks(withMediaType: .video)
682
+ let audioTracks = asset.tracks(withMediaType: .audio)
683
+
684
+ let isValid = !videoTracks.isEmpty || !audioTracks.isEmpty
685
+ let fileType: String
686
+ if !videoTracks.isEmpty {
687
+ fileType = "video"
688
+ } else if !audioTracks.isEmpty {
689
+ fileType = "audio"
690
+ } else {
691
+ fileType = "unknown"
692
+ }
693
+
694
+ let duration = CMTimeGetSeconds(asset.duration) * 1000
695
+
696
+ DispatchQueue.main.async {
697
+ completion(isValid, fileType, isValid ? duration.rounded() : -1)
698
+ }
699
+ }
700
+ }
406
701
  }