import Foundation /// Woher das Bild kommt. public struct PlaybackSource: Sendable, Equatable { public enum Origin: Sendable, Equatable { /// Vom Server — per DirectPlay oder als HLS-Stream des Transcoders. case remote /// Aus dem Download-Ordner. case local } public let url: URL public let origin: Origin /// Ab wo gestartet werden soll. Kommt aus dem zuletzt gemeldeten /// Fortschritt; `.zero` heißt von vorn. public let startTime: Duration public init(url: URL, origin: Origin, startTime: Duration = .zero) { self.url = url self.origin = origin self.startTime = startTime } } /// Was eine Engine von sich aus meldet. /// /// Ein Stream statt beobachtbarer Eigenschaften: AVPlayer meldet über KVO, /// VLCKit über Delegat-Rückrufe. Beide in dieselbe Ereignisfolge zu /// übersetzen hält den Unterschied an genau einer Stelle je Engine — statt in /// jeder View, die den Zustand anzeigt. public enum PlaybackEvent: Sendable, Equatable { case stateChanged(PlaybackState) case timeChanged(Duration) case durationResolved(Duration) case tracksResolved(audio: [MediaTrack], subtitle: [MediaTrack]) case reachedEnd } /// Der gemeinsame Nenner von AVPlayer und VLCKit. /// /// Alles, was die Oberfläche zum Steuern braucht — und nichts darüber hinaus. /// Was hier nicht steht, gibt es für die Views nicht; dadurch bleibt der /// Austausch des Engine unsichtbar. @MainActor public protocol VideoPlayerEngine: AnyObject { var events: AsyncStream { get } var state: PlaybackState { get } var currentTime: Duration { get } var duration: Duration? { get } /// Ob dieser Engine Bild-in-Bild kann. Steuert, ob der Knopf überhaupt /// erscheint — ein Knopf, der bei manchen Dateien nichts tut, ist /// schlimmer als keiner. var supportsPictureInPicture: Bool { get } func load(_ source: PlaybackSource) async throws(PlaybackFailure) func play() func pause() func seek(to time: Duration) async func setRate(_ rate: Double) var audioTracks: [MediaTrack] { get } var subtitleTracks: [MediaTrack] { get } func selectAudioTrack(_ track: MediaTrack?) /// `nil` schaltet die Untertitel ab. func selectSubtitleTrack(_ track: MediaTrack?) /// Was gerade tatsächlich über die Leitung kommt. /// /// Nicht, was angefordert wurde: bei einem HLS-Stream entscheidet der /// Player selbst, welche Stufe er zieht, und die kann von der gewählten /// Obergrenze abweichen. Ohne diese Angabe bleibt „ruckelt es, weil die /// Leitung schlecht ist?" eine Vermutung. var statistics: PlaybackStatistics? { get } /// Gibt Puffer und Dekoder frei. Muss beim Verlassen des Players laufen, /// sonst behält besonders VLCKit den Speicher. func teardown() } /// Was über die Leitung kommt, während gespielt wird. public struct PlaybackStatistics: Sendable, Equatable { /// Was tatsächlich ankommt, in Kilobit je Sekunde. public let observedBitrate: Int? /// Was der Stream für sich angibt. public let indicatedBitrate: Int? public let width: Int? public let height: Int? /// Verworfene Bilder — der ehrlichste Hinweis auf Überlastung. public let droppedFrames: Int? public init( observedBitrate: Int? = nil, indicatedBitrate: Int? = nil, width: Int? = nil, height: Int? = nil, droppedFrames: Int? = nil ) { self.observedBitrate = observedBitrate self.indicatedBitrate = indicatedBitrate self.width = width self.height = height self.droppedFrames = droppedFrames } public var resolution: String? { guard let width, let height, width > 0, height > 0 else { return nil } return "\(width)×\(height)" } } extension VideoPlayerEngine { /// Engines, die nichts melden, melden nichts — statt jeden zu zwingen. public var statistics: PlaybackStatistics? { nil } }