Going live and signal
The motor, the door, the GPU pod, and the cockpit — four pieces behind "signal" and "on-air", and why two of them share a name that means different things.
Overview introduced signal and on-air as two different states. This page is the machinery behind that split: four pieces, each doing one job, and one naming collision worth knowing about before it confuses you.
"Motor" means two different things in this codebase
Don't conflate them
A StreamMotor database row and "the motor" mentioned in the cockpit and door
docstrings are not the same system. Read both before assuming you know which one a
page means.
The registered motor (StreamMotor, table stream_motors, one row per owner) is a
connection to an external service called stream-hub, outside this codebase entirely.
It's identified by hub_url/hub_user/hub_pass (the last two EncryptedText, never
echoed back — GET /stream/hub/motor returns only hasUser/hasPass). This is the
system that drives webcam-platform chat and login automation — see
Platforms, studio, and secrets.
The GPU pod — app/products/stream/core/gpu_pod.py and runpod_api.py — is a
RunPod on-demand box that composes the on-air frame (insets, crops, captions) and holds
recordings. Its own code never uses the word "motor" anywhere — no StreamMotor import,
no hub_url reference. But the docstrings in stream_door_admin.py and
stream_cockpit.py call it "мотор" informally ("пульт спрашивает МОТОР (GPU-под...)",
"записи на боксе мотора") — plain description, not the database row. When this section
says GPU pod, it means this system specifically, never the registered motor.
The GPU pod: pay only while you're live
Running the pod costs about $0.39/hour continuously — roughly $288/month for a pod that
sits idle — so the whole design exists to keep it off between shows. Two facts drive the
implementation, both read directly from runpod_api.py's own docstring:
- RunPod's external ports change on every pod reset. A pod can't be the stable receiving address a creator's encoder points at — the address would break every time the pod stops and restarts.
desiredStatusis a request, not a fact. RunPod reportsRUNNINGthe instant a start is requested, before the container has actually booted or its ports are reachable. Readiness is judged by whether the ports actually respond, not by the status word.
The pod's wake/sleep decision itself lives in gpu_pod.py as pure, network-free logic —
a small JSON state file next to the data directory, written atomically — while the
actual RunPod API calls live in the separate gpu_supervisor worker, so two uvicorn
workers can never race each other into starting the pod twice. The rules it encodes:
signal is only "present" if reconfirmed within a TTL window (a missed notready hook
must not mean forever-on), the pod stops on silence, not on the first notready (a
dropped mobile connection that reconnects in thirty seconds shouldn't cost a restart),
and readiness is judged by port availability, never by the RUNNING label.
The door: always-on, independent of the GPU
Because the GPU pod's address isn't stable, the actual receiving endpoint — the "door" — lives on a separate, permanent box instead, on a fixed port that never changes. This is why signal can appear the instant a creator starts publishing, even before the GPU pod has woken up: the door already knows a feed arrived, independent of whether anything is composing it yet.
The door exposes three roles, split across two files:
- Receiver hooks (
stream_door.py, called only by the localmediamtxprocess, loopback-only even if the shared secret leaks):/authdecides whether to accept a publish by checking the stream key against the database;/readyfires the moment signal starts, waking the GPU pod;/notreadystarts the silence countdown toward sleep. - Panel-facing state and preview (
stream_door_admin.py):GET /stream/door/statefor the panel to poll, and a WebRTC preview path —POST /stream/door/whep/DELETE .../whep/{id}— chosen deliberately over HLS, whose few-second buffer (worse through the door's own blocking proxy) makes watching your own live feed impractical. An HLS proxy fallback exists for networks that block WebRTC's ICE negotiation.
The door only receives — it never decides who's on air
Accepting a publish is not the same as broadcasting it. The door's job stops at "signal present, GPU should wake"; fanning the feed out to platforms is entirely the cockpit's job, on request.
Ingest: your one receiving address
The address a creator's encoder actually points at is built from a StreamIngest row —
one per owner, keyed by a slug rather than the owner's internal ID (the public
MediaMTX path shouldn't expose that), paired with a publish_key. Both fields exist
specifically so the door's /auth hook can check "is this publish allowed?" without
looking anything up by owner identity from the path alone.
The cockpit ties it together
stream_cockpit.py is where a creator actually acts: POST /stream/cockpit/live turns
signal into on-air fan-out (or takes it down), POST /stream/cockpit/record starts or
stops recording the raw incoming feed on the GPU pod's box, and POST /stream/cockpit/auto flips on auto-live — signal itself becomes the trigger, skipping
the manual button. Everything the cockpit shows (GET /stream/cockpit) — whether
there's signal, whether the GPU pod reports a recording in progress, who's currently
fanning out — is a live read across the door and the GPU pod, not a cached flag on the
cockpit's own row.
What's next
Scenes and layers — what the GPU pod actually composes once it's awake.