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>
107 lines
4.4 KiB
Swift
107 lines
4.4 KiB
Swift
import AuthenticationServices
|
|
import Foundation
|
|
import UIKit
|
|
|
|
/// Öffnet die Anmeldeseite von Plex in einem Fenster innerhalb der App.
|
|
///
|
|
/// `ASWebAuthenticationSession` ist dafür der vorgesehene Weg: die Seite kommt
|
|
/// von Plex, das Passwort wird dort eingegeben und erreicht die App nie. Nach
|
|
/// dem Anmelden schickt Plex den Browser auf eine Adresse mit unserem eigenen
|
|
/// Schema, das Fenster schließt sich von selbst, und die App weiß, dass sie
|
|
/// den Token abholen kann.
|
|
///
|
|
/// Ein einfacher `SFSafariViewController` täte es nicht: er meldet nicht, wann
|
|
/// die Rückadresse erreicht wurde, und müsste von Hand geschlossen werden.
|
|
@MainActor
|
|
final class PlexWebAuthenticator: NSObject {
|
|
enum Failure: Error, Equatable {
|
|
/// Der Nutzer hat das Fenster geschlossen. Kein Fehler im eigentlichen
|
|
/// Sinn — die Oberfläche soll darauf kommentarlos zurückgehen.
|
|
case cancelled
|
|
case presentationFailed(String)
|
|
}
|
|
|
|
/// 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?
|
|
|
|
/// Wie das Fenster ausgegangen ist.
|
|
///
|
|
/// `nil`, solange es noch offen ist.
|
|
private(set) var outcome: Failure?
|
|
private(set) var didReturn = false
|
|
|
|
/// Ö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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
extension PlexWebAuthenticator: ASWebAuthenticationPresentationContextProviding {
|
|
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
|
// Das Fenster der App. Über die verbundenen Szenen gesucht, weil eine
|
|
// SwiftUI-App keinen festen Verweis darauf hat — und ein frisch
|
|
// erzeugtes Fenster erschiene hinter dem sichtbaren.
|
|
let scene = UIApplication.shared.connectedScenes
|
|
.compactMap { $0 as? UIWindowScene }
|
|
.first { $0.activationState == .foregroundActive }
|
|
|
|
return scene?.keyWindow ?? ASPresentationAnchor()
|
|
}
|
|
}
|