react-native-video-trim 1.0.10 → 1.0.12
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 +26 -5
- package/android/src/main/java/com/videotrim/VideoTrimModule.java +96 -12
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +1 -0
- package/android/src/main/java/com/videotrim/utils/StorageUtil.java +14 -143
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +2 -10
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +5 -32
- package/android/src/main/res/values/strings.xml +1 -1
- package/ios/VideoTrim.mm +6 -1
- package/ios/VideoTrim.swift +282 -118
- package/ios/VideoTrimmer.swift +808 -0
- package/ios/VideoTrimmerThumb.swift +119 -0
- package/ios/VideoTrimmerViewController.swift +292 -0
- package/lib/commonjs/index.js +64 -4
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +61 -4
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/index.d.ts +46 -3
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/react-native-video-trim.podspec +1 -0
- package/src/index.tsx +75 -5
package/ios/VideoTrim.swift
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import React
|
|
2
2
|
import Photos
|
|
3
|
+
import ffmpegkit
|
|
3
4
|
|
|
4
5
|
@objc(VideoTrim)
|
|
5
|
-
class VideoTrim: RCTEventEmitter
|
|
6
|
-
private
|
|
7
|
-
private var mSaveToPhoto = true
|
|
8
|
-
private var mMaxDuration: Int?
|
|
6
|
+
class VideoTrim: RCTEventEmitter {
|
|
7
|
+
private let FILE_PREFIX = "trimmedVideo"
|
|
9
8
|
private var hasListeners = false
|
|
10
|
-
private var
|
|
9
|
+
private var isShowing = false
|
|
10
|
+
|
|
11
|
+
private var saveToPhoto = true
|
|
12
|
+
private var removeAfterSavedToPhoto = false
|
|
13
|
+
private var enableCancelDialog = true
|
|
14
|
+
private var cancelDialogTitle = "Warning!"
|
|
15
|
+
private var cancelDialogMessage = "Are you sure want to cancel?"
|
|
16
|
+
private var cancelDialogCancelText = "Close"
|
|
17
|
+
private var cancelDialogConfirmText = "Proceed"
|
|
18
|
+
private var enableSaveDialog = true
|
|
19
|
+
private var saveDialogTitle = "Confirmation!"
|
|
20
|
+
private var saveDialogMessage = "Are you sure want to save?"
|
|
21
|
+
private var saveDialogCancelText = "Close"
|
|
22
|
+
private var saveDialogConfirmText = "Proceed"
|
|
23
|
+
private var trimmingText = "Trimming video..."
|
|
11
24
|
|
|
12
25
|
@objc
|
|
13
26
|
static override func requiresMainQueueSetup() -> Bool {
|
|
@@ -29,7 +42,8 @@ class VideoTrim: RCTEventEmitter, UIVideoEditorControllerDelegate, UINavigationC
|
|
|
29
42
|
@objc(isValidVideo:withResolver:withRejecter:)
|
|
30
43
|
func isValidVideo(uri: String, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
31
44
|
if let destPath = copyFileToDocumentDir(uri: uri) {
|
|
32
|
-
resolve(UIVideoEditorController.canEditVideo(atPath: destPath))
|
|
45
|
+
resolve(UIVideoEditorController.canEditVideo(atPath: destPath.path))
|
|
46
|
+
let _ = deleteFile(url: destPath) // remove the file we just copied to document directory
|
|
33
47
|
} else {
|
|
34
48
|
resolve(false)
|
|
35
49
|
}
|
|
@@ -40,54 +54,119 @@ class VideoTrim: RCTEventEmitter, UIVideoEditorControllerDelegate, UINavigationC
|
|
|
40
54
|
if isShowing {
|
|
41
55
|
return
|
|
42
56
|
}
|
|
57
|
+
|
|
58
|
+
saveToPhoto = config["saveToPhoto"] as? Bool ?? true
|
|
59
|
+
removeAfterSavedToPhoto = config["removeAfterSavedToPhoto"] as? Bool ?? false
|
|
43
60
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
// since RN Module is singleton, so we need to reset values everytime instead of reassign
|
|
62
|
+
// Eg. this will not work if change: cancelDialogTitle = config["cancelDialogTitle"] as? String ?? cancelDialogTitle
|
|
63
|
+
// because if we change cancelDialogTitle, the value is still there, and if from RN side we pass undefined, it'll still have previous value
|
|
64
|
+
enableCancelDialog = config["enableCancelDialog"] as? Bool ?? true
|
|
65
|
+
cancelDialogTitle = config["cancelDialogTitle"] as? String ?? "Warning!"
|
|
66
|
+
cancelDialogMessage = config["cancelDialogMessage"] as? String ?? "Are you sure want to cancel?"
|
|
67
|
+
cancelDialogCancelText = config["cancelDialogCancelText"] as? String ?? "Close"
|
|
68
|
+
cancelDialogConfirmText = config["cancelDialogConfirmText"] as? String ?? "Proceed"
|
|
69
|
+
|
|
70
|
+
enableSaveDialog = config["enableSaveDialog"] as? Bool ?? true
|
|
71
|
+
saveDialogTitle = config["saveDialogTitle"] as? String ?? "Confirmation!"
|
|
72
|
+
saveDialogMessage = config["saveDialogMessage"] as? String ?? "Are you sure want to save?"
|
|
73
|
+
saveDialogCancelText = config["saveDialogCancelText"] as? String ?? "Close"
|
|
74
|
+
saveDialogConfirmText = config["saveDialogConfirmText"] as? String ?? "Proceed"
|
|
75
|
+
trimmingText = config["trimmingText"] as? String ?? "Trimming video..."
|
|
47
76
|
|
|
48
|
-
if let maxDuration = config["maxDuration"] as? Int {
|
|
49
|
-
self.mMaxDuration = maxDuration
|
|
50
|
-
}
|
|
51
|
-
|
|
52
77
|
if let destPath = copyFileToDocumentDir(uri: uri) {
|
|
53
|
-
if UIVideoEditorController.canEditVideo(atPath: destPath) {
|
|
78
|
+
if UIVideoEditorController.canEditVideo(atPath: destPath.path) {
|
|
54
79
|
DispatchQueue.main.async {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (self.mMaxDuration != nil) {
|
|
60
|
-
editController.videoMaximumDuration = Double(self.mMaxDuration!)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
editController.delegate = self
|
|
64
|
-
if let root = RCTPresentedViewController() {
|
|
65
|
-
root.present(editController, animated: true, completion: {
|
|
66
|
-
self.emitEventToJS("onShow", eventData: nil)
|
|
67
|
-
self.isShowing = true
|
|
68
|
-
})
|
|
80
|
+
if #available(iOS 13.0, *) {
|
|
81
|
+
let vc = VideoTrimmerViewController()
|
|
82
|
+
vc.asset = AVURLAsset(url: destPath, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])
|
|
69
83
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
if let maxDuration = config["maxDuration"] as? Int {
|
|
85
|
+
vc.maximumDuration = maxDuration
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if let cancelBtnText = config["cancelButtonText"] as? String, !cancelBtnText.isEmpty {
|
|
89
|
+
vc.cancelBtnText = cancelBtnText
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if let saveButtonText = config["saveButtonText"] as? String, !saveButtonText.isEmpty {
|
|
93
|
+
vc.saveButtonText = saveButtonText
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
vc.cancelBtnClicked = {
|
|
97
|
+
if !self.enableCancelDialog {
|
|
98
|
+
let _ = self.deleteFile(url: destPath) // remove the file we just copied to document directory
|
|
99
|
+
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
78
100
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
101
|
+
vc.dismiss(animated: true, completion: {
|
|
102
|
+
self.emitEventToJS("onHide", eventData: nil)
|
|
103
|
+
self.isShowing = false
|
|
104
|
+
})
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Create Alert
|
|
109
|
+
let dialogMessage = UIAlertController(title: self.cancelDialogTitle, message: self.cancelDialogMessage, preferredStyle: .alert)
|
|
110
|
+
|
|
111
|
+
// Create OK button with action handler
|
|
112
|
+
let ok = UIAlertAction(title: self.cancelDialogConfirmText, style: .destructive, handler: { (action) -> Void in
|
|
113
|
+
let _ = self.deleteFile(url: destPath) // remove the file we just copied to document directory
|
|
114
|
+
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
85
115
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
116
|
+
vc.dismiss(animated: true, completion: {
|
|
117
|
+
self.emitEventToJS("onHide", eventData: nil)
|
|
118
|
+
self.isShowing = false
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Create Cancel button with action handlder
|
|
123
|
+
let cancel = UIAlertAction(title: self.cancelDialogCancelText, style: .cancel)
|
|
124
|
+
|
|
125
|
+
//Add OK and Cancel button to an Alert object
|
|
126
|
+
dialogMessage.addAction(ok)
|
|
127
|
+
dialogMessage.addAction(cancel)
|
|
128
|
+
|
|
129
|
+
// Present alert message to user
|
|
130
|
+
if let root = RCTPresentedViewController() {
|
|
131
|
+
root.present(dialogMessage, animated: true, completion: nil)
|
|
89
132
|
}
|
|
90
133
|
}
|
|
134
|
+
|
|
135
|
+
vc.saveBtnClicked = {(selectedRange: CMTimeRange) in
|
|
136
|
+
if !self.enableSaveDialog {
|
|
137
|
+
self.trim(viewController: vc,inputFile: destPath, videoDuration: vc.asset.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Create Alert
|
|
142
|
+
let dialogMessage = UIAlertController(title: self.saveDialogTitle, message: self.saveDialogMessage, preferredStyle: .alert)
|
|
143
|
+
|
|
144
|
+
// Create OK button with action handler
|
|
145
|
+
let ok = UIAlertAction(title: self.saveDialogConfirmText, style: .default, handler: { (action) -> Void in
|
|
146
|
+
self.trim(viewController: vc,inputFile: destPath, videoDuration: vc.asset.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Create Cancel button with action handlder
|
|
150
|
+
let cancel = UIAlertAction(title: self.saveDialogCancelText, style: .cancel)
|
|
151
|
+
|
|
152
|
+
//Add OK and Cancel button to an Alert object
|
|
153
|
+
dialogMessage.addAction(ok)
|
|
154
|
+
dialogMessage.addAction(cancel)
|
|
155
|
+
|
|
156
|
+
// Present alert message to user
|
|
157
|
+
if let root = RCTPresentedViewController() {
|
|
158
|
+
root.present(dialogMessage, animated: true, completion: nil)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
vc.isModalInPresentation = true // prevent modal closed by swipe down
|
|
163
|
+
|
|
164
|
+
if let root = RCTPresentedViewController() {
|
|
165
|
+
root.present(vc, animated: true, completion: {
|
|
166
|
+
self.emitEventToJS("onShow", eventData: nil)
|
|
167
|
+
self.isShowing = true
|
|
168
|
+
})
|
|
169
|
+
}
|
|
91
170
|
}
|
|
92
171
|
}
|
|
93
172
|
} else {
|
|
@@ -100,72 +179,7 @@ class VideoTrim: RCTEventEmitter, UIVideoEditorControllerDelegate, UINavigationC
|
|
|
100
179
|
}
|
|
101
180
|
}
|
|
102
181
|
|
|
103
|
-
func
|
|
104
|
-
didSaveEditedVideoToPath editedVideoPath: String) {
|
|
105
|
-
if (!shouldFireFinishEvent) {
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
shouldFireFinishEvent = false
|
|
109
|
-
|
|
110
|
-
let eventPayload: [String: Any] = ["outputPath": editedVideoPath]
|
|
111
|
-
self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
|
|
112
|
-
|
|
113
|
-
if (mSaveToPhoto) {
|
|
114
|
-
PHPhotoLibrary.requestAuthorization { status in
|
|
115
|
-
guard status == .authorized else {
|
|
116
|
-
let eventPayload: [String: Any] = ["message": "Permission to access Photo Library is not granted"]
|
|
117
|
-
self.emitEventToJS("onError", eventData: eventPayload)
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
PHPhotoLibrary.shared().performChanges({
|
|
122
|
-
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: editedVideoPath))
|
|
123
|
-
request?.creationDate = Date()
|
|
124
|
-
}) { success, error in
|
|
125
|
-
if success {
|
|
126
|
-
print("Edited video saved to Photo Library successfully.")
|
|
127
|
-
} else {
|
|
128
|
-
let eventPayload: [String: Any] = ["message": "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")"]
|
|
129
|
-
self.emitEventToJS("onError", eventData: eventPayload)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// the edit has a known bug where it fires "didSaveEditedVideoToPath" twice, so we have to set its delete to nil right after first call
|
|
136
|
-
// editor.delegate = nil
|
|
137
|
-
|
|
138
|
-
// but with the above solution, somehow it'll close React Native Modal when the editor controller dismissed
|
|
139
|
-
// so we have to create a flag shouldFireFinishEvent here
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
editor.dismiss(animated: true, completion: {
|
|
143
|
-
self.emitEventToJS("onHide", eventData: nil)
|
|
144
|
-
self.isShowing = false
|
|
145
|
-
self.shouldFireFinishEvent = true // reset this flag to true once dismiss
|
|
146
|
-
})
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
func videoEditorControllerDidCancel(_ editor: UIVideoEditorController) {
|
|
150
|
-
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
151
|
-
editor.dismiss(animated: true, completion: {
|
|
152
|
-
self.emitEventToJS("onHide", eventData: nil)
|
|
153
|
-
self.isShowing = false
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
func videoEditorController(_ editor: UIVideoEditorController,
|
|
158
|
-
didFailWithError error: Error) {
|
|
159
|
-
let eventPayload: [String: Any] = ["message": error.localizedDescription]
|
|
160
|
-
self.emitEventToJS("onError", eventData: eventPayload)
|
|
161
|
-
editor.dismiss(animated: true, completion: {
|
|
162
|
-
self.emitEventToJS("onHide", eventData: nil)
|
|
163
|
-
self.isShowing = false
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
private func copyFileToDocumentDir(uri: String) -> String? {
|
|
182
|
+
private func copyFileToDocumentDir(uri: String) -> URL? {
|
|
169
183
|
if let videoURL = URL(string: uri) {
|
|
170
184
|
// Save the video to the document directory
|
|
171
185
|
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
@@ -173,20 +187,17 @@ class VideoTrim: RCTEventEmitter, UIVideoEditorControllerDelegate, UINavigationC
|
|
|
173
187
|
let fileExtension = videoURL.pathExtension
|
|
174
188
|
|
|
175
189
|
// Define the filename with the correct file extension
|
|
176
|
-
let
|
|
190
|
+
let timestamp = Int(Date().timeIntervalSince1970)
|
|
191
|
+
let destinationURL = documentsDirectory.appendingPathComponent("\(FILE_PREFIX)_original_\(timestamp).\(fileExtension)")
|
|
177
192
|
|
|
178
193
|
do {
|
|
179
|
-
// Remove the old file if it exists
|
|
180
|
-
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
|
181
|
-
try FileManager.default.removeItem(at: destinationURL)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
194
|
try FileManager.default.copyItem(at: videoURL, to: destinationURL)
|
|
185
195
|
} catch {
|
|
196
|
+
print("Error while copying file to document directory \(error)")
|
|
186
197
|
return nil
|
|
187
198
|
}
|
|
188
199
|
|
|
189
|
-
return destinationURL
|
|
200
|
+
return destinationURL
|
|
190
201
|
} else {
|
|
191
202
|
return nil
|
|
192
203
|
}
|
|
@@ -199,4 +210,157 @@ class VideoTrim: RCTEventEmitter, UIVideoEditorControllerDelegate, UINavigationC
|
|
|
199
210
|
sendEvent(withName: "VideoTrim", body: modifiedEventData)
|
|
200
211
|
}
|
|
201
212
|
}
|
|
213
|
+
|
|
214
|
+
@objc(listFiles:withRejecter:)
|
|
215
|
+
func listFiles(resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
216
|
+
let files = listFiles()
|
|
217
|
+
resolve(files.map{ $0.absoluteString })
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@objc(cleanFiles:withRejecter:)
|
|
221
|
+
func cleanFiles(resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
222
|
+
let files = listFiles()
|
|
223
|
+
var successCount = 0
|
|
224
|
+
for file in files {
|
|
225
|
+
let state = deleteFile(url: file)
|
|
226
|
+
|
|
227
|
+
if state == 0 {
|
|
228
|
+
successCount += 1
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
resolve(successCount)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@objc(deleteFile:withResolver:withRejecter:)
|
|
236
|
+
func deleteFile(uri: String, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
237
|
+
let state = deleteFile(url: URL(string: uri)!)
|
|
238
|
+
resolve(state == 0)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private func listFiles() -> [URL] {
|
|
242
|
+
var files: [URL] = []
|
|
243
|
+
|
|
244
|
+
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
245
|
+
|
|
246
|
+
do {
|
|
247
|
+
let directoryContents = try FileManager.default.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil)
|
|
248
|
+
|
|
249
|
+
for fileURL in directoryContents {
|
|
250
|
+
if fileURL.lastPathComponent.starts(with: FILE_PREFIX) {
|
|
251
|
+
files.append(fileURL)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
print("[listFiles] Error when retrieving files: \(error)")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return files
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private func deleteFile(url: URL) -> Int {
|
|
262
|
+
do {
|
|
263
|
+
if FileManager.default.fileExists(atPath: url.path) {
|
|
264
|
+
try FileManager.default.removeItem(at: url)
|
|
265
|
+
|
|
266
|
+
return 0
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return 1
|
|
270
|
+
} catch {
|
|
271
|
+
print("[deleteFile] Error deleting files: \(error)")
|
|
272
|
+
|
|
273
|
+
return 2
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@available(iOS 13.0, *)
|
|
278
|
+
private func trim(viewController: VideoTrimmerViewController, inputFile: URL, videoDuration: Double, startTime: Double, endTime: Double) {
|
|
279
|
+
let timestamp = Int(Date().timeIntervalSince1970)
|
|
280
|
+
let outputName = "\(FILE_PREFIX)_\(timestamp).mp4" // use mp4 to prevent any issue with ffmpeg about file extension
|
|
281
|
+
let outputFile = "\(inputFile.deletingLastPathComponent().absoluteURL)\(outputName)"
|
|
282
|
+
let cmd = "-i \(inputFile) -ss \(startTime * 1000)ms -to \(endTime * 1000)ms -c copy \(outputFile)";
|
|
283
|
+
|
|
284
|
+
self.emitEventToJS("onStartTrimming", eventData: nil)
|
|
285
|
+
|
|
286
|
+
// Create Alert
|
|
287
|
+
let dialogMessage = UIAlertController(title: trimmingText, message: nil, preferredStyle: .alert)
|
|
288
|
+
|
|
289
|
+
// Present alert message to user
|
|
290
|
+
let progressView = UIProgressView(frame: .zero)
|
|
291
|
+
progressView.tintColor = .systemBlue
|
|
292
|
+
if let root = RCTPresentedViewController() {
|
|
293
|
+
root.present(dialogMessage, animated: true, completion: {
|
|
294
|
+
dialogMessage.view.addSubview(progressView)
|
|
295
|
+
|
|
296
|
+
progressView.translatesAutoresizingMaskIntoConstraints = false
|
|
297
|
+
NSLayoutConstraint.activate([
|
|
298
|
+
progressView.leadingAnchor.constraint(equalTo: dialogMessage.view.leadingAnchor, constant: 8),
|
|
299
|
+
progressView.trailingAnchor.constraint(equalTo: dialogMessage.view.trailingAnchor, constant: -8),
|
|
300
|
+
progressView.bottomAnchor.constraint(equalTo: dialogMessage.view.bottomAnchor, constant: -8)
|
|
301
|
+
])
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
FFmpegKit.executeAsync(cmd, withCompleteCallback: { session in
|
|
306
|
+
let _ = self.deleteFile(url: inputFile) // remove the file we just copied to document directory
|
|
307
|
+
|
|
308
|
+
let state = session?.getState()
|
|
309
|
+
|
|
310
|
+
if state == .completed {
|
|
311
|
+
let eventPayload: [String: Any] = ["outputPath": outputFile]
|
|
312
|
+
self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
|
|
313
|
+
|
|
314
|
+
if (self.saveToPhoto) {
|
|
315
|
+
PHPhotoLibrary.requestAuthorization { status in
|
|
316
|
+
guard status == .authorized else {
|
|
317
|
+
let eventPayload: [String: Any] = ["message": "Permission to access Photo Library is not granted"]
|
|
318
|
+
self.emitEventToJS("onError", eventData: eventPayload)
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
PHPhotoLibrary.shared().performChanges({
|
|
323
|
+
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(string: outputFile)!)
|
|
324
|
+
request?.creationDate = Date()
|
|
325
|
+
}) { success, error in
|
|
326
|
+
if success {
|
|
327
|
+
print("Edited video saved to Photo Library successfully.")
|
|
328
|
+
|
|
329
|
+
if self.removeAfterSavedToPhoto {
|
|
330
|
+
let _ = self.deleteFile(url: URL(string: outputFile)!)
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
let eventPayload: [String: Any] = ["message": "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")"]
|
|
334
|
+
self.emitEventToJS("onError", eventData: eventPayload)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
let eventPayload: [String: Any] = ["message": "Some error occured"]
|
|
341
|
+
self.emitEventToJS("onError", eventData: eventPayload)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// some how in case we trim a very short video the view controller is still visible after first .dismiss call
|
|
345
|
+
// even the file is successfully saved
|
|
346
|
+
// that's why we need a small delay here to ensure vc will be dismissed
|
|
347
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
348
|
+
dialogMessage.dismiss(animated: false)
|
|
349
|
+
viewController.dismiss(animated: true, completion: {
|
|
350
|
+
self.emitEventToJS("onHide", eventData: nil)
|
|
351
|
+
self.isShowing = false
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
}, withLogCallback: { log in
|
|
355
|
+
|
|
356
|
+
}, withStatisticsCallback: { statistics in
|
|
357
|
+
let timeInMilliseconds = statistics?.getTime() ?? 0;
|
|
358
|
+
if timeInMilliseconds > 0 {
|
|
359
|
+
let completePercentage = timeInMilliseconds / (videoDuration * 1000); // from 0 -> 1
|
|
360
|
+
DispatchQueue.main.async {
|
|
361
|
+
progressView.setProgress(Float(completePercentage), animated: true)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
}
|
|
202
366
|
}
|