Absturz beim Abspielen, und die Annahme dahinter
Der Crashlog vom Gerät:
_dispatch_assert_queue_fail
_swift_task_checkIsolatedSwift
MainActor.assumeIsolated
VLCPlayerEngine.mediaPlayerStateChanged(_:)
VLCKit input_thread_Events
Im Code stand als Begründung für assumeIsolated, VLCKit stelle seine
Rückrufe über die Hauptwarteschlange zu. Das ist falsch — es ruft den
Delegaten von seinem eigenen Input-Thread. Die App brach deshalb hart
ab, sobald VLC den ersten Zustandswechsel meldete, also unmittelbar
beim Drücken auf Play.
Alle drei Delegat-Methoden springen jetzt mit Task { @MainActor in }
sauber hinüber. Die Reihenfolge bleibt erhalten: die Aufgaben werden aus
demselben Thread nacheinander eingereiht.
Dieselbe Annahme steckte im AVPlayer-Motor in den KVO-Beobachtern —
KVO meldet auf dem Thread, der die Änderung ausgelöst hat, und AVPlayer
ändert seinen Zustand auch aus internen Threads. Ebenfalls umgestellt.
Die beiden Rückrufe mit ausdrücklichem queue: .main behalten
assumeIsolated: dort ist die Annahme belegt statt geraten, und der
Zeitbeobachter feuert viermal je Sekunde.
Dazu die Anmeldung: Plex führt den Rücksprung über forwardUrl nicht
zuverlässig aus. Auf dem iPad landet der Browser nach dem Anmelden auf
watch.plex.tv und bleibt stehen — der Token liegt bereit, aber die App
wartete auf einen Rückruf, der nie kam. Das Fenster wird jetzt geöffnet,
ohne darauf zu warten; parallel wird im Sekundentakt beim Server
nachgefragt. Der Rücksprung ist damit eine Annehmlichkeit, keine
Voraussetzung.
132 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -150,47 +150,77 @@ final class FlixySession {
|
||||
/// Öffnet die Anmeldeseite von Plex.
|
||||
///
|
||||
/// Der PIN steckt weiterhin darunter — er ist der einzige Weg, auf dem
|
||||
/// Plex einen Token herausgibt. Sichtbar ist er nicht mehr: Plex liefert
|
||||
/// seine eigene Anmeldemaske aus und schickt die App danach über
|
||||
/// `forwardUrl` zurück.
|
||||
/// Plex einen Token herausgibt. Sichtbar ist er nicht mehr.
|
||||
///
|
||||
/// Das Fenster wird geöffnet, aber **nicht abgewartet**: Plex führt den
|
||||
/// Rücksprung über `forwardUrl` nicht zuverlässig aus. Auf dem iPad landet
|
||||
/// der Browser nach dem Anmelden auf watch.plex.tv und bleibt dort stehen
|
||||
/// — der Token liegt längst bereit, aber die App wartete auf einen
|
||||
/// Rückruf, der nie kam. Stattdessen wird hier im Sekundentakt beim Server
|
||||
/// nachgefragt und nebenbei geprüft, ob das Fenster geschlossen wurde.
|
||||
func beginSignIn() async {
|
||||
phase = .authenticating
|
||||
|
||||
let pin: PlexPin
|
||||
do {
|
||||
let pin = try await authenticator.requestPin()
|
||||
pin = try await authenticator.requestPin()
|
||||
} catch {
|
||||
phase = .failed(message: message(for: error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = pin.authorizationURL(
|
||||
clientIdentifier: identity.clientIdentifier,
|
||||
product: Branding.plexProduct,
|
||||
forwardURL: Branding.authCallbackURL
|
||||
) else {
|
||||
phase = .failed(message: String(localized: "signIn.windowFailed"))
|
||||
guard let url = pin.authorizationURL(
|
||||
clientIdentifier: identity.clientIdentifier,
|
||||
product: Branding.plexProduct,
|
||||
forwardURL: Branding.authCallbackURL
|
||||
) else {
|
||||
phase = .failed(message: String(localized: "signIn.windowFailed"))
|
||||
return
|
||||
}
|
||||
|
||||
webAuthenticator.present(url: url, callbackScheme: Branding.urlScheme)
|
||||
|
||||
// Die Frist entspricht der Gültigkeit des Codes bei Plex.
|
||||
for _ in 0..<900 {
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
|
||||
if let token = try? await authenticator.token(for: pin) {
|
||||
webAuthenticator.dismiss()
|
||||
await connect(accountToken: token)
|
||||
return
|
||||
}
|
||||
|
||||
try await webAuthenticator.authenticate(
|
||||
url: url,
|
||||
callbackScheme: Branding.urlScheme
|
||||
)
|
||||
|
||||
// Das Fenster ist zu, jetzt liegt der Token bereit. Kurz
|
||||
// nachgefragt statt sofort erwartet: zwischen dem Rücksprung und
|
||||
// dem Eintrag bei Plex liegen manchmal ein, zwei Sekunden.
|
||||
let token = try await authenticator.waitForAuthorization(
|
||||
pin: pin,
|
||||
pollInterval: .milliseconds(500),
|
||||
timeout: .seconds(20)
|
||||
)
|
||||
await connect(accountToken: token)
|
||||
|
||||
} catch PlexWebAuthenticator.Failure.cancelled {
|
||||
// Fenster geschlossen. Kein Fehler — kommentarlos zurück.
|
||||
phase = .signedOut
|
||||
} catch let error as PlexWebAuthenticator.Failure {
|
||||
phase = .failed(message: webAuthenticationMessage(for: error))
|
||||
} catch {
|
||||
phase = .failed(message: message(for: error))
|
||||
// Fenster geschlossen, ohne dass ein Token da ist.
|
||||
if webAuthenticator.didReturn {
|
||||
switch webAuthenticator.outcome {
|
||||
case .cancelled, nil:
|
||||
// Abgebrochen oder zurückgesprungen, ohne dass der Token
|
||||
// schon bereitlag. Noch ein paar Sekunden nachfragen,
|
||||
// bevor aufgegeben wird — zwischen Rücksprung und Eintrag
|
||||
// bei Plex liegt manchmal eine Sekunde.
|
||||
if let token = try? await graceTokenLookup(pin: pin) {
|
||||
await connect(accountToken: token)
|
||||
return
|
||||
}
|
||||
phase = .signedOut
|
||||
case .presentationFailed(let description):
|
||||
phase = .failed(message: description)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
webAuthenticator.dismiss()
|
||||
phase = .failed(message: String(localized: "signIn.timedOut"))
|
||||
}
|
||||
|
||||
/// Fragt nach dem Schließen des Fensters noch ein paar Sekunden nach.
|
||||
private func graceTokenLookup(pin: PlexPin) async throws -> String? {
|
||||
for _ in 0..<5 {
|
||||
if let token = try? await authenticator.token(for: pin) { return token }
|
||||
try? await Task.sleep(for: .milliseconds(700))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cancelSignIn() {
|
||||
@@ -199,12 +229,6 @@ final class FlixySession {
|
||||
phase = .signedOut
|
||||
}
|
||||
|
||||
private func webAuthenticationMessage(for failure: PlexWebAuthenticator.Failure) -> String {
|
||||
switch failure {
|
||||
case .cancelled: String(localized: "signIn.cancelled")
|
||||
case .presentationFailed(let description): description
|
||||
}
|
||||
}
|
||||
|
||||
func signOut() {
|
||||
authorizationTask?.cancel()
|
||||
|
||||
Reference in New Issue
Block a user