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 /// Wie viel Vorrat der Player anlegen soll. /// /// `nil` überlässt es dem System. Das ist für eine Datei auf der Platte /// richtig und für einen Sender richtig — bei einem Strom über eine /// wackelige Leitung ist es zu wenig: der Systemwert ist auf schnellen /// Start ausgelegt, nicht darauf, einen Aussetzer zu überstehen. /// /// **Wer das entscheidet, muss mehr wissen als der Motor.** Ob es ein /// Sender ist, ob die Datei lokal liegt — das weiß der Player, nicht die /// Engine. Deshalb reist die Zahl mit der Quelle, statt dass die Engine /// rät. public let forwardBuffer: Duration? public init( url: URL, origin: Origin, startTime: Duration = .zero, forwardBuffer: Duration? = nil ) { self.url = url self.origin = origin self.startTime = startTime self.forwardBuffer = forwardBuffer } } /// 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 } /// Ob dieser Engine sein Bild an ein AirPlay-Gerät geben kann. /// /// Nur der Systemplayer kann das. VLC zeichnet selbst und kennt die /// Empfängerliste nicht — ein Knopf dafür führte dort ins Leere. var supportsAirPlay: 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 } /// Deckelt, welche Sprosse der Player höchstens ziehen darf. /// /// **Der billige Hebel.** Er wirkt ohne Neuaufbau: kein Anhalten am /// Server, keine neue Aushandlung, kein Sprung zurück an die alte Stelle. /// `nil` hebt die Grenze auf. /// /// Zwei Einschränkungen gehören dazugesagt. Der Wert greift erst an der /// nächsten Schnipselgrenze, also mit einigen Sekunden Verzug — ein Grund /// mehr, nicht bei jedem Ausschlag daran zu drehen. Und **bei einer /// Playlist mit einer einzigen Stufe tut er nichts**: der Player spielt /// sie auch dann, wenn sie über der Grenze liegt. Der Wert ist ein /// Wunsch, keine Vorschrift. func setPeakBitrate(_ kilobits: Int?) /// 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. /// /// **Nachlaufend und leicht misszuverstehen.** Spielt der Player /// zufrieden eine kleine Sprosse, holt er wenig — und dieser Wert sieht /// schlecht aus, ohne dass die Leitung schlecht wäre. Als alleiniges /// Kriterium für eine Regelung taugt er deshalb nicht. public let observedBitrate: Int? /// Was der Stream für sich angibt — also auf welcher Sprosse man steht. public let indicatedBitrate: Int? public let width: Int? public let height: Int? /// Verworfene Bilder — der ehrlichste Hinweis auf Überlastung. public let droppedFrames: Int? /// Wie viele Stufen die Playlist überhaupt anbietet. /// /// **Die wichtigste Zahl hier**, und die einzige, die die Frage /// beantwortet, ob es etwas zum Umschalten gibt. Sie kommt vom Parser des /// Players, nicht aus einer Vermutung über den Server: `1` heißt, dass /// alles Nachregeln ins Leere greift, ganz gleich was bestellt wurde. /// /// `nil` bei allem, was keine Playlist ist — eine Datei hat keine Leiter, /// und das ist die richtige Aussage, nicht „null Stufen". public let variantCount: Int? /// Die Sprossen selbst, aufsteigend, in Kilobit je Sekunde. /// /// Nötig, um eine Stufe tiefer zu **zielen**: ein Wert zwischen zwei /// Sprossen wählt dieselbe wie die untere und macht jede Anzeige darüber /// zur Lüge. public let variantBitrates: [Int] /// Wie weit der Puffer vor der Abspielstelle reicht. /// /// **Der einzige echte Frühwarner.** Schrumpft er schneller, als die Zeit /// vergeht, kommt weniger herein als hinausgeht — und das ist zehn bis /// zwanzig Sekunden vor dem Rückler zu sehen. Alles andere meldet sich /// erst, wenn das Bild schon steht. public let bufferedAhead: Duration? /// Apples eigene Einschätzung. Früh, aber grob. public let isLikelyToKeepUp: Bool? /// Wie oft es seit dem Start gestockt hat. public let stallCount: Int /// Wie lange insgesamt gewartet wurde. public let stalledTime: Duration public init( observedBitrate: Int? = nil, indicatedBitrate: Int? = nil, width: Int? = nil, height: Int? = nil, droppedFrames: Int? = nil, variantCount: Int? = nil, variantBitrates: [Int] = [], bufferedAhead: Duration? = nil, isLikelyToKeepUp: Bool? = nil, stallCount: Int = 0, stalledTime: Duration = .zero ) { self.observedBitrate = observedBitrate self.indicatedBitrate = indicatedBitrate self.width = width self.height = height self.droppedFrames = droppedFrames self.variantCount = variantCount self.variantBitrates = variantBitrates self.bufferedAhead = bufferedAhead self.isLikelyToKeepUp = isLikelyToKeepUp self.stallCount = stallCount self.stalledTime = stalledTime } public var resolution: String? { guard let width, let height, width > 0, height > 0 else { return nil } return "\(width)×\(height)" } /// Ob es überhaupt etwas zum Umschalten gibt. /// /// Bei `false` ist `preferredPeakBitRate` wirkungslos — AVPlayer spielt /// die eine Stufe auch dann, wenn sie über der Grenze liegt. Genau dafür /// gibt es die Sicherheitsleine. public var hasLadder: Bool { (variantCount ?? 0) > 1 } /// Die nächste Sprosse unterhalb der angegebenen — oder die unterste. /// /// Gibt `nil` zurück, wenn es nichts Tieferes gibt. Dann ist der billige /// Weg erschöpft. public func rung(below bitrate: Int) -> Int? { variantBitrates.last { $0 < bitrate } } } extension VideoPlayerEngine { /// Engines, die nichts melden, melden nichts — statt jeden zu zwingen. public var statistics: PlaybackStatistics? { nil } /// Wer keine Leiter hat, kann auch nichts deckeln. Kein Fehler, nur /// nichts zu tun — bei einer Datei gibt es keine Stufen. public func setPeakBitrate(_ kilobits: Int?) {} /// Wer nichts dazu sagt, kann es nicht. public var supportsAirPlay: Bool { false } }