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) }