Plugins overview
StreamHub ships a small plugin framework that lets features plug into both the backend and the dashboard with zero edits to any central registry. A plugin is just a file you drop in — the framework discovers it, lists it in the per-app Plugins marketplace, validates its config against a typed schema, and (when a plugin declares one) owns the lifecycle of its worker process.
This page explains how the framework works and how to install, enable and configure a plugin. Each built-in plugin then has its own page documenting its full configuration.
How plugins are discovered
Section titled “How plugins are discovered”Discovery happens independently on each side, so a plugin can be backend-only,
frontend-only, or both. The two halves are matched by their shared id.
- Backend — drop
streamhub-core/src/plugins/<id>/plugin.meta.tsthat default-exportsdefinePlugin({ ... })(the contract lives in the single filesrc/modules/plugins/plugin.contract.ts— the ONE file a plugin author imports). The registry service globs the filesystem and dynamically imports eachplugin.meta.ts. This backend manifest is the source of truth for a plugin’s id, category, UI slot,configSchemadefaults, and whether it needs a worker. - Frontend — drop
streamhub-web/src/plugins/<id>/index.tsxthat default-exports aPluginModule.discovery.tspicks it up eagerly at build time viaimport.meta.glob('./*/index.{ts,tsx}')— one level of sub-folder only, so the framework’s own flat files are never mistaken for plugins. The frontend module supplies the React component that renders in the UI slot.
Categories
Section titled “Categories”Every plugin declares a category, which drives grouping in the marketplace UI:
| Category | Meaning |
|---|---|
tool |
An on-demand utility (e.g. a diagnostic panel or a player overlay). |
processor |
Consumes/analyzes the media stream, typically via a worker. |
panel |
A self-contained dashboard surface for the app. |
UI slots
Section titled “UI slots”A plugin’s ui field tells the frontend where its component renders:
| Slot | Where it renders |
|---|---|
app-tab |
A full section (tab) inside an app in the dashboard. |
panel |
A panel mounted wherever a <PluginSlot placement="panel"> lives (the marketplace’s active-panels area). |
player-overlay |
Drawn on top of the video player, client-side. This is the auto-mounted slot: whenever a player-overlay plugin is installed + enabled for an app, its overlay renders on that app’s players — including the public /play and /embed pages. |
Config schema
Section titled “Config schema”Each plugin declares a configSchema: a list of typed, labelled fields. The
supported field types are string, number, boolean, select and secret.
Every field must have a default — an install with no config is immediately
valid. Fields may also declare required: true (an install cannot be enabled
until the field is non-empty), min/max bounds for numbers, options for
selects, and placeholder/help hints for the form. The dashboard renders a
generic settings form straight from this schema; the backend validates any
config you submit against it.
Workers
Section titled “Workers”A plugin sets needsWorker: true and provides a worker.spawn(ctx) function to
run an external process per app. spawn is a pure function of a read-only
context (app, resolved config, appDir, dataDir, livekitUrl) that returns
exactly what to run — { command, args, env, cwd }. The framework owns the
whole lifecycle: enabling the plugin (re)starts the worker, disabling stops it,
and start/stop/status/logs are exposed over the API. The core never needs to know
what the worker actually is. The yolo and
deface plugins use this to spawn Python detection workers.
Live-data channel (worker to player)
Section titled “Live-data channel (worker to player)”A worker plugin often needs to stream small, fast-changing runtime data — face
or object bounding boxes, for example — straight to its own player overlay,
without an operator-owned callback URL in the loop. The framework provides a
tiny, in-memory “latest payload” channel for exactly that, keyed by
(app, plugin, room):
| Method & path | Auth | Purpose |
|---|---|---|
POST /apps/:app/plugins/:id/live |
Ingest token (not Bearer) | Worker-only. Push the latest payload for a room. |
GET /apps/:app/plugins/:id/live |
Public, no auth | Player-only. Read the latest payload for a room. |
POSTis called by the plugin’s own worker process, never by a client. Auth is not the usual Bearer guard: the worker echoes the per-start token the framework injected asSTREAMHUB_INGEST_TOKEN(alongsideSTREAMHUB_INGEST_URL) in theX-Plugin-Ingest-Tokenheader — a push carrying a stale or missing token (e.g. after the worker restarted) is rejected. The body is a JSON object — the plugin’s own payload shape, size-capped — stored as the LATEST value for that(app, plugin, room); this is a live overlay feed, not an event log, so no history is kept and nothing touches SQLite.GETis polled by player overlays, including the anonymous/playand/embedpages. It answers only for an installed and enabledplayer-overlayplugin (404otherwise) and returns{ ts, ageMs, payload }(allnullwhen nothing has been pushed yet), so the overlay can apply its own staleness policy.
The deface plugin is the reference example: its worker
POSTs detected face boxes on every sampled frame, and the player overlay polls
GET :id/live to draw the masks client-side.
Installing, enabling and configuring a plugin
Section titled “Installing, enabling and configuring a plugin”An install is per app. There are two equivalent paths.
- Open the app and go to its Plugins tab (the marketplace).
- Find the plugin card and click Install.
- Toggle the install to Active.
- Click Configure, adjust the fields (the form is generated from the plugin’s config schema), and save.
All routes are scoped under /api/v1/apps/:app/plugins and use a sk_ bearer
token.
-
Install (idempotent):
Terminal window curl -X POST https://YOUR-DOMAIN/api/v1/apps/live/plugins/timestamp/install \-H "Authorization: Bearer sk_..." -
Enable and/or configure —
PATCHacceptsenabledand/or aconfigobject (validated against the plugin’s schema; for worker plugins, enabling (re)starts the worker and disabling stops it):Terminal window curl -X PATCH https://YOUR-DOMAIN/api/v1/apps/live/plugins/timestamp \-H "Authorization: Bearer sk_..." \-H "Content-Type: application/json" \-d '{"enabled": true, "config": {"position": "top-left"}}' -
Inspect logs (worker output + persisted lines, newest last;
limitoptional, default 200, max 1000):Terminal window curl https://YOUR-DOMAIN/api/v1/apps/live/plugins/yolo/logs?limit=200 \-H "Authorization: Bearer sk_..."
Other plugin routes
Section titled “Other plugin routes”| Method & path | Purpose |
|---|---|
GET /apps/:app/plugins |
Marketplace: every built-in plugin + this app’s install/config state. |
GET /apps/:app/plugins/:id |
One marketplace entry (manifest + install state). |
POST /apps/:app/plugins/:id/install |
Install into the app (idempotent). |
PATCH /apps/:app/plugins/:id |
Enable/disable and/or reconfigure. |
DELETE /apps/:app/plugins/:id |
Uninstall (stops its worker). |
GET /apps/:app/plugins/:id/logs |
Per-plugin logs. |
POST /apps/:app/plugins/:id/worker/start |
Explicitly (re)start a worker (needsWorker plugins). |
POST /apps/:app/plugins/:id/worker/stop |
Explicitly stop a worker. |
GET /apps/:app/plugins/:id/worker/status |
Current worker state. |
POST /apps/:app/plugins/:id/live |
Worker-only live-data ingest (ingest-token auth — see above). |
GET /apps/:app/plugins/:id/live |
Public, no auth — latest live-data payload for an enabled player-overlay plugin. |
GET /apps/:app/plugins/public |
Public, no auth — this app’s enabled player-overlay plugins for anonymous players. |
Built-in plugins
Section titled “Built-in plugins”| Plugin | Category | UI slot | Worker | Summary |
|---|---|---|---|---|
| Cockpit | panel |
panel |
No | CCTV-style, drag-and-drop grid of every live stream in the app. |
| Quality / Stream Health | tool |
panel |
No | Bandwidth + latency test distilled into a green/amber/red traffic light. |
| Radio | panel |
app-tab |
No | Audio-only WebRTC radio: go on air, count listeners, mint listen tokens. |
| Video Streaming | tool |
app-tab |
No | Go live with webcam + mic and forward the room to RTMP via server egress. |
| Timestamp CCTV | tool |
player-overlay |
No | Live CCTV-style date/time stamp drawn on the player. |
| Watermark | tool |
player-overlay |
No | Text watermark drawn in a corner of the player. |
| YOLO Object Detection | processor |
player-overlay |
Yes | Python worker runs YOLO over the live stream and POSTs detections to a callback. |
| Deface — Face Obfuscation | processor |
player-overlay |
Yes | Python worker detects faces (CenterFace) and the player overlay blurs, mosaics or masks them client-side. |