react-native-video-trim 1.0.9 → 1.0.11
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 +38 -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 +279 -115
- 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,49 +54,117 @@ 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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
if #available(iOS 13.0, *) {
|
|
81
|
+
let vc = VideoTrimmerViewController()
|
|
82
|
+
vc.asset = AVURLAsset(url: destPath, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])
|
|
83
|
+
|
|
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
|
+
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
73
99
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
100
|
+
vc.dismiss(animated: true, completion: {
|
|
101
|
+
self.emitEventToJS("onHide", eventData: nil)
|
|
102
|
+
self.isShowing = false
|
|
103
|
+
})
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Create Alert
|
|
108
|
+
let dialogMessage = UIAlertController(title: self.cancelDialogTitle, message: self.cancelDialogMessage, preferredStyle: .alert)
|
|
109
|
+
|
|
110
|
+
// Create OK button with action handler
|
|
111
|
+
let ok = UIAlertAction(title: self.cancelDialogConfirmText, style: .destructive, handler: { (action) -> Void in
|
|
112
|
+
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
80
113
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
114
|
+
vc.dismiss(animated: true, completion: {
|
|
115
|
+
self.emitEventToJS("onHide", eventData: nil)
|
|
116
|
+
self.isShowing = false
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Create Cancel button with action handlder
|
|
121
|
+
let cancel = UIAlertAction(title: self.cancelDialogCancelText, style: .cancel)
|
|
122
|
+
|
|
123
|
+
//Add OK and Cancel button to an Alert object
|
|
124
|
+
dialogMessage.addAction(ok)
|
|
125
|
+
dialogMessage.addAction(cancel)
|
|
126
|
+
|
|
127
|
+
// Present alert message to user
|
|
128
|
+
if let root = RCTPresentedViewController() {
|
|
129
|
+
root.present(dialogMessage, animated: true, completion: nil)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
vc.saveBtnClicked = {(selectedRange: CMTimeRange) in
|
|
134
|
+
if !self.enableSaveDialog {
|
|
135
|
+
self.trim(viewController: vc,inputFile: destPath, videoDuration: vc.asset.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Create Alert
|
|
140
|
+
let dialogMessage = UIAlertController(title: self.saveDialogTitle, message: self.saveDialogMessage, preferredStyle: .alert)
|
|
141
|
+
|
|
142
|
+
// Create OK button with action handler
|
|
143
|
+
let ok = UIAlertAction(title: self.saveDialogConfirmText, style: .default, handler: { (action) -> Void in
|
|
144
|
+
self.trim(viewController: vc,inputFile: destPath, videoDuration: vc.asset.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Create Cancel button with action handlder
|
|
148
|
+
let cancel = UIAlertAction(title: self.saveDialogCancelText, style: .cancel)
|
|
149
|
+
|
|
150
|
+
//Add OK and Cancel button to an Alert object
|
|
151
|
+
dialogMessage.addAction(ok)
|
|
152
|
+
dialogMessage.addAction(cancel)
|
|
153
|
+
|
|
154
|
+
// Present alert message to user
|
|
155
|
+
if let root = RCTPresentedViewController() {
|
|
156
|
+
root.present(dialogMessage, animated: true, completion: nil)
|
|
84
157
|
}
|
|
85
|
-
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
vc.isModalInPresentation = true // prevent modal closed by swipe down
|
|
161
|
+
|
|
162
|
+
if let root = RCTPresentedViewController() {
|
|
163
|
+
root.present(vc, animated: true, completion: {
|
|
164
|
+
self.emitEventToJS("onShow", eventData: nil)
|
|
165
|
+
self.isShowing = true
|
|
166
|
+
})
|
|
167
|
+
}
|
|
86
168
|
}
|
|
87
169
|
}
|
|
88
170
|
} else {
|
|
@@ -95,72 +177,7 @@ class VideoTrim: RCTEventEmitter, UIVideoEditorControllerDelegate, UINavigationC
|
|
|
95
177
|
}
|
|
96
178
|
}
|
|
97
179
|
|
|
98
|
-
func
|
|
99
|
-
didSaveEditedVideoToPath editedVideoPath: String) {
|
|
100
|
-
if (!shouldFireFinishEvent) {
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
shouldFireFinishEvent = false
|
|
104
|
-
|
|
105
|
-
let eventPayload: [String: Any] = ["outputPath": editedVideoPath]
|
|
106
|
-
self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
|
|
107
|
-
|
|
108
|
-
if (mSaveToPhoto) {
|
|
109
|
-
PHPhotoLibrary.requestAuthorization { status in
|
|
110
|
-
guard status == .authorized else {
|
|
111
|
-
let eventPayload: [String: Any] = ["message": "Permission to access Photo Library is not granted"]
|
|
112
|
-
self.emitEventToJS("onError", eventData: eventPayload)
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
PHPhotoLibrary.shared().performChanges({
|
|
117
|
-
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: editedVideoPath))
|
|
118
|
-
request?.creationDate = Date()
|
|
119
|
-
}) { success, error in
|
|
120
|
-
if success {
|
|
121
|
-
print("Edited video saved to Photo Library successfully.")
|
|
122
|
-
} else {
|
|
123
|
-
let eventPayload: [String: Any] = ["message": "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")"]
|
|
124
|
-
self.emitEventToJS("onError", eventData: eventPayload)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// the edit has a known bug where it fires "didSaveEditedVideoToPath" twice, so we have to set its delete to nil right after first call
|
|
131
|
-
// editor.delegate = nil
|
|
132
|
-
|
|
133
|
-
// but with the above solution, somehow it'll close React Native Modal when the editor controller dismissed
|
|
134
|
-
// so we have to create a flag shouldFireFinishEvent here
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
editor.dismiss(animated: true, completion: {
|
|
138
|
-
self.emitEventToJS("onHide", eventData: nil)
|
|
139
|
-
self.isShowing = false
|
|
140
|
-
self.shouldFireFinishEvent = true // reset this flag to true once dismiss
|
|
141
|
-
})
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
func videoEditorControllerDidCancel(_ editor: UIVideoEditorController) {
|
|
145
|
-
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
146
|
-
editor.dismiss(animated: true, completion: {
|
|
147
|
-
self.emitEventToJS("onHide", eventData: nil)
|
|
148
|
-
self.isShowing = false
|
|
149
|
-
})
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
func videoEditorController(_ editor: UIVideoEditorController,
|
|
153
|
-
didFailWithError error: Error) {
|
|
154
|
-
let eventPayload: [String: Any] = ["message": error.localizedDescription]
|
|
155
|
-
self.emitEventToJS("onError", eventData: eventPayload)
|
|
156
|
-
editor.dismiss(animated: true, completion: {
|
|
157
|
-
self.emitEventToJS("onHide", eventData: nil)
|
|
158
|
-
self.isShowing = false
|
|
159
|
-
})
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
private func copyFileToDocumentDir(uri: String) -> String? {
|
|
180
|
+
private func copyFileToDocumentDir(uri: String) -> URL? {
|
|
164
181
|
if let videoURL = URL(string: uri) {
|
|
165
182
|
// Save the video to the document directory
|
|
166
183
|
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
@@ -168,20 +185,17 @@ class VideoTrim: RCTEventEmitter, UIVideoEditorControllerDelegate, UINavigationC
|
|
|
168
185
|
let fileExtension = videoURL.pathExtension
|
|
169
186
|
|
|
170
187
|
// Define the filename with the correct file extension
|
|
171
|
-
let
|
|
188
|
+
let timestamp = Int(Date().timeIntervalSince1970)
|
|
189
|
+
let destinationURL = documentsDirectory.appendingPathComponent("\(FILE_PREFIX)_original_\(timestamp).\(fileExtension)")
|
|
172
190
|
|
|
173
191
|
do {
|
|
174
|
-
// Remove the old file if it exists
|
|
175
|
-
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
|
176
|
-
try FileManager.default.removeItem(at: destinationURL)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
192
|
try FileManager.default.copyItem(at: videoURL, to: destinationURL)
|
|
180
193
|
} catch {
|
|
194
|
+
print("Error while copying file to document directory \(error)")
|
|
181
195
|
return nil
|
|
182
196
|
}
|
|
183
197
|
|
|
184
|
-
return destinationURL
|
|
198
|
+
return destinationURL
|
|
185
199
|
} else {
|
|
186
200
|
return nil
|
|
187
201
|
}
|
|
@@ -194,4 +208,154 @@ class VideoTrim: RCTEventEmitter, UIVideoEditorControllerDelegate, UINavigationC
|
|
|
194
208
|
sendEvent(withName: "VideoTrim", body: modifiedEventData)
|
|
195
209
|
}
|
|
196
210
|
}
|
|
211
|
+
|
|
212
|
+
@objc(listFiles:withRejecter:)
|
|
213
|
+
func listFiles(resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
214
|
+
let files = listFiles()
|
|
215
|
+
resolve(files.map{ $0.absoluteString })
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@objc(cleanFiles:withRejecter:)
|
|
219
|
+
func cleanFiles(resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
220
|
+
let files = listFiles()
|
|
221
|
+
var successCount = 0
|
|
222
|
+
for file in files {
|
|
223
|
+
let state = deleteFile(url: file)
|
|
224
|
+
|
|
225
|
+
if state == 0 {
|
|
226
|
+
successCount += 1
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
resolve(successCount)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@objc(deleteFile:withResolver:withRejecter:)
|
|
234
|
+
func deleteFile(uri: String, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
235
|
+
let state = deleteFile(url: URL(string: uri)!)
|
|
236
|
+
resolve(state == 0)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private func listFiles() -> [URL] {
|
|
240
|
+
var files: [URL] = []
|
|
241
|
+
|
|
242
|
+
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
243
|
+
|
|
244
|
+
do {
|
|
245
|
+
let directoryContents = try FileManager.default.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil)
|
|
246
|
+
|
|
247
|
+
for fileURL in directoryContents {
|
|
248
|
+
if fileURL.lastPathComponent.starts(with: FILE_PREFIX) {
|
|
249
|
+
files.append(fileURL)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
print("[listFiles] Error when retrieving files: \(error)")
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return files
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private func deleteFile(url: URL) -> Int {
|
|
260
|
+
do {
|
|
261
|
+
if FileManager.default.fileExists(atPath: url.path) {
|
|
262
|
+
try FileManager.default.removeItem(at: url)
|
|
263
|
+
|
|
264
|
+
return 0
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return 1
|
|
268
|
+
} catch {
|
|
269
|
+
print("[deleteFile] Error deleting files: \(error)")
|
|
270
|
+
|
|
271
|
+
return 2
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@available(iOS 13.0, *)
|
|
276
|
+
private func trim(viewController: VideoTrimmerViewController, inputFile: URL, videoDuration: Double, startTime: Double, endTime: Double) {
|
|
277
|
+
let timestamp = Int(Date().timeIntervalSince1970)
|
|
278
|
+
let outputName = "\(FILE_PREFIX)_\(timestamp).mp4" // use mp4 to prevent any issue with ffmpeg about file extension
|
|
279
|
+
let outputFile = "\(inputFile.deletingLastPathComponent().absoluteURL)\(outputName)"
|
|
280
|
+
let cmd = "-i \(inputFile) -ss \(startTime * 1000)ms -to \(endTime * 1000)ms -c copy \(outputFile)";
|
|
281
|
+
|
|
282
|
+
self.emitEventToJS("onStartTrimming", eventData: nil)
|
|
283
|
+
|
|
284
|
+
// Create Alert
|
|
285
|
+
let dialogMessage = UIAlertController(title: trimmingText, message: nil, preferredStyle: .alert)
|
|
286
|
+
|
|
287
|
+
// Present alert message to user
|
|
288
|
+
let progressView = UIProgressView(frame: .zero)
|
|
289
|
+
progressView.tintColor = .systemBlue
|
|
290
|
+
if let root = RCTPresentedViewController() {
|
|
291
|
+
root.present(dialogMessage, animated: true, completion: {
|
|
292
|
+
dialogMessage.view.addSubview(progressView)
|
|
293
|
+
|
|
294
|
+
progressView.translatesAutoresizingMaskIntoConstraints = false
|
|
295
|
+
NSLayoutConstraint.activate([
|
|
296
|
+
progressView.leadingAnchor.constraint(equalTo: dialogMessage.view.leadingAnchor, constant: 8),
|
|
297
|
+
progressView.trailingAnchor.constraint(equalTo: dialogMessage.view.trailingAnchor, constant: -8),
|
|
298
|
+
progressView.bottomAnchor.constraint(equalTo: dialogMessage.view.bottomAnchor, constant: -8)
|
|
299
|
+
])
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
FFmpegKit.executeAsync(cmd, withCompleteCallback: { session in
|
|
304
|
+
let _ = self.deleteFile(url: inputFile) // remove the file we just copied to document directory
|
|
305
|
+
|
|
306
|
+
let state = session?.getState()
|
|
307
|
+
|
|
308
|
+
if state == .completed {
|
|
309
|
+
let eventPayload: [String: Any] = ["outputPath": outputFile]
|
|
310
|
+
self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
|
|
311
|
+
|
|
312
|
+
if (self.saveToPhoto) {
|
|
313
|
+
PHPhotoLibrary.requestAuthorization { status in
|
|
314
|
+
guard status == .authorized else {
|
|
315
|
+
let eventPayload: [String: Any] = ["message": "Permission to access Photo Library is not granted"]
|
|
316
|
+
self.emitEventToJS("onError", eventData: eventPayload)
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
PHPhotoLibrary.shared().performChanges({
|
|
321
|
+
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(string: outputFile)!)
|
|
322
|
+
request?.creationDate = Date()
|
|
323
|
+
}) { success, error in
|
|
324
|
+
if success {
|
|
325
|
+
print("Edited video saved to Photo Library successfully.")
|
|
326
|
+
|
|
327
|
+
if self.removeAfterSavedToPhoto {
|
|
328
|
+
let _ = self.deleteFile(url: URL(string: outputFile)!)
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
let eventPayload: [String: Any] = ["message": "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")"]
|
|
332
|
+
self.emitEventToJS("onError", eventData: eventPayload)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
let eventPayload: [String: Any] = ["message": "Some error occured"]
|
|
339
|
+
self.emitEventToJS("onError", eventData: eventPayload)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
DispatchQueue.main.async {
|
|
343
|
+
dialogMessage.dismiss(animated: false)
|
|
344
|
+
viewController.dismiss(animated: true, completion: {
|
|
345
|
+
self.emitEventToJS("onHide", eventData: nil)
|
|
346
|
+
self.isShowing = false
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
}, withLogCallback: { log in
|
|
350
|
+
|
|
351
|
+
}, withStatisticsCallback: { statistics in
|
|
352
|
+
let timeInMilliseconds = statistics?.getTime() ?? 0;
|
|
353
|
+
if timeInMilliseconds > 0 {
|
|
354
|
+
let completePercentage = timeInMilliseconds / (videoDuration * 1000); // from 0 -> 1
|
|
355
|
+
DispatchQueue.main.async {
|
|
356
|
+
progressView.setProgress(Float(completePercentage), animated: true)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
}
|
|
197
361
|
}
|