Files
Flaro/Flixy/Session/PlexWebAuthenticator.swift
T
GuidoandClaude Opus 5 a2b3f901ed 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>
2026-09-11 21:25:17 +02:00

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()
}
}