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>
153 lines
5.9 KiB
Swift
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
|