HTTP control API¶
Streamcatcher can run a small localhost HTTP API so another program — or an AI agent — can open a stream, drive the look-around, and pull frames. The motivating scenario: point a vision model at a 360 camera, let it aim the view and grab stills, and answer questions like "is there a blue wall?".
It's an optional extra so the core CLI stays lean:
Running the server¶
streamcatcher serve # binds 127.0.0.1:8000
streamcatcher serve --host 0.0.0.0 --port 9000 --token changeme
| Option | Default | Meaning |
|---|---|---|
--host |
127.0.0.1 |
Bind address (localhost only by default) |
--port |
8000 |
Port to listen on |
--token |
— (STREAMCATCHER_API_TOKEN) |
Require Authorization: Bearer <token> on every request |
Interactive OpenAPI docs are served at /docs — an agent-readable schema of
every endpoint.
Security posture
The server binds 127.0.0.1 by default. When a token is unset there is no
auth (relying on the localhost bind); set --token (or
STREAMCATCHER_API_TOKEN) before exposing it more widely. Stream URLs and
their credentials are never returned in any response. See
Security.
Endpoints¶
| Method & path | Purpose |
|---|---|
POST /session |
Open a session for a stream URL; returns an id + state (the URL is never echoed back). 201 |
GET /sessions |
List open session ids. |
GET /session/{id}/state |
Current projection and viewport orientation. |
GET /session/{id}/frame |
Current look-around viewport as a JPEG — how an agent "sees" now. |
GET /session/{id}/panorama |
Raw, pre-reprojection frame as a JPEG (the full field of view). |
GET /session/{id}/stream.mjpg |
MJPEG stream of the viewport (fps-capped). |
POST /session/{id}/look |
Pan/tilt/zoom by {pan, tilt, zoom} degree deltas. |
POST /session/{id}/look/{action} |
Discrete step: pan_left, pan_right, tilt_up, tilt_down, zoom_in, zoom_out. |
DELETE /session/{id} |
Close the session. 204 |
Status codes: unknown session id → 404; invalid request body (e.g. an unknown
projection) → 422; unknown discrete action → 404; too many sessions → 429;
the stream won't open → 502.
Sessions are reaped after an idle timeout (default 300s) and capped in number
(default 8); tune with STREAMCATCHER_API_IDLE_TIMEOUT,
STREAMCATCHER_API_MAX_SESSIONS, and STREAMCATCHER_API_STREAM_FPS.
By default each frame request reads on demand. Set
STREAMCATCHER_API_READER_ENABLED=true to give every session a background reader
thread that keeps the latest frame cached (refresh rate capped by
STREAMCATCHER_API_READER_FPS, default 30), so /frame and /panorama return
the cached frame instead of blocking on a read. After a drop the cache holds the
last good frame until the stream returns.
Walkthrough¶
Open a session, aim the camera, and grab the view an agent would reason over:
# 1. Open a session (the URL is not echoed back)
curl -sX POST localhost:8000/session \
-H 'Content-Type: application/json' \
-d '{"url": "rtsp://user:pass@camera.local/live", "projection": "equirect"}'
# -> {"id":"a1b2c3","projection":"equirect","yaw_deg":0.0,"pitch_deg":0.0,"hfov_deg":100.0}
# 2. Look right and up
curl -sX POST localhost:8000/session/a1b2c3/look \
-H 'Content-Type: application/json' -d '{"pan": 45, "tilt": 15}'
# 3. Grab the current viewport as a JPEG
curl -s localhost:8000/session/a1b2c3/frame -o view.jpg
# 4. Close it
curl -sX DELETE localhost:8000/session/a1b2c3
import requests
base = "http://localhost:8000"
# 1. Open a session — the URL is accepted but never returned
r = requests.post(f"{base}/session", json={
"url": "rtsp://user:pass@camera.local/live",
"projection": "equirect",
})
sid = r.json()["id"]
# 2. Aim the virtual camera
requests.post(f"{base}/session/{sid}/look", json={"pan": 45, "tilt": 15})
requests.post(f"{base}/session/{sid}/look/zoom_in")
# 3. Pull the frame the agent should reason over
frame = requests.get(f"{base}/session/{sid}/frame").content # JPEG bytes
# 4. Done
requests.delete(f"{base}/session/{sid}")
With a token set, add -H 'Authorization: Bearer changeme' (or
headers={"Authorization": "Bearer changeme"}) to every request.
How it's built¶
Route handlers are plain synchronous def, so Starlette runs them in its thread
pool and a blocking cap.read() never stalls the event loop. Frames are read
on demand under a per-session lock by default; enabling the background reader
(above) instead has a per-session thread keep the latest frame cached. A single
background task reaps idle sessions. The server is a thin shell over the same
headless StreamSession the GUI uses.