react-native-video-trim 4.1.0 → 5.0.1
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/LICENSE +1 -1
- package/README.md +89 -76
- package/VideoTrim.podspec +3 -3
- package/android/build.gradle +6 -53
- package/android/gradle.properties +1 -1
- package/android/src/main/AndroidManifest.xml +1 -1
- package/android/src/main/java/com/{margelo/nitro/videotrim/VideoTrim.kt → videotrim/VideoTrimModule.kt} +246 -232
- package/android/src/main/java/com/videotrim/VideoTrimPackage.kt +33 -0
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/enums/ErrorCode.java +1 -1
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/interfaces/IVideoTrimmerView.java +1 -1
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/interfaces/VideoTrimListener.java +5 -4
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/MediaMetadataUtil.java +1 -1
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/StorageUtil.java +1 -1
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/VideoTrimmerUtil.java +20 -18
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/widgets/VideoTrimmerView.java +44 -45
- package/ios/AssetLoader.h +19 -0
- package/ios/AssetLoader.mm +87 -0
- package/ios/ErrorCode.h +9 -0
- package/ios/ProgressAlertController.h +15 -0
- package/ios/ProgressAlertController.mm +78 -0
- package/ios/VideoTrim.h +31 -0
- package/ios/VideoTrim.mm +663 -0
- package/ios/VideoTrimmer.h +67 -0
- package/ios/VideoTrimmer.mm +863 -0
- package/ios/VideoTrimmerThumb.h +23 -0
- package/ios/VideoTrimmerThumb.mm +175 -0
- package/ios/VideoTrimmerViewController.h +52 -0
- package/ios/VideoTrimmerViewController.mm +533 -0
- package/lib/module/NativeVideoTrim.js +5 -0
- package/lib/module/NativeVideoTrim.js.map +1 -0
- package/lib/module/index.js +24 -24
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NativeVideoTrim.d.ts +107 -0
- package/lib/typescript/src/NativeVideoTrim.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +16 -10
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +15 -18
- package/src/NativeVideoTrim.ts +113 -0
- package/src/index.tsx +29 -31
- package/android/CMakeLists.txt +0 -24
- package/android/src/main/cpp/cpp-adapter.cpp +0 -6
- package/android/src/main/java/com/margelo/nitro/videotrim/VideoTrimPackage.kt +0 -22
- package/ios/AssetLoader.swift +0 -99
- package/ios/ErrorCode.swift +0 -17
- package/ios/ProgressAlertController.swift +0 -100
- package/ios/VideoTrim.swift +0 -67
- package/ios/VideoTrimImpl.swift +0 -957
- package/ios/VideoTrimmer.swift +0 -872
- package/ios/VideoTrimmerThumb.swift +0 -175
- package/ios/VideoTrimmerViewController.swift +0 -557
- package/lib/module/VideoTrim.nitro.js +0 -4
- package/lib/module/VideoTrim.nitro.js.map +0 -1
- package/lib/typescript/src/VideoTrim.nitro.d.ts +0 -257
- package/lib/typescript/src/VideoTrim.nitro.d.ts.map +0 -1
- package/nitrogen/generated/android/c++/JEditorConfig.hpp +0 -237
- package/nitrogen/generated/android/c++/JFileValidationResult.hpp +0 -61
- package/nitrogen/generated/android/c++/JFunc_void.hpp +0 -74
- package/nitrogen/generated/android/c++/JFunc_void_std__string_std__unordered_map_std__string__std__string_.hpp +0 -89
- package/nitrogen/generated/android/c++/JHybridVideoTrimSpec.cpp +0 -151
- package/nitrogen/generated/android/c++/JHybridVideoTrimSpec.hpp +0 -68
- package/nitrogen/generated/android/c++/JTrimOptions.hpp +0 -109
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/EditorConfig.kt +0 -72
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/FileValidationResult.kt +0 -28
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/Func_void.kt +0 -80
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/Func_void_std__string_std__unordered_map_std__string__std__string_.kt +0 -80
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/HybridVideoTrimSpec.kt +0 -86
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/TrimOptions.kt +0 -40
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/videotrimOnLoad.kt +0 -35
- package/nitrogen/generated/android/videotrim+autolinking.cmake +0 -78
- package/nitrogen/generated/android/videotrim+autolinking.gradle +0 -27
- package/nitrogen/generated/android/videotrimOnLoad.cpp +0 -50
- package/nitrogen/generated/android/videotrimOnLoad.hpp +0 -25
- package/nitrogen/generated/ios/VideoTrim+autolinking.rb +0 -60
- package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Bridge.cpp +0 -96
- package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Bridge.hpp +0 -374
- package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Umbrella.hpp +0 -56
- package/nitrogen/generated/ios/VideoTrimAutolinking.mm +0 -33
- package/nitrogen/generated/ios/VideoTrimAutolinking.swift +0 -25
- package/nitrogen/generated/ios/c++/HybridVideoTrimSpecSwift.cpp +0 -11
- package/nitrogen/generated/ios/c++/HybridVideoTrimSpecSwift.hpp +0 -127
- package/nitrogen/generated/ios/swift/EditorConfig.swift +0 -541
- package/nitrogen/generated/ios/swift/FileValidationResult.swift +0 -57
- package/nitrogen/generated/ios/swift/Func_void.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_FileValidationResult.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_double.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_std__string_std__unordered_map_std__string__std__string_.swift +0 -54
- package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +0 -46
- package/nitrogen/generated/ios/swift/HybridVideoTrimSpec.swift +0 -54
- package/nitrogen/generated/ios/swift/HybridVideoTrimSpec_cxx.swift +0 -241
- package/nitrogen/generated/ios/swift/TrimOptions.swift +0 -189
- package/nitrogen/generated/shared/c++/EditorConfig.hpp +0 -253
- package/nitrogen/generated/shared/c++/FileValidationResult.hpp +0 -77
- package/nitrogen/generated/shared/c++/HybridVideoTrimSpec.cpp +0 -27
- package/nitrogen/generated/shared/c++/HybridVideoTrimSpec.hpp +0 -80
- package/nitrogen/generated/shared/c++/TrimOptions.hpp +0 -125
- package/src/VideoTrim.nitro.ts +0 -263
package/ios/VideoTrimImpl.swift
DELETED
|
@@ -1,957 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// VideoTrimImpl.swift
|
|
3
|
-
// VideoTrim
|
|
4
|
-
//
|
|
5
|
-
// Created by Duc Trung Mai on 21/5/25.
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
import Photos
|
|
9
|
-
import ffmpegkit
|
|
10
|
-
|
|
11
|
-
class VideoTrimImpl: NSObject
|
|
12
|
-
{
|
|
13
|
-
private let FILE_PREFIX = "trimmedVideo"
|
|
14
|
-
private let BEFORE_TRIM_PREFIX = "beforeTrim"
|
|
15
|
-
private var isShowing = false
|
|
16
|
-
private var vc: VideoTrimmerViewController?
|
|
17
|
-
private var outputFile: URL?
|
|
18
|
-
private var isVideoType = true
|
|
19
|
-
private var editorConfig: EditorConfig!
|
|
20
|
-
private var onEvent: ((_ eventName: String, _ payload: Dictionary<String, String>) -> Void)?
|
|
21
|
-
|
|
22
|
-
func showEditor(
|
|
23
|
-
uri: String,
|
|
24
|
-
editorConfig: EditorConfig,
|
|
25
|
-
onEvent: ((_ eventName: String, _ payload: Dictionary<String, String>) -> Void)?
|
|
26
|
-
) {
|
|
27
|
-
if isShowing {
|
|
28
|
-
return
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
self.editorConfig = editorConfig
|
|
32
|
-
self.onEvent = onEvent
|
|
33
|
-
self.isVideoType = editorConfig.type == "video"
|
|
34
|
-
|
|
35
|
-
let destPath: URL?
|
|
36
|
-
if uri.starts(with: "http://") || uri.starts(with: "https://") {
|
|
37
|
-
destPath = URL(string: uri)
|
|
38
|
-
} else {
|
|
39
|
-
destPath = renameFile(at: URL(string: uri)!, newName: BEFORE_TRIM_PREFIX)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
guard let destPath = destPath else {
|
|
43
|
-
onError(message: "Fail to rename file", code: .invalidFilePath)
|
|
44
|
-
self.isShowing = false
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
DispatchQueue.main.async {
|
|
49
|
-
self.vc = VideoTrimmerViewController()
|
|
50
|
-
|
|
51
|
-
guard let vc = self.vc else { return }
|
|
52
|
-
|
|
53
|
-
vc.configure(config: editorConfig)
|
|
54
|
-
|
|
55
|
-
vc.cancelBtnClicked = {
|
|
56
|
-
if !self.editorConfig.enableCancelDialog {
|
|
57
|
-
self.emitEventToJS("onCancel", eventData: nil)
|
|
58
|
-
|
|
59
|
-
vc.dismiss(
|
|
60
|
-
animated: true,
|
|
61
|
-
completion: {
|
|
62
|
-
self.emitEventToJS("onHide", eventData: nil)
|
|
63
|
-
self.isShowing = false
|
|
64
|
-
})
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Create Alert
|
|
69
|
-
let dialogMessage = UIAlertController(
|
|
70
|
-
title: self.editorConfig.cancelDialogTitle, message: self.editorConfig.cancelDialogMessage,
|
|
71
|
-
preferredStyle: .alert)
|
|
72
|
-
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
73
|
-
|
|
74
|
-
// Create OK button with action handler
|
|
75
|
-
let ok = UIAlertAction(
|
|
76
|
-
title: self.editorConfig.cancelDialogConfirmText, style: .destructive,
|
|
77
|
-
handler: { (action) -> Void in
|
|
78
|
-
self.emitEventToJS("onCancel", eventData: nil)
|
|
79
|
-
|
|
80
|
-
vc.dismiss(
|
|
81
|
-
animated: true,
|
|
82
|
-
completion: {
|
|
83
|
-
self.emitEventToJS("onHide", eventData: nil)
|
|
84
|
-
self.isShowing = false
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
// Create Cancel button with action handlder
|
|
89
|
-
let cancel = UIAlertAction(
|
|
90
|
-
title: self.editorConfig.cancelDialogCancelText, style: .cancel)
|
|
91
|
-
|
|
92
|
-
//Add OK and Cancel button to an Alert object
|
|
93
|
-
dialogMessage.addAction(ok)
|
|
94
|
-
dialogMessage.addAction(cancel)
|
|
95
|
-
|
|
96
|
-
// Present alert message to user
|
|
97
|
-
if let root = RCTPresentedViewController() {
|
|
98
|
-
root.present(dialogMessage, animated: true, completion: nil)
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
vc.saveBtnClicked = { (selectedRange: CMTimeRange) in
|
|
105
|
-
if !self.editorConfig.enableSaveDialog {
|
|
106
|
-
self.trim(
|
|
107
|
-
viewController: vc, inputFile: destPath,
|
|
108
|
-
videoDuration: self.vc!.asset!.duration.seconds,
|
|
109
|
-
startTime: selectedRange.start.seconds,
|
|
110
|
-
endTime: selectedRange.end.seconds)
|
|
111
|
-
return
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Create Alert
|
|
115
|
-
let dialogMessage = UIAlertController(
|
|
116
|
-
title: self.editorConfig.saveDialogTitle, message: self.editorConfig.saveDialogMessage,
|
|
117
|
-
preferredStyle: .alert)
|
|
118
|
-
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
119
|
-
|
|
120
|
-
// Create OK button with action handler
|
|
121
|
-
let ok = UIAlertAction(
|
|
122
|
-
title: self.editorConfig.saveDialogConfirmText, style: .default,
|
|
123
|
-
handler: { (action) -> Void in
|
|
124
|
-
self.trim(
|
|
125
|
-
viewController: vc, inputFile: destPath,
|
|
126
|
-
videoDuration: vc.asset!.duration.seconds,
|
|
127
|
-
startTime: selectedRange.start.seconds,
|
|
128
|
-
endTime: selectedRange.end.seconds)
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
// Create Cancel button with action handlder
|
|
132
|
-
let cancel = UIAlertAction(
|
|
133
|
-
title: self.editorConfig.saveDialogCancelText, style: .cancel)
|
|
134
|
-
|
|
135
|
-
//Add OK and Cancel button to an Alert object
|
|
136
|
-
dialogMessage.addAction(ok)
|
|
137
|
-
dialogMessage.addAction(cancel)
|
|
138
|
-
|
|
139
|
-
// Present alert message to user
|
|
140
|
-
if let root = RCTPresentedViewController() {
|
|
141
|
-
root.present(dialogMessage, animated: true, completion: nil)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
vc.isModalInPresentation = true // prevent modal closed by swipe down
|
|
148
|
-
|
|
149
|
-
if editorConfig.fullScreenModalIOS {
|
|
150
|
-
vc.modalPresentationStyle = .fullScreen
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if let root = RCTPresentedViewController() {
|
|
154
|
-
root.present(
|
|
155
|
-
vc, animated: true,
|
|
156
|
-
completion: {
|
|
157
|
-
self.emitEventToJS("onShow", eventData: nil)
|
|
158
|
-
self.isShowing = true
|
|
159
|
-
|
|
160
|
-
// start loading asset after view is finished presenting
|
|
161
|
-
// otherwise it may run too fast for local file and autoplay looks weird
|
|
162
|
-
let assetLoader = AssetLoader()
|
|
163
|
-
assetLoader.delegate = self
|
|
164
|
-
assetLoader.loadAsset(url: destPath, isVideoType: self.isVideoType)
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private func copyFileToDocumentDir(uri: String) -> URL? {
|
|
174
|
-
if let videoURL = URL(string: uri) {
|
|
175
|
-
// Save the video to the document directory
|
|
176
|
-
let documentsDirectory = FileManager.default.urls(
|
|
177
|
-
for: .documentDirectory, in: .userDomainMask
|
|
178
|
-
).first!
|
|
179
|
-
// Extract the file extension from the videoURL
|
|
180
|
-
let fileExtension = videoURL.pathExtension
|
|
181
|
-
|
|
182
|
-
// Define the filename with the correct file extension
|
|
183
|
-
let timestamp = Int(Date().timeIntervalSince1970)
|
|
184
|
-
let destinationURL = documentsDirectory.appendingPathComponent(
|
|
185
|
-
"\(FILE_PREFIX)_original_\(timestamp).\(fileExtension)")
|
|
186
|
-
|
|
187
|
-
do {
|
|
188
|
-
try FileManager.default.copyItem(at: videoURL, to: destinationURL)
|
|
189
|
-
} catch {
|
|
190
|
-
print("Error while copying file to document directory \(error)")
|
|
191
|
-
return nil
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return destinationURL
|
|
195
|
-
} else {
|
|
196
|
-
return nil
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
private func emitEventToJS(_ eventName: String, eventData: [String: String]?) {
|
|
201
|
-
onEvent?(eventName, eventData ?? [:])
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
func listFiles() -> [URL] {
|
|
205
|
-
var files: [URL] = []
|
|
206
|
-
|
|
207
|
-
let documentsDirectory = FileManager.default.urls(
|
|
208
|
-
for: .documentDirectory, in: .userDomainMask
|
|
209
|
-
).first!
|
|
210
|
-
|
|
211
|
-
do {
|
|
212
|
-
let directoryContents = try FileManager.default.contentsOfDirectory(
|
|
213
|
-
at: documentsDirectory, includingPropertiesForKeys: nil)
|
|
214
|
-
|
|
215
|
-
for fileURL in directoryContents {
|
|
216
|
-
let last = fileURL.lastPathComponent
|
|
217
|
-
if last.starts(with: FILE_PREFIX) || last.starts(with: BEFORE_TRIM_PREFIX) {
|
|
218
|
-
files.append(fileURL)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
} catch {
|
|
222
|
-
print("[listFiles] Error when retrieving files: \(error)")
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return files
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
func deleteFile(url: URL) -> Int {
|
|
229
|
-
do {
|
|
230
|
-
if FileManager.default.fileExists(atPath: url.path) {
|
|
231
|
-
try FileManager.default.removeItem(at: url)
|
|
232
|
-
|
|
233
|
-
return 0
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return 1
|
|
237
|
-
} catch {
|
|
238
|
-
print("[deleteFile] Error deleting files: \(error)")
|
|
239
|
-
|
|
240
|
-
return 2
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
private func trim(
|
|
245
|
-
viewController: VideoTrimmerViewController, inputFile: URL,
|
|
246
|
-
videoDuration: Double, startTime: Double, endTime: Double
|
|
247
|
-
) {
|
|
248
|
-
vc?.pausePlayer()
|
|
249
|
-
|
|
250
|
-
let timestamp = Int(Date().timeIntervalSince1970)
|
|
251
|
-
let outputName = "\(FILE_PREFIX)_\(timestamp).\(editorConfig.outputExt)"
|
|
252
|
-
let documentsDirectory = FileManager.default.urls(
|
|
253
|
-
for: .documentDirectory, in: .userDomainMask
|
|
254
|
-
).first!
|
|
255
|
-
outputFile = documentsDirectory.appendingPathComponent(outputName)
|
|
256
|
-
|
|
257
|
-
let formatter = DateFormatter()
|
|
258
|
-
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"
|
|
259
|
-
formatter.timeZone = TimeZone(identifier: "UTC")
|
|
260
|
-
let dateTime = formatter.string(from: Date())
|
|
261
|
-
|
|
262
|
-
emitEventToJS("onStartTrimming", eventData: nil)
|
|
263
|
-
|
|
264
|
-
var ffmpegSession: FFmpegSession?
|
|
265
|
-
let progressAlert = ProgressAlertController()
|
|
266
|
-
progressAlert.modalPresentationStyle = .overFullScreen
|
|
267
|
-
progressAlert.modalTransitionStyle = .crossDissolve
|
|
268
|
-
progressAlert.setTitle(editorConfig.trimmingText)
|
|
269
|
-
|
|
270
|
-
if editorConfig.enableCancelTrimming {
|
|
271
|
-
progressAlert.setCancelTitle(editorConfig.cancelTrimmingButtonText)
|
|
272
|
-
progressAlert.showCancelBtn()
|
|
273
|
-
progressAlert.onDismiss = {
|
|
274
|
-
if self.editorConfig.enableCancelTrimmingDialog {
|
|
275
|
-
let dialogMessage = UIAlertController(
|
|
276
|
-
title: self.editorConfig.cancelTrimmingDialogTitle,
|
|
277
|
-
message: self.editorConfig.cancelTrimmingDialogMessage, preferredStyle: .alert)
|
|
278
|
-
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
279
|
-
|
|
280
|
-
// Create OK button with action handler
|
|
281
|
-
let ok = UIAlertAction(
|
|
282
|
-
title: self.editorConfig.cancelDialogConfirmText, style: .destructive,
|
|
283
|
-
handler: { (action) -> Void in
|
|
284
|
-
|
|
285
|
-
if let ffmpegSession = ffmpegSession {
|
|
286
|
-
ffmpegSession.cancel()
|
|
287
|
-
} else {
|
|
288
|
-
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
progressAlert.dismiss(animated: true)
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
// Create Cancel button with action handlder
|
|
295
|
-
let cancel = UIAlertAction(
|
|
296
|
-
title: self.editorConfig.cancelDialogCancelText, style: .cancel)
|
|
297
|
-
|
|
298
|
-
//Add OK and Cancel button to an Alert object
|
|
299
|
-
dialogMessage.addAction(ok)
|
|
300
|
-
dialogMessage.addAction(cancel)
|
|
301
|
-
|
|
302
|
-
// Present alert message to user
|
|
303
|
-
if let root = RCTPresentedViewController() {
|
|
304
|
-
root.present(dialogMessage, animated: true, completion: nil)
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
} else {
|
|
309
|
-
if let ffmpegSession = ffmpegSession {
|
|
310
|
-
ffmpegSession.cancel()
|
|
311
|
-
} else {
|
|
312
|
-
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
progressAlert.dismiss(animated: true)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if let root = RCTPresentedViewController() {
|
|
322
|
-
root.present(progressAlert, animated: true, completion: nil)
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
var cmds = [
|
|
326
|
-
"-ss",
|
|
327
|
-
"\(startTime * 1000)ms",
|
|
328
|
-
"-to",
|
|
329
|
-
"\(endTime * 1000)ms",
|
|
330
|
-
]
|
|
331
|
-
|
|
332
|
-
if self.editorConfig.enableRotation {
|
|
333
|
-
cmds = cmds + [
|
|
334
|
-
"-display_rotation",
|
|
335
|
-
"\(self.editorConfig.rotationAngle)",
|
|
336
|
-
]
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
cmds = cmds + [
|
|
340
|
-
"-i",
|
|
341
|
-
"\(inputFile)",
|
|
342
|
-
"-c",
|
|
343
|
-
"copy",
|
|
344
|
-
"-metadata",
|
|
345
|
-
"creation_time=\(dateTime)",
|
|
346
|
-
outputFile!.absoluteString,
|
|
347
|
-
]
|
|
348
|
-
|
|
349
|
-
print("Command: ", cmds.joined(separator: " "))
|
|
350
|
-
|
|
351
|
-
let eventPayload: [String: String] = [
|
|
352
|
-
"command": cmds.joined(separator: " ")
|
|
353
|
-
]
|
|
354
|
-
self.emitEventToJS("onLog", eventData: eventPayload)
|
|
355
|
-
|
|
356
|
-
ffmpegSession = FFmpegKit.execute(
|
|
357
|
-
withArgumentsAsync: cmds,
|
|
358
|
-
withCompleteCallback: { session in
|
|
359
|
-
|
|
360
|
-
// always hide progressAlert
|
|
361
|
-
DispatchQueue.main.async {
|
|
362
|
-
// need to wait for it to fully dimissed before presenting new viewcontroller
|
|
363
|
-
progressAlert.dismiss(animated: true, completion: {
|
|
364
|
-
DispatchQueue.global(qos: .default).async {
|
|
365
|
-
let state = session?.getState()
|
|
366
|
-
let returnCode = session?.getReturnCode()
|
|
367
|
-
|
|
368
|
-
if ReturnCode.isSuccess(returnCode) {
|
|
369
|
-
let eventPayload: [String: String] = [
|
|
370
|
-
"outputPath": self.outputFile!.absoluteString,
|
|
371
|
-
"startTime": String((startTime * 1000).rounded()),
|
|
372
|
-
"endTime": String((endTime * 1000).rounded()),
|
|
373
|
-
"duration": String((videoDuration * 1000).rounded()),
|
|
374
|
-
]
|
|
375
|
-
self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
|
|
376
|
-
|
|
377
|
-
if self.editorConfig.saveToPhoto && self.isVideoType {
|
|
378
|
-
PHPhotoLibrary.requestAuthorization { status in
|
|
379
|
-
guard status == .authorized else {
|
|
380
|
-
self.onError(
|
|
381
|
-
message: "Permission to access Photo Library is not granted",
|
|
382
|
-
code: .noPhotoPermission)
|
|
383
|
-
return
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
PHPhotoLibrary.shared().performChanges({
|
|
387
|
-
let request =
|
|
388
|
-
PHAssetChangeRequest.creationRequestForAssetFromVideo(
|
|
389
|
-
atFileURL: self.outputFile!)
|
|
390
|
-
request?.creationDate = Date()
|
|
391
|
-
}) { success, error in
|
|
392
|
-
if success {
|
|
393
|
-
print("Edited video saved to Photo Library successfully.")
|
|
394
|
-
|
|
395
|
-
if self.editorConfig.removeAfterSavedToPhoto {
|
|
396
|
-
let _ = self.deleteFile(url: self.outputFile!)
|
|
397
|
-
}
|
|
398
|
-
} else {
|
|
399
|
-
self.onError(
|
|
400
|
-
message:
|
|
401
|
-
"Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")",
|
|
402
|
-
code: .failToSaveToPhoto)
|
|
403
|
-
if self.editorConfig.removeAfterFailedToSavePhoto {
|
|
404
|
-
let _ = self.deleteFile(url: self.outputFile!)
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
} else if self.editorConfig.openDocumentsOnFinish {
|
|
410
|
-
self.saveFileToFilesApp(fileURL: self.outputFile!)
|
|
411
|
-
|
|
412
|
-
// must return otherwise editor will close
|
|
413
|
-
return
|
|
414
|
-
} else if self.editorConfig.openShareSheetOnFinish {
|
|
415
|
-
self.shareFile(fileURL: self.outputFile!)
|
|
416
|
-
|
|
417
|
-
// must return otherwise editor will close
|
|
418
|
-
return
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if self.editorConfig.closeWhenFinish {
|
|
422
|
-
self.closeEditor()
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
} else if ReturnCode.isCancel(returnCode) {
|
|
426
|
-
// CANCEL
|
|
427
|
-
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
428
|
-
} else {
|
|
429
|
-
// FAILURE
|
|
430
|
-
self.onError(
|
|
431
|
-
message:
|
|
432
|
-
"Command failed with state \(String(describing: FFmpegKitConfig.sessionState(toString: state ?? .failed))) and rc \(String(describing: returnCode)).\(String(describing: session?.getFailStackTrace()))",
|
|
433
|
-
code: .trimmingFailed)
|
|
434
|
-
if self.editorConfig.closeWhenFinish {
|
|
435
|
-
self.closeEditor()
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
})
|
|
440
|
-
}
|
|
441
|
-
},
|
|
442
|
-
withLogCallback: { log in
|
|
443
|
-
guard let log = log else { return }
|
|
444
|
-
|
|
445
|
-
print("FFmpeg process started with log " + (log.getMessage()))
|
|
446
|
-
|
|
447
|
-
let eventPayload: [String: String] = [
|
|
448
|
-
"level": String(log.getLevel()),
|
|
449
|
-
"message": log.getMessage() ?? "",
|
|
450
|
-
"sessionId": String(log.getSessionId()),
|
|
451
|
-
]
|
|
452
|
-
self.emitEventToJS("onLog", eventData: eventPayload)
|
|
453
|
-
|
|
454
|
-
},
|
|
455
|
-
withStatisticsCallback: { statistics in
|
|
456
|
-
guard let statistics = statistics else { return }
|
|
457
|
-
|
|
458
|
-
let timeInMilliseconds = statistics.getTime()
|
|
459
|
-
if timeInMilliseconds > 0 {
|
|
460
|
-
let completePercentage = timeInMilliseconds / (videoDuration * 1000) // from 0 -> 1
|
|
461
|
-
DispatchQueue.main.async {
|
|
462
|
-
progressAlert.setProgress(Float(completePercentage))
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
let eventPayload: [String: String] = [
|
|
467
|
-
"sessionId": String(statistics.getSessionId()),
|
|
468
|
-
"videoFrameNumber": String(statistics.getVideoFrameNumber()),
|
|
469
|
-
"videoFps": String(statistics.getVideoFps()),
|
|
470
|
-
"videoQuality": String(statistics.getVideoQuality()),
|
|
471
|
-
"size": String(statistics.getSize()),
|
|
472
|
-
"time": String(statistics.getTime()),
|
|
473
|
-
"bitrate": String(statistics.getBitrate()),
|
|
474
|
-
"speed": String(statistics.getSpeed()),
|
|
475
|
-
]
|
|
476
|
-
self.emitEventToJS("onStatistics", eventData: eventPayload)
|
|
477
|
-
})
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
private func saveFileToFilesApp(fileURL: URL) {
|
|
481
|
-
DispatchQueue.main.async {
|
|
482
|
-
let documentPicker = UIDocumentPickerViewController(
|
|
483
|
-
url: fileURL, in: .exportToService)
|
|
484
|
-
documentPicker.delegate = self
|
|
485
|
-
documentPicker.modalPresentationStyle = .formSheet
|
|
486
|
-
|
|
487
|
-
if let root = RCTPresentedViewController() {
|
|
488
|
-
root.present(documentPicker, animated: true, completion: nil)
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
private func shareFile(fileURL: URL) {
|
|
494
|
-
DispatchQueue.main.async {
|
|
495
|
-
// Create an instance of UIActivityViewController
|
|
496
|
-
let activityViewController = UIActivityViewController(
|
|
497
|
-
activityItems: [fileURL], applicationActivities: nil)
|
|
498
|
-
|
|
499
|
-
activityViewController.completionWithItemsHandler = {
|
|
500
|
-
activityType, completed, returnedItems, error in
|
|
501
|
-
|
|
502
|
-
if let error = error {
|
|
503
|
-
let message = "Sharing error: \(error.localizedDescription)"
|
|
504
|
-
print(message)
|
|
505
|
-
self.onError(message: message, code: .failToShare)
|
|
506
|
-
|
|
507
|
-
if self.editorConfig.removeAfterFailedToShare {
|
|
508
|
-
let _ = self.deleteFile(url: fileURL)
|
|
509
|
-
}
|
|
510
|
-
return
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if completed {
|
|
514
|
-
print("User completed the sharing activity")
|
|
515
|
-
if self.editorConfig.removeAfterShared {
|
|
516
|
-
let _ = self.deleteFile(url: fileURL)
|
|
517
|
-
}
|
|
518
|
-
} else {
|
|
519
|
-
print("User cancelled or failed to complete the sharing activity")
|
|
520
|
-
if self.editorConfig.removeAfterFailedToShare {
|
|
521
|
-
let _ = self.deleteFile(url: fileURL)
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
self.closeEditor()
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Present the share sheet
|
|
530
|
-
if let root = RCTPresentedViewController() {
|
|
531
|
-
root.present(activityViewController, animated: true, completion: nil)
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
private func shareFile(fileURL: URL, options: TrimOptions) {
|
|
539
|
-
DispatchQueue.main.async {
|
|
540
|
-
// Create an instance of UIActivityViewController
|
|
541
|
-
let activityViewController = UIActivityViewController(
|
|
542
|
-
activityItems: [fileURL], applicationActivities: nil)
|
|
543
|
-
|
|
544
|
-
activityViewController.completionWithItemsHandler = {
|
|
545
|
-
activityType, completed, returnedItems, error in
|
|
546
|
-
|
|
547
|
-
if let error = error {
|
|
548
|
-
let message = "Sharing error: \(error.localizedDescription)"
|
|
549
|
-
print(message)
|
|
550
|
-
|
|
551
|
-
if options.removeAfterFailedToShare {
|
|
552
|
-
let _ = self.deleteFile(url: fileURL)
|
|
553
|
-
}
|
|
554
|
-
return
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
if completed {
|
|
558
|
-
print("User completed the sharing activity")
|
|
559
|
-
if options.removeAfterShared {
|
|
560
|
-
let _ = self.deleteFile(url: fileURL)
|
|
561
|
-
}
|
|
562
|
-
} else {
|
|
563
|
-
print("User cancelled or failed to complete the sharing activity")
|
|
564
|
-
if options.removeAfterFailedToShare {
|
|
565
|
-
let _ = self.deleteFile(url: fileURL)
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Present the share sheet
|
|
571
|
-
if let root = RCTPresentedViewController() {
|
|
572
|
-
root.present(activityViewController, animated: true, completion: nil)
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
func closeEditor(_ onComplete: (() -> Void)? = nil) {
|
|
578
|
-
guard let vc = vc else { return }
|
|
579
|
-
// some how in case we trim a very short video the view controller is still visible after first .dismiss call
|
|
580
|
-
// even the file is successfully saved
|
|
581
|
-
// that's why we need a small delay here to ensure vc will be dismissed
|
|
582
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
583
|
-
vc.dismiss(
|
|
584
|
-
animated: true,
|
|
585
|
-
completion: {
|
|
586
|
-
// self.emitEventToJS("onHide", eventData: ["": ""])
|
|
587
|
-
onComplete?()
|
|
588
|
-
self.isShowing = false
|
|
589
|
-
})
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
func isValidFile(uri: String) async -> FileValidationResult {
|
|
594
|
-
let fileURL = URL(string: uri)!
|
|
595
|
-
let result = await checkFileValidity(url: fileURL)
|
|
596
|
-
|
|
597
|
-
if result.isValid {
|
|
598
|
-
print("Valid \(result.fileType) file with duration: \(result.duration) milliseconds")
|
|
599
|
-
} else {
|
|
600
|
-
print("Invalid file")
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
return FileValidationResult(isValid: result.isValid, fileType: result.fileType, duration: result.duration)
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
func trim(url: String, options: TrimOptions) async throws -> String {
|
|
607
|
-
let timestamp = Int(Date().timeIntervalSince1970)
|
|
608
|
-
let outputName = "\(FILE_PREFIX)_\(timestamp).\(options.outputExt)"
|
|
609
|
-
let documentsDirectory = FileManager.default.urls(
|
|
610
|
-
for: .documentDirectory, in: .userDomainMask
|
|
611
|
-
).first!
|
|
612
|
-
let outputPath = documentsDirectory.appendingPathComponent(outputName)
|
|
613
|
-
|
|
614
|
-
let formatter = DateFormatter()
|
|
615
|
-
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"
|
|
616
|
-
formatter.timeZone = TimeZone(identifier: "UTC")
|
|
617
|
-
let dateTime = formatter.string(from: Date())
|
|
618
|
-
|
|
619
|
-
return try await withCheckedThrowingContinuation { continuation in
|
|
620
|
-
var destPath = url
|
|
621
|
-
if !(url.starts(with: "http://") || url.starts(with: "https://")) {
|
|
622
|
-
let renamed = renameFile(at: URL(string: url)!, newName: "\(BEFORE_TRIM_PREFIX)_\(timestamp)")
|
|
623
|
-
|
|
624
|
-
guard let r = renamed else {
|
|
625
|
-
continuation.resume(
|
|
626
|
-
throwing: NSError(
|
|
627
|
-
domain: "VideoTrim",
|
|
628
|
-
code: -996,
|
|
629
|
-
userInfo: [
|
|
630
|
-
NSLocalizedDescriptionKey: "Fail to rename file"
|
|
631
|
-
]
|
|
632
|
-
)
|
|
633
|
-
)
|
|
634
|
-
return
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
destPath = r.absoluteString
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
var cmds = [
|
|
641
|
-
"-ss",
|
|
642
|
-
"\(options.startTime)ms",
|
|
643
|
-
"-to",
|
|
644
|
-
"\(options.endTime)ms",
|
|
645
|
-
]
|
|
646
|
-
|
|
647
|
-
if options.enableRotation {
|
|
648
|
-
cmds = cmds + [
|
|
649
|
-
"-display_rotation",
|
|
650
|
-
"\(options.rotationAngle)",
|
|
651
|
-
]
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
cmds = cmds + [
|
|
655
|
-
"-i",
|
|
656
|
-
"\(destPath)",
|
|
657
|
-
"-c",
|
|
658
|
-
"copy",
|
|
659
|
-
"-metadata",
|
|
660
|
-
"creation_time=\(dateTime)",
|
|
661
|
-
outputPath.absoluteString,
|
|
662
|
-
]
|
|
663
|
-
|
|
664
|
-
print("Command: ", cmds.joined(separator: " "))
|
|
665
|
-
|
|
666
|
-
FFmpegKit.execute(
|
|
667
|
-
withArgumentsAsync: cmds,
|
|
668
|
-
withCompleteCallback: { session in
|
|
669
|
-
let state = session?.getState()
|
|
670
|
-
let returnCode = session?.getReturnCode()
|
|
671
|
-
if ReturnCode.isSuccess(returnCode) {
|
|
672
|
-
if options.saveToPhoto && (options.type == "video") {
|
|
673
|
-
PHPhotoLibrary.requestAuthorization { status in
|
|
674
|
-
guard status == .authorized else {
|
|
675
|
-
continuation.resume(
|
|
676
|
-
throwing: NSError(
|
|
677
|
-
domain: "VideoTrim",
|
|
678
|
-
code: -998,
|
|
679
|
-
userInfo: [
|
|
680
|
-
NSLocalizedDescriptionKey: "Permission to access Photo Library is not granted"
|
|
681
|
-
]
|
|
682
|
-
)
|
|
683
|
-
)
|
|
684
|
-
return
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
PHPhotoLibrary.shared().performChanges({
|
|
688
|
-
let request =
|
|
689
|
-
PHAssetChangeRequest.creationRequestForAssetFromVideo(
|
|
690
|
-
atFileURL: outputPath)
|
|
691
|
-
request?.creationDate = Date()
|
|
692
|
-
}) { success, error in
|
|
693
|
-
if success {
|
|
694
|
-
print("Edited video saved to Photo Library successfully.")
|
|
695
|
-
|
|
696
|
-
if options.removeAfterSavedToPhoto {
|
|
697
|
-
let _ = self.deleteFile(url: outputPath)
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
continuation.resume(returning: outputPath.absoluteString)
|
|
701
|
-
} else {
|
|
702
|
-
if options.removeAfterFailedToSavePhoto {
|
|
703
|
-
let _ = self.deleteFile(url: outputPath)
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
continuation.resume(
|
|
707
|
-
throwing: NSError(
|
|
708
|
-
domain: "VideoTrim",
|
|
709
|
-
code: -997,
|
|
710
|
-
userInfo: [
|
|
711
|
-
NSLocalizedDescriptionKey: "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")"
|
|
712
|
-
]
|
|
713
|
-
)
|
|
714
|
-
)
|
|
715
|
-
return
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
} else {
|
|
722
|
-
if options.openDocumentsOnFinish {
|
|
723
|
-
self.saveFileToFilesApp(fileURL: outputPath)
|
|
724
|
-
} else if options.openShareSheetOnFinish {
|
|
725
|
-
self.shareFile(fileURL: outputPath)
|
|
726
|
-
}
|
|
727
|
-
continuation.resume(returning: outputPath.absoluteString)
|
|
728
|
-
}
|
|
729
|
-
} else if ReturnCode.isCancel(returnCode) {
|
|
730
|
-
// CANCEL
|
|
731
|
-
continuation.resume(
|
|
732
|
-
throwing: NSError(
|
|
733
|
-
domain: "VideoTrim",
|
|
734
|
-
code: -999,
|
|
735
|
-
userInfo: [
|
|
736
|
-
NSLocalizedDescriptionKey: "Trimming cancelled"
|
|
737
|
-
]
|
|
738
|
-
)
|
|
739
|
-
)
|
|
740
|
-
} else {
|
|
741
|
-
// FAILURE
|
|
742
|
-
continuation.resume(
|
|
743
|
-
throwing: NSError(
|
|
744
|
-
domain: "VideoTrim",
|
|
745
|
-
code: -1,
|
|
746
|
-
userInfo: [
|
|
747
|
-
NSLocalizedDescriptionKey: "Command failed with state \(String(describing: FFmpegKitConfig.sessionState(toString: state ?? .failed))) and rc \(String(describing: returnCode)).\(String(describing: session?.getFailStackTrace()))"
|
|
748
|
-
]
|
|
749
|
-
)
|
|
750
|
-
)
|
|
751
|
-
}
|
|
752
|
-
},
|
|
753
|
-
withLogCallback: { log in
|
|
754
|
-
print("FFmpeg process started with log " + (log!.getMessage()))
|
|
755
|
-
},
|
|
756
|
-
withStatisticsCallback: { statistics in
|
|
757
|
-
})
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
private func onError(message: String, code: ErrorCode) {
|
|
762
|
-
let eventPayload: [String: String] = [
|
|
763
|
-
"message": message,
|
|
764
|
-
"errorCode": code.rawValue,
|
|
765
|
-
]
|
|
766
|
-
self.emitEventToJS("onError", eventData: eventPayload)
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
private func checkFileValidity(url: URL) async -> FileValidationResult {
|
|
770
|
-
let asset = AVAsset(url: url)
|
|
771
|
-
|
|
772
|
-
do {
|
|
773
|
-
// Load duration and tracks asynchronously
|
|
774
|
-
let (duration, tracks) = try await asset.load(.duration, .tracks)
|
|
775
|
-
// Check for video or audio tracks
|
|
776
|
-
let videoTracks = tracks.filter { $0.mediaType == .video }
|
|
777
|
-
let audioTracks = tracks.filter { $0.mediaType == .audio }
|
|
778
|
-
|
|
779
|
-
let isValid = !videoTracks.isEmpty || !audioTracks.isEmpty
|
|
780
|
-
let fileType: String
|
|
781
|
-
if !videoTracks.isEmpty {
|
|
782
|
-
fileType = "video"
|
|
783
|
-
} else if !audioTracks.isEmpty {
|
|
784
|
-
fileType = "audio"
|
|
785
|
-
} else {
|
|
786
|
-
fileType = "unknown"
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
let durationMs = CMTimeGetSeconds(duration) * 1000
|
|
790
|
-
|
|
791
|
-
return FileValidationResult(
|
|
792
|
-
isValid: isValid,
|
|
793
|
-
fileType: fileType,
|
|
794
|
-
duration: isValid ? durationMs.rounded() : -1
|
|
795
|
-
)
|
|
796
|
-
} catch {
|
|
797
|
-
return FileValidationResult(isValid: false, fileType: "unknown", duration: -1)
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
private func renameFile(at url: URL, newName: String) -> URL? {
|
|
803
|
-
let fileManager = FileManager.default
|
|
804
|
-
|
|
805
|
-
// Get the directory of the existing file
|
|
806
|
-
let directory = url.deletingLastPathComponent()
|
|
807
|
-
|
|
808
|
-
// Get the file extension
|
|
809
|
-
let fileExtension = url.pathExtension
|
|
810
|
-
|
|
811
|
-
// Create the new file URL with the new name and the same extension
|
|
812
|
-
let newFileURL = directory.appendingPathComponent(newName)
|
|
813
|
-
.appendingPathExtension(fileExtension)
|
|
814
|
-
|
|
815
|
-
// Check if a file with the new name already exists
|
|
816
|
-
if fileManager.fileExists(atPath: newFileURL.path) {
|
|
817
|
-
do {
|
|
818
|
-
// If the file exists, remove it first to avoid conflicts
|
|
819
|
-
try fileManager.removeItem(at: newFileURL)
|
|
820
|
-
} catch {
|
|
821
|
-
print("Error removing existing file: \(error)")
|
|
822
|
-
return nil
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
do {
|
|
827
|
-
// Rename (move) the file
|
|
828
|
-
try fileManager.moveItem(at: url, to: newFileURL)
|
|
829
|
-
print("File renamed successfully to \(newFileURL.absoluteString)")
|
|
830
|
-
return newFileURL
|
|
831
|
-
} catch {
|
|
832
|
-
print("Error renaming file: \(error)")
|
|
833
|
-
return nil
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
extension VideoTrimImpl: AssetLoaderDelegate {
|
|
839
|
-
func assetLoader(
|
|
840
|
-
_ loader: AssetLoader, didFailWithError error: any Error, forKey key: String
|
|
841
|
-
) {
|
|
842
|
-
let message = "Failed to load \(key): \(error.localizedDescription)"
|
|
843
|
-
print("Failed to load \(key)", message)
|
|
844
|
-
|
|
845
|
-
self.onError(message: message, code: .failToLoadMedia)
|
|
846
|
-
vc?.onAssetFailToLoad()
|
|
847
|
-
|
|
848
|
-
if editorConfig.alertOnFailToLoad {
|
|
849
|
-
let dialogMessage = UIAlertController(
|
|
850
|
-
title: editorConfig.alertOnFailTitle, message: editorConfig.alertOnFailMessage,
|
|
851
|
-
preferredStyle: .alert)
|
|
852
|
-
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
853
|
-
|
|
854
|
-
// Create Cancel button with action handlder
|
|
855
|
-
let ok = UIAlertAction(title: editorConfig.alertOnFailCloseText, style: .default)
|
|
856
|
-
|
|
857
|
-
//Add OK and Cancel button to an Alert object
|
|
858
|
-
dialogMessage.addAction(ok)
|
|
859
|
-
|
|
860
|
-
// Present alert message to user
|
|
861
|
-
|
|
862
|
-
if let root = RCTPresentedViewController() {
|
|
863
|
-
root.present(dialogMessage, animated: true, completion: nil)
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
func assetLoaderDidSucceed(_ loader: AssetLoader) {
|
|
869
|
-
print("Asset loaded successfully")
|
|
870
|
-
|
|
871
|
-
vc?.asset = loader.asset
|
|
872
|
-
|
|
873
|
-
let eventPayload: [String: String] = [
|
|
874
|
-
"duration": String(loader.asset!.duration.seconds * 1000)
|
|
875
|
-
]
|
|
876
|
-
self.emitEventToJS("onLoad", eventData: eventPayload)
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
extension VideoTrimImpl: UIDocumentPickerDelegate {
|
|
881
|
-
func documentPicker(
|
|
882
|
-
_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]
|
|
883
|
-
) {
|
|
884
|
-
if editorConfig.removeAfterSavedToDocuments {
|
|
885
|
-
let _ = deleteFile(url: outputFile!)
|
|
886
|
-
}
|
|
887
|
-
closeEditor()
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController)
|
|
891
|
-
{
|
|
892
|
-
if editorConfig.removeAfterFailedToSaveDocuments {
|
|
893
|
-
let _ = deleteFile(url: outputFile!)
|
|
894
|
-
}
|
|
895
|
-
closeEditor()
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// Because somehow we can't import React here to swift (podspec React-Core or bridging header doesn't work)
|
|
900
|
-
// hence we'll need to reimplement RCTPresentedViewController
|
|
901
|
-
|
|
902
|
-
// Equivalent to RCTSharedApplication
|
|
903
|
-
func RCTSharedApplication() -> UIApplication? {
|
|
904
|
-
// Safely access the shared UIApplication instance
|
|
905
|
-
return UIApplication.perform(NSSelectorFromString("sharedApplication"))?.takeUnretainedValue() as? UIApplication
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// Equivalent to RCTKeyWindow
|
|
909
|
-
func RCTKeyWindow() -> UIWindow? {
|
|
910
|
-
guard let sharedApp = RCTSharedApplication() else {
|
|
911
|
-
return nil
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
let connectedScenes = sharedApp.connectedScenes
|
|
915
|
-
var foregroundActiveScene: UIScene?
|
|
916
|
-
var foregroundInactiveScene: UIScene?
|
|
917
|
-
|
|
918
|
-
for scene in connectedScenes {
|
|
919
|
-
guard scene is UIWindowScene else {
|
|
920
|
-
continue
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
if scene.activationState == .foregroundActive {
|
|
924
|
-
foregroundActiveScene = scene
|
|
925
|
-
break
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
if foregroundInactiveScene == nil && scene.activationState == .foregroundInactive {
|
|
929
|
-
foregroundInactiveScene = scene
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
let sceneToUse = foregroundActiveScene ?? foregroundInactiveScene
|
|
934
|
-
|
|
935
|
-
if let windowScene = sceneToUse as? UIWindowScene {
|
|
936
|
-
return windowScene.keyWindow
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
return nil
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// Equivalent to RCTPresentedViewController
|
|
943
|
-
func RCTPresentedViewController() -> UIViewController? {
|
|
944
|
-
guard let rootController = RCTKeyWindow()?.rootViewController else {
|
|
945
|
-
return nil
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
var controller = rootController
|
|
949
|
-
var presentedController = controller.presentedViewController
|
|
950
|
-
|
|
951
|
-
while presentedController != nil && !presentedController!.isBeingDismissed {
|
|
952
|
-
controller = presentedController!
|
|
953
|
-
presentedController = controller.presentedViewController
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
return controller
|
|
957
|
-
}
|