Architecture¶
Streamcatcher is small, but deliberately layered so the same logic serves a desktop window, a headless script, and an HTTP server without duplication.
Offline-first, secrets-safe¶
Two principles shape everything:
- Offline-first. Importing the package touches no network, no window, and no
decoder. OpenCV (
cv2) is imported lazily, insideStreamSession.open()— so the CLI, the factory, and the entire test suite import and run with OpenCV absent. The defaultstubbackend does nothing but log, which keeps development and CI fully offline. The web stack (fastapi,uvicorn) is likewise imported only insidecreate_app. - Secrets-safe. A stream URL can embed
user:pass@host, so it's treated as a secret end to end (see Security).
The layers¶
CLI (Typer) HTTP API (FastAPI, optional extra)
play │ serve create_app → routes
│ │
┌───────┴────────┐ │
│ Player factory │ │
└───────┬────────┘ │
┌─────┴─────┐ │
│ backends │ StubPlayer / OpenCvPlayer │
└─────┬─────┘ (window + keys) │
└──────────────┬───────────────────┘
▼
StreamSession ← the one control core
open · read · render · look ·
snapshot · reconnect · state
│
┌────────────┴─────────────┐
▼ ▼
reprojection reconnect
(pure NumPy) (backoff)
StreamSession — the control core¶
StreamSession owns the OpenCV capture and the optional 360
viewport and exposes programmatic controls with no window and no keyboard
loop. Both the GUI player and the HTTP server drive it, so open / read / render /
look-around / snapshot / reconnect logic lives in exactly one place.
Backends and the factory¶
A tiny Player protocol (play / stop /
snapshot / is_playing) has two implementations:
StubPlayer— the offline default; records requests instead of doing them.OpenCvPlayer— a thin shell overStreamSessionthat adds a native window, thewaitKeyloop, and the keyboard bindings (W/A/S/D,+/-,p,q).
The factory picks one from Settings.backend. Because the OpenCV backend
lazy-loads cv2 inside play(), importing the factory never needs OpenCV.
Reprojection¶
reprojection.py builds cv2.remap lookup
tables with pure NumPy — equirectangular→pinhole — so the geometry is
deterministic, GPU-free, and unit-testable without a live stream. See
360° & cameras for the math. Maps are cached and rebuilt only when
the view moves.
Reconnect¶
reconnect.py is a pure module: a frozen
ReconnectPolicy and an infinite exponential-backoff generator. StreamSession
exposes reconnect(); the OpenCV backend wires the retry loop into its frame
loop, staying responsive to q / window-close between waits. The viewport
orientation survives a reconnect.
Configuration¶
Settings (pydantic-settings) is populated from
STREAMCATCHER_* environment variables, with CLI flags overriding them only when
passed. The stream URL is a SecretStr; display_url is the credential-stripped
form safe to log.