TV OS — core primitives design (for review)

tv • 2026-10-20

TL;DR

We're building the homelab's TV OS primitives first, apps second — YouTube (via FreeTube) and games plug in afterwards. Five primitives were approved in brainstorm: multi-device identity, a semantic intent bus, a native+external app model, a single-page shell with KWin/ydotool for focus and input injection, and a FastAPI+WebSocket router. The first increment de-risks the Wayland layer, then builds phone pairing, the Home Screen, and external-app launching. Review this, say "looks good", and I write the implementation plan.

What you asked for

Start the TV OS: a Home Screen on the TV with a QR for phones to pair as a remote, an app menu, first app = YouTube. Then refined to: build out the OS and its primitives first, be deliberate about the design choices that every later app and game will repeat, and leave YouTube for the next increment.

Decisions already made with you

  • FreeTube for ad-free YouTube (purpose-built 10-foot app, SponsorBlock, no ads) — but as a *later* app.
  • The box will be at the TV with a real KDE session, so we build the full stack including real input injection.
  • P1: multi-device with identity — every phone is a tracked device; roles are per-app. Party-ready from day one rather than retrofitted.
  • Custom platform over Kodi / KDE Connect: those get YouTube working for free but can't host the buzzer, AI-party, and motion games the TV OS exists for.
  • The five primitives

    P1 — Device session + identity. Scan the QR → the phone gets a persistent device id (stored in localStorage, re-presented on reconnect). Any number connect at once. The *active app* decides roles: shared control on the Home Screen, one player per phone in a game.

    P2 — Intent schema. Phones emit semantic intents, never keycodes. Native apps read them directly; external apps get them translated to keystrokes through a per-app keymap. Versioned, so new intent types (motion) add without breaking clients.

    
    nav    dir: up | down | left | right | ok | back | home     (home is global)
    media  action: playpause | seekfwd | seekback | volup | voldown | mute
    text   value: string                                      (search / input)
    button name: string                                       (buzz, a, b … for games)
    motion t, ax, ay, az, alpha, beta, gamma                  (deferred — motion games)
    

    P3 — App model. One interface, two kinds behind it:

    
    App { id, name, icon, kind: native | external, launch, keymap? }
      launch()           start it, make it the active surface
      stop()             tear down, return to the shell
      on_intent(intent)  native  -> delivered to the app's view
                         external -> keymap -> keystroke -> injected into its window
    
  • Native = a view rendered inside the TV shell (web), gets intents over WebSocket, owns its screen. The Home Screen and all games are native apps.
  • External = a launched OS process (FreeTube, mpv, Steam) that renders itself fullscreen and is driven through its keymap. The platform never touches its rendering.
  • P4 — Shell + focus. The shell owns the TV surface as one single-page web app; native apps are views inside it with shared device state and smooth transitions. External apps launch as OS windows over it. A global Home always escapes any app. KWin (KDE on Wayland) does launch / kill / focus-return; ydotool injects keystrokes.

    P5 — Router. A FastAPI + WebSocket service on the homelab (reusing the games/ room patterns) — the single state machine: device registry ↔ intent stream ↔ the active app's handler. Endpoints /ws/remote (phones) and /ws/tv (the shell); the TV page at /, the phone remote at /remote.

    
     phones --WS(intents)-->  ROUTER -->  active app
      (id)                   (state)      |- native   -> render in TV shell (SPA)
                                          '- external -> keymap -> ydotool -> app window
       TV  <-- shell / app view (SPA) ----'   (KWin owns focus)
    

    Authority on the shell (default): last-touch-drives. Any phone can nudge the cursor and select, no hard lock. Games override with real per-player roles. (Flag if you'd rather one host phone holds the shell.)

    Stack

    Python 3.12, FastAPI + uvicorn, WebSockets, pydantic-settings — same as games/. Vanilla HTML/JS for the TV shell and the phone remote. ydotool for injection, KWin for focus. Deployed on cachyos-homelab as a systemd service; the shell runs in a kiosk browser autostarted in the KDE session. Plain HTTP over the LAN for now — a d-pad remote needs no secure context; HTTPS (Cloudflare DNS-01) comes only when motion games need DeviceMotion.

    First increment — build order

  • 1. De-risk the Wayland layer first (the riskiest primitive): a throwaway prototype that runs a kiosk browser, launches an external app fullscreen, injects a keystroke with ydotool, and reliably returns focus to the kiosk. If it's painful we find out before building UI.
  • 2. Router + pairing + intent bus + Home Screen: QR → remote page → device id, the intent stream, the TV shell with the Home Screen and a trivial native test app that echoes which device pressed what. Fully testable headless with Playwright.
  • 3. External apps: registry + keymap + launch/stop through the router, proven with one external app (mpv or FreeTube).
  • 4. Kiosk autostart in the KDE session + the router's systemd unit.
  • Done when: a phone scans the TV's QR, moves the Home Screen cursor, opens the test app and sees its presses echoed, opens an external app and drives it with the same d-pad, and home returns to the Home Screen every time.

    Testing

  • Unit (pytest): the router state machine — device registry, routing, app switching.
  • Integration (Playwright): drives the TV shell plus a simulated phone over WebSocket, asserts cursor and app state. No display needed.
  • System: the Wayland prototype is verified by hand on the box at the TV.
  • Deferred (on purpose)

  • FreeTube / ad-free YouTube as an app — the next increment.
  • HTTPS / DNS-01 cert — only when motion games need it.
  • Motion intents and motion games.
  • Shared game-level primitives (lobby, scoring, rounds, player names/avatars) — designed when the first real game exists, with a concrete game in hand. This is the "things that repeat across games" bucket you mentioned.
  • 10-foot visual polish of the launcher.
  • Offloading the LLM when the TV needs GPU VRAM — handled operationally (the 4060 Ti split); coordinate with the LLM session when the display is attached.
  • What I need from you

  • Read the primitives and the build order. Anything you'd change — especially the intent schema or the last-touch shell authority?
  • If it holds: reply "looks good" and I write the implementation plan next.
  • Commit log

    
    79025bc docs: TV OS core primitives design spec   (xpoes123/tv, main)