diff --git a/Flixy/Views/Player/PlayerView.swift b/Flixy/Views/Player/PlayerView.swift index 885daf4..49723c9 100644 --- a/Flixy/Views/Player/PlayerView.swift +++ b/Flixy/Views/Player/PlayerView.swift @@ -86,14 +86,17 @@ struct PlayerView: View { failure(message: message) case .playing: - // Wartet auf Daten: ohne Anzeige sieht das aus wie ein - // Standbild, und man weiß nicht, ob noch etwas kommt. + // **Der Kreisel gilt immer**, mit oder ohne + // Bedienelemente. // - // Nur, solange die Bedienelemente aus sind. Sind sie - // an, steht der Kreisel **im** Abspielknopf — dort, wo - // sonst das Symbol ist. Vorher lag er dahinter und das - // Dreieck davor: es sah pausiert aus, war es nicht. - if model.isBuffering && !showsControls { + // Er ist keine Verzierung der Bedienung, sondern die + // Auskunft „es kommt gleich weiter" — und die braucht + // man gerade dann, wenn man nichts eingeblendet hat + // und vor einem stehenden Bild sitzt. Die Knöpfe + // darüber lassen an seiner Stelle Platz, damit er + // hindurchscheint und nicht zwei Anzeigen + // übereinanderliegen. + if model.isBuffering { ProgressView() .tint(.white) .scaleEffect(1.4) @@ -368,19 +371,21 @@ struct PlayerView: View { scheduleControlsHide() } label: { ZStack { - // Solange auf Daten gewartet wird, steht hier der Kreisel - // — kein Dreieck. Ein Dreieck heißt „angehalten, tipp - // mich", und beides stimmte nicht: es lief bereits, und - // der Tipp bewirkte nichts. + // Solange auf Daten gewartet wird, steht hier **nichts**. + // + // Kein Dreieck: das hieße „angehalten, tipp mich", und + // beides stimmt nicht. Aber auch kein zweiter Kreisel — + // der eine liegt schon darunter und scheint durch diese + // Lücke hindurch. Die Fläche bleibt tippbar, damit man + // ein endloses Laden auch anhalten kann. if model.isBuffering { - ProgressView() - .tint(.white) - .scaleEffect(1.4) + Color.clear } else { Image(systemName: model.isPlaying ? "pause.fill" : "play.fill") .font(.system(size: 52)) } } + .contentShape(.rect) // Feste Größe, damit die beiden Knöpfe daneben stehen // bleiben. Sie wanderten bisher bei jedem Wechsel ein Stück, // weil Dreieck und Doppelbalken verschieden breit sind. diff --git a/Packages/FlixyKit/Sources/FlixyPlayback/Engine/AVPlayerEngine.swift b/Packages/FlixyKit/Sources/FlixyPlayback/Engine/AVPlayerEngine.swift index 45c1907..e2a2806 100644 --- a/Packages/FlixyKit/Sources/FlixyPlayback/Engine/AVPlayerEngine.swift +++ b/Packages/FlixyKit/Sources/FlixyPlayback/Engine/AVPlayerEngine.swift @@ -60,6 +60,18 @@ public final class AVPlayerEngine: VideoPlayerEngine { /// `statistics` später „keine Leiter" von „eine Stufe". private var variantBitrates: [Int] = [] + /// Ob überhaupt jemand spielen wollte. + /// + /// **Der Unterschied zwischen „pausiert" und „kommt nicht voran".** + /// `timeControlStatus` meldet beides als `.paused`: das Anhalten durch + /// den Nutzer und den Fall, dass `play()` gerufen wurde, der Player aber + /// nicht losläuft — weil die Quelle noch nicht bereit ist oder weil nichts + /// ankommt. Ohne diese Unterscheidung zeigte der Player ein + /// Abspielsymbol, als hätte man selbst angehalten, während in Wahrheit + /// auf Daten gewartet wurde. Wer dann tippte, drückte auf Play bei etwas, + /// das längst laufen sollte. + private var wantsToPlay = false + /// Wie oft es gestockt hat, seit diese Quelle läuft. /// /// Aus `playbackStalledNotification` und **nicht** aus dem @@ -81,6 +93,7 @@ public final class AVPlayerEngine: VideoPlayerEngine { // Die Buchführung gehört zur Quelle, nicht zum Motor: ein neuer Titel // fängt bei null an, sonst schleppte die Zahl den vorigen Film mit. variantBitrates = [] + wantsToPlay = false stallCount = 0 accumulatedStall = .zero bufferingSince = nil @@ -170,10 +183,12 @@ public final class AVPlayerEngine: VideoPlayerEngine { } public func play() { + wantsToPlay = true player.play() } public func pause() { + wantsToPlay = false player.pause() } @@ -342,7 +357,12 @@ public final class AVPlayerEngine: VideoPlayerEngine { guard let self else { return } switch player.timeControlStatus { case .playing: self.state = .playing - case .paused: if self.state != .ended { self.state = .paused } + case .paused: + guard self.state != .ended else { break } + // Stillstand mit Absicht ist Pause. Stillstand ohne + // Absicht ist Warten — und gehört als solches + // gemeldet, nicht als Pause. + self.state = self.wantsToPlay ? .buffering : .paused case .waitingToPlayAtSpecifiedRate: self.state = .buffering @unknown default: break }