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