TEASEDocs
ProductsStreamConcepts

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 podapp/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.
  • desiredStatus is a request, not a fact. RunPod reports RUNNING the 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 local mediamtx process, loopback-only even if the shared secret leaks): /auth decides whether to accept a publish by checking the stream key against the database; /ready fires the moment signal starts, waking the GPU pod; /notready starts the silence countdown toward sleep.
  • Panel-facing state and preview (stream_door_admin.py): GET /stream/door/state for 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.

On this page