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