diff --git a/Sources/MuxUploadSDK/InputStandardization/UploadInputStandardizationWorker.swift b/Sources/MuxUploadSDK/InputStandardization/UploadInputStandardizationWorker.swift index 37b3d927..d1367bf3 100644 --- a/Sources/MuxUploadSDK/InputStandardization/UploadInputStandardizationWorker.swift +++ b/Sources/MuxUploadSDK/InputStandardization/UploadInputStandardizationWorker.swift @@ -5,9 +5,72 @@ import AVFoundation import Foundation +protocol Standardizable { } + +extension AVAsset: Standardizable { } + +enum StandardizationResult { + case success(standardizedAsset: AVAsset) + case failure(error: Error) +} + +enum StandardizationStrategy { + // Prefer using export session whenever possible + case exportSession +} + class UploadInputStandardizationWorker { var sourceInput: AVAsset? var standardizedInput: AVAsset? + + func standardize( + sourceAsset: AVAsset, + maximumResolution: UploadOptions.InputStandardization.MaximumResolution, + outputURL: URL, + completion: @escaping (AVAsset, AVAsset?, URL?, Bool) -> () + ) { + + let availableExportPresets = AVAssetExportSession.allExportPresets() + + let exportPreset: String + if maximumResolution == .preset1280x720 { + exportPreset = AVAssetExportPreset1280x720 + } else { + exportPreset = AVAssetExportPreset1920x1080 + } + + guard availableExportPresets.contains(where: { + $0 == exportPreset + }) else { + // TODO: Use VideoToolbox if export preset unavailable + completion(sourceAsset, nil, nil, false) + return + } + + guard let exportSession = AVAssetExportSession( + asset: sourceAsset, + presetName: exportPreset + ) else { + // TODO: Use VideoToolbox if export session fails to initialize + completion(sourceAsset, nil, nil, false) + return + } + + exportSession.outputFileType = .mp4 + exportSession.outputURL = outputURL + + // TODO: Use Swift Concurrency + exportSession.exportAsynchronously { + if let exportError = exportSession.error { + completion(sourceAsset, nil, nil, false) + } else if let standardizedAssetURL = exportSession.outputURL { + let standardizedAsset = AVAsset(url: standardizedAssetURL) + completion(sourceAsset, standardizedAsset, outputURL, true) + } else { + completion(sourceAsset, nil, nil, false) + } + } + } } diff --git a/Sources/MuxUploadSDK/InputStandardization/UploadInputStandardizer.swift b/Sources/MuxUploadSDK/InputStandardization/UploadInputStandardizer.swift index 6e164591..f08ad06c 100644 --- a/Sources/MuxUploadSDK/InputStandardization/UploadInputStandardizer.swift +++ b/Sources/MuxUploadSDK/InputStandardization/UploadInputStandardizer.swift @@ -6,5 +6,32 @@ import AVFoundation import Foundation class UploadInputStandardizer { - + var workers: [String: UploadInputStandardizationWorker] = [:] + + func standardize( + id: String, + sourceAsset: AVAsset, + maximumResolution: UploadOptions.InputStandardization.MaximumResolution, + outputURL: URL, + completion: @escaping (AVAsset, AVAsset?, URL?, Bool) -> () + ) { + let worker = UploadInputStandardizationWorker() + + worker.standardize( + sourceAsset: sourceAsset, + maximumResolution: maximumResolution, + outputURL: outputURL, + completion: completion + ) + workers[id] = worker + } + + // Storing the worker might not be necessary if an + // alternative reference is in place outside the + // stack frame + func acknowledgeCompletion( + id: String + ) { + workers[id] = nil + } } diff --git a/Sources/MuxUploadSDK/PublicAPI/MuxUpload.swift b/Sources/MuxUploadSDK/PublicAPI/MuxUpload.swift index 42c5c76c..49fabaaf 100644 --- a/Sources/MuxUploadSDK/PublicAPI/MuxUpload.swift +++ b/Sources/MuxUploadSDK/PublicAPI/MuxUpload.swift @@ -149,6 +149,18 @@ public final class MuxUpload : Hashable, Equatable { */ public var inputStatusHandler: InputStatusHandler? + /** + Confirms upload if input standardization did not succeed + */ + public typealias NonStandardInputHandler = () -> Bool + + /** + If set will be executed by the SDK when input standardization + hadn't succeeded, return to continue the upload + or return to cancel the upload + */ + public var nonStandardInputHandler: NonStandardInputHandler? + private let manageBySDK: Bool var id: String { uploadInfo.id @@ -474,6 +486,28 @@ public final class MuxUpload : Hashable, Equatable { switch inspectionResult { case .inspectionFailure: + // TODO: Request upload confirmation + // before proceeding + + guard let nonStandardInputHandler = self.nonStandardInputHandler else { + self.startNetworkTransport( + videoFile: videoFile + ) + return + } + + let shouldCancelUpload = nonStandardInputHandler() + + if !shouldCancelUpload { + self.startNetworkTransport( + videoFile: videoFile + ) + } else { + self.fileWorker?.cancel() + self.uploadManager.acknowledgeUpload(id: self.id) + self.input.processUploadCancellation() + } + self.startNetworkTransport(videoFile: videoFile) case .standard: self.startNetworkTransport(videoFile: videoFile) @@ -488,8 +522,54 @@ public final class MuxUpload : Hashable, Equatable { """ ) - // Skip format standardization - self.startNetworkTransport(videoFile: videoFile) + // TODO: inject Date() for testing purposes + let outputFileName = "upload-\(Date().timeIntervalSince1970)" + + let outputDirectory = FileManager.default.temporaryDirectory + let outputURL = URL( + fileURLWithPath: outputFileName, + relativeTo: outputDirectory + ) + let maximumResolution = self.input + .uploadInfo + .options + .inputStandardization + .maximumResolution + + self.inputStandardizer.standardize( + id: self.id, + sourceAsset: AVAsset(url: videoFile), + maximumResolution: maximumResolution, + outputURL: outputURL + ) { sourceAsset, standardizedAsset, outputURL, success in + + if let outputURL, success { + self.startNetworkTransport( + videoFile: outputURL + ) + } else { + guard let nonStandardInputHandler = self.nonStandardInputHandler else { + self.startNetworkTransport( + videoFile: videoFile + ) + return + } + + let shouldCancelUpload = nonStandardInputHandler() + + if !shouldCancelUpload { + self.startNetworkTransport( + videoFile: videoFile + ) + } else { + self.fileWorker?.cancel() + self.uploadManager.acknowledgeUpload(id: self.id) + self.input.processUploadCancellation() + } + } + + self.inputStandardizer.acknowledgeCompletion(id: self.id) + } } } } diff --git a/apps/Test App/Test App/Model/UploadListModel.swift b/apps/Test App/Test App/Model/UploadListModel.swift index 4ea67d5f..585dfb9e 100644 --- a/apps/Test App/Test App/Model/UploadListModel.swift +++ b/apps/Test App/Test App/Model/UploadListModel.swift @@ -35,7 +35,7 @@ class UploadListModel : ObservableObject { self.lastKnownUploads = Array(uploadSet) .sorted( by: { lhs, rhs in - lhs.uploadStatus.startTime >= rhs.uploadStatus.startTime + (lhs.uploadStatus?.startTime ?? 0) >= (rhs.uploadStatus?.startTime ?? 0) } ) } diff --git a/apps/Test App/Test App/Screens/UploadListView.swift b/apps/Test App/Test App/Screens/UploadListView.swift index c332c3b6..1781d79c 100644 --- a/apps/Test App/Test App/Screens/UploadListView.swift +++ b/apps/Test App/Test App/Screens/UploadListView.swift @@ -125,11 +125,11 @@ fileprivate struct ListItem: View { } private func statusLine(status: MuxUpload.TransportStatus?) -> String { - guard let status = status, let progress = status.progress, status.startTime > 0 else { + guard let status = status, let progress = status.progress, let startTime = status.startTime, startTime > 0 else { return "missing status" } - let totalTimeSecs = status.updatedTime - status.startTime + let totalTimeSecs = status.updatedTime - (status.startTime ?? 0) let totalTimeMs = Int64((totalTimeSecs) * 1000) let kbytesPerSec = (progress.completedUnitCount) / totalTimeMs // bytes/milli = kb/sec let fourSigs = NumberFormatter()