Files
Flaro/Flixy/Session/TopShelfWriter.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

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