Stillstand ohne Absicht ist kein Pausieren
`timeControlStatus` meldet zwei verschiedene Dinge als `.paused`: das Anhalten durch den Nutzer und den Fall, dass `play()` gerufen wurde, der Player aber nicht losläuft — weil die Quelle nicht bereit ist oder weil nichts ankommt. Die Engine hat beides als „pausiert" weitergegeben. Sichtbar war das als Player mit Abspieldreieck und Scrubber auf 0:00, während in Wahrheit auf Daten gewartet wurde. Wer dann tippte, drückte auf Play bei etwas, das längst laufen sollte. Die Engine führt jetzt mit, ob überhaupt jemand spielen wollte. Stillstand mit Absicht ist Pause, Stillstand ohne Absicht ist Warten. Dazu die Anzeigeregel, wie gewünscht: **der Kreisel gilt immer**, mit oder ohne eingeblendete Bedienung. Er ist keine Verzierung der Knöpfe, sondern die Auskunft „es kommt gleich weiter" — und die braucht man gerade dann, wenn nichts eingeblendet ist und man vor einem stehenden Bild sitzt. Das Play-Pause-Symbol erscheint dagegen nur mit der übrigen Bedienung, und an seiner Stelle bleibt beim Laden eine Lücke, damit der Kreisel hindurchscheint statt zwei Anzeigen übereinanderzulegen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
baa542690c
commit
ab597ee2f0
@@ -86,14 +86,17 @@ struct PlayerView: View {
|
|||||||
failure(message: message)
|
failure(message: message)
|
||||||
|
|
||||||
case .playing:
|
case .playing:
|
||||||
// Wartet auf Daten: ohne Anzeige sieht das aus wie ein
|
// **Der Kreisel gilt immer**, mit oder ohne
|
||||||
// Standbild, und man weiß nicht, ob noch etwas kommt.
|
// Bedienelemente.
|
||||||
//
|
//
|
||||||
// Nur, solange die Bedienelemente aus sind. Sind sie
|
// Er ist keine Verzierung der Bedienung, sondern die
|
||||||
// an, steht der Kreisel **im** Abspielknopf — dort, wo
|
// Auskunft „es kommt gleich weiter" — und die braucht
|
||||||
// sonst das Symbol ist. Vorher lag er dahinter und das
|
// man gerade dann, wenn man nichts eingeblendet hat
|
||||||
// Dreieck davor: es sah pausiert aus, war es nicht.
|
// und vor einem stehenden Bild sitzt. Die Knöpfe
|
||||||
if model.isBuffering && !showsControls {
|
// darüber lassen an seiner Stelle Platz, damit er
|
||||||
|
// hindurchscheint und nicht zwei Anzeigen
|
||||||
|
// übereinanderliegen.
|
||||||
|
if model.isBuffering {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.tint(.white)
|
.tint(.white)
|
||||||
.scaleEffect(1.4)
|
.scaleEffect(1.4)
|
||||||
@@ -368,19 +371,21 @@ struct PlayerView: View {
|
|||||||
scheduleControlsHide()
|
scheduleControlsHide()
|
||||||
} label: {
|
} label: {
|
||||||
ZStack {
|
ZStack {
|
||||||
// Solange auf Daten gewartet wird, steht hier der Kreisel
|
// Solange auf Daten gewartet wird, steht hier **nichts**.
|
||||||
// — kein Dreieck. Ein Dreieck heißt „angehalten, tipp
|
//
|
||||||
// mich", und beides stimmte nicht: es lief bereits, und
|
// Kein Dreieck: das hieße „angehalten, tipp mich", und
|
||||||
// der Tipp bewirkte nichts.
|
// 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 {
|
if model.isBuffering {
|
||||||
ProgressView()
|
Color.clear
|
||||||
.tint(.white)
|
|
||||||
.scaleEffect(1.4)
|
|
||||||
} else {
|
} else {
|
||||||
Image(systemName: model.isPlaying ? "pause.fill" : "play.fill")
|
Image(systemName: model.isPlaying ? "pause.fill" : "play.fill")
|
||||||
.font(.system(size: 52))
|
.font(.system(size: 52))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.contentShape(.rect)
|
||||||
// Feste Größe, damit die beiden Knöpfe daneben stehen
|
// Feste Größe, damit die beiden Knöpfe daneben stehen
|
||||||
// bleiben. Sie wanderten bisher bei jedem Wechsel ein Stück,
|
// bleiben. Sie wanderten bisher bei jedem Wechsel ein Stück,
|
||||||
// weil Dreieck und Doppelbalken verschieden breit sind.
|
// weil Dreieck und Doppelbalken verschieden breit sind.
|
||||||
|
|||||||
@@ -60,6 +60,18 @@ public final class AVPlayerEngine: VideoPlayerEngine {
|
|||||||
/// `statistics` später „keine Leiter" von „eine Stufe".
|
/// `statistics` später „keine Leiter" von „eine Stufe".
|
||||||
private var variantBitrates: [Int] = []
|
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.
|
/// Wie oft es gestockt hat, seit diese Quelle läuft.
|
||||||
///
|
///
|
||||||
/// Aus `playbackStalledNotification` und **nicht** aus dem
|
/// 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
|
// 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.
|
// fängt bei null an, sonst schleppte die Zahl den vorigen Film mit.
|
||||||
variantBitrates = []
|
variantBitrates = []
|
||||||
|
wantsToPlay = false
|
||||||
stallCount = 0
|
stallCount = 0
|
||||||
accumulatedStall = .zero
|
accumulatedStall = .zero
|
||||||
bufferingSince = nil
|
bufferingSince = nil
|
||||||
@@ -170,10 +183,12 @@ public final class AVPlayerEngine: VideoPlayerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func play() {
|
public func play() {
|
||||||
|
wantsToPlay = true
|
||||||
player.play()
|
player.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func pause() {
|
public func pause() {
|
||||||
|
wantsToPlay = false
|
||||||
player.pause()
|
player.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,7 +357,12 @@ public final class AVPlayerEngine: VideoPlayerEngine {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch player.timeControlStatus {
|
switch player.timeControlStatus {
|
||||||
case .playing: self.state = .playing
|
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
|
case .waitingToPlayAtSpecifiedRate: self.state = .buffering
|
||||||
@unknown default: break
|
@unknown default: break
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user