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