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

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

Powered by TurnKey Linux.