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