Schritt sieben. Wiedergabe, Spurwahl, Titelzeile, „Intro überspringen". **Die schwierige Stelle war die Spurwahl.** Unsere Ton- und Untertitelwahl geht als `audioStreamIndex` an Jellyfin und verlangt bei einem Transcode einen **neuen Stream**. Apples eingebauter Spurwähler zeigt dagegen die Auswahlgruppen des **laufenden** Streams — beim Transcode genau eine. Er ist die richtige Oberfläche für die falsche Liste. Unsere Menüs sitzen deshalb über `transportBarCustomMenuItems` daneben; `customInfoViewControllers` wäre falsch, denn die Tonspur ist ein Bedienelement der Wiedergabe und kein Begleitmaterial. **Der Neuaufbau wird angesagt, nicht erklärt.** Jeder Eintrag, der Sekunden mit schwarzem Bild kostet, trägt das als Untertitel — vorher, nicht hinterher. Wer drückt, sieht den Haken sofort wandern, weil das Menü sich schliesst und darin kein Platz für einen Kreisel ist. Das schwarze Bild deckt eine Überlagerung in `contentOverlayView` zu, die hinter den Bedienelementen liegt und keinen Fokus nimmt — `customOverlayViewController` täte beides nicht und machte die Fernbedienung währenddessen tot. Und solange umgebaut wird, sind alle Einträge gesperrt: sonst räumt `restart()` eine Engine ab, die eine andere gerade baut. **Die Regel dahinter ist jetzt prüfbar.** „Wann kostet ein Spurwechsel einen Neuaufbau" lag bisher mitten im Vollzug von `selectAudioStream` und liess sich nur an einem echten Server mit mehreren Tonspuren auslösen. Als `TrackMenu` steht sie neben den übrigen Entscheidungsregeln, mit sieben Tests — darunter der Fall, auf den der Kommentar dort seit jeher hinweist: stimmt die Spurzahl nicht überein, ist die Zuordnung über die Reihenfolge falsch, und es muss neu aufgebaut werden. **Zwei stille Fehler nebenbei.** Die Tonsitzung stand hinter `#if os(iOS)` und wäre auf tvOS nie gesetzt worden — verfügbar ist sie dort seit 9.0, man hätte es nur daran gemerkt, dass sich die Wiedergabe gegen anderen Ton nicht durchsetzt. Und `PlayerModel.downloads` ist jetzt optional statt eines leeren Koordinators: ein leerer bräuchte einen `ModelContainer`, und der ist das Einzige, wofür diese App SwiftData benutzt. **Belegt statt vermutet.** Im Simulator lief die Wiedergabe gegen die Attrappe, und die Titelzeile zeigte „Unbekannt". Statt an AVKit zu drehen, hat ein Protokolleintrag die Frage beantwortet: `externalMetadata` wird gesetzt, der Wert stimmt, und „Unbekannt" ist wörtlich das, was die Attrappe für unbekannte Kennungen liefert. Geblieben ist davon nur die Erkenntnis, dass die Beschriftung auf das Item warten muss — beim Austausch des Players gibt es noch keines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
303 lines
12 KiB
YAML
303 lines
12 KiB
YAML
name: Flixy
|
|
|
|
options:
|
|
bundleIdPrefix: com.scarriffleservices
|
|
deploymentTarget:
|
|
iOS: "26.0"
|
|
macOS: "26.0"
|
|
tvOS: "26.0"
|
|
createIntermediateGroups: true
|
|
groupSortPosition: top
|
|
developmentLanguage: en
|
|
|
|
settings:
|
|
base:
|
|
DEVELOPMENT_TEAM: PP34X97WS3
|
|
SWIFT_VERSION: "6.0"
|
|
MARKETING_VERSION: "0.1.0"
|
|
CURRENT_PROJECT_VERSION: "1"
|
|
SWIFT_STRICT_CONCURRENCY: complete
|
|
|
|
packages:
|
|
FlixyKit:
|
|
path: Packages/FlixyKit
|
|
# Eigenes Package, weil das VLCKit-Binärpaket 920 MB wiegt und sonst bei
|
|
# jedem `swift test` in FlixyKit mitgezogen würde.
|
|
FlixyVLC:
|
|
path: Packages/FlixyVLC
|
|
|
|
targets:
|
|
Flixy:
|
|
type: application
|
|
platform: iOS
|
|
# Ausdrücklich aufgeschrieben, statt Xcode raten zu lassen.
|
|
#
|
|
# Xcode legt für jedes Ziel von selbst ein Schema an — aber im
|
|
# Benutzerordner, und `xcodegen generate` räumt den mit weg. Danach
|
|
# findet `xcodebuild` kein Schema mehr und kann die Pakete nicht
|
|
# auflösen. Hier steht es im Projekt und überlebt jedes Neuerzeugen.
|
|
scheme:
|
|
testTargets:
|
|
- FlixyUITests
|
|
sources:
|
|
# Das Icon ist ein Bündel, also technisch ein Ordner. Ohne diesen
|
|
# Ausschluss läuft XcodeGen hinein und trägt icon.json und play.svg
|
|
# einzeln ein — dann fehlt das Icon, ohne dass irgendwo ein Fehler
|
|
# auftaucht.
|
|
#
|
|
- path: Flixy
|
|
excludes:
|
|
- "AppIcon.icon"
|
|
- path: Flixy/AppIcon.icon
|
|
type: file
|
|
dependencies:
|
|
- target: DownloadWidget
|
|
# Die neutrale Grundschicht. Ausdrücklich genannt, obwohl sie über
|
|
# PlexKit ohnehin im Baum hängt: die App soll sie direkt benutzen und
|
|
# nicht davon abhängen, dass ein bestimmter Server sie mitschleppt.
|
|
- package: FlixyKit
|
|
product: MediaKit
|
|
- package: FlixyKit
|
|
product: PlexKit
|
|
# Der zweite Server. Steht neben PlexKit, nicht darin: die beiden teilen
|
|
# die neutrale Schicht und sonst nichts.
|
|
- package: FlixyKit
|
|
product: JellyfinKit
|
|
- package: FlixyKit
|
|
product: FlixyDesign
|
|
- package: FlixyKit
|
|
product: FlixyPlayback
|
|
- package: FlixyKit
|
|
product: FlixyDownloads
|
|
- package: FlixyVLC
|
|
product: FlixyVLC
|
|
settings:
|
|
base:
|
|
PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flixy
|
|
PRODUCT_NAME: Flixy
|
|
# Verweist auf AppIcon.icon. Der frühere leere AppIcon.appiconset im
|
|
# Asset-Katalog ist entfernt — zwei Dinge namens AppIcon, und Xcode
|
|
# nimmt das falsche.
|
|
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
|
# iPhone und iPad aus demselben Target.
|
|
TARGETED_DEVICE_FAMILY: "1,2"
|
|
CODE_SIGN_STYLE: Automatic
|
|
info:
|
|
path: Flixy/Info.plist
|
|
properties:
|
|
CFBundleDisplayName: Flixy
|
|
UILaunchScreen:
|
|
UIColorName: LaunchBackground
|
|
|
|
# Der harte Schalter für „nur dunkel". Eine Ansicht mit
|
|
# .preferredColorScheme(.dark) allein reicht nicht: Systemdialoge,
|
|
# Statusleiste und Tastatur richten sich danach nicht.
|
|
UIUserInterfaceStyle: Dark
|
|
|
|
# Alle drei bleiben eingetragen: die Info.plist sagt, was die App
|
|
# grundsätzlich kann. Was davon gerade gilt, entscheidet zur Laufzeit
|
|
# OrientationLock über den App-Delegaten — Hochformat beim Blättern,
|
|
# Querformat im Player.
|
|
UISupportedInterfaceOrientations:
|
|
- UIInterfaceOrientationPortrait
|
|
- UIInterfaceOrientationLandscapeLeft
|
|
- UIInterfaceOrientationLandscapeRight
|
|
UISupportedInterfaceOrientations~ipad:
|
|
- UIInterfaceOrientationPortrait
|
|
- UIInterfaceOrientationPortraitUpsideDown
|
|
- UIInterfaceOrientationLandscapeLeft
|
|
- UIInterfaceOrientationLandscapeRight
|
|
|
|
# Ohne diesen Modus hält iOS die Wiedergabe an, sobald die App in den
|
|
# Hintergrund geht — und Bild-in-Bild gäbe es gar nicht.
|
|
UIBackgroundModes:
|
|
- audio
|
|
|
|
# Das Schema, über das Plex nach der Anmeldung zurückschickt. Ohne
|
|
# diesen Eintrag kennt iOS es nicht, und ASWebAuthenticationSession
|
|
# käme nie zurück — das Anmeldefenster bliebe offen stehen.
|
|
CFBundleURLTypes:
|
|
- CFBundleURLName: com.scarriffleservices.flixy.auth
|
|
CFBundleURLSchemes:
|
|
- com.scarriffleservices.flixy
|
|
|
|
# Die Ausfuhr-Erklärung zur Verschlüsselung.
|
|
#
|
|
# `false` heißt: die App nutzt ausschließlich Verschlüsselung, die
|
|
# unter die Ausnahmeregelung fällt. Das trifft hier zu — HTTPS über
|
|
# URLSession und der Schlüsselbund für die Tokens, beides vom
|
|
# Betriebssystem gestellt. Eigene Algorithmen gibt es keine.
|
|
#
|
|
# Ohne diesen Eintrag fragt App Store Connect bei JEDEM Build erneut
|
|
# („Missing Compliance"), und der Build lässt sich bis dahin nicht
|
|
# verteilen.
|
|
#
|
|
# Zu beachten, falls sich das ändert: VLCKit ist ein fremdes Binary
|
|
# und bringt eigene Netzwerkbibliotheken mit. Solange nur Dateien vom
|
|
# eigenen Server abgespielt werden, bleibt es bei Standardverfahren.
|
|
# Käme irgendwann etwas mit eigener Verschlüsselung dazu — DRM etwa —,
|
|
# gehört dieser Wert überprüft.
|
|
ITSAppUsesNonExemptEncryption: false
|
|
|
|
# Ohne diesen Schlüssel lehnt ActivityKit jede Anfrage ab — und
|
|
# zwar wortlos, was die Suche nach dem Grund unangenehm macht.
|
|
NSSupportsLiveActivities: true
|
|
|
|
NSAppTransportSecurity:
|
|
# Plex-Server im Heimnetz laufen überwiegend auf blankem HTTP an
|
|
# Port 32400. Diese Ausnahme erlaubt genau das, ohne die
|
|
# Transportsicherheit für das offene Netz abzuschalten.
|
|
NSAllowsLocalNetworking: true
|
|
|
|
# Seit iOS 14 verlangt jeder Zugriff auf Adressen im eigenen Netz eine
|
|
# Freigabe. Fehlt der Text, verweigert das System sie kommentarlos und
|
|
# die Serversuche findet dauerhaft nichts.
|
|
NSLocalNetworkUsageDescription: >-
|
|
Flixy benötigt Zugriff auf das lokale Netzwerk, um deinen
|
|
Plex-Server zu finden und Medien direkt von ihm zu laden.
|
|
|
|
# Die Fassung für den Fernseher.
|
|
#
|
|
# **Eigene Ansichten, geteilte Modelle.** Die Quellenpfade sind die ganze
|
|
# Aussage dieses Blocks: `FlixyTV/` gehört nur hierher, `Flixy/Session/`
|
|
# gehört beiden. Keine Datei muss dafür umziehen — XcodeGen nimmt Pfade je
|
|
# Ziel, und die iOS-Fassung sieht von der ganzen Sache nichts.
|
|
FlixyTV:
|
|
type: application
|
|
platform: tvOS
|
|
# Aus demselben Grund ausgeschrieben wie bei Flixy — siehe dort.
|
|
scheme: {}
|
|
sources:
|
|
# Die eigenen Ansichten.
|
|
- path: FlixyTV
|
|
# Die Modelle.
|
|
- path: Flixy/Session
|
|
excludes:
|
|
# Ein Fernseher dreht sich nicht.
|
|
- "OrientationLock.swift"
|
|
# Heruntergeladen wird auf dem Fernseher nichts.
|
|
#
|
|
# `DownloadRefiller.swift` und `DownloadService.swift` kommen
|
|
# trotzdem mit: `PlayerModel` nennt den Nachfüller in einem Zweig,
|
|
# der auf tvOS nie läuft (`downloads` ist dort `nil`), und der
|
|
# Nachfüller wiederum den Dienst. Sie auszuschliessen hiesse, eine
|
|
# Plattformfrage mitten in zweitausend Zeilen Wiedergabelogik zu
|
|
# tragen — teurer als zwei Dateien mitzuübersetzen, die niemand
|
|
# aufruft. Beide sind reine Übersetzer ohne UIKit.
|
|
# Reines Modell trotz des Ordnernamens: LoadState, ErrorText und
|
|
# MediaRoute liegen zusammen in einer Datei.
|
|
- path: Flixy/Views/LoadState.swift
|
|
type: file
|
|
# Der Player. **Ohne PlayerView** — die zeichnet auf iOS das Bild und
|
|
# alle Bedienelemente selbst; auf dem Fernseher macht das Apples
|
|
# AVPlayerViewController. Das Modell darunter ist dasselbe.
|
|
- path: Flixy/Views/Player/PlayerModel.swift
|
|
type: file
|
|
- path: Flixy/Views/Player/VideoSurface.swift
|
|
type: file
|
|
- path: Flixy/Branding.swift
|
|
type: file
|
|
# Die Texte. Dieselben Schlüssel, dieselben Übersetzungen.
|
|
- path: Flixy/Resources/Localizable.xcstrings
|
|
type: file
|
|
dependencies:
|
|
- package: FlixyKit
|
|
product: MediaKit
|
|
# Mitgenommen, obwohl Plex in Fassung 1 nicht angeboten wird:
|
|
# FlixySession kennt PlexResource und PlexBackend in seiner
|
|
# Zustandsmaschine. Das Paket mitzunehmen ist billiger, als die Sitzung
|
|
# zu spalten — PlexClientIdentity benutzt nur UIDevice, und das gibt es
|
|
# auf tvOS.
|
|
- package: FlixyKit
|
|
product: PlexKit
|
|
- package: FlixyKit
|
|
product: JellyfinKit
|
|
- package: FlixyKit
|
|
product: FlixyDesign
|
|
- package: FlixyKit
|
|
product: FlixyPlayback
|
|
# Nur damit `PlayerModel` den Typ nennen darf. Angelegt wird nie einer:
|
|
# FlixyTVApp reicht `nil` durch, und damit gibt es auf tvOS auch keinen
|
|
# ModelContainer und kein SwiftData.
|
|
- package: FlixyKit
|
|
product: FlixyDownloads
|
|
settings:
|
|
base:
|
|
PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flixy.tv
|
|
PRODUCT_NAME: Flixy
|
|
# Nur Apple TV.
|
|
TARGETED_DEVICE_FAMILY: "3"
|
|
# Kein AppIcon.icon: Icon Composer deckt iOS, macOS und watchOS ab,
|
|
# tvOS nicht. Der Name zeigt auf die Brand-Assets-Gruppe im Katalog,
|
|
# und der Katalog liegt unter FlixyTV/Resources.
|
|
ASSETCATALOG_COMPILER_APPICON_NAME: "App Icon & Top Shelf Image"
|
|
CODE_SIGN_STYLE: Automatic
|
|
info:
|
|
path: FlixyTV/Info.plist
|
|
properties:
|
|
CFBundleDisplayName: Flixy
|
|
UIUserInterfaceStyle: Dark
|
|
ITSAppUsesNonExemptEncryption: false
|
|
# Was hier **nicht** steht, und warum:
|
|
#
|
|
# UILaunchScreen — tvOS hat kein Startbild dieser Art.
|
|
# UISupportedInterfaceOrientations — ein Fernseher dreht sich nicht.
|
|
# NSSupportsLiveActivities — ActivityKit fehlt im tvOS-SDK.
|
|
# CFBundleURLTypes — das Schema ist für die Plex-Rückkehr.
|
|
# UIBackgroundModes: audio — ein Apple TV schickt keine App in den
|
|
# Hintergrund, aus dem sie weiterspielen müsste.
|
|
NSAppTransportSecurity:
|
|
NSAllowsLocalNetworking: true
|
|
NSLocalNetworkUsageDescription: >-
|
|
Flixy benötigt Zugriff auf das lokale Netzwerk, um deinen
|
|
Jellyfin-Server zu erreichen.
|
|
|
|
# Nur zum Hinsehen: fährt die App mit der Attrappe in einen bestimmten
|
|
# Zustand und hält sie dort an, damit von außen fotografiert werden kann.
|
|
# Kein Teil der App, wird nie mitgeliefert.
|
|
FlixyUITests:
|
|
type: bundle.ui-testing
|
|
platform: iOS
|
|
sources:
|
|
- FlixyUITests
|
|
dependencies:
|
|
- target: Flixy
|
|
settings:
|
|
base:
|
|
PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flixy.uitests
|
|
TEST_TARGET_NAME: Flixy
|
|
TARGETED_DEVICE_FAMILY: "1,2"
|
|
CODE_SIGN_STYLE: Automatic
|
|
GENERATE_INFOPLIST_FILE: YES
|
|
|
|
# Die Live Activity für Downloads. Eine eigene Erweiterung, weil iOS die
|
|
# Darstellung im Sperrbildschirm in einem getrennten Prozess zeichnet — die
|
|
# App selbst läuft zu dem Zeitpunkt meist gar nicht.
|
|
# Dieselben Quellen, dasselbe Aussehen.
|
|
DownloadWidget:
|
|
type: app-extension
|
|
platform: iOS
|
|
sources:
|
|
- path: DownloadWidget
|
|
dependencies:
|
|
- package: FlixyKit
|
|
product: FlixyDownloads
|
|
- package: FlixyKit
|
|
product: FlixyDesign
|
|
settings:
|
|
base:
|
|
PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flixy.downloadwidget
|
|
PRODUCT_NAME: DownloadWidget
|
|
CODE_SIGN_STYLE: Automatic
|
|
SKIP_INSTALL: YES
|
|
# Von XcodeGen erzeugt statt von Hand geschrieben. Eine eigene Datei ohne
|
|
# CFBundleIdentifier führt zu einer Erweiterung ganz ohne Kennung — und
|
|
# der Fehler lautet dann „not prefixed with the parent app's bundle
|
|
# identifier", was auf eine falsche Fährte führt.
|
|
info:
|
|
path: DownloadWidget/Info.plist
|
|
properties:
|
|
CFBundleDisplayName: Flixy Downloads
|
|
NSExtension:
|
|
NSExtensionPointIdentifier: com.apple.widgetkit-extension
|