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:
@@ -21,42 +21,74 @@ final class PlexWebAuthenticator: NSObject {
|
||||
case presentationFailed(String)
|
||||
}
|
||||
|
||||
/// Führt die Anmeldung durch und kehrt zurück, sobald Plex die App
|
||||
/// zurückgeschickt hat.
|
||||
func authenticate(url: URL, callbackScheme: String) async throws(Failure) {
|
||||
do {
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
|
||||
let session = ASWebAuthenticationSession(
|
||||
url: url,
|
||||
callbackURLScheme: callbackScheme
|
||||
) { _, error in
|
||||
if let error {
|
||||
continuation.resume(throwing: error)
|
||||
} else {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
/// Das laufende Fenster — damit es sich auch von außen schließen lässt.
|
||||
///
|
||||
/// Nötig, weil Plex den Rücksprung nicht immer ausführt: manchmal landet
|
||||
/// der Browser nach dem Anmelden auf watch.plex.tv und bleibt dort stehen.
|
||||
/// Der Token liegt dann längst bereit, nur das Fenster weiß es nicht.
|
||||
private var session: ASWebAuthenticationSession?
|
||||
|
||||
session.presentationContextProvider = self
|
||||
// `false`, damit eine bestehende Plex-Anmeldung in Safari
|
||||
// mitgenutzt wird: wer dort schon angemeldet ist, braucht nur
|
||||
// zu bestätigen statt das Passwort erneut einzugeben.
|
||||
session.prefersEphemeralWebBrowserSession = false
|
||||
/// Wie das Fenster ausgegangen ist.
|
||||
///
|
||||
/// `nil`, solange es noch offen ist.
|
||||
private(set) var outcome: Failure?
|
||||
private(set) var didReturn = false
|
||||
|
||||
guard session.start() else {
|
||||
continuation.resume(throwing: Failure.presentationFailed(
|
||||
String(localized: "signIn.windowFailed")
|
||||
))
|
||||
return
|
||||
/// Öffnet das Anmeldefenster und kehrt **sofort** zurück.
|
||||
///
|
||||
/// Bewusst nicht abwartend: 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 längst
|
||||
/// bereit, aber ein abwartender Aufruf hinge auf einen Rückruf, der nie
|
||||
/// kommt. Der Aufrufer fragt deshalb parallel beim Server nach und sieht
|
||||
/// hier nur nach, ob das Fenster zwischendurch geschlossen wurde.
|
||||
func present(url: URL, callbackScheme: String) {
|
||||
outcome = nil
|
||||
didReturn = false
|
||||
|
||||
let session = ASWebAuthenticationSession(
|
||||
url: url,
|
||||
callbackURLScheme: callbackScheme
|
||||
) { [weak self] _, error in
|
||||
// ASWebAuthenticationSession ruft den Abschlussblock auf dem
|
||||
// Hauptthread auf — anders als VLCKit, wo dieselbe Annahme die App
|
||||
// zum Absturz gebracht hat. Hier ist sie dokumentiert.
|
||||
MainActor.assumeIsolated {
|
||||
guard let self else { return }
|
||||
if let error = error as? ASWebAuthenticationSessionError,
|
||||
error.code == .canceledLogin {
|
||||
self.outcome = .cancelled
|
||||
} else if let error {
|
||||
self.outcome = .presentationFailed(error.localizedDescription)
|
||||
}
|
||||
self.didReturn = true
|
||||
self.session = nil
|
||||
}
|
||||
} catch let error as ASWebAuthenticationSessionError where error.code == .canceledLogin {
|
||||
throw .cancelled
|
||||
} catch let error as Failure {
|
||||
throw error
|
||||
} catch {
|
||||
throw .presentationFailed(error.localizedDescription)
|
||||
}
|
||||
|
||||
session.presentationContextProvider = self
|
||||
// `false`, damit eine bestehende Plex-Anmeldung in Safari mitgenutzt
|
||||
// wird: wer dort schon angemeldet ist, braucht nur zu bestätigen.
|
||||
session.prefersEphemeralWebBrowserSession = false
|
||||
|
||||
guard session.start() else {
|
||||
outcome = .presentationFailed(String(localized: "signIn.windowFailed"))
|
||||
didReturn = true
|
||||
return
|
||||
}
|
||||
self.session = session
|
||||
}
|
||||
}
|
||||
|
||||
extension PlexWebAuthenticator {
|
||||
/// Schließt das Anmeldefenster.
|
||||
///
|
||||
/// Wird gerufen, sobald der Token auf anderem Weg eingetroffen ist — sonst
|
||||
/// bliebe der Browser mit der Plex-Seite offen stehen, obwohl die App
|
||||
/// schon angemeldet ist.
|
||||
func dismiss() {
|
||||
session?.cancel()
|
||||
session = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user