You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
127 lines
4.4 KiB
127 lines
4.4 KiB
import AVFoundation
|
|
import CoreMedia
|
|
|
|
/// Liest Dimension, Codec, Framerate und Dauer einer Videodatei. Zustandslos,
|
|
/// ein einziger Async-Entry-Point — der Rest von DropTwoCore braucht nie
|
|
/// direkten AVFoundation-Zugriff auf die Quelldatei.
|
|
public enum VideoInspector {
|
|
public static func inspect(_ url: URL) async throws -> VideoInfo {
|
|
let asset = AVURLAsset(url: url)
|
|
|
|
let tracks: [AVAssetTrack]
|
|
do {
|
|
tracks = try await asset.load(.tracks)
|
|
} catch {
|
|
throw VideoInspectorError.assetUnreadable(underlying: error)
|
|
}
|
|
|
|
guard let videoTrack = tracks.first(where: { $0.mediaType == .video }) else {
|
|
throw VideoInspectorError.noVideoTrack
|
|
}
|
|
let hasAudioTrack = tracks.contains { $0.mediaType == .audio }
|
|
|
|
do {
|
|
let naturalSize = try await videoTrack.load(.naturalSize)
|
|
let preferredTransform = try await videoTrack.load(.preferredTransform)
|
|
let frameRate = try await videoTrack.load(.nominalFrameRate)
|
|
let formatDescriptions = try await videoTrack.load(.formatDescriptions)
|
|
let duration = try await asset.load(.duration)
|
|
|
|
let transformedSize = naturalSize.applying(preferredTransform)
|
|
let pixelSize = CGSize(width: abs(transformedSize.width), height: abs(transformedSize.height))
|
|
|
|
return VideoInfo(
|
|
pixelSize: pixelSize,
|
|
codec: codec(fromFormatDescriptions: formatDescriptions),
|
|
frameRate: frameRate,
|
|
duration: duration,
|
|
hasAudioTrack: hasAudioTrack
|
|
)
|
|
} catch let error as VideoInspectorError {
|
|
throw error
|
|
} catch {
|
|
throw VideoInspectorError.assetUnreadable(underlying: error)
|
|
}
|
|
}
|
|
|
|
private static func codec(fromFormatDescriptions formatDescriptions: [CMFormatDescription]) -> VideoCodec {
|
|
guard let description = formatDescriptions.first else {
|
|
return .other(fourCC: "????")
|
|
}
|
|
let subType = CMFormatDescriptionGetMediaSubType(description)
|
|
switch subType {
|
|
case kCMVideoCodecType_H264:
|
|
return .h264
|
|
case kCMVideoCodecType_HEVC:
|
|
return .hevc
|
|
case kCMVideoCodecType_AppleProRes422Proxy:
|
|
return .appleProRes422Proxy
|
|
case kCMVideoCodecType_AppleProRes422LT:
|
|
return .appleProRes422LT
|
|
case kCMVideoCodecType_AppleProRes422:
|
|
return .appleProRes422
|
|
case kCMVideoCodecType_AppleProRes422HQ:
|
|
return .appleProRes422HQ
|
|
case kCMVideoCodecType_AppleProRes4444:
|
|
return .appleProRes4444
|
|
case kCMVideoCodecType_AppleProRes4444XQ:
|
|
return .appleProRes4444XQ
|
|
default:
|
|
return .other(fourCC: fourCCString(subType))
|
|
}
|
|
}
|
|
|
|
private static func fourCCString(_ code: FourCharCode) -> String {
|
|
let bytes: [UInt8] = [
|
|
UInt8((code >> 24) & 0xFF),
|
|
UInt8((code >> 16) & 0xFF),
|
|
UInt8((code >> 8) & 0xFF),
|
|
UInt8(code & 0xFF)
|
|
]
|
|
return String(bytes: bytes, encoding: .ascii) ?? "????"
|
|
}
|
|
}
|
|
|
|
/// Ergebnis einer erfolgreichen Inspektion.
|
|
public struct VideoInfo: Equatable {
|
|
/// Orientierungskorrigiert — `preferredTransform` bereits angewendet.
|
|
public let pixelSize: CGSize
|
|
public let codec: VideoCodec
|
|
public let frameRate: Float
|
|
public let duration: CMTime
|
|
public let hasAudioTrack: Bool
|
|
|
|
public init(pixelSize: CGSize, codec: VideoCodec, frameRate: Float, duration: CMTime, hasAudioTrack: Bool) {
|
|
self.pixelSize = pixelSize
|
|
self.codec = codec
|
|
self.frameRate = frameRate
|
|
self.duration = duration
|
|
self.hasAudioTrack = hasAudioTrack
|
|
}
|
|
|
|
/// 9:16 mit kleiner Toleranz — echtes Footage rundet gelegentlich leicht ab.
|
|
public var isNineBySixteen: Bool {
|
|
guard pixelSize.height > 0 else { return false }
|
|
let ratio = pixelSize.width / pixelSize.height
|
|
let target: CGFloat = 9.0 / 16.0
|
|
return abs(ratio - target) < 0.01
|
|
}
|
|
}
|
|
|
|
public enum VideoCodec: Equatable {
|
|
case h264
|
|
case hevc
|
|
case appleProRes422Proxy
|
|
case appleProRes422LT
|
|
case appleProRes422
|
|
case appleProRes422HQ
|
|
case appleProRes4444
|
|
case appleProRes4444XQ
|
|
case other(fourCC: String)
|
|
}
|
|
|
|
public enum VideoInspectorError: Error {
|
|
case noVideoTrack
|
|
case assetUnreadable(underlying: Error)
|
|
}
|