Vier Fehler aus dem ersten echten Gebrauch

Kein Video lief jemals an
  AVPlayerEngine.load() wartete auf asset.load(.duration) und die
  Spurenliste, BEVOR es zurückkehrte — und erst danach rief das Modell
  play(). AVFoundation muss für die Dauer aber so viel von der Datei
  holen, bis es den Index findet, und der sitzt bei MP4 wie MKV oft am
  Ende. Über eine langsame Leitung sind das Minuten, in denen nur ein
  Ladekreis zu sehen war, obwohl der Player längst hätte anfangen können.
  Jetzt kehrt load() zurück, sobald das Item hängt; Dauer und Spuren
  kommen nach und melden sich über den Ereignisstrom.
  Dazu AVURLAssetPreferPreciseDurationAndTimingKey: false — sonst
  durchsucht AVFoundation bei manchen Formaten die ganze Datei.

  Zweiter Fund an derselben Stelle: VLCMedia(url:) ist nullable. Ging es
  schief, blieb media still nil, play() tat nichts und es gab keinen
  Fehler — nur einen Ladekreis ohne Ende. Wird jetzt gemeldet.

Kein Querformat im Player
  requestGeometryUpdate allein dreht nicht: das System dreht sofort
  zurück, solange die erlaubten Ausrichtungen es nicht hergeben. Die
  Erlaubnis kommt nur über den App-Delegaten, deshalb OrientationLock
  als Brücke. Player dreht ins Querformat, beim Verlassen zurück. iPad
  bleibt frei — dort ist Querformat beim Blättern der Normalfall.

Uhrzeit über dem Film
  Die Statusleiste war nur ausgeblendet, solange die Bedienelemente weg
  waren. Jetzt immer.

Sehr langer Start
  Zwei Ursachen. Erstens legte der Initialisierer der App die
  Hintergrund-URLSession an — die spricht über XPC mit nsurlsessiond und
  verzögerte den allerersten Bildaufbau. Jetzt erst bei Bedarf, und wer
  keine Downloads hat, braucht sie nie. Zweitens fragte jeder Start erst
  plex.tv nach der Serverliste und suchte die Verbindung neu. Die zuletzt
  funktionierende wird jetzt gemerkt (Server-Token im Schlüsselbund) und
  mit kurzer Zeitgrenze zuerst probiert; die vollständige Suche greift
  nur noch, wenn sie nicht mehr antwortet — etwa wenn man von zuhause
  weggeht.

Dazu ein Hinweis im Player, wenn die Verbindung über die gedrosselte
Plex-Vermittlung läuft. Sonst sucht man den Fehler in der App.

132 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-09-11 20:43:26 +02:00
co-authored by Claude Opus 5
parent 1756ed11cc
commit 222ac1c968
11 changed files with 394 additions and 66 deletions
+83 -2
View File
@@ -43,8 +43,18 @@ final class FlixySession {
private enum Key {
static let preferredServer = "flixy.server.clientIdentifier"
static let cachedConnectionURI = "flixy.server.connectionURI"
static let cachedServerName = "flixy.server.name"
static let cachedConnectionIsLocal = "flixy.server.connectionIsLocal"
}
/// Unter welchem Namen der Server-Token im Schlüsselbund liegt.
///
/// Nicht in den Voreinstellungen: er gibt vollen Zugriff auf die
/// Mediathek, genau wie der Konto-Token.
private static let serverTokenAccount = "plex.server"
/// Wird für die Anmelde-Adresse gebraucht: plex.tv ordnet den
/// bestätigten Code darüber diesem Gerät zu.
var clientIdentifier: String { identity.clientIdentifier }
@@ -81,9 +91,60 @@ final class FlixySession {
phase = .signedOut
return
}
// Erst der kurze Weg: die Verbindung, die beim letzten Mal
// funktioniert hat. Ohne ihn kostet jeder Start eine Anfrage an
// plex.tv plus eine neue Verbindungssuche — zusammen mehrere
// Sekunden, in denen nur ein Ladekreis zu sehen ist.
if await restoreFromRememberedConnection() { return }
await connect(accountToken: token)
}
/// Nimmt die zuletzt funktionierende Verbindung wieder auf.
///
/// Gibt `false` zurück, wenn nichts gespeichert ist oder der Server nicht
/// mehr darüber erreichbar ist — dann übernimmt die vollständige Suche.
/// Das ist der Fall, wenn man von zuhause weggeht: die lokale Adresse
/// antwortet nicht mehr, und der direkte Weg muss neu gefunden werden.
private func restoreFromRememberedConnection() async -> Bool {
guard let identifier = defaults.string(forKey: Key.preferredServer),
let uri = defaults.string(forKey: Key.cachedConnectionURI),
let serverToken = tokenStore.token(for: Self.serverTokenAccount)
else { return false }
let name = defaults.string(forKey: Key.cachedServerName) ?? ""
phase = .connecting(serverName: name)
let connection = PlexConnection(
uri: uri,
local: defaults.bool(forKey: Key.cachedConnectionIsLocal)
)
let resource = PlexResource(
name: name,
clientIdentifier: identifier,
accessToken: serverToken,
connections: [connection]
)
// Kurze Zeitgrenze: wenn die gemerkte Adresse nicht sofort antwortet,
// ist die vollständige Suche schneller als weiteres Warten.
let selector = PlexConnectionSelector(identity: identity, probeTimeout: .seconds(2))
guard await selector.selectConnection(for: resource) != nil,
let server = PlexServer(resource: resource, connection: connection, identity: identity)
else { return false }
self.server = server
do {
libraries = try await server.libraries().filter(\.isSupported)
phase = .ready
return true
} catch {
self.server = nil
return false
}
}
// MARK: - Anmeldung
/// Öffnet die Anmeldeseite von Plex.
@@ -148,7 +209,7 @@ final class FlixySession {
func signOut() {
authorizationTask?.cancel()
authenticator.signOut()
defaults.removeObject(forKey: Key.preferredServer)
forgetRememberedConnection()
server = nil
libraries = []
phase = .signedOut
@@ -207,7 +268,7 @@ final class FlixySession {
}
self.server = server
defaults.set(resource.clientIdentifier, forKey: Key.preferredServer)
remember(resource: resource, connection: connection)
do {
libraries = try await server.libraries().filter(\.isSupported)
@@ -217,6 +278,26 @@ final class FlixySession {
}
}
/// Merkt sich, worüber es funktioniert hat.
private func remember(resource: PlexResource, connection: PlexConnection) {
defaults.set(resource.clientIdentifier, forKey: Key.preferredServer)
defaults.set(connection.uri, forKey: Key.cachedConnectionURI)
defaults.set(connection.local, forKey: Key.cachedConnectionIsLocal)
defaults.set(resource.name, forKey: Key.cachedServerName)
if let token = resource.accessToken {
try? tokenStore.store(token, for: Self.serverTokenAccount)
}
}
/// Vergisst die gemerkte Verbindung.
private func forgetRememberedConnection() {
for key in [Key.preferredServer, Key.cachedConnectionURI,
Key.cachedConnectionIsLocal, Key.cachedServerName] {
defaults.removeObject(forKey: key)
}
tokenStore.remove(for: Self.serverTokenAccount)
}
/// Nach einem Fehler noch einmal versuchen, ohne die Anmeldung zu verlieren.
func retry() async {
if let token = authenticator.storedToken {