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