react-native-video-trim 2.0.0 → 2.2.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 +168 -33
- package/android/src/main/AndroidManifest.xml +13 -0
- package/android/src/main/java/com/videotrim/VideoTrimModule.java +282 -75
- package/android/src/main/java/com/videotrim/enums/ErrorCode.java +10 -0
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +4 -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 +26 -16
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +310 -81
- 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 +71 -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/ProgressAlertController.swift +100 -0
- package/ios/VideoTrim.mm +4 -2
- package/ios/VideoTrim.swift +472 -177
- package/ios/VideoTrimmer.swift +16 -10
- package/ios/VideoTrimmerViewController.swift +191 -22
- package/lib/commonjs/index.js +25 -55
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +24 -55
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/index.d.ts +215 -9
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +229 -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,53 @@ 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 cancelButtonText = "Cancel"
|
|
33
|
+
private var saveButtonText = "Save"
|
|
34
|
+
private var vc: VideoTrimmerViewController?
|
|
35
|
+
private var isVideoType = true
|
|
36
|
+
private var outputExt = "mp4"
|
|
37
|
+
private var openDocumentsOnFinish = false
|
|
38
|
+
private var openShareSheetOnFinish = false
|
|
39
|
+
private var outputFile: URL?
|
|
40
|
+
private var closeWhenFinish = true
|
|
41
|
+
private var enableCancelTrimming = true;
|
|
42
|
+
private var cancelTrimmingButtonText = "Cancel";
|
|
43
|
+
private var enableCancelTrimmingDialog = true;
|
|
44
|
+
private var cancelTrimmingDialogTitle = "Warning!";
|
|
45
|
+
private var cancelTrimmingDialogMessage = "Are you sure want to trimming?";
|
|
46
|
+
private var cancelTrimmingDialogCancelText = "Close";
|
|
47
|
+
private var cancelTrimmingDialogConfirmText = "Proceed";
|
|
48
|
+
private var alertOnFailToLoad = true;
|
|
49
|
+
private var alertOnFailTitle = "Error";
|
|
50
|
+
private var alertOnFailMessage = "Fail to load media. Possibly invalid file or no network connection";
|
|
51
|
+
private var alertOnFailCloseText = "Close";
|
|
14
52
|
|
|
15
53
|
@objc
|
|
16
54
|
static override func requiresMainQueueSetup() -> Bool {
|
|
@@ -29,150 +67,155 @@ class VideoTrim: RCTEventEmitter {
|
|
|
29
67
|
hasListeners = false
|
|
30
68
|
}
|
|
31
69
|
|
|
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
70
|
@objc(showEditor:withConfig:)
|
|
43
71
|
func showEditor(uri: String, config: NSDictionary){
|
|
44
72
|
if isShowing {
|
|
45
73
|
return
|
|
46
74
|
}
|
|
47
|
-
|
|
48
|
-
|
|
75
|
+
saveToPhoto = config["saveToPhoto"] as? Bool ?? false
|
|
76
|
+
|
|
49
77
|
removeAfterSavedToPhoto = config["removeAfterSavedToPhoto"] as? Bool ?? false
|
|
78
|
+
removeAfterFailedToSavePhoto = config["removeAfterFailedToSavePhoto"] as? Bool ?? false
|
|
79
|
+
removeAfterSavedToDocuments = config["removeAfterSavedToDocuments"] as? Bool ?? false
|
|
80
|
+
removeAfterFailedToSaveDocuments = config["removeAfterFailedToSaveDocuments"] as? Bool ?? false
|
|
81
|
+
removeAfterShared = config["removeAfterShared"] as? Bool ?? false
|
|
82
|
+
removeAfterFailedToShare = config["removeAfterFailedToShare"] as? Bool ?? false
|
|
50
83
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
84
|
+
enableCancelDialog = config["enableCancelDialog"] as? Bool ?? true
|
|
85
|
+
cancelDialogTitle = config["cancelDialogTitle"] as? String ?? "Warning!"
|
|
86
|
+
cancelDialogMessage = config["cancelDialogMessage"] as? String ?? "Are you sure want to cancel?"
|
|
87
|
+
cancelDialogCancelText = config["cancelDialogCancelText"] as? String ?? "Close"
|
|
88
|
+
cancelDialogConfirmText = config["cancelDialogConfirmText"] as? String ?? "Proceed"
|
|
89
|
+
|
|
90
|
+
enableSaveDialog = config["enableSaveDialog"] as? Bool ?? true
|
|
91
|
+
saveDialogTitle = config["saveDialogTitle"] as? String ?? "Confirmation!"
|
|
92
|
+
saveDialogMessage = config["saveDialogMessage"] as? String ?? "Are you sure want to save?"
|
|
93
|
+
saveDialogCancelText = config["saveDialogCancelText"] as? String ?? "Close"
|
|
94
|
+
saveDialogConfirmText = config["saveDialogConfirmText"] as? String ?? "Proceed"
|
|
62
95
|
trimmingText = config["trimmingText"] as? String ?? "Trimming video..."
|
|
63
|
-
|
|
96
|
+
fullScreenModalIOS = config["fullScreenModalIOS"] as? Bool ?? false
|
|
97
|
+
isVideoType = (config["type"] as? String ?? "video") == "video"
|
|
98
|
+
outputExt = config["outputExt"] as? String ?? "mp4"
|
|
99
|
+
openDocumentsOnFinish = config["openDocumentsOnFinish"] as? Bool ?? false
|
|
100
|
+
openShareSheetOnFinish = config["openShareSheetOnFinish"] as? Bool ?? false
|
|
64
101
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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)
|
|
102
|
+
closeWhenFinish = config["closeWhenFinish"] as? Bool ?? true
|
|
103
|
+
enableCancelTrimming = config["enableCancelTrimming"] as? Bool ?? true
|
|
104
|
+
cancelTrimmingButtonText = config["cancelTrimmingButtonText"] as? String ?? "Cancel"
|
|
105
|
+
enableCancelTrimmingDialog = config["enableCancelTrimmingDialog"] as? Bool ?? true
|
|
106
|
+
cancelTrimmingDialogTitle = config["cancelTrimmingDialogTitle"] as? String ?? "Warning!"
|
|
107
|
+
cancelTrimmingDialogMessage = config["cancelTrimmingDialogMessage"] as? String ?? "Are you sure want to cancel trimming?"
|
|
108
|
+
cancelTrimmingDialogCancelText = config["cancelTrimmingDialogCancelText"] as? String ?? "Close"
|
|
109
|
+
cancelTrimmingDialogConfirmText = config["cancelTrimmingDialogConfirmText"] as? String ?? "Proceed"
|
|
110
|
+
alertOnFailToLoad = config["alertOnFailToLoad"] as? Bool ?? true
|
|
111
|
+
alertOnFailTitle = config["alertOnFailTitle"] as? String ?? "Error"
|
|
112
|
+
alertOnFailMessage = config["alertOnFailMessage"] as? String ?? "Fail to load media. Possibly invalid file or no network connection"
|
|
113
|
+
alertOnFailCloseText = config["alertOnFailCloseText"] as? String ?? "Close"
|
|
114
|
+
|
|
115
|
+
if let cancelBtnText = config["cancelButtonText"] as? String, !cancelBtnText.isEmpty {
|
|
116
|
+
self.cancelButtonText = cancelBtnText
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if let saveButtonText = config["saveButtonText"] as? String, !saveButtonText.isEmpty {
|
|
120
|
+
self.saveButtonText = saveButtonText
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let destPath = URL(string: uri)
|
|
124
|
+
guard let destPath = destPath else { return }
|
|
125
|
+
|
|
126
|
+
DispatchQueue.main.async {
|
|
127
|
+
self.vc = VideoTrimmerViewController()
|
|
128
|
+
|
|
129
|
+
guard let vc = self.vc else { return }
|
|
130
|
+
|
|
131
|
+
vc.configure(config: config)
|
|
143
132
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
vc.cancelBtnClicked = {
|
|
134
|
+
if !self.enableCancelDialog {
|
|
135
|
+
self.emitEventToJS("onCancel", eventData: nil)
|
|
136
|
+
|
|
137
|
+
vc.dismiss(animated: true, completion: {
|
|
138
|
+
self.emitEventToJS("onHide", eventData: nil)
|
|
139
|
+
self.isShowing = false
|
|
140
|
+
})
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Create Alert
|
|
145
|
+
let dialogMessage = UIAlertController(title: self.cancelDialogTitle, message: self.cancelDialogMessage, preferredStyle: .alert)
|
|
146
|
+
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
147
|
+
|
|
148
|
+
// Create OK button with action handler
|
|
149
|
+
let ok = UIAlertAction(title: self.cancelDialogConfirmText, style: .destructive, handler: { (action) -> Void in
|
|
150
|
+
self.emitEventToJS("onCancel", eventData: nil)
|
|
151
|
+
|
|
152
|
+
vc.dismiss(animated: true, completion: {
|
|
153
|
+
self.emitEventToJS("onHide", eventData: nil)
|
|
154
|
+
self.isShowing = false
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Create Cancel button with action handlder
|
|
159
|
+
let cancel = UIAlertAction(title: self.cancelDialogCancelText, style: .cancel)
|
|
160
|
+
|
|
161
|
+
//Add OK and Cancel button to an Alert object
|
|
162
|
+
dialogMessage.addAction(ok)
|
|
163
|
+
dialogMessage.addAction(cancel)
|
|
164
|
+
|
|
165
|
+
// Present alert message to user
|
|
166
|
+
if let root = RCTPresentedViewController() {
|
|
167
|
+
root.present(dialogMessage, animated: true, completion: nil)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
vc.saveBtnClicked = {(selectedRange: CMTimeRange) in
|
|
172
|
+
if !self.enableSaveDialog {
|
|
173
|
+
self.trim(viewController: vc,inputFile: destPath, videoDuration: self.vc!.asset!.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Create Alert
|
|
178
|
+
let dialogMessage = UIAlertController(title: self.saveDialogTitle, message: self.saveDialogMessage, preferredStyle: .alert)
|
|
179
|
+
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
147
180
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
self.isShowing = true
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
}
|
|
181
|
+
// Create OK button with action handler
|
|
182
|
+
let ok = UIAlertAction(title: self.saveDialogConfirmText, style: .default, handler: { (action) -> Void in
|
|
183
|
+
self.trim(viewController: vc,inputFile: destPath, videoDuration: vc.asset!.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// Create Cancel button with action handlder
|
|
187
|
+
let cancel = UIAlertAction(title: self.saveDialogCancelText, style: .cancel)
|
|
188
|
+
|
|
189
|
+
//Add OK and Cancel button to an Alert object
|
|
190
|
+
dialogMessage.addAction(ok)
|
|
191
|
+
dialogMessage.addAction(cancel)
|
|
192
|
+
|
|
193
|
+
// Present alert message to user
|
|
194
|
+
if let root = RCTPresentedViewController() {
|
|
195
|
+
root.present(dialogMessage, animated: true, completion: nil)
|
|
167
196
|
}
|
|
168
|
-
} else {
|
|
169
|
-
let eventPayload: [String: Any] = ["message": "File is not a valid video"]
|
|
170
|
-
self.emitEventToJS("onError", eventData: eventPayload)
|
|
171
197
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
198
|
+
|
|
199
|
+
vc.isModalInPresentation = true // prevent modal closed by swipe down
|
|
200
|
+
|
|
201
|
+
if self.fullScreenModalIOS {
|
|
202
|
+
vc.modalPresentationStyle = .fullScreen
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if let root = RCTPresentedViewController() {
|
|
206
|
+
root.present(vc, animated: true, completion: {
|
|
207
|
+
self.emitEventToJS("onShow", eventData: nil)
|
|
208
|
+
self.isShowing = true
|
|
209
|
+
|
|
210
|
+
// start loading asset after view is finished presenting
|
|
211
|
+
// otherwise it may run too fast for local file and autoplay looks weird
|
|
212
|
+
let assetLoader = AssetLoader()
|
|
213
|
+
assetLoader.delegate = self
|
|
214
|
+
assetLoader.loadAsset(url: destPath, isVideoType: self.isVideoType)
|
|
215
|
+
})
|
|
216
|
+
}
|
|
175
217
|
}
|
|
218
|
+
|
|
176
219
|
}
|
|
177
220
|
|
|
178
221
|
private func copyFileToDocumentDir(uri: String) -> URL? {
|
|
@@ -270,36 +313,73 @@ class VideoTrim: RCTEventEmitter {
|
|
|
270
313
|
}
|
|
271
314
|
}
|
|
272
315
|
|
|
273
|
-
@available(iOS 13.0, *)
|
|
274
316
|
private func trim(viewController: VideoTrimmerViewController, inputFile: URL, videoDuration: Double, startTime: Double, endTime: Double) {
|
|
317
|
+
vc?.pausePlayer()
|
|
318
|
+
|
|
275
319
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
276
|
-
let outputName = "\(FILE_PREFIX)_\(timestamp)
|
|
277
|
-
let
|
|
320
|
+
let outputName = "\(FILE_PREFIX)_\(timestamp).\(outputExt)"
|
|
321
|
+
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
322
|
+
outputFile = documentsDirectory.appendingPathComponent(outputName)
|
|
278
323
|
|
|
279
324
|
let formatter = DateFormatter()
|
|
280
325
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"
|
|
281
326
|
formatter.timeZone = TimeZone(identifier: "UTC")
|
|
282
327
|
let dateTime = formatter.string(from: Date())
|
|
283
328
|
|
|
284
|
-
|
|
329
|
+
emitEventToJS("onStartTrimming", eventData: nil)
|
|
330
|
+
|
|
331
|
+
var ffmpegSession: FFmpegSession?
|
|
332
|
+
let progressAlert = ProgressAlertController()
|
|
333
|
+
progressAlert.modalPresentationStyle = .overFullScreen
|
|
334
|
+
progressAlert.modalTransitionStyle = .crossDissolve
|
|
335
|
+
progressAlert.setTitle(trimmingText)
|
|
285
336
|
|
|
286
|
-
|
|
287
|
-
|
|
337
|
+
if enableCancelTrimming {
|
|
338
|
+
progressAlert.setCancelTitle(cancelTrimmingButtonText)
|
|
339
|
+
progressAlert.showCancelBtn()
|
|
340
|
+
progressAlert.onDismiss = {
|
|
341
|
+
if self.enableCancelTrimmingDialog {
|
|
342
|
+
let dialogMessage = UIAlertController(title: self.cancelTrimmingDialogTitle, message: self.cancelTrimmingDialogMessage, preferredStyle: .alert)
|
|
343
|
+
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
288
344
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
345
|
+
// Create OK button with action handler
|
|
346
|
+
let ok = UIAlertAction(title: self.cancelDialogConfirmText, style: .destructive, handler: { (action) -> Void in
|
|
347
|
+
|
|
348
|
+
if let ffmpegSession = ffmpegSession {
|
|
349
|
+
ffmpegSession.cancel()
|
|
350
|
+
} else {
|
|
351
|
+
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
progressAlert.dismiss(animated: true)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
// Create Cancel button with action handlder
|
|
358
|
+
let cancel = UIAlertAction(title: self.cancelDialogCancelText, style: .cancel)
|
|
359
|
+
|
|
360
|
+
//Add OK and Cancel button to an Alert object
|
|
361
|
+
dialogMessage.addAction(ok)
|
|
362
|
+
dialogMessage.addAction(cancel)
|
|
363
|
+
|
|
364
|
+
// Present alert message to user
|
|
365
|
+
if let root = RCTPresentedViewController() {
|
|
366
|
+
root.present(dialogMessage, animated: true, completion: nil)
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
if let ffmpegSession = ffmpegSession {
|
|
370
|
+
ffmpegSession.cancel()
|
|
371
|
+
} else {
|
|
372
|
+
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
progressAlert.dismiss(animated: true)
|
|
376
|
+
}
|
|
295
377
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
])
|
|
302
|
-
})
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if let root = RCTPresentedViewController() {
|
|
382
|
+
root.present(progressAlert, animated: true, completion: nil)
|
|
303
383
|
}
|
|
304
384
|
|
|
305
385
|
let cmds = [
|
|
@@ -313,60 +393,83 @@ class VideoTrim: RCTEventEmitter {
|
|
|
313
393
|
"copy",
|
|
314
394
|
"-metadata",
|
|
315
395
|
"creation_time=\(dateTime)",
|
|
316
|
-
outputFile
|
|
396
|
+
outputFile!.absoluteString
|
|
397
|
+
]
|
|
398
|
+
|
|
399
|
+
print("Command: ", cmds.joined(separator: " "))
|
|
400
|
+
|
|
401
|
+
let eventPayload: [String: Any] = [
|
|
402
|
+
"command": cmds.joined(separator: " ")
|
|
317
403
|
]
|
|
404
|
+
self.emitEventToJS("onLog", eventData: eventPayload)
|
|
318
405
|
|
|
319
|
-
FFmpegKit.execute(withArgumentsAsync: cmds, withCompleteCallback: { session in
|
|
320
|
-
|
|
406
|
+
ffmpegSession = FFmpegKit.execute(withArgumentsAsync: cmds, withCompleteCallback: { session in
|
|
407
|
+
|
|
408
|
+
// always hide progressAlert
|
|
409
|
+
DispatchQueue.main.async {
|
|
410
|
+
progressAlert.dismiss(animated: true)
|
|
411
|
+
}
|
|
321
412
|
|
|
322
413
|
let state = session?.getState()
|
|
323
414
|
let returnCode = session?.getReturnCode()
|
|
324
415
|
|
|
325
416
|
if ReturnCode.isSuccess(returnCode) {
|
|
326
|
-
let eventPayload: [String: Any] = ["outputPath": outputFile, "startTime": startTime, "endTime": endTime, "duration": videoDuration]
|
|
417
|
+
let eventPayload: [String: Any] = ["outputPath": self.outputFile!.absoluteString, "startTime": (startTime * 1000).rounded(), "endTime": (endTime * 1000).rounded(), "duration": (videoDuration * 1000).rounded()]
|
|
327
418
|
self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
|
|
328
419
|
|
|
329
|
-
if (self.saveToPhoto) {
|
|
420
|
+
if (self.saveToPhoto && self.isVideoType) {
|
|
330
421
|
PHPhotoLibrary.requestAuthorization { status in
|
|
331
422
|
guard status == .authorized else {
|
|
332
|
-
|
|
333
|
-
self.emitEventToJS("onError", eventData: eventPayload)
|
|
423
|
+
self.onError(message: "Permission to access Photo Library is not granted", code: .noPhotoPermission)
|
|
334
424
|
return
|
|
335
425
|
}
|
|
336
426
|
|
|
337
427
|
PHPhotoLibrary.shared().performChanges({
|
|
338
|
-
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL:
|
|
428
|
+
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: self.outputFile!)
|
|
339
429
|
request?.creationDate = Date()
|
|
340
430
|
}) { success, error in
|
|
341
431
|
if success {
|
|
342
432
|
print("Edited video saved to Photo Library successfully.")
|
|
343
433
|
|
|
344
434
|
if self.removeAfterSavedToPhoto {
|
|
345
|
-
let _ = self.deleteFile(url:
|
|
435
|
+
let _ = self.deleteFile(url: self.outputFile!)
|
|
346
436
|
}
|
|
347
437
|
} else {
|
|
348
|
-
|
|
349
|
-
self.
|
|
438
|
+
self.onError(message: "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")", code: .failToSaveToPhoto)
|
|
439
|
+
if self.removeAfterFailedToSavePhoto {
|
|
440
|
+
let _ = self.deleteFile(url: self.outputFile!)
|
|
441
|
+
}
|
|
350
442
|
}
|
|
351
443
|
}
|
|
352
444
|
}
|
|
445
|
+
} else if self.openDocumentsOnFinish {
|
|
446
|
+
self.saveFileToFilesApp(fileURL: self.outputFile!)
|
|
447
|
+
|
|
448
|
+
// must return otherwise editor will close
|
|
449
|
+
return
|
|
450
|
+
} else if self.openShareSheetOnFinish {
|
|
451
|
+
self.shareFile(fileURL: self.outputFile!)
|
|
452
|
+
|
|
453
|
+
// must return otherwise editor will close
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if self.closeWhenFinish {
|
|
458
|
+
self.closeEditor()
|
|
353
459
|
}
|
|
460
|
+
|
|
461
|
+
} else if ReturnCode.isCancel(returnCode) {
|
|
462
|
+
// CANCEL
|
|
463
|
+
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
354
464
|
} else {
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
self.
|
|
465
|
+
// FAILURE
|
|
466
|
+
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)
|
|
467
|
+
if self.closeWhenFinish {
|
|
468
|
+
self.closeEditor()
|
|
469
|
+
}
|
|
358
470
|
}
|
|
359
471
|
|
|
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
|
-
}
|
|
472
|
+
|
|
370
473
|
}, withLogCallback: { log in
|
|
371
474
|
guard let log = log else { return }
|
|
372
475
|
|
|
@@ -381,12 +484,12 @@ class VideoTrim: RCTEventEmitter {
|
|
|
381
484
|
|
|
382
485
|
}, withStatisticsCallback: { statistics in
|
|
383
486
|
guard let statistics = statistics else { return }
|
|
384
|
-
|
|
487
|
+
|
|
385
488
|
let timeInMilliseconds = statistics.getTime()
|
|
386
489
|
if timeInMilliseconds > 0 {
|
|
387
490
|
let completePercentage = timeInMilliseconds / (videoDuration * 1000); // from 0 -> 1
|
|
388
491
|
DispatchQueue.main.async {
|
|
389
|
-
|
|
492
|
+
progressAlert.setProgress(Float(completePercentage))
|
|
390
493
|
}
|
|
391
494
|
}
|
|
392
495
|
|
|
@@ -403,4 +506,196 @@ class VideoTrim: RCTEventEmitter {
|
|
|
403
506
|
self.emitEventToJS("onStatistics", eventData: eventPayload)
|
|
404
507
|
})
|
|
405
508
|
}
|
|
509
|
+
|
|
510
|
+
func assetLoader(_ loader: AssetLoader, didFailWithError error: any Error, forKey key: String) {
|
|
511
|
+
let message = "Failed to load \(key): \(error.localizedDescription)"
|
|
512
|
+
print(message)
|
|
513
|
+
|
|
514
|
+
self.onError(message: message, code: .failToLoadMedia)
|
|
515
|
+
vc?.onAssetFailToLoad()
|
|
516
|
+
|
|
517
|
+
if alertOnFailToLoad {
|
|
518
|
+
let dialogMessage = UIAlertController(title: alertOnFailTitle, message: alertOnFailMessage, preferredStyle: .alert)
|
|
519
|
+
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
520
|
+
|
|
521
|
+
// Create Cancel button with action handlder
|
|
522
|
+
let ok = UIAlertAction(title: alertOnFailCloseText, style: .default)
|
|
523
|
+
|
|
524
|
+
//Add OK and Cancel button to an Alert object
|
|
525
|
+
dialogMessage.addAction(ok)
|
|
526
|
+
|
|
527
|
+
// Present alert message to user
|
|
528
|
+
if let root = RCTPresentedViewController() {
|
|
529
|
+
root.present(dialogMessage, animated: true, completion: nil)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
func assetLoaderDidSucceed(_ loader: AssetLoader) {
|
|
535
|
+
print("Asset loaded successfully")
|
|
536
|
+
|
|
537
|
+
vc?.asset = loader.asset
|
|
538
|
+
|
|
539
|
+
let eventPayload: [String: Any] = [
|
|
540
|
+
"duration": loader.asset!.duration.seconds * 1000,
|
|
541
|
+
]
|
|
542
|
+
self.emitEventToJS("onLoad", eventData: eventPayload)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
private func saveFileToFilesApp(fileURL: URL) {
|
|
548
|
+
DispatchQueue.main.async {
|
|
549
|
+
let documentPicker = UIDocumentPickerViewController(url: fileURL, in: .exportToService)
|
|
550
|
+
documentPicker.delegate = self
|
|
551
|
+
documentPicker.modalPresentationStyle = .formSheet
|
|
552
|
+
if let root = RCTPresentedViewController() {
|
|
553
|
+
root.present(documentPicker, animated: true, completion: nil)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private func shareFile(fileURL: URL) {
|
|
559
|
+
DispatchQueue.main.async {
|
|
560
|
+
// Create an instance of UIActivityViewController
|
|
561
|
+
let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
|
|
562
|
+
|
|
563
|
+
activityViewController.completionWithItemsHandler = { activityType, completed, returnedItems, error in
|
|
564
|
+
|
|
565
|
+
if let error = error {
|
|
566
|
+
let message = "Sharing error: \(error.localizedDescription)"
|
|
567
|
+
print(message)
|
|
568
|
+
self.onError(message: message, code: .failToShare)
|
|
569
|
+
|
|
570
|
+
if self.removeAfterFailedToShare {
|
|
571
|
+
let _ = self.deleteFile(url: self.outputFile!)
|
|
572
|
+
}
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if completed {
|
|
577
|
+
print("User completed the sharing activity")
|
|
578
|
+
if self.removeAfterShared {
|
|
579
|
+
let _ = self.deleteFile(url: self.outputFile!)
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
print("User cancelled or failed to complete the sharing activity")
|
|
583
|
+
if self.removeAfterFailedToShare {
|
|
584
|
+
let _ = self.deleteFile(url: self.outputFile!)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
self.closeEditor()
|
|
589
|
+
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Present the share sheet
|
|
593
|
+
if let root = RCTPresentedViewController() {
|
|
594
|
+
root.present(activityViewController, animated: true, completion: nil)
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
|
601
|
+
if removeAfterSavedToDocuments {
|
|
602
|
+
let _ = deleteFile(url: outputFile!)
|
|
603
|
+
}
|
|
604
|
+
closeEditor()
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
|
608
|
+
if removeAfterFailedToSaveDocuments {
|
|
609
|
+
let _ = deleteFile(url: outputFile!)
|
|
610
|
+
}
|
|
611
|
+
closeEditor()
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
@objc(closeEditor:withRejecter:)
|
|
615
|
+
func closeEditor(resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
616
|
+
closeEditor()
|
|
617
|
+
resolve(true)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private func closeEditor() {
|
|
621
|
+
guard let vc = vc else { return }
|
|
622
|
+
// some how in case we trim a very short video the view controller is still visible after first .dismiss call
|
|
623
|
+
// even the file is successfully saved
|
|
624
|
+
// that's why we need a small delay here to ensure vc will be dismissed
|
|
625
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
626
|
+
vc.dismiss(animated: true, completion: {
|
|
627
|
+
self.emitEventToJS("onHide", eventData: nil)
|
|
628
|
+
self.isShowing = false
|
|
629
|
+
})
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
@objc(isValidFile:withResolver:withRejecter:)
|
|
634
|
+
func isValidFile(uri: String, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
635
|
+
let fileURL = URL(string: uri)!
|
|
636
|
+
checkFileValidity(url: fileURL) { isValid, fileType, duration in
|
|
637
|
+
if isValid {
|
|
638
|
+
print("Valid \(fileType) file with duration: \(duration) milliseconds")
|
|
639
|
+
} else {
|
|
640
|
+
print("Invalid file")
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
let payload: [String: Any] = [
|
|
644
|
+
"isValid": isValid,
|
|
645
|
+
"fileType": fileType,
|
|
646
|
+
"duration": duration
|
|
647
|
+
]
|
|
648
|
+
resolve(payload)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private func onError(message: String, code: ErrorCode) {
|
|
654
|
+
let eventPayload: [String: String] = [
|
|
655
|
+
"message": message,
|
|
656
|
+
"errorCode": code.rawValue
|
|
657
|
+
]
|
|
658
|
+
self.emitEventToJS("onError", eventData: eventPayload)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private func checkFileValidity(url: URL, completion: @escaping (Bool, String, Double) -> Void) {
|
|
662
|
+
let asset = AVAsset(url: url)
|
|
663
|
+
|
|
664
|
+
// Load the duration and tracks asynchronously
|
|
665
|
+
asset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) {
|
|
666
|
+
var error: NSError? = nil
|
|
667
|
+
|
|
668
|
+
// Check if the duration and tracks are loaded
|
|
669
|
+
let durationStatus = asset.statusOfValue(forKey: "duration", error: &error)
|
|
670
|
+
let tracksStatus = asset.statusOfValue(forKey: "tracks", error: &error)
|
|
671
|
+
|
|
672
|
+
// Ensure both properties are loaded successfully
|
|
673
|
+
guard durationStatus == .loaded, tracksStatus == .loaded, error == nil else {
|
|
674
|
+
DispatchQueue.main.async {
|
|
675
|
+
completion(false, "unknown", -1)
|
|
676
|
+
}
|
|
677
|
+
return
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Check if the asset contains any video or audio tracks
|
|
681
|
+
let videoTracks = asset.tracks(withMediaType: .video)
|
|
682
|
+
let audioTracks = asset.tracks(withMediaType: .audio)
|
|
683
|
+
|
|
684
|
+
let isValid = !videoTracks.isEmpty || !audioTracks.isEmpty
|
|
685
|
+
let fileType: String
|
|
686
|
+
if !videoTracks.isEmpty {
|
|
687
|
+
fileType = "video"
|
|
688
|
+
} else if !audioTracks.isEmpty {
|
|
689
|
+
fileType = "audio"
|
|
690
|
+
} else {
|
|
691
|
+
fileType = "unknown"
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
let duration = CMTimeGetSeconds(asset.duration) * 1000
|
|
695
|
+
|
|
696
|
+
DispatchQueue.main.async {
|
|
697
|
+
completion(isValid, fileType, isValid ? duration.rounded() : -1)
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
406
701
|
}
|