Player: beim Umstellen sah man den Filmanfang statt seiner Stelle
Ein frisch angeforderter Stream beginnt vorn, puffert und springt erst danach an die Stelle, an der man war. Zehn Sekunden lang lief sichtbar der Filmanfang — und der Eindruck, die Stelle sei weg und man müsse sie wiederfinden. Der Umstieg wird jetzt abgedeckt, solange der Player noch nicht dort ist, und die Abdeckung nennt die Stelle ausdrücklich. Genau darum geht es: nicht, dass es schneller wird, sondern dass niemand denkt, er habe etwas verloren. Aufgehoben wird sie, sobald der Player innerhalb von zwei Sekunden um die Stelle liegt. Dazu eine Notbremse nach dreißig Sekunden — springt er nie dorthin, bliebe sonst für immer eine schwarze Fläche über dem Bild. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
171ba99908
commit
33dfac352a
+2790
-2774
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,13 @@ final class PlayerModel {
|
||||
private(set) var quality: PlayerQuality = .automatic
|
||||
/// Ob gerade auf eine andere Stufe umgestellt wird.
|
||||
private(set) var isChangingQuality = false
|
||||
/// Wohin nach dem Umstellen zurückgesprungen wird.
|
||||
///
|
||||
/// Solange das gesetzt ist, ist das Bild nicht die Stelle, an der man war:
|
||||
/// ein frisch angeforderter Stream beginnt vorn, puffert und springt erst
|
||||
/// danach. Sichtbar war in dieser Zeit der **Filmanfang** — und der
|
||||
/// Eindruck, die Stelle verloren zu haben.
|
||||
private(set) var resumeTarget: Duration?
|
||||
|
||||
/// Was gerade über die Leitung kommt. Beim Öffnen der Wahl abgefragt.
|
||||
var statistics: PlaybackStatistics? { engine?.statistics }
|
||||
@@ -210,6 +217,7 @@ final class PlayerModel {
|
||||
defer { isChangingQuality = false }
|
||||
|
||||
let resumeAt = currentTime
|
||||
resumeTarget = resumeAt
|
||||
quality = newQuality
|
||||
|
||||
// Den alten Transcoder anhalten, bevor der neue anläuft. Sonst rechnet
|
||||
@@ -246,6 +254,13 @@ final class PlayerModel {
|
||||
}
|
||||
|
||||
await load(source, decision: newDecision)
|
||||
|
||||
// Notbremse: springt der Player nie an die Stelle, bliebe sonst für
|
||||
// immer eine Abdeckung über dem Bild.
|
||||
Task { [weak self] in
|
||||
try? await Task.sleep(for: .seconds(30))
|
||||
self?.resumeTarget = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Wiedergabe aus dem Download-Ordner.
|
||||
@@ -483,6 +498,7 @@ final class PlayerModel {
|
||||
/// weggenommen. Was man hörte: der alte Ton lief weiter — auch nachdem
|
||||
/// die App im Hintergrund war —, und das Neue blieb stumm.
|
||||
func silence() {
|
||||
resumeTarget = nil
|
||||
eventTask?.cancel()
|
||||
reportTask?.cancel()
|
||||
|
||||
@@ -666,6 +682,11 @@ final class PlayerModel {
|
||||
}
|
||||
case .timeChanged(let time):
|
||||
currentTime = time
|
||||
// Angekommen: ab hier zeigt das Bild wieder die Stelle,
|
||||
// an der man war.
|
||||
if let target = resumeTarget, time + .seconds(2) >= target {
|
||||
resumeTarget = nil
|
||||
}
|
||||
updateActiveMarker()
|
||||
case .durationResolved(let value):
|
||||
duration = value
|
||||
|
||||
@@ -51,6 +51,13 @@ struct PlayerView: View {
|
||||
// die Reihenfolge, und alles Spätere liegt darüber.
|
||||
seekZones
|
||||
|
||||
// Deckt den Umstieg ab. Ohne sie sieht man den Filmanfang,
|
||||
// während der neue Stream anläuft — und denkt, die Stelle sei
|
||||
// weg. Die Abdeckung nennt sie ausdrücklich.
|
||||
if let target = model.resumeTarget {
|
||||
restoring(to: target)
|
||||
}
|
||||
|
||||
switch model.phase {
|
||||
case .preparing:
|
||||
preparing(for: model)
|
||||
@@ -330,6 +337,26 @@ struct PlayerView: View {
|
||||
.animation(.snappy, value: model.hasReachedEnd)
|
||||
}
|
||||
|
||||
/// Die Abdeckung während des Umstellens.
|
||||
private func restoring(to target: Duration) -> some View {
|
||||
ZStack {
|
||||
Color.black.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: FlixySpacing.regular) {
|
||||
ProgressView().tint(.white)
|
||||
Text("player.restoring")
|
||||
.font(.flixyCardTitle)
|
||||
.foregroundStyle(.white)
|
||||
// Die Stelle ausdrücklich nennen: der ganze Zweck ist, dass
|
||||
// niemand denkt, er müsse sie wiederfinden.
|
||||
Text(target.timecode)
|
||||
.font(.system(.title3, design: .monospaced))
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
private func preparing(for model: PlayerModel) -> some View {
|
||||
VStack(spacing: FlixySpacing.loose) {
|
||||
ProgressView().tint(.white)
|
||||
|
||||
Reference in New Issue
Block a user