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>
113 lines
4.9 KiB
Swift
113 lines
4.9 KiB
Swift
// **Die ganze Datei nur für den Fernseher.**
|
|
//
|
|
// Sie liegt im geteilten Ordner, weil `Flixy/Session/` von beiden Zielen
|
|
// übersetzt wird — und `TVServices` gibt es auf iOS nicht. Ein `#if` um den
|
|
// einen Import genügte dabei nicht: der Abhängigkeits-Sucher von Clang prüft
|
|
// ihn trotzdem und bricht mit „Unable to resolve module dependency:
|
|
// TVServices" ab, ohne den Fernseher zu erwähnen. Dieselbe Falle wie bei
|
|
// siehe `PlaybackSettings`.
|
|
#if os(tvOS)
|
|
import Foundation
|
|
import MediaKit
|
|
import TVServices
|
|
|
|
/// Legt für das Regal über der App hin, was es zeigen soll.
|
|
///
|
|
/// **Beim Laden der Startseite, nicht auf einem Zeitgeber.** Das Regal wird
|
|
/// gezeichnet, wenn jemand im Menü über der App steht — und dann ist die App
|
|
/// nicht gestartet. Was dort steht, ist also immer das, was beim letzten
|
|
/// Öffnen zu sehen war. Ein Zeitgeber änderte daran nichts und hielte nur die
|
|
/// App wach.
|
|
@MainActor
|
|
enum TopShelfWriter {
|
|
/// Die App-Gruppe, die App und Erweiterung teilen.
|
|
static let group = "group.com.scarriffleservices.flixy"
|
|
|
|
/// Die Kante, in der das Regal Plakate zeigt.
|
|
///
|
|
/// **Apples Mass für die doppelte Auflösung**, und für beide Stufen
|
|
/// dasselbe Bild. Ein Apple TV zeichnet das Regal ohnehin doppelt
|
|
/// aufgelöst; zwei Adressen abzulegen hiesse, dem Server zwei Grössen
|
|
/// desselben Plakats abzuverlangen, damit eine davon nie gebraucht wird.
|
|
private static let posterWidth = 666
|
|
private static let posterHeight = 1000
|
|
|
|
static func update(
|
|
rows: [MediaRowGroup],
|
|
backend: (any MediaBackend)?
|
|
) {
|
|
guard let backend else { return }
|
|
|
|
let abschnitte = rows
|
|
.prefix(TopShelfSnapshot.sectionLimit)
|
|
.map { reihe in
|
|
TopShelfSnapshot.Section(
|
|
id: reihe.id,
|
|
title: reihe.title,
|
|
entries: reihe.items
|
|
.prefix(TopShelfSnapshot.entryLimit)
|
|
.map { item in
|
|
TopShelfSnapshot.Entry(
|
|
id: item.id,
|
|
title: item.rowTitle,
|
|
subtitle: item.cardSubtitle,
|
|
// **Das Hochkant-Plakat, nicht `thumb`.** Bei
|
|
// einer Folge ist `thumb` das Szenenbild; im
|
|
// Regal stünde davon ein zufälliger
|
|
// Ausschnitt. Dieselbe Rückfallkette wie auf
|
|
// den Kacheln.
|
|
imageURL: backend.artworkURL(
|
|
path: item.posterArtworkPath,
|
|
width: posterWidth,
|
|
height: posterHeight
|
|
),
|
|
progress: item.watchProgress
|
|
)
|
|
}
|
|
)
|
|
}
|
|
.filter { !$0.entries.isEmpty }
|
|
|
|
TopShelfStore.write(
|
|
TopShelfSnapshot(sections: Array(abschnitte)),
|
|
group: group
|
|
)
|
|
|
|
// **Und dem System sagen, dass es neu fragen soll.**
|
|
//
|
|
// Ohne diese Zeile bleibt die Erweiterung ungefragt, und im Header
|
|
// steht, was dann geschieht: „If the completion handler is not called
|
|
// in a timely manner or is called with empty content the system will
|
|
// display the static image instead." Genau das war zu sehen — das
|
|
// Plakat aus dem Asset-Katalog, gross und unverändert, während die
|
|
// Datei daneben längst drei Reihen enthielt.
|
|
//
|
|
// Die Erweiterung selbst kann das nicht auslösen: sie läuft nur,
|
|
// wenn sie gefragt wird.
|
|
TVTopShelfContentProvider.topShelfContentDidChange()
|
|
}
|
|
|
|
/// Was zuletzt abgelegt wurde — in einem Satz, zum Hinsehen.
|
|
///
|
|
/// **Weil man dem Regal sonst nichts ansieht.** Bleibt es leer, gibt es
|
|
/// genau drei Möglichkeiten: die App-Gruppe fehlt, die Datei ist nicht da,
|
|
/// oder sie ist da und das System fragt trotzdem nicht. Von aussen sehen
|
|
/// alle drei gleich aus — nämlich nach dem grossen Plakat aus dem
|
|
/// Asset-Katalog. Diese Zeile unterscheidet sie.
|
|
static var status: String {
|
|
guard let ort = TopShelfStore.location(group: group) else {
|
|
return String(localized: "topShelf.noGroup")
|
|
}
|
|
guard let abzug = TopShelfStore.read(group: group) else {
|
|
let daIst = FileManager.default.fileExists(atPath: ort.path)
|
|
return daIst
|
|
? String(localized: "topShelf.unreadable")
|
|
: String(localized: "topShelf.noFile")
|
|
}
|
|
let einträge = abzug.sections.reduce(0) { $0 + $1.entries.count }
|
|
let zeit = abzug.savedAt.formatted(date: .omitted, time: .shortened)
|
|
return String(localized: "topShelf.written \(abzug.sections.count) \(einträge) \(zeit)")
|
|
}
|
|
}
|
|
#endif
|