react-native-video-trim 4.1.0 → 5.0.1

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