Files
Flaro/Flixy/Session/PlexWebAuthenticator.swift
T
ScarriffleandClaude Opus 5 6b927c21b0 VLC ist weg — es gibt nur noch einen Spieler
Du hattest recht mit der Erwartung, nur nicht mit dem Stand: entfernt war er
nicht. Was wir gemacht haben, war, ihm Arbeit wegzunehmen — DTS, TrueHD und
Vorbis gehen seit Kurzem über das Umpacken beim Server. Übrig blieb er an
einer Stelle, `PlaybackDecider.fallback`.

Jetzt ist er ganz weg, samt dem 920-MB-Binärpaket.

**Was an seine Stelle tritt.** Die Entscheidung sagt nicht mehr, *wer* spielt,
sondern *was ankommt*: direkt, umgepackt oder umgerechnet. `PlaybackEngineKind`
fällt damit weg, `directPlay` trägt keine Engine mehr, und `PlaybackPolicy`
kennt kein `allowsFallbackEngine`.

**Das Auffangnetz bleibt, nur anders geknüpft.** Scheiterte das Umpacken am
Player, sprang bisher VLC ein — der Server entscheidet erst beim Ausliefern,
ob er die Ströme wirklich kopieren kann, und manche Antworten nimmt AVPlayer
nicht an. Statt der zweiten Engine rechnet der Server jetzt neu
(`fallBackToTranscode`). Teurer für ihn, aber es kommt etwas an, statt einer
schwarzen Wand mit einer Fehlernummer.

**Offline wäre das ein Rückschritt gewesen** — dort gibt es keinen Server, der
einspringt. Deshalb ging der vorige Commit voraus: was heruntergeladen wird,
kommt schon in einer Hülle, die der Systemplayer öffnet.

Was dadurch überall gilt, auch für Dateien, die bisher bei VLC landeten:
Bild-in-Bild, AirPlay, Bedienung im Sperrbildschirm — und dasselbe Verhalten
auf dem Apple TV, wo es VLC ohnehin nie gab.

Die Attrappe heißt ihren Schalter jetzt `--dts` statt `--vlc`: der Fall ist
geblieben, sein Ziel nicht.

iOS und tvOS bauen, 494 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 08:04:23 +02:00

153 lines
5.9 KiB
Swift

#if os(tvOS)
import Foundation
/// **Auf dem Fernseher gibt es kein Anmeldefenster.**
///
/// `ASWebAuthenticationSession` steht im tvOS-SDK, aber fast alles daran ist
/// als nicht verfügbar markiert: kein `presentationContextProvider`, kein
/// `prefersEphemeralWebBrowserSession`, nicht einmal `cancel()`. Ein
/// Fernseher hat auch keinen Browser, in dem sich jemand bei Plex anmelden
/// würde — und Plex ist in der ersten Fassung ohnehin nicht dabei.
///
/// **Der Typ bleibt trotzdem stehen.** `FlixySession` hält ihn als Feld und
/// ruft ihn an sechs Stellen; ihn herauszuschneiden hiesse, die
/// Zustandsmaschine der Anmeldung für eine Plattform zu spalten, die diesen
/// Zweig nie betritt. Dieselbe Bauform wie bei `OrientationLock`: derselbe
/// Name, dieselben Aufrufe, und nichts passiert.
@MainActor
final class PlexWebAuthenticator {
enum Failure: Error, Equatable {
case cancelled
case presentationFailed(String)
}
/// Immer „ging nicht": wer hier landet, soll nicht warten.
private(set) var outcome: Failure?
private(set) var didReturn = false
func present(url: URL, callbackScheme: String) {
outcome = .presentationFailed(String(localized: "signIn.windowFailed"))
didReturn = true
}
func dismiss() {}
}
#else
import AuthenticationServices
import Foundation
#if canImport(UIKit)
import UIKit
#else
import AppKit
#endif
/// Ö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 mancher Rückruf, 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. Gesucht statt gehalten, weil eine SwiftUI-App
// keinen festen Verweis darauf hat — und ein frisch erzeugtes Fenster
// erschiene hinter dem sichtbaren.
#if canImport(UIKit)
let scene = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first { $0.activationState == .foregroundActive }
return scene?.keyWindow ?? ASPresentationAnchor()
#else
return NSApplication.shared.keyWindow
?? NSApplication.shared.windows.first
?? ASPresentationAnchor()
#endif
}
}
#endif