Architecture
This document explains how sub-screen-player is put together, what each part is responsible for, and where new code belongs.
Overview
Section titled “Overview” ssp CLI scripts / apps (future) web screens, plugins │ │ │ └──── HTTP / WebSocket (127.0.0.1:7920) ─────┘ │┌──────────────────────┼──────────────────────────────────────── ssp-server ──┐│ api/ routes, auth, WebSocket streams ││ manager finds displays, hotplug, what each display shows (Content) ││ sources/ built-in screens that draw frames (clock, still image) │└──────────────────────┼───────────────────────────────────────────────────────┘ │ Frame (landscape RGB, panel size)┌──────────────────────┼──────────────────────────────────────────── ssp-core ──┐│ Presenter latest-frame-wins queue, encoder thread, device thread, ││ frame pacing, duplicate skipping, keep-alives ││ Encoder rotate to the panel's orientation, encode (JPEG) ││ Display trait every driver implements ││ Transport trait for the byte channel; hid::HidTransport implements it │└──────────────────────┼─────────────────────────────────────────────────────────┘ │ EncodedImage┌──────────────────────┼─────────────────────────────── ssp-driver-<model> ──────┐│ protocol pure functions that build the device's reports ││ Display implementation: show, save, brightness, power, keep-alive ││ Driver USB ids it handles, how to open the device │└──────────────────────┼─────────────────────────────────────────────────────────┘ │ reports USB (HID)The ssp binary contains all of it: ssp serve runs the daemon, and every other command is a
small HTTP client of that daemon.
Crates and responsibilities
Section titled “Crates and responsibilities”| Crate | Path | Responsible for | Must not |
|---|---|---|---|
ssp-core |
crates/core |
The Display/Driver/Transport traits, HID access, frames and encoding, the Presenter |
Know any device’s protocol |
ssp-driver-<model> |
crates/drivers/<model> |
One family of devices: its protocol, capabilities and quirks | Depend on other drivers, the server or async code |
ssp-server |
crates/server |
The daemon: device management, built-in sources, config, HTTP/WebSocket API | Contain device-specific bytes |
sub-screen-player (bin ssp) |
crates/cli |
Command line, autostart, logging setup | Talk to devices directly (except listing them when no daemon runs) |
Dependencies only point downwards: cli → server → drivers/* → core.
Key types
Section titled “Key types”Frame(core): what callers draw. Always landscape and exactly the panel’s size, 8-bit RGB. Images of other sizes are fitted withFrame::fit(contain,cover,stretch).PanelSpec(core): the panel’s size as callers see it, the rotation needed on the wire and the image format. For example, the D92 is 1920x462, turned 90° clockwise, JPEG.EncodedImage(core): a frame after rotation and encoding, ready for a driver.Display(core trait, implemented by drivers):show(live, not stored),save(stored on the device),set_brightness,wake,sleep,clear,keep_alive. Operations a model lacks keep the default implementation, which returnsError::Unsupported.Capabilities(core): what a display supports, its frame-rate limit, keep-alive interval and largest accepted image. The rest of the system reads these instead of checking models.Driver(core trait): the USB interfaces a driver handles (UsbMatch) andopen. The daemon’s driver list iscrates/server/src/drivers.rs. Which drivers are in use is aDriverSelection(--driveroptions,[drivers]in the config) applied withRegistry::select; drivers markedexperimentalare only used when named.Content(server): what a display is told to show: nothing, an image, the clock or a stream. It is kept per display id, so a replugged display carries on.Source(server trait): something that draws frames and says when the picture changes next (the clock, a still image).
Life of a frame
Section titled “Life of a frame”- A frame is produced: a
Sourcerenders it on the display’s player thread, or a WebSocket/HTTP client sends one and the server decodes it. Presenter::submitstores it in a one-frame slot and returns at once. A frame still waiting in the slot is dropped (counted asdropped).- The encoder thread takes the frame, rotates it and encodes it. If the result is larger than the device accepts, it lowers the JPEG quality step by step.
- The device thread sends the newest encoded frame once the frame interval
(
1 / max_fps) has passed. A frame identical to the one on screen is skipped (counted asduplicates). Encoding the next frame overlaps with sending this one, so the slower of the two sets the frame rate, not their sum. - The driver’s
Display::showturns the image into reports (protocol.rs) and writes them through itsTransport.
Control commands (brightness, power, clear, save) go through the same device thread, in
order, and the caller waits for the result. Keep-alives are sent on that thread every
keep_alive_interval.
Threads
Section titled “Threads”| Thread | Count | Job |
|---|---|---|
| tokio runtime | a few | HTTP and WebSocket handling. Blocking work goes to spawn_blocking. |
ssp-scan |
1 | Scans USB every 2 s, opens new displays and notices unplugged ones |
ssp-encode-<id> |
1 per display | Rotates and encodes frames |
ssp-device-<id> |
1 per display | Owns the Display; sends frames, commands and keep-alives |
ssp-player-<id> |
0–1 per display | Runs the current built-in Source (e.g. ticks the clock) |
A Display is only ever used by its device thread, so drivers need no locking.
Errors and reconnection
Section titled “Errors and reconnection”Error::TransportandError::Disconnectedare fatal: the presenter stops and fails queued commands. On its next scan the manager drops the device, keeps itsContent, and reopens it when it shows up again.- Other errors (
InvalidArgument,Image,Unsupported) are returned to the caller and the display keeps running. - When a device fails to open (e.g. another program holds it), the manager retries every 10 s and logs the first failure only.
Security model
Section titled “Security model”- The API listens on
127.0.0.1by default. The config refuses a non-loopbacklistenaddress unless atokenis set. - Without a token, requests whose
HostorOriginheader is not loopback are rejected. This stops web pages in the user’s browser from driving the display (CSRF, DNS rebinding). - With a token, every request needs
Authorization: Bearer <token>or?token=<token>.
Extension points
Section titled “Extension points”What exists today and where planned features fit:
| Extension | Status | Where it fits |
|---|---|---|
| New display models | available | A new crate under crates/drivers/ (guide) |
| External programs drawing frames | available | WebSocket stream or HTTP image endpoint (API) |
| Built-in screens | available | A Source in crates/server/src/sources/ plus a Content variant |
| Non-HID transports (USB bulk, serial) | planned | A new Transport implementation in crates/core |
| Web (HTML/CSS) screens | planned | A Source that renders a page, or an external client streaming frames |
| WASM plugins | planned | A Source that hosts a sandboxed plugin |
| Layouts composed in the config | planned | A Source that combines other sources, configured in config.rs |
The Source trait and the stream API are the two seams new kinds of content plug into. Keep
them small and stable.