react-native-video-trim 2.0.0 → 2.1.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/README.md +33 -9
- package/android/src/main/AndroidManifest.xml +13 -0
- package/android/src/main/java/com/videotrim/VideoTrimModule.java +185 -64
- package/android/src/main/java/com/videotrim/enums/ErrorCode.java +11 -0
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +2 -1
- package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.java +75 -0
- package/android/src/main/java/com/videotrim/utils/StorageUtil.java +2 -2
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +15 -8
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +239 -70
- package/android/src/main/res/drawable/airpodsmax.xml +19 -0
- package/android/src/main/res/drawable/exclamationmark_triangle_fill.xml +15 -0
- package/android/src/main/res/drawable/thumb_container_bg.xml +8 -0
- package/android/src/main/res/layout/video_trimmer_view.xml +51 -4
- package/android/src/main/res/xml/file_paths.xml +5 -0
- package/ios/AssetLoader.swift +99 -0
- package/ios/ErrorCode.swift +16 -0
- package/ios/VideoTrim.mm +4 -2
- package/ios/VideoTrim.swift +380 -167
- package/ios/VideoTrimmer.swift +16 -10
- package/ios/VideoTrimmerViewController.swift +78 -12
- package/lib/commonjs/index.js +20 -57
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +19 -57
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/index.d.ts +47 -9
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +56 -66
- package/android/src/main/java/iknow/android/utils/BuildConfig.java +0 -18
- package/android/src/main/java/iknow/android/utils/DateUtil.java +0 -64
package/ios/VideoTrim.swift
CHANGED
|
@@ -2,15 +2,45 @@ import React
|
|
|
2
2
|
import Photos
|
|
3
3
|
import ffmpegkit
|
|
4
4
|
|
|
5
|
+
@available(iOS 13.0, *)
|
|
5
6
|
@objc(VideoTrim)
|
|
6
|
-
class VideoTrim: RCTEventEmitter {
|
|
7
|
+
class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDelegate {
|
|
7
8
|
private let FILE_PREFIX = "trimmedVideo"
|
|
8
9
|
private var hasListeners = false
|
|
9
10
|
private var isShowing = false
|
|
10
11
|
|
|
11
|
-
private var saveToPhoto =
|
|
12
|
+
private var saveToPhoto = false
|
|
12
13
|
private var removeAfterSavedToPhoto = false
|
|
14
|
+
private var removeAfterFailedToSavePhoto = false
|
|
15
|
+
private var removeAfterSavedToDocuments = false
|
|
16
|
+
private var removeAfterFailedToSaveDocuments = false
|
|
17
|
+
private var removeAfterShared = false
|
|
18
|
+
private var removeAfterFailedToShare = false
|
|
19
|
+
|
|
13
20
|
private var trimmingText = "Trimming video..."
|
|
21
|
+
private var enableCancelDialog = true
|
|
22
|
+
private var cancelDialogTitle = "Warning!"
|
|
23
|
+
private var cancelDialogMessage = "Are you sure want to cancel?"
|
|
24
|
+
private var cancelDialogCancelText = "Close"
|
|
25
|
+
private var cancelDialogConfirmText = "Proceed"
|
|
26
|
+
private var enableSaveDialog = true
|
|
27
|
+
private var saveDialogTitle = "Confirmation!"
|
|
28
|
+
private var saveDialogMessage = "Are you sure want to save?"
|
|
29
|
+
private var saveDialogCancelText = "Close"
|
|
30
|
+
private var saveDialogConfirmText = "Proceed"
|
|
31
|
+
private var fullScreenModalIOS = false
|
|
32
|
+
private var maxDuration: Int?
|
|
33
|
+
private var minDuration: Int?
|
|
34
|
+
private var cancelButtonText = "Cancel"
|
|
35
|
+
private var saveButtonText = "Save"
|
|
36
|
+
private var vc: VideoTrimmerViewController?
|
|
37
|
+
private var isVideoType = true
|
|
38
|
+
private var outputExt = "mp4"
|
|
39
|
+
private var openDocumentsOnFinish = false
|
|
40
|
+
private var openShareSheetOnFinish = false
|
|
41
|
+
private var outputFile: URL?
|
|
42
|
+
private var enableHapticFeedback = true
|
|
43
|
+
|
|
14
44
|
|
|
15
45
|
@objc
|
|
16
46
|
static override func requiresMainQueueSetup() -> Bool {
|
|
@@ -29,150 +59,152 @@ class VideoTrim: RCTEventEmitter {
|
|
|
29
59
|
hasListeners = false
|
|
30
60
|
}
|
|
31
61
|
|
|
32
|
-
@objc(isValidVideo:withResolver:withRejecter:)
|
|
33
|
-
func isValidVideo(uri: String, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
34
|
-
if let destPath = copyFileToDocumentDir(uri: uri) {
|
|
35
|
-
resolve(UIVideoEditorController.canEditVideo(atPath: destPath.path))
|
|
36
|
-
let _ = deleteFile(url: destPath) // remove the file we just copied to document directory
|
|
37
|
-
} else {
|
|
38
|
-
resolve(false)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
62
|
@objc(showEditor:withConfig:)
|
|
43
63
|
func showEditor(uri: String, config: NSDictionary){
|
|
44
64
|
if isShowing {
|
|
45
65
|
return
|
|
46
66
|
}
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
saveToPhoto = config["saveToPhoto"] as? Bool ?? false
|
|
68
|
+
|
|
49
69
|
removeAfterSavedToPhoto = config["removeAfterSavedToPhoto"] as? Bool ?? false
|
|
70
|
+
removeAfterFailedToSavePhoto = config["removeAfterFailedToSavePhoto"] as? Bool ?? false
|
|
71
|
+
removeAfterSavedToDocuments = config["removeAfterSavedToDocuments"] as? Bool ?? false
|
|
72
|
+
removeAfterFailedToSaveDocuments = config["removeAfterFailedToSaveDocuments"] as? Bool ?? false
|
|
73
|
+
removeAfterShared = config["removeAfterShared"] as? Bool ?? false
|
|
74
|
+
removeAfterFailedToShare = config["removeAfterFailedToShare"] as? Bool ?? false
|
|
50
75
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
let enableSaveDialog = config["enableSaveDialog"] as? Bool ?? true
|
|
58
|
-
let saveDialogTitle = config["saveDialogTitle"] as? String ?? "Confirmation!"
|
|
59
|
-
let saveDialogMessage = config["saveDialogMessage"] as? String ?? "Are you sure want to save?"
|
|
60
|
-
let saveDialogCancelText = config["saveDialogCancelText"] as? String ?? "Close"
|
|
61
|
-
let saveDialogConfirmText = config["saveDialogConfirmText"] as? String ?? "Proceed"
|
|
62
|
-
trimmingText = config["trimmingText"] as? String ?? "Trimming video..."
|
|
63
|
-
let fullScreenModalIOS = config["fullScreenModalIOS"] as? Bool ?? false
|
|
76
|
+
enableCancelDialog = config["enableCancelDialog"] as? Bool ?? true
|
|
77
|
+
cancelDialogTitle = config["cancelDialogTitle"] as? String ?? "Warning!"
|
|
78
|
+
cancelDialogMessage = config["cancelDialogMessage"] as? String ?? "Are you sure want to cancel?"
|
|
79
|
+
cancelDialogCancelText = config["cancelDialogCancelText"] as? String ?? "Close"
|
|
80
|
+
cancelDialogConfirmText = config["cancelDialogConfirmText"] as? String ?? "Proceed"
|
|
64
81
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
vc.minimumDuration = minDuration
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if let cancelBtnText = config["cancelButtonText"] as? String, !cancelBtnText.isEmpty {
|
|
81
|
-
vc.cancelBtnText = cancelBtnText
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if let saveButtonText = config["saveButtonText"] as? String, !saveButtonText.isEmpty {
|
|
85
|
-
vc.saveButtonText = saveButtonText
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
vc.cancelBtnClicked = {
|
|
89
|
-
if !enableCancelDialog {
|
|
90
|
-
let _ = self.deleteFile(url: destPath) // remove the file we just copied to document directory
|
|
91
|
-
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
92
|
-
|
|
93
|
-
vc.dismiss(animated: true, completion: {
|
|
94
|
-
self.emitEventToJS("onHide", eventData: nil)
|
|
95
|
-
self.isShowing = false
|
|
96
|
-
})
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Create Alert
|
|
101
|
-
let dialogMessage = UIAlertController(title: cancelDialogTitle, message: cancelDialogMessage, preferredStyle: .alert)
|
|
102
|
-
|
|
103
|
-
// Create OK button with action handler
|
|
104
|
-
let ok = UIAlertAction(title: cancelDialogConfirmText, style: .destructive, handler: { (action) -> Void in
|
|
105
|
-
let _ = self.deleteFile(url: destPath) // remove the file we just copied to document directory
|
|
106
|
-
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
107
|
-
|
|
108
|
-
vc.dismiss(animated: true, completion: {
|
|
109
|
-
self.emitEventToJS("onHide", eventData: nil)
|
|
110
|
-
self.isShowing = false
|
|
111
|
-
})
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
// Create Cancel button with action handlder
|
|
115
|
-
let cancel = UIAlertAction(title: cancelDialogCancelText, style: .cancel)
|
|
116
|
-
|
|
117
|
-
//Add OK and Cancel button to an Alert object
|
|
118
|
-
dialogMessage.addAction(ok)
|
|
119
|
-
dialogMessage.addAction(cancel)
|
|
120
|
-
|
|
121
|
-
// Present alert message to user
|
|
122
|
-
if let root = RCTPresentedViewController() {
|
|
123
|
-
root.present(dialogMessage, animated: true, completion: nil)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
vc.saveBtnClicked = {(selectedRange: CMTimeRange) in
|
|
128
|
-
if !enableSaveDialog {
|
|
129
|
-
self.trim(viewController: vc,inputFile: destPath, videoDuration: vc.asset.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Create Alert
|
|
134
|
-
let dialogMessage = UIAlertController(title: saveDialogTitle, message: saveDialogMessage, preferredStyle: .alert)
|
|
135
|
-
|
|
136
|
-
// Create OK button with action handler
|
|
137
|
-
let ok = UIAlertAction(title: saveDialogConfirmText, style: .default, handler: { (action) -> Void in
|
|
138
|
-
self.trim(viewController: vc,inputFile: destPath, videoDuration: vc.asset.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
// Create Cancel button with action handlder
|
|
142
|
-
let cancel = UIAlertAction(title: saveDialogCancelText, style: .cancel)
|
|
82
|
+
enableSaveDialog = config["enableSaveDialog"] as? Bool ?? true
|
|
83
|
+
saveDialogTitle = config["saveDialogTitle"] as? String ?? "Confirmation!"
|
|
84
|
+
saveDialogMessage = config["saveDialogMessage"] as? String ?? "Are you sure want to save?"
|
|
85
|
+
saveDialogCancelText = config["saveDialogCancelText"] as? String ?? "Close"
|
|
86
|
+
saveDialogConfirmText = config["saveDialogConfirmText"] as? String ?? "Proceed"
|
|
87
|
+
trimmingText = config["trimmingText"] as? String ?? "Trimming video..."
|
|
88
|
+
fullScreenModalIOS = config["fullScreenModalIOS"] as? Bool ?? false
|
|
89
|
+
isVideoType = (config["type"] as? String ?? "video") == "video"
|
|
90
|
+
outputExt = config["outputExt"] as? String ?? "mp4"
|
|
91
|
+
openDocumentsOnFinish = config["openDocumentsOnFinish"] as? Bool ?? false
|
|
92
|
+
openShareSheetOnFinish = config["openShareSheetOnFinish"] as? Bool ?? false
|
|
93
|
+
enableHapticFeedback = config["enableHapticFeedback"] as? Bool ?? true
|
|
143
94
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
95
|
+
if let maxDuration = config["maxDuration"] as? Int {
|
|
96
|
+
self.maxDuration = maxDuration
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if let minDuration = config["minDuration"] as? Int {
|
|
100
|
+
self.minDuration = minDuration
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if let cancelBtnText = config["cancelButtonText"] as? String, !cancelBtnText.isEmpty {
|
|
104
|
+
self.cancelButtonText = cancelBtnText
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if let saveButtonText = config["saveButtonText"] as? String, !saveButtonText.isEmpty {
|
|
108
|
+
self.saveButtonText = saveButtonText
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let destPath = URL(string: uri)
|
|
112
|
+
guard let destPath = destPath else { return }
|
|
113
|
+
let assetLoader = AssetLoader()
|
|
114
|
+
assetLoader.delegate = self
|
|
115
|
+
assetLoader.loadAsset(url: destPath, isVideoType: isVideoType)
|
|
116
|
+
|
|
117
|
+
DispatchQueue.main.async {
|
|
118
|
+
self.vc = VideoTrimmerViewController()
|
|
119
|
+
|
|
120
|
+
guard let vc = self.vc else { return }
|
|
121
|
+
|
|
122
|
+
vc.maximumDuration = self.maxDuration
|
|
123
|
+
vc.minimumDuration = self.minDuration
|
|
124
|
+
vc.cancelBtnText = self.cancelButtonText
|
|
125
|
+
vc.saveButtonText = self.saveButtonText
|
|
126
|
+
vc.isVideoType = self.isVideoType
|
|
127
|
+
vc.enableHapticFeedback = self.enableHapticFeedback
|
|
147
128
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
129
|
+
|
|
130
|
+
vc.cancelBtnClicked = {
|
|
131
|
+
if !self.enableCancelDialog {
|
|
132
|
+
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
133
|
+
|
|
134
|
+
vc.dismiss(animated: true, completion: {
|
|
135
|
+
self.emitEventToJS("onHide", eventData: nil)
|
|
136
|
+
self.isShowing = false
|
|
137
|
+
})
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Create Alert
|
|
142
|
+
let dialogMessage = UIAlertController(title: self.cancelDialogTitle, message: self.cancelDialogMessage, preferredStyle: .alert)
|
|
143
|
+
|
|
144
|
+
// Create OK button with action handler
|
|
145
|
+
let ok = UIAlertAction(title: self.cancelDialogConfirmText, style: .destructive, handler: { (action) -> Void in
|
|
146
|
+
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
147
|
+
|
|
148
|
+
vc.dismiss(animated: true, completion: {
|
|
149
|
+
self.emitEventToJS("onHide", eventData: nil)
|
|
150
|
+
self.isShowing = false
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Create Cancel button with action handlder
|
|
155
|
+
let cancel = UIAlertAction(title: self.cancelDialogCancelText, style: .cancel)
|
|
156
|
+
|
|
157
|
+
//Add OK and Cancel button to an Alert object
|
|
158
|
+
dialogMessage.addAction(ok)
|
|
159
|
+
dialogMessage.addAction(cancel)
|
|
160
|
+
|
|
161
|
+
// Present alert message to user
|
|
162
|
+
if let root = RCTPresentedViewController() {
|
|
163
|
+
root.present(dialogMessage, animated: true, completion: nil)
|
|
167
164
|
}
|
|
168
|
-
} else {
|
|
169
|
-
let eventPayload: [String: Any] = ["message": "File is not a valid video"]
|
|
170
|
-
self.emitEventToJS("onError", eventData: eventPayload)
|
|
171
165
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
166
|
+
|
|
167
|
+
vc.saveBtnClicked = {(selectedRange: CMTimeRange) in
|
|
168
|
+
if !self.enableSaveDialog {
|
|
169
|
+
self.trim(viewController: vc,inputFile: destPath, videoDuration: self.vc!.asset!.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Create Alert
|
|
174
|
+
let dialogMessage = UIAlertController(title: self.saveDialogTitle, message: self.saveDialogMessage, preferredStyle: .alert)
|
|
175
|
+
|
|
176
|
+
// Create OK button with action handler
|
|
177
|
+
let ok = UIAlertAction(title: self.saveDialogConfirmText, style: .default, handler: { (action) -> Void in
|
|
178
|
+
self.trim(viewController: vc,inputFile: destPath, videoDuration: vc.asset!.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// Create Cancel button with action handlder
|
|
182
|
+
let cancel = UIAlertAction(title: self.saveDialogCancelText, style: .cancel)
|
|
183
|
+
|
|
184
|
+
//Add OK and Cancel button to an Alert object
|
|
185
|
+
dialogMessage.addAction(ok)
|
|
186
|
+
dialogMessage.addAction(cancel)
|
|
187
|
+
|
|
188
|
+
// Present alert message to user
|
|
189
|
+
if let root = RCTPresentedViewController() {
|
|
190
|
+
root.present(dialogMessage, animated: true, completion: nil)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
vc.isModalInPresentation = true // prevent modal closed by swipe down
|
|
195
|
+
|
|
196
|
+
if self.fullScreenModalIOS {
|
|
197
|
+
vc.modalPresentationStyle = .fullScreen
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if let root = RCTPresentedViewController() {
|
|
201
|
+
root.present(vc, animated: true, completion: {
|
|
202
|
+
self.emitEventToJS("onShow", eventData: nil)
|
|
203
|
+
self.isShowing = true
|
|
204
|
+
})
|
|
205
|
+
}
|
|
175
206
|
}
|
|
207
|
+
|
|
176
208
|
}
|
|
177
209
|
|
|
178
210
|
private func copyFileToDocumentDir(uri: String) -> URL? {
|
|
@@ -270,11 +302,11 @@ class VideoTrim: RCTEventEmitter {
|
|
|
270
302
|
}
|
|
271
303
|
}
|
|
272
304
|
|
|
273
|
-
@available(iOS 13.0, *)
|
|
274
305
|
private func trim(viewController: VideoTrimmerViewController, inputFile: URL, videoDuration: Double, startTime: Double, endTime: Double) {
|
|
275
306
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
276
|
-
let outputName = "\(FILE_PREFIX)_\(timestamp)
|
|
277
|
-
let
|
|
307
|
+
let outputName = "\(FILE_PREFIX)_\(timestamp).\(outputExt)"
|
|
308
|
+
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
309
|
+
outputFile = documentsDirectory.appendingPathComponent(outputName)
|
|
278
310
|
|
|
279
311
|
let formatter = DateFormatter()
|
|
280
312
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"
|
|
@@ -284,20 +316,20 @@ class VideoTrim: RCTEventEmitter {
|
|
|
284
316
|
self.emitEventToJS("onStartTrimming", eventData: nil)
|
|
285
317
|
|
|
286
318
|
// Create Alert
|
|
287
|
-
let
|
|
288
|
-
|
|
319
|
+
let progressDialog = UIAlertController(title: trimmingText, message: nil, preferredStyle: .alert)
|
|
320
|
+
|
|
289
321
|
// Present alert message to user
|
|
290
322
|
let progressView = UIProgressView(frame: .zero)
|
|
291
323
|
progressView.tintColor = .systemBlue
|
|
292
324
|
if let root = RCTPresentedViewController() {
|
|
293
|
-
root.present(
|
|
294
|
-
|
|
325
|
+
root.present(progressDialog, animated: true, completion: {
|
|
326
|
+
progressDialog.view.addSubview(progressView)
|
|
295
327
|
|
|
296
328
|
progressView.translatesAutoresizingMaskIntoConstraints = false
|
|
297
329
|
NSLayoutConstraint.activate([
|
|
298
|
-
progressView.leadingAnchor.constraint(equalTo:
|
|
299
|
-
progressView.trailingAnchor.constraint(equalTo:
|
|
300
|
-
progressView.bottomAnchor.constraint(equalTo:
|
|
330
|
+
progressView.leadingAnchor.constraint(equalTo: progressDialog.view.leadingAnchor, constant: 8),
|
|
331
|
+
progressView.trailingAnchor.constraint(equalTo: progressDialog.view.trailingAnchor, constant: -8),
|
|
332
|
+
progressView.bottomAnchor.constraint(equalTo: progressDialog.view.bottomAnchor, constant: -8)
|
|
301
333
|
])
|
|
302
334
|
})
|
|
303
335
|
}
|
|
@@ -313,60 +345,70 @@ class VideoTrim: RCTEventEmitter {
|
|
|
313
345
|
"copy",
|
|
314
346
|
"-metadata",
|
|
315
347
|
"creation_time=\(dateTime)",
|
|
316
|
-
outputFile
|
|
348
|
+
outputFile!.absoluteString
|
|
317
349
|
]
|
|
318
350
|
|
|
351
|
+
print("Command: ", cmds.joined(separator: " "))
|
|
352
|
+
|
|
353
|
+
let eventPayload: [String: Any] = [
|
|
354
|
+
"command": cmds.joined(separator: " ")
|
|
355
|
+
]
|
|
356
|
+
self.emitEventToJS("onLog", eventData: eventPayload)
|
|
357
|
+
|
|
319
358
|
FFmpegKit.execute(withArgumentsAsync: cmds, withCompleteCallback: { session in
|
|
320
|
-
|
|
359
|
+
DispatchQueue.main.async {
|
|
360
|
+
progressDialog.dismiss(animated: true)
|
|
361
|
+
}
|
|
321
362
|
|
|
322
363
|
let state = session?.getState()
|
|
323
364
|
let returnCode = session?.getReturnCode()
|
|
324
|
-
|
|
365
|
+
|
|
325
366
|
if ReturnCode.isSuccess(returnCode) {
|
|
326
|
-
let eventPayload: [String: Any] = ["outputPath": outputFile, "startTime": startTime, "endTime": endTime, "duration": videoDuration]
|
|
367
|
+
let eventPayload: [String: Any] = ["outputPath": self.outputFile!.absoluteString, "startTime": (startTime * 1000).rounded(), "endTime": (endTime * 1000).rounded(), "duration": (videoDuration * 1000).rounded()]
|
|
327
368
|
self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
|
|
328
369
|
|
|
329
|
-
if (self.saveToPhoto) {
|
|
370
|
+
if (self.saveToPhoto && self.isVideoType) {
|
|
330
371
|
PHPhotoLibrary.requestAuthorization { status in
|
|
331
372
|
guard status == .authorized else {
|
|
332
|
-
|
|
333
|
-
self.emitEventToJS("onError", eventData: eventPayload)
|
|
373
|
+
self.onError(message: "Permission to access Photo Library is not granted", code: .noPhotoPermission)
|
|
334
374
|
return
|
|
335
375
|
}
|
|
336
376
|
|
|
337
377
|
PHPhotoLibrary.shared().performChanges({
|
|
338
|
-
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL:
|
|
378
|
+
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: self.outputFile!)
|
|
339
379
|
request?.creationDate = Date()
|
|
340
380
|
}) { success, error in
|
|
341
381
|
if success {
|
|
342
382
|
print("Edited video saved to Photo Library successfully.")
|
|
343
383
|
|
|
344
384
|
if self.removeAfterSavedToPhoto {
|
|
345
|
-
let _ = self.deleteFile(url:
|
|
385
|
+
let _ = self.deleteFile(url: self.outputFile!)
|
|
346
386
|
}
|
|
347
387
|
} else {
|
|
348
|
-
|
|
349
|
-
self.
|
|
388
|
+
self.onError(message: "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")", code: .failToSaveToPhoto)
|
|
389
|
+
if self.removeAfterFailedToSavePhoto {
|
|
390
|
+
let _ = self.deleteFile(url: self.outputFile!)
|
|
391
|
+
}
|
|
350
392
|
}
|
|
351
393
|
}
|
|
352
394
|
}
|
|
395
|
+
} else if self.openDocumentsOnFinish {
|
|
396
|
+
self.saveFileToFilesApp(fileURL: self.outputFile!)
|
|
397
|
+
|
|
398
|
+
// must return otherwise editor will close
|
|
399
|
+
return
|
|
400
|
+
} else if self.openShareSheetOnFinish {
|
|
401
|
+
self.shareFile(fileURL: self.outputFile!)
|
|
402
|
+
|
|
403
|
+
// must return otherwise editor will close
|
|
404
|
+
return
|
|
353
405
|
}
|
|
354
406
|
} else {
|
|
355
407
|
// CANCEL + FAILURE
|
|
356
|
-
|
|
357
|
-
self.emitEventToJS("onError", eventData: eventPayload)
|
|
408
|
+
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)
|
|
358
409
|
}
|
|
359
410
|
|
|
360
|
-
|
|
361
|
-
// even the file is successfully saved
|
|
362
|
-
// that's why we need a small delay here to ensure vc will be dismissed
|
|
363
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
364
|
-
dialogMessage.dismiss(animated: false)
|
|
365
|
-
viewController.dismiss(animated: true, completion: {
|
|
366
|
-
self.emitEventToJS("onHide", eventData: nil)
|
|
367
|
-
self.isShowing = false
|
|
368
|
-
})
|
|
369
|
-
}
|
|
411
|
+
self.closeEditor()
|
|
370
412
|
}, withLogCallback: { log in
|
|
371
413
|
guard let log = log else { return }
|
|
372
414
|
|
|
@@ -381,7 +423,7 @@ class VideoTrim: RCTEventEmitter {
|
|
|
381
423
|
|
|
382
424
|
}, withStatisticsCallback: { statistics in
|
|
383
425
|
guard let statistics = statistics else { return }
|
|
384
|
-
|
|
426
|
+
|
|
385
427
|
let timeInMilliseconds = statistics.getTime()
|
|
386
428
|
if timeInMilliseconds > 0 {
|
|
387
429
|
let completePercentage = timeInMilliseconds / (videoDuration * 1000); // from 0 -> 1
|
|
@@ -403,4 +445,175 @@ class VideoTrim: RCTEventEmitter {
|
|
|
403
445
|
self.emitEventToJS("onStatistics", eventData: eventPayload)
|
|
404
446
|
})
|
|
405
447
|
}
|
|
448
|
+
|
|
449
|
+
func assetLoader(_ loader: AssetLoader, didFailWithError error: any Error, forKey key: String) {
|
|
450
|
+
let message = "Failed to load \(key): \(error.localizedDescription)"
|
|
451
|
+
print(message)
|
|
452
|
+
|
|
453
|
+
self.onError(message: message, code: .failToLoadVideo)
|
|
454
|
+
vc?.onAssetFailToLoad()
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
func assetLoaderDidSucceed(_ loader: AssetLoader) {
|
|
458
|
+
print("Asset loaded successfully")
|
|
459
|
+
|
|
460
|
+
vc?.asset = loader.asset
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
private func saveFileToFilesApp(fileURL: URL) {
|
|
466
|
+
DispatchQueue.main.async {
|
|
467
|
+
let documentPicker = UIDocumentPickerViewController(url: fileURL, in: .exportToService)
|
|
468
|
+
documentPicker.delegate = self
|
|
469
|
+
documentPicker.modalPresentationStyle = .formSheet
|
|
470
|
+
if let root = RCTPresentedViewController() {
|
|
471
|
+
root.present(documentPicker, animated: true, completion: nil)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private func shareFile(fileURL: URL) {
|
|
477
|
+
DispatchQueue.main.async {
|
|
478
|
+
// Create an instance of UIActivityViewController
|
|
479
|
+
let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
|
|
480
|
+
|
|
481
|
+
activityViewController.completionWithItemsHandler = { activityType, completed, returnedItems, error in
|
|
482
|
+
|
|
483
|
+
if let error = error {
|
|
484
|
+
let message = "Sharing error: \(error.localizedDescription)"
|
|
485
|
+
print(message)
|
|
486
|
+
self.onError(message: message, code: .failToShare)
|
|
487
|
+
|
|
488
|
+
if self.removeAfterFailedToShare {
|
|
489
|
+
let _ = self.deleteFile(url: self.outputFile!)
|
|
490
|
+
}
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if completed {
|
|
495
|
+
print("User completed the sharing activity")
|
|
496
|
+
if self.removeAfterShared {
|
|
497
|
+
let _ = self.deleteFile(url: self.outputFile!)
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
print("User cancelled or failed to complete the sharing activity")
|
|
501
|
+
if self.removeAfterFailedToShare {
|
|
502
|
+
let _ = self.deleteFile(url: self.outputFile!)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
self.closeEditor()
|
|
507
|
+
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Present the share sheet
|
|
511
|
+
if let root = RCTPresentedViewController() {
|
|
512
|
+
root.present(activityViewController, animated: true, completion: nil)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
|
519
|
+
if removeAfterSavedToDocuments {
|
|
520
|
+
let _ = deleteFile(url: outputFile!)
|
|
521
|
+
}
|
|
522
|
+
closeEditor()
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
|
526
|
+
if removeAfterFailedToSaveDocuments {
|
|
527
|
+
let _ = deleteFile(url: outputFile!)
|
|
528
|
+
}
|
|
529
|
+
closeEditor()
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
@objc(closeEditor:withRejecter:)
|
|
533
|
+
func closeEditor(resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
534
|
+
closeEditor()
|
|
535
|
+
resolve(true)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private func closeEditor() {
|
|
539
|
+
guard let vc = vc else { return }
|
|
540
|
+
// some how in case we trim a very short video the view controller is still visible after first .dismiss call
|
|
541
|
+
// even the file is successfully saved
|
|
542
|
+
// that's why we need a small delay here to ensure vc will be dismissed
|
|
543
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
544
|
+
vc.dismiss(animated: true, completion: {
|
|
545
|
+
self.emitEventToJS("onHide", eventData: nil)
|
|
546
|
+
self.isShowing = false
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
@objc(isValidFile:withResolver:withRejecter:)
|
|
552
|
+
func isValidFile(uri: String, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
553
|
+
let fileURL = URL(string: uri)!
|
|
554
|
+
checkFileValidity(url: fileURL) { isValid, fileType, duration in
|
|
555
|
+
if isValid {
|
|
556
|
+
print("Valid \(fileType) file with duration: \(duration) milliseconds")
|
|
557
|
+
} else {
|
|
558
|
+
print("Invalid file")
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let payload: [String: Any] = [
|
|
562
|
+
"isValid": isValid,
|
|
563
|
+
"fileType": fileType,
|
|
564
|
+
"duration": duration
|
|
565
|
+
]
|
|
566
|
+
resolve(payload)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private func onError(message: String, code: ErrorCode) {
|
|
572
|
+
let eventPayload: [String: String] = [
|
|
573
|
+
"message": message,
|
|
574
|
+
"errorCode": code.rawValue
|
|
575
|
+
]
|
|
576
|
+
self.emitEventToJS("onError", eventData: eventPayload)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private func checkFileValidity(url: URL, completion: @escaping (Bool, String, Double) -> Void) {
|
|
580
|
+
let asset = AVAsset(url: url)
|
|
581
|
+
|
|
582
|
+
// Load the duration and tracks asynchronously
|
|
583
|
+
asset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) {
|
|
584
|
+
var error: NSError? = nil
|
|
585
|
+
|
|
586
|
+
// Check if the duration and tracks are loaded
|
|
587
|
+
let durationStatus = asset.statusOfValue(forKey: "duration", error: &error)
|
|
588
|
+
let tracksStatus = asset.statusOfValue(forKey: "tracks", error: &error)
|
|
589
|
+
|
|
590
|
+
// Ensure both properties are loaded successfully
|
|
591
|
+
guard durationStatus == .loaded, tracksStatus == .loaded, error == nil else {
|
|
592
|
+
DispatchQueue.main.async {
|
|
593
|
+
completion(false, "unknown", -1)
|
|
594
|
+
}
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Check if the asset contains any video or audio tracks
|
|
599
|
+
let videoTracks = asset.tracks(withMediaType: .video)
|
|
600
|
+
let audioTracks = asset.tracks(withMediaType: .audio)
|
|
601
|
+
|
|
602
|
+
let isValid = !videoTracks.isEmpty || !audioTracks.isEmpty
|
|
603
|
+
let fileType: String
|
|
604
|
+
if !videoTracks.isEmpty {
|
|
605
|
+
fileType = "video"
|
|
606
|
+
} else if !audioTracks.isEmpty {
|
|
607
|
+
fileType = "audio"
|
|
608
|
+
} else {
|
|
609
|
+
fileType = "unknown"
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
let duration = CMTimeGetSeconds(asset.duration) * 1000
|
|
613
|
+
|
|
614
|
+
DispatchQueue.main.async {
|
|
615
|
+
completion(isValid, fileType, isValid ? duration.rounded() : -1)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
406
619
|
}
|