react-native-video-trim 3.0.8 → 3.0.10

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