react-native-video-trim 3.0.9 → 4.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/LICENSE +1 -1
- package/README.md +64 -61
- package/VideoTrim.podspec +24 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +77 -51
- package/android/gradle.properties +5 -5
- package/android/src/main/AndroidManifest.xml +4 -2
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/videotrim/VideoTrim.kt +629 -0
- package/android/src/main/java/com/margelo/nitro/videotrim/VideoTrimPackage.kt +22 -0
- package/android/src/main/java/com/{videotrim → margelo/nitro/videotrim}/enums/ErrorCode.java +1 -1
- package/android/src/main/java/com/{videotrim → margelo/nitro/videotrim}/interfaces/IVideoTrimmerView.java +1 -1
- package/android/src/main/java/com/{videotrim → margelo/nitro/videotrim}/interfaces/VideoTrimListener.java +6 -4
- package/android/src/main/java/com/{videotrim → margelo/nitro/videotrim}/utils/MediaMetadataUtil.java +1 -1
- package/android/src/main/java/com/{videotrim → margelo/nitro/videotrim}/utils/StorageUtil.java +3 -1
- package/android/src/main/java/com/margelo/nitro/videotrim/utils/VideoTrimmerUtil.java +157 -0
- package/android/src/main/java/com/{videotrim → margelo/nitro/videotrim}/widgets/VideoTrimmerView.java +44 -72
- package/ios/AssetLoader.swift +2 -2
- package/ios/ErrorCode.swift +2 -2
- package/ios/ProgressAlertController.swift +2 -2
- package/ios/VideoTrim.swift +38 -739
- package/ios/VideoTrimImpl.swift +860 -0
- package/ios/VideoTrimmer.swift +2 -3
- package/ios/VideoTrimmerThumb.swift +33 -26
- package/ios/VideoTrimmerViewController.swift +47 -28
- package/lib/module/VideoTrim.nitro.js +4 -0
- package/lib/module/VideoTrim.nitro.js.map +1 -0
- package/lib/module/index.js +71 -22
- package/lib/module/index.js.map +1 -1
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/{index.d.ts → src/VideoTrim.nitro.d.ts} +63 -89
- package/lib/typescript/src/VideoTrim.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +41 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitrogen/generated/android/c++/JEditorConfig.hpp +229 -0
- package/nitrogen/generated/android/c++/JFileValidationResult.hpp +61 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +74 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_std__unordered_map_std__string__std__string_.hpp +89 -0
- package/nitrogen/generated/android/c++/JHybridVideoTrimSpec.cpp +131 -0
- package/nitrogen/generated/android/c++/JHybridVideoTrimSpec.hpp +67 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/EditorConfig.kt +70 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/FileValidationResult.kt +28 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/Func_void.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/Func_void_std__string_std__unordered_map_std__string__std__string_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/HybridVideoTrimSpec.kt +82 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/videotrimOnLoad.kt +35 -0
- package/nitrogen/generated/android/videotrim+autolinking.cmake +78 -0
- package/nitrogen/generated/android/videotrim+autolinking.gradle +27 -0
- package/nitrogen/generated/android/videotrimOnLoad.cpp +50 -0
- package/nitrogen/generated/android/videotrimOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/VideoTrim+autolinking.rb +60 -0
- package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Bridge.cpp +88 -0
- package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Bridge.hpp +331 -0
- package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Umbrella.hpp +53 -0
- package/nitrogen/generated/ios/VideoTrimAutolinking.mm +33 -0
- package/nitrogen/generated/ios/VideoTrimAutolinking.swift +25 -0
- package/nitrogen/generated/ios/c++/HybridVideoTrimSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridVideoTrimSpecSwift.hpp +116 -0
- package/nitrogen/generated/ios/swift/EditorConfig.swift +519 -0
- package/nitrogen/generated/ios/swift/FileValidationResult.swift +57 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_FileValidationResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_std__unordered_map_std__string__std__string_.swift +54 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridVideoTrimSpec.swift +53 -0
- package/nitrogen/generated/ios/swift/HybridVideoTrimSpec_cxx.swift +222 -0
- package/nitrogen/generated/shared/c++/EditorConfig.hpp +245 -0
- package/nitrogen/generated/shared/c++/FileValidationResult.hpp +77 -0
- package/nitrogen/generated/shared/c++/HybridVideoTrimSpec.cpp +26 -0
- package/nitrogen/generated/shared/c++/HybridVideoTrimSpec.hpp +76 -0
- package/package.json +75 -71
- package/src/VideoTrim.nitro.ts +244 -0
- package/src/index.tsx +87 -258
- package/android/src/main/AndroidManifestDeprecated.xml +0 -3
- package/android/src/main/java/com/videotrim/VideoTrimModule.java +0 -600
- package/android/src/main/java/com/videotrim/VideoTrimPackage.java +0 -28
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +0 -270
- package/ios/VideoTrim-Bridging-Header.h +0 -2
- package/ios/VideoTrim.mm +0 -17
- package/ios/VideoTrim.xcodeproj/project.pbxproj +0 -283
- package/lib/commonjs/index.js +0 -87
- package/lib/commonjs/index.js.map +0 -1
- package/lib/typescript/index.d.ts.map +0 -1
- package/react-native-video-trim.podspec +0 -41
package/ios/VideoTrim.swift
CHANGED
|
@@ -1,761 +1,60 @@
|
|
|
1
|
-
import
|
|
1
|
+
import NitroModules
|
|
2
|
+
import ffmpegkit
|
|
2
3
|
import Photos
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
@objc(VideoTrim)
|
|
6
|
-
class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDelegate {
|
|
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
|
|
77
|
-
}
|
|
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
|
|
5
|
+
class VideoTrim: HybridVideoTrimSpec {
|
|
104
6
|
|
|
105
|
-
|
|
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
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if let saveButtonText = config["saveButtonText"] as? String, !saveButtonText.isEmpty {
|
|
124
|
-
self.saveButtonText = saveButtonText
|
|
125
|
-
}
|
|
7
|
+
private let impl = VideoTrimImpl()
|
|
126
8
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
9
|
+
public func showEditor(
|
|
10
|
+
filePath: String,
|
|
11
|
+
config: EditorConfig,
|
|
12
|
+
onEvent: @escaping (_ eventName: String, _ payload: Dictionary<String, String>) -> Void
|
|
13
|
+
) throws {
|
|
14
|
+
impl.showEditor(uri: filePath, editorConfig: config, onEvent: onEvent)
|
|
131
15
|
}
|
|
132
16
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
vc.configure(config: config)
|
|
139
|
-
|
|
140
|
-
vc.cancelBtnClicked = {
|
|
141
|
-
if !self.enableCancelDialog {
|
|
142
|
-
self.emitEventToJS("onCancel", eventData: nil)
|
|
143
|
-
|
|
144
|
-
vc.dismiss(animated: true, completion: {
|
|
145
|
-
self.emitEventToJS("onHide", eventData: nil)
|
|
146
|
-
self.isShowing = false
|
|
147
|
-
})
|
|
148
|
-
return
|
|
17
|
+
func listFiles() throws -> Promise<[String]> {
|
|
18
|
+
return Promise.async {
|
|
19
|
+
// This runs on a separate Thread, and can use `await` syntax!
|
|
20
|
+
let files = self.impl.listFiles().map { $0.absoluteString }
|
|
21
|
+
return files
|
|
149
22
|
}
|
|
150
|
-
|
|
151
|
-
// Create Alert
|
|
152
|
-
let dialogMessage = UIAlertController(title: self.cancelDialogTitle, message: self.cancelDialogMessage, preferredStyle: .alert)
|
|
153
|
-
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
154
|
-
|
|
155
|
-
// Create OK button with action handler
|
|
156
|
-
let ok = UIAlertAction(title: self.cancelDialogConfirmText, style: .destructive, handler: { (action) -> Void in
|
|
157
|
-
self.emitEventToJS("onCancel", eventData: nil)
|
|
158
|
-
|
|
159
|
-
vc.dismiss(animated: true, completion: {
|
|
160
|
-
self.emitEventToJS("onHide", eventData: nil)
|
|
161
|
-
self.isShowing = false
|
|
162
|
-
})
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
// Create Cancel button with action handlder
|
|
166
|
-
let cancel = UIAlertAction(title: self.cancelDialogCancelText, style: .cancel)
|
|
167
|
-
|
|
168
|
-
//Add OK and Cancel button to an Alert object
|
|
169
|
-
dialogMessage.addAction(ok)
|
|
170
|
-
dialogMessage.addAction(cancel)
|
|
171
|
-
|
|
172
|
-
// Present alert message to user
|
|
173
|
-
if let root = RCTPresentedViewController() {
|
|
174
|
-
root.present(dialogMessage, animated: true, completion: nil)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
vc.saveBtnClicked = {(selectedRange: CMTimeRange) in
|
|
179
|
-
if !self.enableSaveDialog {
|
|
180
|
-
self.trim(viewController: vc,inputFile: destPath, videoDuration: self.vc!.asset!.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
|
|
181
|
-
return
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Create Alert
|
|
185
|
-
let dialogMessage = UIAlertController(title: self.saveDialogTitle, message: self.saveDialogMessage, preferredStyle: .alert)
|
|
186
|
-
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
187
|
-
|
|
188
|
-
// Create OK button with action handler
|
|
189
|
-
let ok = UIAlertAction(title: self.saveDialogConfirmText, style: .default, handler: { (action) -> Void in
|
|
190
|
-
self.trim(viewController: vc,inputFile: destPath, videoDuration: vc.asset!.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
// Create Cancel button with action handlder
|
|
194
|
-
let cancel = UIAlertAction(title: self.saveDialogCancelText, style: .cancel)
|
|
195
|
-
|
|
196
|
-
//Add OK and Cancel button to an Alert object
|
|
197
|
-
dialogMessage.addAction(ok)
|
|
198
|
-
dialogMessage.addAction(cancel)
|
|
199
|
-
|
|
200
|
-
// Present alert message to user
|
|
201
|
-
if let root = RCTPresentedViewController() {
|
|
202
|
-
root.present(dialogMessage, animated: true, completion: nil)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
vc.isModalInPresentation = true // prevent modal closed by swipe down
|
|
207
|
-
|
|
208
|
-
if self.fullScreenModalIOS {
|
|
209
|
-
vc.modalPresentationStyle = .fullScreen
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if let root = RCTPresentedViewController() {
|
|
213
|
-
root.present(vc, animated: true, completion: {
|
|
214
|
-
self.emitEventToJS("onShow", eventData: nil)
|
|
215
|
-
self.isShowing = true
|
|
216
|
-
|
|
217
|
-
// start loading asset after view is finished presenting
|
|
218
|
-
// otherwise it may run too fast for local file and autoplay looks weird
|
|
219
|
-
let assetLoader = AssetLoader()
|
|
220
|
-
assetLoader.delegate = self
|
|
221
|
-
assetLoader.loadAsset(url: destPath, isVideoType: self.isVideoType)
|
|
222
|
-
})
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private func copyFileToDocumentDir(uri: String) -> URL? {
|
|
229
|
-
if let videoURL = URL(string: uri) {
|
|
230
|
-
// Save the video to the document directory
|
|
231
|
-
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
232
|
-
// Extract the file extension from the videoURL
|
|
233
|
-
let fileExtension = videoURL.pathExtension
|
|
234
|
-
|
|
235
|
-
// Define the filename with the correct file extension
|
|
236
|
-
let timestamp = Int(Date().timeIntervalSince1970)
|
|
237
|
-
let destinationURL = documentsDirectory.appendingPathComponent("\(FILE_PREFIX)_original_\(timestamp).\(fileExtension)")
|
|
238
|
-
|
|
239
|
-
do {
|
|
240
|
-
try FileManager.default.copyItem(at: videoURL, to: destinationURL)
|
|
241
|
-
} catch {
|
|
242
|
-
print("Error while copying file to document directory \(error)")
|
|
243
|
-
return nil
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return destinationURL
|
|
247
|
-
} else {
|
|
248
|
-
return nil
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
private func emitEventToJS(_ eventName: String, eventData: [String: Any]?) {
|
|
253
|
-
if hasListeners {
|
|
254
|
-
var modifiedEventData = eventData ?? [:] // If eventData is nil, create an empty dictionary
|
|
255
|
-
modifiedEventData["name"] = eventName
|
|
256
|
-
sendEvent(withName: "VideoTrim", body: modifiedEventData)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
@objc(listFiles:withRejecter:)
|
|
261
|
-
func listFiles(resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
262
|
-
let files = listFiles()
|
|
263
|
-
resolve(files.map{ $0.absoluteString })
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
@objc(cleanFiles:withRejecter:)
|
|
267
|
-
func cleanFiles(resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
268
|
-
let files = listFiles()
|
|
269
|
-
var successCount = 0
|
|
270
|
-
for file in files {
|
|
271
|
-
let state = deleteFile(url: file)
|
|
272
|
-
|
|
273
|
-
if state == 0 {
|
|
274
|
-
successCount += 1
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
resolve(successCount)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
@objc(deleteFile:withResolver:withRejecter:)
|
|
282
|
-
func deleteFile(uri: String, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
283
|
-
let state = deleteFile(url: URL(string: uri)!)
|
|
284
|
-
resolve(state == 0)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private func listFiles() -> [URL] {
|
|
288
|
-
var files: [URL] = []
|
|
289
|
-
|
|
290
|
-
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
291
|
-
|
|
292
|
-
do {
|
|
293
|
-
let directoryContents = try FileManager.default.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil)
|
|
294
|
-
|
|
295
|
-
for fileURL in directoryContents {
|
|
296
|
-
if fileURL.lastPathComponent.starts(with: FILE_PREFIX) {
|
|
297
|
-
files.append(fileURL)
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
} catch {
|
|
301
|
-
print("[listFiles] Error when retrieving files: \(error)")
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return files
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
private func deleteFile(url: URL) -> Int {
|
|
308
|
-
do {
|
|
309
|
-
if FileManager.default.fileExists(atPath: url.path) {
|
|
310
|
-
try FileManager.default.removeItem(at: url)
|
|
311
|
-
|
|
312
|
-
return 0
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return 1
|
|
316
|
-
} catch {
|
|
317
|
-
print("[deleteFile] Error deleting files: \(error)")
|
|
318
|
-
|
|
319
|
-
return 2
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
private func trim(viewController: VideoTrimmerViewController, inputFile: URL, videoDuration: Double, startTime: Double, endTime: Double) {
|
|
324
|
-
vc?.pausePlayer()
|
|
325
|
-
|
|
326
|
-
// Generate output file URL
|
|
327
|
-
let timestamp = Int(Date().timeIntervalSince1970)
|
|
328
|
-
let inputExtension = inputFile.pathExtension.lowercased()
|
|
329
|
-
let outputName = "\(FILE_PREFIX)_\(timestamp).\(inputExtension)"
|
|
330
|
-
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
331
|
-
outputFile = documentsDirectory.appendingPathComponent(outputName)
|
|
332
|
-
|
|
333
|
-
let formatter = DateFormatter()
|
|
334
|
-
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"
|
|
335
|
-
formatter.timeZone = TimeZone(identifier: "UTC")
|
|
336
|
-
let dateTime = formatter.string(from: Date())
|
|
337
|
-
|
|
338
|
-
emitEventToJS("onStartTrimming", eventData: nil)
|
|
339
|
-
|
|
340
|
-
let progressAlert = ProgressAlertController()
|
|
341
|
-
progressAlert.modalPresentationStyle = .overFullScreen
|
|
342
|
-
progressAlert.modalTransitionStyle = .crossDissolve
|
|
343
|
-
progressAlert.setTitle(trimmingText)
|
|
344
|
-
|
|
345
|
-
if enableCancelTrimming {
|
|
346
|
-
progressAlert.setCancelTitle(cancelTrimmingButtonText)
|
|
347
|
-
progressAlert.showCancelBtn()
|
|
348
|
-
progressAlert.onDismiss = {
|
|
349
|
-
if self.enableCancelTrimmingDialog {
|
|
350
|
-
let dialogMessage = UIAlertController(title: self.cancelTrimmingDialogTitle, message: self.cancelTrimmingDialogMessage, preferredStyle: .alert)
|
|
351
|
-
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
352
|
-
|
|
353
|
-
let ok = UIAlertAction(title: self.cancelDialogConfirmText, style: .destructive) { _ in
|
|
354
|
-
self.exportSession?.cancelExport()
|
|
355
|
-
progressAlert.dismiss(animated: true)
|
|
356
|
-
}
|
|
357
|
-
let cancel = UIAlertAction(title: self.cancelDialogCancelText, style: .cancel)
|
|
358
|
-
dialogMessage.addAction(ok)
|
|
359
|
-
dialogMessage.addAction(cancel)
|
|
360
|
-
|
|
361
|
-
if let root = RCTPresentedViewController() {
|
|
362
|
-
root.present(dialogMessage, animated: true, completion: nil)
|
|
363
|
-
}
|
|
364
|
-
} else {
|
|
365
|
-
self.exportSession?.cancelExport()
|
|
366
|
-
progressAlert.dismiss(animated: true)
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if let root = RCTPresentedViewController() {
|
|
372
|
-
root.present(progressAlert, animated: true, completion: nil)
|
|
373
23
|
}
|
|
374
24
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
exportSession = session
|
|
387
|
-
|
|
388
|
-
var fileType: AVFileType
|
|
389
|
-
switch inputExtension {
|
|
390
|
-
case "mp4": fileType = .mp4
|
|
391
|
-
case "mov": fileType = .mov
|
|
392
|
-
case "m4a": fileType = .m4a
|
|
393
|
-
case "mp3": fileType = .mp3
|
|
394
|
-
case "wav": fileType = .wav
|
|
395
|
-
case "aif", "aiff": fileType = .aiff
|
|
396
|
-
case "caf": fileType = .caf
|
|
397
|
-
default: fileType = .mp4
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
let supportedTypes = session.supportedFileTypes
|
|
401
|
-
let finalOutputURL: URL
|
|
402
|
-
if preset == AVAssetExportPresetAppleM4A && !supportedTypes.contains(fileType) {
|
|
403
|
-
fileType = .m4a
|
|
404
|
-
let adjustedOutputName = "\(FILE_PREFIX)_\(timestamp).m4a"
|
|
405
|
-
finalOutputURL = documentsDirectory.appendingPathComponent(adjustedOutputName)
|
|
406
|
-
print("Adjusting output from '\(inputExtension)' to '.m4a' for audio encoding")
|
|
407
|
-
} else {
|
|
408
|
-
finalOutputURL = outputFile!
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
try? FileManager.default.removeItem(at: finalOutputURL)
|
|
412
|
-
session.outputURL = finalOutputURL
|
|
413
|
-
session.outputFileType = fileType
|
|
414
|
-
session.timeRange = CMTimeRange(start: CMTime(seconds: startTime, preferredTimescale: 600), duration: CMTime(seconds: endTime - startTime, preferredTimescale: 600))
|
|
415
|
-
|
|
416
|
-
// Add creation date metadata
|
|
417
|
-
let metadataItem = AVMutableMetadataItem()
|
|
418
|
-
metadataItem.key = AVMetadataKey.commonKeyCreationDate as NSCopying & NSObjectProtocol
|
|
419
|
-
metadataItem.keySpace = .common
|
|
420
|
-
metadataItem.value = dateTime as NSCopying & NSObjectProtocol
|
|
421
|
-
session.metadata = [metadataItem]
|
|
422
|
-
|
|
423
|
-
// Progress and completion
|
|
424
|
-
let manager = ExportSessionManager(session: session)
|
|
425
|
-
timer = Timer.scheduledTimer(withTimeInterval: progressUpdateInterval, repeats: true) { timer in
|
|
426
|
-
let progress = manager.progress
|
|
427
|
-
DispatchQueue.main.async {
|
|
428
|
-
progressAlert.setProgress(progress)
|
|
429
|
-
}
|
|
430
|
-
let statsPayload: [String: Any] = [
|
|
431
|
-
"time": Int(Double(progress) * videoDuration * 1000),
|
|
432
|
-
"progress": progress
|
|
433
|
-
]
|
|
434
|
-
self.emitEventToJS("onStatistics", eventData: statsPayload)
|
|
435
|
-
if manager.isFinished { timer.invalidate() }
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
session.exportAsynchronously {
|
|
439
|
-
manager.markFinished()
|
|
440
|
-
self.timer?.invalidate()
|
|
441
|
-
self.timer = nil
|
|
442
|
-
|
|
443
|
-
DispatchQueue.main.async {
|
|
444
|
-
progressAlert.dismiss(animated: true)
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
let finalProgress = manager.progress
|
|
448
|
-
if finalProgress >= 1.0 {
|
|
449
|
-
let eventPayload: [String: Any] = [
|
|
450
|
-
"outputPath": finalOutputURL.absoluteString,
|
|
451
|
-
"startTime": (startTime * 1000).rounded(),
|
|
452
|
-
"endTime": (endTime * 1000).rounded(),
|
|
453
|
-
"duration": (videoDuration * 1000).rounded()
|
|
454
|
-
]
|
|
455
|
-
self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
|
|
456
|
-
|
|
457
|
-
if self.saveToPhoto && isVideo {
|
|
458
|
-
PHPhotoLibrary.requestAuthorization { status in
|
|
459
|
-
guard status == .authorized else {
|
|
460
|
-
self.onError(message: "Permission to access Photo Library is not granted", code: .noPhotoPermission)
|
|
461
|
-
return
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
PHPhotoLibrary.shared().performChanges({
|
|
465
|
-
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: finalOutputURL)
|
|
466
|
-
request?.creationDate = Date()
|
|
467
|
-
}) { success, error in
|
|
468
|
-
if success {
|
|
469
|
-
print("Edited video saved to Photo Library successfully.")
|
|
470
|
-
if self.removeAfterSavedToPhoto {
|
|
471
|
-
let _ = self.deleteFile(url: finalOutputURL)
|
|
472
|
-
}
|
|
473
|
-
} else {
|
|
474
|
-
print(error ?? "Error saving edited video to Photo Library")
|
|
475
|
-
self.onError(message: "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")", code: .failToSaveToPhoto)
|
|
476
|
-
if self.removeAfterFailedToSavePhoto {
|
|
477
|
-
let _ = self.deleteFile(url: finalOutputURL)
|
|
25
|
+
public func cleanFiles() throws -> Promise<Double> {
|
|
26
|
+
return Promise.async {
|
|
27
|
+
// This runs on a separate Thread, and can use `await` syntax!
|
|
28
|
+
let files = self.impl.listFiles()
|
|
29
|
+
var successCount = 0
|
|
30
|
+
for file in files {
|
|
31
|
+
let state = self.impl.deleteFile(url: file)
|
|
32
|
+
|
|
33
|
+
if state == 0 {
|
|
34
|
+
successCount += 1
|
|
478
35
|
}
|
|
479
|
-
}
|
|
480
36
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
self.saveFileToFilesApp(fileURL: finalOutputURL)
|
|
484
|
-
return
|
|
485
|
-
} else if self.openShareSheetOnFinish {
|
|
486
|
-
self.shareFile(fileURL: finalOutputURL)
|
|
487
|
-
return
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if self.closeWhenFinish {
|
|
491
|
-
self.closeEditor()
|
|
492
|
-
}
|
|
493
|
-
} else if let error = manager.error {
|
|
494
|
-
if self.exportSession?.status == .cancelled {
|
|
495
|
-
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
496
|
-
} else {
|
|
497
|
-
print(error)
|
|
498
|
-
self.onError(message: "Trimming failed: \(error.localizedDescription)", code: .trimmingFailed)
|
|
499
|
-
if self.closeWhenFinish {
|
|
500
|
-
self.closeEditor()
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
} else {
|
|
504
|
-
self.onError(message: "Export failed or was cancelled", code: .trimmingFailed)
|
|
505
|
-
if self.closeWhenFinish {
|
|
506
|
-
self.closeEditor()
|
|
37
|
+
|
|
38
|
+
return Double(successCount)
|
|
507
39
|
}
|
|
508
|
-
}
|
|
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()
|
|
518
|
-
|
|
519
|
-
if alertOnFailToLoad {
|
|
520
|
-
let dialogMessage = UIAlertController(title: alertOnFailTitle, message: alertOnFailMessage, preferredStyle: .alert)
|
|
521
|
-
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
522
|
-
|
|
523
|
-
// Create Cancel button with action handlder
|
|
524
|
-
let ok = UIAlertAction(title: alertOnFailCloseText, style: .default)
|
|
525
|
-
|
|
526
|
-
//Add OK and Cancel button to an Alert object
|
|
527
|
-
dialogMessage.addAction(ok)
|
|
528
|
-
|
|
529
|
-
// Present alert message to user
|
|
530
|
-
if let root = RCTPresentedViewController() {
|
|
531
|
-
root.present(dialogMessage, animated: true, completion: nil)
|
|
532
|
-
}
|
|
533
40
|
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
func assetLoaderDidSucceed(_ loader: AssetLoader) {
|
|
537
|
-
print("Asset loaded successfully")
|
|
538
41
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
self.emitEventToJS("onLoad", eventData: eventPayload)
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
private func saveFileToFilesApp(fileURL: URL) {
|
|
550
|
-
DispatchQueue.main.async {
|
|
551
|
-
let documentPicker = UIDocumentPickerViewController(url: fileURL, in: .exportToService)
|
|
552
|
-
documentPicker.delegate = self
|
|
553
|
-
documentPicker.modalPresentationStyle = .formSheet
|
|
554
|
-
if let root = RCTPresentedViewController() {
|
|
555
|
-
root.present(documentPicker, animated: true, completion: nil)
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
private func shareFile(fileURL: URL) {
|
|
561
|
-
DispatchQueue.main.async {
|
|
562
|
-
// Create an instance of UIActivityViewController
|
|
563
|
-
let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
|
|
564
|
-
|
|
565
|
-
activityViewController.completionWithItemsHandler = { activityType, completed, returnedItems, error in
|
|
566
|
-
|
|
567
|
-
if let error = error {
|
|
568
|
-
let message = "Sharing error: \(error.localizedDescription)"
|
|
569
|
-
print(message)
|
|
570
|
-
self.onError(message: message, code: .failToShare)
|
|
571
|
-
|
|
572
|
-
if self.removeAfterFailedToShare {
|
|
573
|
-
let _ = self.deleteFile(url: self.outputFile!)
|
|
574
|
-
}
|
|
575
|
-
return
|
|
42
|
+
public func deleteFile(filePath: String) throws -> Promise<Bool> {
|
|
43
|
+
return Promise.async {
|
|
44
|
+
// This runs on a separate Thread, and can use `await` syntax!
|
|
45
|
+
let state = self.impl.deleteFile(url: URL(string: filePath)!)
|
|
46
|
+
return state == 0
|
|
576
47
|
}
|
|
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
|
-
}
|
|
598
48
|
}
|
|
599
49
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
|
603
|
-
if removeAfterSavedToDocuments {
|
|
604
|
-
let _ = deleteFile(url: outputFile!)
|
|
605
|
-
}
|
|
606
|
-
closeEditor()
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
|
610
|
-
if removeAfterFailedToSaveDocuments {
|
|
611
|
-
let _ = deleteFile(url: outputFile!)
|
|
612
|
-
}
|
|
613
|
-
closeEditor()
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
@objc(closeEditor:withRejecter:)
|
|
617
|
-
func closeEditor(resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
618
|
-
closeEditor()
|
|
619
|
-
resolve(true)
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
private func closeEditor() {
|
|
623
|
-
guard let vc = vc else { return }
|
|
624
|
-
// some how in case we trim a very short video the view controller is still visible after first .dismiss call
|
|
625
|
-
// even the file is successfully saved
|
|
626
|
-
// that's why we need a small delay here to ensure vc will be dismissed
|
|
627
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
628
|
-
vc.dismiss(animated: true, completion: {
|
|
629
|
-
self.emitEventToJS("onHide", eventData: nil)
|
|
630
|
-
self.isShowing = false
|
|
631
|
-
})
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
@objc(isValidFile:withResolver:withRejecter:)
|
|
636
|
-
func isValidFile(uri: String, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
637
|
-
let fileURL = URL(string: uri)!
|
|
638
|
-
checkFileValidity(url: fileURL) { isValid, fileType, duration in
|
|
639
|
-
if isValid {
|
|
640
|
-
print("Valid \(fileType) file with duration: \(duration) milliseconds")
|
|
641
|
-
} else {
|
|
642
|
-
print("Invalid file")
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
let payload: [String: Any] = [
|
|
646
|
-
"isValid": isValid,
|
|
647
|
-
"fileType": fileType,
|
|
648
|
-
"duration": duration
|
|
649
|
-
]
|
|
650
|
-
resolve(payload)
|
|
50
|
+
public func closeEditor(onComplete: @escaping () -> Void) throws {
|
|
51
|
+
impl.closeEditor(onComplete)
|
|
651
52
|
}
|
|
652
53
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
"message": message,
|
|
658
|
-
"errorCode": code.rawValue
|
|
659
|
-
]
|
|
660
|
-
self.emitEventToJS("onError", eventData: eventPayload)
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
private func checkFileValidity(url: URL, completion: @escaping (Bool, String, Double) -> Void) {
|
|
664
|
-
let asset = AVAsset(url: url)
|
|
665
|
-
|
|
666
|
-
// Load the duration and tracks asynchronously
|
|
667
|
-
asset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) {
|
|
668
|
-
var error: NSError? = nil
|
|
669
|
-
|
|
670
|
-
// Check if the duration and tracks are loaded
|
|
671
|
-
let durationStatus = asset.statusOfValue(forKey: "duration", error: &error)
|
|
672
|
-
let tracksStatus = asset.statusOfValue(forKey: "tracks", error: &error)
|
|
673
|
-
|
|
674
|
-
// Ensure both properties are loaded successfully
|
|
675
|
-
guard durationStatus == .loaded, tracksStatus == .loaded, error == nil else {
|
|
676
|
-
DispatchQueue.main.async {
|
|
677
|
-
completion(false, "unknown", -1)
|
|
54
|
+
public func isValidFile(url: String) throws -> Promise<FileValidationResult> {
|
|
55
|
+
return Promise.async {
|
|
56
|
+
// This runs on a separate Thread, and can use```` `await` syntax!
|
|
57
|
+
return await self.impl.isValidFile(uri: url)
|
|
678
58
|
}
|
|
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
|
-
}
|
|
701
59
|
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// private func renameFile(at url: URL, newName: String) -> URL? {
|
|
705
|
-
// let fileManager = FileManager.default
|
|
706
|
-
//
|
|
707
|
-
// // Get the directory of the existing file
|
|
708
|
-
// let directory = url.deletingLastPathComponent()
|
|
709
|
-
//
|
|
710
|
-
// // Get the file extension
|
|
711
|
-
// let fileExtension = url.pathExtension
|
|
712
|
-
//
|
|
713
|
-
// // Create the new file URL with the new name and the same extension
|
|
714
|
-
// let newFileURL = directory.appendingPathComponent(newName).appendingPathExtension(fileExtension)
|
|
715
|
-
//
|
|
716
|
-
// // Check if a file with the new name already exists
|
|
717
|
-
// if fileManager.fileExists(atPath: newFileURL.path) {
|
|
718
|
-
// do {
|
|
719
|
-
// // If the file exists, remove it first to avoid conflicts
|
|
720
|
-
// try fileManager.removeItem(at: newFileURL)
|
|
721
|
-
// } catch {
|
|
722
|
-
// print("Error removing existing file: \(error)")
|
|
723
|
-
// return nil
|
|
724
|
-
// }
|
|
725
|
-
// }
|
|
726
|
-
//
|
|
727
|
-
// do {
|
|
728
|
-
// // Rename (move) the file
|
|
729
|
-
// try fileManager.moveItem(at: url, to: newFileURL)
|
|
730
|
-
// print("File renamed successfully to \(newFileURL.absoluteString)")
|
|
731
|
-
// return newFileURL
|
|
732
|
-
// } catch {
|
|
733
|
-
// print("Error renaming file: \(error)")
|
|
734
|
-
// return nil
|
|
735
|
-
// }
|
|
736
|
-
// }
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
/// Helper class to manage export session state for legacy API without capturing in closures
|
|
740
|
-
private class ExportSessionManager {
|
|
741
|
-
private weak var session: AVAssetExportSession?
|
|
742
|
-
private(set) var isFinished: Bool = false
|
|
743
|
-
|
|
744
|
-
init(session: AVAssetExportSession) {
|
|
745
|
-
self.session = session
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
var progress: Float {
|
|
749
|
-
session?.progress ?? 0.0
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
func markFinished() {
|
|
753
|
-
isFinished = true
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Reintroduce error for legacy path, suppressing deprecation warning
|
|
757
|
-
@available(iOS, deprecated: 18.0, message: "Used only for iOS < 18 compatibility")
|
|
758
|
-
var error: Error? {
|
|
759
|
-
session?.error
|
|
760
|
-
}
|
|
761
60
|
}
|