Skip to content

Commit

Permalink
feat: expose 4K resolution cap when requesting to standardize inputs (#…
Browse files Browse the repository at this point in the history
…105)

* feat: expose 4K resolution cap when requesting to standardize inputs

* adjust cutoff max dimension check when inspecting video input

* pass back inspection error separately

* thread through the maximum resolution

* fix: handle edge case where input is standard but rescaling still necessary

* fix: use lower frame rate limit for 4K video
  • Loading branch information
andrewjl-mux authored Jun 3, 2024
1 parent 0f4098a commit b673e57
Show file tree
Hide file tree
Showing 13 changed files with 342 additions and 200 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import AVFoundation
import Foundation

enum UploadInputFormatInspectionResult {

struct UploadInputFormatInspectionResult {
enum NonstandardInputReason {
case videoCodec
case audioCodec
Expand All @@ -21,40 +20,42 @@ enum UploadInputFormatInspectionResult {
case unsupportedPixelFormat
}

case inspectionFailure(duration: CMTime)
case standard(duration: CMTime)
case nonstandard(
reasons: [NonstandardInputReason],
duration: CMTime
)
var nonStandardInputReasons: [NonstandardInputReason] = []

var isStandard: Bool {
if case Self.standard = self {
return true
} else {
return false
}
var isStandardInput: Bool {
nonStandardInputReasons.isEmpty
}

var sourceInputDuration: CMTime {
switch self {
case .inspectionFailure(duration: let duration):
return duration
case .standard(duration: let duration):
return duration
case .nonstandard(_, duration: let duration):
return duration
}
}
struct RescalingDetails {
var maximumDesiredResolutionPreset: DirectUploadOptions.InputStandardization.MaximumResolution = .default

var recordedResolution: CMVideoDimensions = CMVideoDimensions(width: 0, height: 0)

var nonstandardInputReasons: [NonstandardInputReason]? {
if case Self.nonstandard(let nonstandardInputReasons, _) = self {
return nonstandardInputReasons
} else {
return nil
var needsRescaling: Bool {
switch maximumDesiredResolutionPreset {
case .default, .preset1920x1080:
if max(recordedResolution.width, recordedResolution.height) > 1920 {
return true
} else {
return false
}
case .preset1280x720:
if max(recordedResolution.width, recordedResolution.height) > 1280 {
return true
} else {
return false
}
case .preset3840x2160:
if max(recordedResolution.width, recordedResolution.height) > 3840 {
return true
} else {
return false
}
}
}
}

var rescalingDetails: RescalingDetails = RescalingDetails()
}

extension UploadInputFormatInspectionResult.NonstandardInputReason: CustomStringConvertible {
Expand Down
70 changes: 50 additions & 20 deletions Sources/MuxUploadSDK/InputInspection/UploadInputInspector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@ import AVFoundation
import CoreMedia
import Foundation

typealias UploadInputInspectionCompletionHandler = (UploadInputFormatInspectionResult?, CMTime, Error?) -> ()

protocol UploadInputInspector {
func performInspection(
sourceInput: AVAsset,
completionHandler: @escaping (UploadInputFormatInspectionResult) -> ()
maximumResolution: DirectUploadOptions.InputStandardization.MaximumResolution,
completionHandler: @escaping UploadInputInspectionCompletionHandler
)
}

struct UploadInputInspectionError: Error {

static let inspectionFailure = UploadInputInspectionError()

}

class AVFoundationUploadInputInspector: UploadInputInspector {

static let shared = AVFoundationUploadInputInspector()
Expand All @@ -24,7 +33,8 @@ class AVFoundationUploadInputInspector: UploadInputInspector {
// methods.
func performInspection(
sourceInput: AVAsset,
completionHandler: @escaping (UploadInputFormatInspectionResult) -> ()
maximumResolution: DirectUploadOptions.InputStandardization.MaximumResolution,
completionHandler: @escaping UploadInputInspectionCompletionHandler
) {
// TODO: Eventually load audio tracks too
if #available(iOS 15, *) {
Expand All @@ -33,7 +43,9 @@ class AVFoundationUploadInputInspector: UploadInputInspector {
) { tracks, error in
if error != nil {
completionHandler(
.inspectionFailure(duration: CMTime.zero)
nil,
CMTime.zero,
UploadInputInspectionError.inspectionFailure
)
return
}
Expand All @@ -42,6 +54,7 @@ class AVFoundationUploadInputInspector: UploadInputInspector {
self.inspect(
sourceInput: sourceInput,
tracks: tracks,
maximumResolution: maximumResolution,
completionHandler: completionHandler
)
}
Expand All @@ -60,6 +73,7 @@ class AVFoundationUploadInputInspector: UploadInputInspector {
self.inspect(
sourceInput: sourceInput,
tracks: tracks,
maximumResolution: maximumResolution,
completionHandler: completionHandler
)
}
Expand All @@ -69,14 +83,17 @@ class AVFoundationUploadInputInspector: UploadInputInspector {
func inspect(
sourceInput: AVAsset,
tracks: [AVAssetTrack],
completionHandler: @escaping (UploadInputFormatInspectionResult) -> ()
maximumResolution: DirectUploadOptions.InputStandardization.MaximumResolution,
completionHandler: @escaping UploadInputInspectionCompletionHandler
) {
switch tracks.count {
case 0:
// Nothing to inspect, therefore nothing to standardize
// declare as already standard
completionHandler(
.standard(duration: CMTime.zero)
UploadInputFormatInspectionResult(),
CMTime.zero,
nil
)
case 1:

Expand All @@ -95,16 +112,18 @@ class AVFoundationUploadInputInspector: UploadInputInspector {
) {
guard let formatDescriptions = track.formatDescriptions as? [CMFormatDescription] else {
completionHandler(
.inspectionFailure(
duration: sourceInputDuration
)
nil,
sourceInputDuration,
UploadInputInspectionError.inspectionFailure
)
return
}

guard let formatDescription = formatDescriptions.first else {
completionHandler(
.inspectionFailure(duration: sourceInputDuration)
nil,
sourceInputDuration,
UploadInputInspectionError.inspectionFailure
)
return
}
Expand All @@ -115,7 +134,7 @@ class AVFoundationUploadInputInspector: UploadInputInspector {
formatDescription
)

if max(videoDimensions.width, videoDimensions.height) > 1920 {
if max(videoDimensions.width, videoDimensions.height) > 3840 {
nonStandardReasons.append(.videoResolution)
}

Expand All @@ -128,26 +147,37 @@ class AVFoundationUploadInputInspector: UploadInputInspector {
}

let frameRate = track.nominalFrameRate
if frameRate > 120.0 {
nonStandardReasons.append(.videoFrameRate)
}

if nonStandardReasons.isEmpty {
completionHandler(
.standard(duration: sourceInputDuration)
)
if max(videoDimensions.width, videoDimensions.height) > 1920 {
if frameRate > 60.0 {
nonStandardReasons.append(.videoFrameRate)
}
} else {
completionHandler(.nonstandard(reasons: nonStandardReasons, duration: sourceInputDuration))
if frameRate > 120.0 {
nonStandardReasons.append(.videoFrameRate)
}
}

completionHandler(
UploadInputFormatInspectionResult(
nonStandardInputReasons: nonStandardReasons,
rescalingDetails: UploadInputFormatInspectionResult.RescalingDetails(
maximumDesiredResolutionPreset: maximumResolution,
recordedResolution: videoDimensions
)
),
sourceInputDuration,
nil
)
}
}
}
default:
// Inspection fails for multi-video track inputs
// for the time being
completionHandler(
.inspectionFailure(duration: CMTime.zero)
nil,
CMTime.zero,
UploadInputInspectionError.inspectionFailure
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,24 @@ class UploadInputStandardizationWorker {

func standardize(
sourceAsset: AVAsset,
maximumResolution: DirectUploadOptions.InputStandardization.MaximumResolution,
rescalingDetails: UploadInputFormatInspectionResult.RescalingDetails,
outputURL: URL,
completion: @escaping (AVAsset, AVAsset?, Error?) -> ()
) {

let availableExportPresets = AVAssetExportSession.allExportPresets()

let exportPreset: String
if maximumResolution == .preset1280x720 {

switch rescalingDetails.maximumDesiredResolutionPreset {
case .default:
exportPreset = AVAssetExportPreset1920x1080
case .preset1280x720:
exportPreset = AVAssetExportPreset1280x720
} else {
case .preset1920x1080:
exportPreset = AVAssetExportPreset1920x1080
case .preset3840x2160:
exportPreset = AVAssetExportPreset3840x2160
}

guard availableExportPresets.contains(where: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ class UploadInputStandardizer {
func standardize(
id: String,
sourceAsset: AVAsset,
maximumResolution: DirectUploadOptions.InputStandardization.MaximumResolution,
rescalingDetails: UploadInputFormatInspectionResult.RescalingDetails,
outputURL: URL,
completion: @escaping (AVAsset, AVAsset?, Error?) -> ()
) {
let worker = UploadInputStandardizationWorker()

worker.standardize(
sourceAsset: sourceAsset,
maximumResolution: maximumResolution,
rescalingDetails: rescalingDetails,
outputURL: outputURL,
completion: completion
)
Expand Down
Loading

0 comments on commit b673e57

Please sign in to comment.