tvOS „Physarum Ambient" — Lean-back-App (T1) — Design¶
Datum: 2026-06-15
Scope: Neues tvOS-Ziel der nativen App (apple/PhysarumApp, Ziel Physarum, supportedDestinations enthält tvOS bereits). Engine unberührt (kein Shader-/Param-Layout-Eingriff; identischer Metal-Pfad). Web-App unberührt. macOS/iOS-Verhalten unverändert (alle tvOS-Pfade #if os(tvOS)-gekapselt).
Status: T1 implementiert (Commits 8f38a43…95e0442, 2026-06-15) — Build/Test grün (macOS 174/0, tvOS+iOS Sim-Build, EngineCheck, Sim-Render verifiziert); Geräte-Sign-off ausstehend (s. unten). (Brainstorm + Plan 2026-06-15.)
Folge-Inkrement: T2 = Phone-als-Second-Screen-Controller (eigene Spec → Plan). Diese Spec definiert die Intent-Naht, an der T2 andockt, baut T2 aber nicht.
Strategischer Kontext¶
Apple TV ist der Sweet-Spot der Post-v1.4-Ambiance/Mood-Richtung („build for the artifact, not income"). tvOS ist kein Port der iPad-App, sondern ein eigener Lean-back-Durchgang auf geteiltem Fundament (Metal-Engine + Model/Presets geteilt; WKWebView fällt auf tvOS weg — WebGPU ist dort kein Thema). Ergebnis dieses Inkrements: eine eigenständige, mit der Siri Remote allein voll bedienbare Ambiance-App, die ein kuratiertes Preset-Set als atmende Playlist rendert.
Befund (Codebase-Map + Recherche, 2026-06-15)¶
Wiederverwendung ist hoch — ~80 % ist plattform-agnostisch:
- 1:1 wiederverwendbar (Metal/Foundation, kein UIKit/AppKit): gesamte PhysarumEngine (SimEngine, Renderer, ShaderLibrary, Model/*), Player/RenderCoordinator.swift (Frame-Loop via MTKViewDelegate, Thermik-/Low-Power-Drossel, 3-Buffer-Semaphore), Player/LiveSnapshot.swift, Model/AppState.swift (select/advance/reset, Undo, Custom-Presets), Model/PresetStore.swift, Model/PresetCatalog.swift, Model/ParamRegistry.swift, Model/PresetMorph.swift (morphParams), Model/RandomStrategy.swift, Audio/AudioAnalyzer.swift (+ AudioBands/AudioFactors — auf tvOS inert, keine Quelle).
- Build-Stand: tvOS ist bereits deklariert — project.yml deploymentTarget.tvOS: "17.0", Target Physarum.supportedDestinations: [macOS, iOS, tvOS]; PhysarumEngine/Package.swift platforms: [… .tvOS(.v17)]. Shader transpilieren plattform-agnostisch nach MSL (apple/scripts/transpile-shaders.sh → Resources/Shaders/*.metal); Apple TV 4K unterstützt Metal voll.
Blocker, die vor tvOS-Lauf gefixt werden müssen (heute unguarded iOS/iPad-APIs):
1. Capture/CaptureSaver.swift — PHPhotoLibrary/Photos (auf tvOS nicht vorhanden) → #if os(iOS) || os(macOS).
2. Audio/AudioInput.swift — AVAudioSession-Setup #if os(iOS); tvOS hat kein Mikrofon → AudioInput auf tvOS nicht instanziieren (Stub/weglassen).
3. Model/DeviceIdiom.swift — UIDevice.current.userInterfaceIdiom liefert auf tvOS .tv (fällt heute auf .phone zurück) → .tv-Fall ergänzen.
4. App/RootView.swift, Library/LibraryView.swift — horizontalSizeClass (tvOS kennt keine Size-Classes) und PresentationDetent-Sheets (tvOS hat keine Sheets) → tvOS nutzt nicht RootView, sondern eine eigene TVRootScene.
5. App/PhysarumApp.swift — @main Scene: tvOS-Zweig auf TVRootScene (statt der macOS/iOS-RootView).
6. PhysarumAppTests.supportedDestinations enthält tvOS nicht → für die pure tvOS-Logik ergänzen (oder geteiltes Test-Target).
tvOS-Plattform-Constraints (Recherche, mit Quellen):
- App-Store-Regel 2.4.3: eine tvOS-App muss allein mit der Siri Remote voll bedienbar sein. „Funktioniert nur mit gekoppeltem Phone" = Ablehnung. → Das Phone (T2) ist additiv, der Remote-Befehlssatz muss vollständig sein. (Release ist laut Cockpit aufgeschoben → jetzt Design-Prinzip, später harter Gate.)
- Idle-Timer/Screensaver: ohne UIApplication.shared.isIdleTimerDisabled = true startet nach Idle der Aerial-Screensaver. Flag setzt sich auf manchen tvOS-Releases zurück → ggf. re-asserten.
- Kein Background-Rendering: GPU-Arbeit nach Backgrounding schlägt fehl (kIOGPUCommandBufferCallbackErrorBackgroundExecutionNotPermitted). Render-Loop bei scenePhase != .active pausieren + in-flight Command-Buffer drainen, bei Foreground fortsetzen.
- Thermik: Apple TV 4K ist lüfterlos und drosselt unter Dauerlast. Mehrstunden-Vollbild-Metal ist der Worst Case → an ProcessInfo.thermalState adaptieren (Drossel-Logik existiert im RenderCoordinator).
- Kein Drittanbieter-Screensaver/Aerial-Extension-API auf tvOS → es ist eine normale Full-screen-App, die sich per Idle-Timer selbst wach hält.
- T2-Vorgriff (nur Naht hier): idiomatischer Phone↔TV-Link 2026 = DeviceDiscoveryUI (DDDevicePickerViewController auf tvOS → liefert NWConnection), Fallback rohes Network.framework + Bonjour. Braucht Apple TV 4K + echtes Gerät (kein Simulator), eine Verbindung pro App. GameController/CloudKit als Live-Transport ausgeschlossen.
Entscheidungen (aus dem Brainstorm)¶
- TV autark, Phone additiv. Vollständiger Siri-Remote-Befehlssatz auf dem TV; das Phone (T2) speist später denselben Intent-Kanal als reicher Live-Controller.
- T1 zuerst, eigenständig. Diese Spec = nur die Lean-back-App. T2 (Phone-Link) bekommt eigene Spec → Plan. T1 definiert die abstrakte Intent-Naht, baut T2 nicht.
- Auto-Cycle-Playlist als Default. Langsamer kuratierter Durchlauf mit weichem
morphParams-Übergang; Remote kann skippen und den Cycle pinnen (= aktuelles Preset stehen lassen). - Visuell-only T1; Phone-Mic-Audio ist T2-Ziel. Audio-Reaktivität auf dem TV inert (keine Quelle). Der Intent-Kanal wird so designt, dass T2 einen leichten Audio-Bänder-Stream vom Phone andocken kann.
- Architektur: SwiftUI-Shell + zentrale
ControlIntent-Schicht (AmbientControllerals einziger Sink). UIKit nur als kleiner Press-Bridge, falls SwiftUIs tvOS-Gesten-API an einer Stelle nicht reicht.
Architektur¶
tvOS bekommt eine eigene Szene (TVRootScene), die RootView (macOS/iOS) nie berührt. Sie hostet das full-bleed MetalView plus eine summon-bare Overlay-Ebene. Den Kern bildet ein AmbientController (@Observable/ObservableObject) mit einem Eingang apply(_ intent: ControlIntent). Jede Eingabequelle — heute die Siri Remote, in T2 die NWConnection — wird zu ControlIntent übersetzt und in diesen Sink geleitet. Diese Intent-Naht ist der einzige Integrationspunkt für T2.
Siri Remote ─▶ RemoteInput ─┐
├─▶ AmbientController.apply(intent) ─▶ AppState.select / Playback-State
PlaylistDirector (tick) ────┘ └─▶ RenderCoordinator.switchPreset | morph
(T2: NWConnection ─▶ decode ─▶ ControlIntent ─▶ same sink) AmbientController publiziert State ─▶ (T2: an Phone)
Neue Dateien (tvOS-only, apple/PhysarumApp/Sources/TV/)¶
TVRootScene.swift—@main-tvOS-Zweig; bautRenderCoordinator+AmbientController, hostetTVPlayerView.TVPlayerView.swift— full-bleedMetalView(neuer Representable-Zweig) +TVOverlayViewals bedingtes Overlay; Idle-Timer-/Lifecycle-Handling.AmbientController.swift— besitzt Playback-State (currentPresetID,isCycling, laufender Morph),playlist;apply(_ intent:); publiziert State für Overlay (und T2).ControlIntent.swift—enum ControlIntent { case next, previous, togglePlaylist, jumpTo(Preset.ID), showOverlay, dismissOverlay }. Erweiterbar in T2 (setParam,audioBands) — als eigener Fall, kein Umbau.PlaylistDirector.swift— Auto-Cycle-Logik (pure, testbar): Dwell-Timer (~60 s, tunebar), Reihenfolge/Auswahl des nächsten Presets, Morph-Trigger. Kein UIKit.- ~~
RemoteInput.swift~~ — umgesetzt als pureremoteMoveIntent(_:)inControlIntent.swift(host-testbar,#if os(tvOS) || os(macOS)); die SwiftUI-Gesten-Adapter (onMoveCommand/onPlayPauseCommand/onExitCommand) leben direkt inTVPlayerView. Kein separates File nötig. TVOverlayView.swift— summon-bares SwiftUI-Overlay: aktueller Preset-Name, horizontales fokussierbares Preset-Karussell (nutzt vorhandeneResources/Thumbnails) zum Direktsprung, Auto/Pin-Toggle; Auto-Dismiss nach Idle.
Angepasste Dateien¶
Player/MetalView.swift—#if os(tvOS)UIViewRepresentable-Zweig (KernlogikmakeCoordinator/syncwiederverwendet; ~20 LOC Plattform-Block).Model/DeviceIdiom.swift—.tv-Fall.Model/SimFormat.swift(FieldTier) — neuer.tv-Tier mit eigener Identität;FieldTier.currentmappt tvOS →.tv. Startwert konservativ, final am Gerät getunt (netzbetrieben → potenziell großzügiger als Phone, aber Mehrstunden-Thermik ist der Constraint).Model/PlayerPolicy.swift— tvOS-Pause-/FPS-Heuristik (Background-Pause,thermalState-Drossel; bestehende pure Fns wiederverwenden).App/PhysarumApp.swift—#if os(tvOS)Scene →TVRootScene.Capture/CaptureSaver.swift,Audio/AudioInput.swift— Plattform-Guards (s. Befund 1–2).project.yml— ggf. tvOS-spezifische Signing-Overrides;Info-Keys: tvOS braucht keinUILaunchScreen, keinNSPhotoLibraryAddUsageDescription, keinNSMicrophoneUsageDescription.
Datenfluss im Detail¶
- Render:
RenderCoordinatortreibtSimEngine+Rendererpro Frame in dasMTKView. Full-bleed 16:9 via vorhandenem adaptivem Sim-Format (Feld nimmt Screen-Aspect an → randlos). - Steuerung:
RemoteInputmappt Remote-Gesten →ControlIntent→AmbientController.apply. Der Controller mutiertAppState.select(...)bzw. Playback-State und ruftRenderCoordinator.switchPreset/ startet einen Morph. - Auto-Cycle:
PlaylistDirector-Tick erzeugt intern.next; der Übergang interpoliert viamorphParamsüber ~3–5 s (kein harter Schnitt). - T2-Naht (nicht in T1 gebaut): eingehende
NWConnection-Nachricht dekodiert zu denselbenControlIntents;AmbientControllerpubliziert seinen State (aktuelles Preset, Cycling) zurück, damit das Phone-UI spiegelt; Phone-Mic-Bänder gehen als künftiger Intent-/Stream-Fall anRenderCoordinator.applyAudio.
Preset-Set & Übergänge¶
- Playlist-Quelle: die 25 gebündelten Built-in-Presets (
Resources/Presets), in kuratierter Reihenfolge. (Custom-Presets entstehen nur über den Editor, den T1 nicht hat → in T1 keine Custom-Presets; T2 kann sie später beisteuern.) - Übergang:
morphParams-Interpolation ab-/zugehend über ~3–5 s. Dwell pro Preset ~60 s. Beide Werte als Konstanten mit klarer Benennung (am Gerät tunebar).
Ambient-Lifecycle¶
isIdleTimerDisabled = truebeim Erscheinen; bei Bedarf re-asserten.- Render-Loop pausieren + Command-Buffer drainen bei
scenePhase != .active/ Background; bei Foreground fortsetzen (in-flight-Buffer vor dem Suspend abschließen). preferredFramesPerSecondanProcessInfo.thermalStatekoppeln (bestehende Drossel-Logik)..tv-FieldTier hält die Dauerlast konservativ.
Siri-Remote-Mapping (2.4.3-vollständig)¶
| Eingabe | Aktion |
|---|---|
| Swipe/Klick ◀ ▶ | vorheriges / nächstes Preset (.previous / .next) |
| Play/Pause | Playlist pinnen ↔ wieder atmen lassen (.togglePlaylist) |
| Swipe ▲ | Overlay einblenden (.showOverlay) |
| Menu / Back | Overlay schließen (.dismissOverlay); ohne Overlay → App verlassen (System) |
| Klick (Select) im Overlay | zum fokussierten Preset springen (.jumpTo) |
Standard-Gesten der 2nd-gen Siri Remote (Clickpad-Swipe + 5-Wege-Klick) funktionieren auch auf rein direktionalen Remotes; Menu muss bei offenem Overlay konsumiert werden (sonst Sprung zum Home-Screen).
Testing¶
- Pure Logik auf macOS-Host (bestehendes Muster,
swift test/xcodebuild test):PlaylistDirector(Auswahl/Dwell/Reihenfolge/Morph-Scheduling),RemoteInput→ControlIntent-Mapping,AmbientController-State-Transitions (next/prev/pin, jumpTo, Overlay-Toggle). - Engine-Rendering: identischer Metal-Pfad, kein neuer Shader → bestehende Host-GPU-Tests decken die Optik.
PhysarumAppTestsum tvOS-Destination erweitern (für die pure tvOS-Logik).- Geräte-Sign-off (Johannes, ATV 4K vorhanden ✓): full-bleed-Render, vollständiges Remote-Set, Mehrstunden-Thermik, Idle-Timer-/Screensaver-Suppression, Background-Pause/Resume. T1 ist im tvOS-Simulator entwickelbar (Render + Remote); Thermik/Real-Feel und der ganze spätere T2-Link laufen auf der vorhandenen Hardware. (DeviceDiscoveryUI für T2 braucht ATV 4K, kein HD — erfüllt.)
Top Shelf / Store¶
- Statisches
1920×720-Hero-Bild (gerendertes Frame) als Top-Shelf-Banner — nice-to-have, kein Gate.TVTopShelfContentProvider(Preset-Tiles mit Deep-Link) optional später. - Positionierung Entertainment/Lifestyle; Differenzierung gegen die verschärfte 4.2/4.3-„kein statisches Wallpaper"-Hürde = generative Echtzeit-Engine + morphende Playlist + (T2) Cross-Device-Steuerung + Audio.
Non-Goals (T1)¶
- Kein Phone-Link / DeviceDiscoveryUI / Network.framework (= T2).
- Keine Audio-Reaktivität (keine Quelle auf tvOS; kommt via Phone-Mic in T2).
- Kein Editor, kein Capture/Export, keine Custom-Presets, kein Photos-Save auf tvOS.
- Kein nativer Release (laut Cockpit aufgeschoben).
- Engine/Shader/Param-Layout unverändert; Web/macOS/iOS unverändert.
Offene Punkte (am Gerät zu klären)¶
- Apple TV 4K vorhanden ✓ (Gen am Gerät zu bestätigen — alle ATV-4K-Gens erfüllen die Metal-/DeviceDiscoveryUI-Anforderung; der genaue
.tv-FieldTier-Wert wird ohnehin am Gerät getunt). .tv-FieldTier-Werte (Feldgröße/Agent-Cap) — Startwert konservativ, final per Mehrstunden-Thermik-Test.- Dwell-/Morph-Dauern — Default gesetzt, am Gerät feinjustieren.
isIdleTimerDisabled-Reset-Verhalten auf tvOS 26 — am Gerät verifizieren (Reports spannen tvOS 11–14). Re-Assert aufscenePhase == .activeist implementiert.
Umsetzung & Geräte-Sign-off (2026-06-15)¶
Subagent-driven gebaut (6 Plan-Tasks, je Impl→Spec-Review→Quality-Review) + holistischer 3-Linsen-Final-Review (Korrektheit / Integration-Regress / tvOS-HIG-Lifecycle). Der HIG-Review fand einen Laufzeit-2.4.3-Fehler trotz grünem Build: das Overlay-Karussell war nicht fokussierbar (Player-Layer fing die Swipes ab) → gefixt (Player-Layer gibt Fokus frei, wenn das Overlay offen ist; Karussell bekommt Fokus via @FocusState + .focusSection(), landet auf dem aktuellen Preset). Plus: GPU-Drain vor Suspend, Idle-Timer-Re-Assert.
Verifiziert (headless): macOS 174 Tests/0 Fehler · tvOS-Sim-Build · iOS-Sim-Build · iOS-Test-Build · EngineCheck · tvOS-Sim-Launch + Full-bleed-Render-Screenshot.
Geräte-Sign-off (Johannes, ATV 4K) — noch offen, headless nicht prüfbar:
- [ ] Full-bleed-Render am echten Gerät (16:9, randlos).
- [ ] Auto-Cycle + Morph-Übergänge weich (kein harter Schnitt; Dwell/Morph-Feel — Defaults 60 s / 4 s, tunebar).
- [ ] Siri-Remote-Set vollständig allein bedienbar: Swipe ◀▶ prev/next · Play/Pause pin/resume · Swipe ▲ Overlay · im Overlay: Karussell navigierbar + Select springt (der gefixte 2.4.3-Pfad) · Menu schließt Overlay / verlässt sonst zu Home.
- [ ] Mehrstunden-Thermik (lüfterloses ATV 4K; ggf. .tv-FieldTier von 512 anheben oder FPS-Drossel justieren).
- [ ] Idle-Timer/Aerial-Suppression über einen Background-Zyklus hinweg.
- [ ] Background-Pause/Resume sauber (kein GPU-Background-Crash).