Cam Wall had no URL — the only way in was the toggle on the Printers page,
so it could not be bookmarked, linked, or shown on a wall-mounted screen.
Add a standalone /camwall route. Signed in, it is the wall as it was. For a
TV or Pi with no login, it authenticates with a long-lived token in the URL.
A kiosk needs the printer list and per-printer status, both of which sit
behind PRINTERS_READ. Rather than widen camera_stream to cover GET /printers
— whose response carries serial_number and ip_address, which have no business
on a screen in a shared room — add a read-only feed at
GET /api/v1/camwall/printers that serves only what a tile draws, and gate it
on a new camwall token scope. The print filename is not served at all: a token
wall renders the compact overlay, so the part on the bed is never named.
The scope is separate rather than a widening: camera_stream tokens are already
in the wild, minted to hand out video, and must not gain the ability to
enumerate a fleet by name. camera_stream is refused by the feed; camwall
passes the stream gate so its own tiles fill.
Kiosk walls drop the settings popover and click-through entirely (not merely
hidden — a passive screen must carry no focusable control it cannot act on),
cap the overlay at compact, and poll rather than open a WebSocket. maxLive,
interval and status can be set from the URL, clamped to the popover's ranges.
streams when one viewer closes
1) Offline tiles now show OFF (not LIVE)
CameraWall.modeByPrinter assigned 'live' to any visible printer
without considering status.connected, so a disconnected X1C wasted
a live-budget slot AND rendered the red LIVE chip on top of the
WifiOff placeholder. Disconnected printers now map to 'paused' and
don't decrement liveBudget — the existing WifiOff + Off chip
rendering takes over.
2) /camera/stop no longer kills other viewers' streams
The cam-wall tile, EmbeddedCameraViewer, and the /camera/:id popup
all subscribe to the same fan-out broadcaster for a printer.
/camera/stop used to unconditionally shutdown_broadcaster() + kill
every ffmpeg process for the printer, so closing the embedded viewer
while the cam-wall tile of the same printer was live force-killed
the source the tile was pulling from — the tile's <img> errored.
New get_subscriber_count(key) accessor in camera_fanout.py exposes
the broadcaster's subscriber list length. /camera/stop now reads
that first; when >= 1 subscriber is still attached, return
{stopped: 0, skipped: true} and leave the broadcaster + ffmpeg
processes alone. The leaving viewer's HTTP teardown still runs the
natural iter_subscriber.finally -> unsubscribe path, so its slot is
released; the broadcaster keeps serving the other viewers. Single-
viewer close still hits the immediate force-teardown (count is 0).
New view toggle on the Printers page renders a responsive grid of live
camera tiles instead of printer cards. Reuses the existing /camera/stream
fan-out so the backend ffmpeg pipeline is unchanged.
To stay sustainable on the median Pi 4 install, only on-screen tiles run
live, and only up to a per-user cap (default 4). Other visible tiles
fall back to periodic /camera/snapshot polling (default 8s). Off-screen
tiles pause entirely. Tiles POST /camera/stop on unmount and on
leave-live so the backend transcoder slot is released the same way
EmbeddedCameraViewer does it.
CameraTile is a 3-mode leaf (live / snapshot / paused) with a single
<img> and an onError no-signal fallback. CameraWall is the scheduler:
IntersectionObserver tracks visibility, a stable walker over the sorted
printer list assigns live slots first-N-visible to avoid LRU churn. Same
['printerStatus', id] React Query cache the cards already populate, so
flipping between Cards and Cam Wall is instant.
Tile click honours the existing Settings camera_view_mode preference
(window vs embedded). Both wall settings are per-user localStorage
(camWallMaxLive, camWallSnapshotSec) — a Pi 4 user and a NUC user want
different caps.