Files
spoopyghosty0--frameport/docs/SHOWCASE.md
T
spoopyghosty0 13736d2524 Showcase: placeholders in the setup command; Monitor charts grow live on film
- The install tutorial (app screen and card) shows curl -fsS <your-PC-address>:8765/<one-time-code> | bash, so
  nobody takes the example for their own command; the card says FramePort fills them in.
- Videos: opening the Monitor tab no longer replays two minutes of samples (its charts raced to catch up); the
  charts start there and grow live, as in the app. The pretend frame rate holds ~72 fps with a short, shallow dip
  now and then (it sat at 55-61 fps for seconds). Both videos re-recorded.
2026-10-09 11:20:13 -04:00

8.8 KiB

Screenshots and videos

The pictures in the docs and the videos are rendered by scripts, never taken by hand. The scripts show the real FramePort GUI with a demo library and a pretend Steam Frame, drive it the way a person would, and film or photograph it:

What Source Output
Docs screenshots docs/showcase/shots.yaml docs/images/*.png
Demo tour (about 95 s) docs/showcase/videos/tour.yaml docs/media/frameport-tour.mp4 (+ .jpg poster), docs/images/tour-teaser.webp (README)
Install tutorial (about 90 s) docs/showcase/videos/install.yaml docs/media/frameport-install.mp4 (+ .jpg poster)

Nothing comes from anyone's own library. The demo library (docs/showcase/demo-library.yaml) lists real catalog games, so recipes and patches are the ones FramePort really uses. Their technical analysis (demo-analyses.json: engine, VR API, libraries, sizes) was exported from real APK analyses without any paths. Art, in-game screenshots and store details come from the same public sources the app uses and are cached in ~/.cache/frameport-showcase. The renderer refuses to run in FramePort's own data folder, scans the demo data for home paths and IP addresses first, and stops when a store didn't send art (placeholders never reach the docs; --allow-missing-art overrides that). Frame discovery is switched off during a render (no real device on your network can appear), and the setup command shows placeholders for the address and code (each PC has its own), never this computer's.

Running it

You need the dev extras, Chromium for Playwright and ffmpeg:

UV_LINK_MODE=copy uv sync --extra dev
uv run playwright install chromium        # once
uv run python scripts/showcase/render_docs.py               # all screenshots → docs/images (changed ones only)
uv run python scripts/showcase/render_docs.py --only game,monitor --out /tmp/shots
uv run python scripts/showcase/record_video.py install --draft   # quick 720p check of a storyboard edit
uv run python scripts/showcase/record_video.py              # every video → docs/media (+ the README teaser)
uv run python scripts/showcase/record_video.py tour install # some

Every screenshot is rendered twice, each in a fresh window, and kept only when both agree (the headless GPU now and then draws a glyph wrong); a third render settles a disagreement, and --fast renders once for trying out an edit. A shot whose steps fail is tried again and never written. A screenshot is rewritten only when it visibly changed (render_docs.changed: anti-aliasing noise doesn't count), so running it twice changes nothing. --check writes nothing and exits 1 when a docs image is out of date.

For videos, --names prints what can be clicked after each scene and SHOWCASE_DEBUG=1 prints every step with its start time (which step waited); --draft --scenes a,b records only some scenes. A video is encoded at the best quality that fits its budget_mb (12 by default), a teaser at 4 MB.

Videos

One storyboard per video in docs/showcase/videos/<name>.yaml; adding a file adds a video (CI and releases pick it up by itself). The output must be docs/media/frameport-<name>.mp4: releases attach it as FramePort-<name>.mp4.

output: docs/media/frameport-install.mp4
teaser: docs/images/tour-teaser.webp     # optional: a looping WebP from the scenes' `teaser` parts
budget_mb: 12
poster_at: 9                             # the poster frame, seconds after the title card
start:
  profile: fresh          # demo = FramePort in use (games installed, BONELAB running); fresh = its first start
  frame: disconnected     # connected (default) or disconnected
  route: welcome          # the page filming starts on (default library)
  stream: live            # the monitor stream: live (default) or frozen
setup:                    # steps before filming starts (a failure stops the recording)
  - hook: {name: first_run, args: ["D:/Games", 1.4]}
vars: {game: com.playful.LuckysTale}     # ${game} in any step
title: {heading: Installing FramePort, line: …, seconds: 3.4}
end: {heading: You're set, line: …, footer: github.com/spoopyghosty0/frameport, seconds: 4}
scenes:
  - name: connect                        # a filmed scene
    caption: ["Connect your Steam Frame", "Once: FramePort shows a command to run on the Frame"]
    teaser: [0.6, 4.0]                   # optional: this part goes into the teaser
    hold: 1.5                            # seconds of filming after the last step (default 0.6)
    steps: [...]
  - name: on-the-frame                   # an instruction card: for what happens outside FramePort
    seconds: 10
    card:
      eyebrow: "Step 3 · On the Frame, first time only"
      heading: Run the setup command
      steps: ["In the SteamVR dashboard: **Launch a program** → **Desktop**", …]   # **bold** = a button or menu
      code: "curl -fsS <your-PC-address>:8765/<one-time-code> | bash"
      note: "Your address and code differ."

Filmed scenes are one continuous session: each starts where the last one left off. Cards are rendered at the end and slotted in, with crossfades between everything. The fresh profile starts FramePort as on a new computer: an empty library, the welcome screen and a Frame that isn't set up. Its games wait for the pretend folder scan.

Writing steps

Shots and scenes use one step language (scripts/showcase/steps.py). A step is a one-key mapping:

- go: monitor                                  # a sidebar route
- nav: "Monitor"                               # click that tab (checked: the page really changed)
- open_game: ${game}                           # or {package: …, advanced: true}
- call: {fn: settings_dialog, args: ["${game}"]}   # any app method
- hover: "Batman: Arkham Shadow"               # the pointer: by name…
- click: {name: "Lucky's Tale", dy: 0.35}      # …offset inside the element (fraction of its size, or px)
- click: "Install on Frame"                    # right_click, double_click, drag: [from, …, to]
- type: "ri"                                   # press: Escape
- wait_for: "BONELAB · 2026-10-04 21:50"
- wait: 1.2
- hook: {name: held_install, args: ["${new_game}"]}   # the pretend Frame's scripted events

Elements are found by name through Flutter's accessibility tree: their visible text, a tooltip or a semantics label. Exact names win over prefixes, and buttons and cards win over plain text. When several match, the innermost one wins (a list carries the text of its rows; the row is meant). Icon-only buttons need a tooltip to be found, which also helps screen readers. The pointer moves on eased, slightly curved paths and is drawn into the picture, with a ring on every click. To open a game from a cover card, click its title (dy: 0.35): the card's middle is its Play/Install button.

Hooks (steps.HOOKS) cover what a real Frame would do:

  • fake_install: an install through the real job queue with every real stage name and a launch test that passes.
  • held_install: an install that stops mid-upload, for screenshots of the progress.
  • connect and disconnect
  • first_run: a first start. The welcome screen's tool download is a pretend one, and "Scan a folder…" scans a folder at once (the native folder picker can't be scripted).
  • pairing_done: the Frame ran the setup command, so FramePort connects.
  • live_stream: a test picture through the real relay.
  • monitor_details, type_tab, select_files and settings_section.

In a video, clicking Install runs the scripted install, and the game is then installed on the pretend Frame.

Pictures must not change from one run to the next:

  • Screenshot and file dates are fixed.
  • The Monitor's stream stops after its two-minute history.
  • The pointer leaves the page before a still picture is taken.
  • Each shot gets a fresh window and browser.
  • A picture is taken once the page has stopped changing.

Live data, such as the live view's relay port and data rate, doesn't belong in a docs screenshot.

How it runs on GitHub

The showcase workflow (.github/workflows/showcase.yml):

  1. When it runs: on pushes to main that touch the UI, its translations or the showcase files, and on Run workflow.
  2. What it renders: the screenshots every time. Videos when their storyboard changed (all of them when the scripts changed), or the ones named on a manual run (all, or e.g. tour install). UI tweaks alone don't re-commit 10 MB videos.
  3. What happens next: every render is uploaded as the run's showcase artifact. When a picture changed, the workflow opens or updates the pull request on branch showcase/update, which you review and merge.

Releases attach every committed video as FramePort-<name>.mp4.

These are renders the README doesn't show (yet):

Installing Live view
Appearance Game page