How it works
web3live runs the whole path from a camera to a viewer on one machine: ingest, delivery, chat and the pages around them. It relays to Binance Live and X when those platforms allow it, but it never needs them to reach its own audience.
This page is what is actually deployed, not a plan. Every number below was measured on the running system.
A channel costs no CPU
A browser publishing to this platform already sends H.264 video and Opus audio, which is exactly what WebRTC forwards natively. So a channel is pure passthrough — no ffmpeg process, no re-encoding, nothing that grows with the number of channels. Thirty channels cost the same CPU as one.
Only the shared main source is transcoded, and only because the platform relays need RTMP, which cannot carry Opus. That is one fixed cost, not a per-channel one.
Publish with WHIP, play with WHEP. Both are one HTTP POST carrying an SDP.
The codec rules are not negotiable
WebRTC will not accept H.264 with B-frames, and it will not carry AAC. RTMP is the mirror image: it cannot carry Opus. Anything that has to reach both a web viewer and a platform therefore gets encoded twice, and the encoder settings are pinned rather than left to defaults.
The AI host publishes VP8 because that is what its WebRTC stack negotiated. Web viewers are unaffected, but that stream would need re-encoding before it could be relayed to a platform.
profile-level-id=42001f is level 3.1, which caps at 720p. A 1440p screen capture exceeds it, and the encoder then emits one frame and stalls.
A stalled encoder is the failure that lies
When the capture is larger than the negotiated H.264 level can encode, the connection reports "connected", the local preview keeps playing, and viewers get a black rectangle. Nothing throws. The only evidence is that framesSent never leaves zero.
So the studio reads it back from the peer connection five seconds after going live, and says plainly what happened and what to do about it — over the picture that is contradicting it, because that preview is the reason the problem is invisible in the first place.
WebRTC behind NAT
A cloud instance only ever sees its private address, so MediaMTX advertises 172.31.x as its ICE candidate and no external viewer can reach the media. The stream negotiates successfully and then delivers nothing — the same symptom as a stalled encoder, from a completely different cause.
The fix is one setting, and it is the single most important line in the compose file for any NAT deployment.
MTX_WEBRTCADDITIONALHOSTS=<public IP>. Host networking is required too: on a bridge network MediaMTX advertises the container address instead.
Channels outlive the process that made them
The channel registry reconciles against MediaMTX every three seconds: what is publishing, how many are watching, when it started. Those are facts about the world, so they are read rather than stored.
Identity is the opposite. Title, publisher and owner token are held in Postgres, because restarting the control plane while someone is broadcasting used to lose them — the stream kept running and came back adopted from the media server as "Recovered channel", with the audience still watching a channel that had forgotten its own name.
The AI host renders only the mouth
The avatar is not generated. A source video is pre-processed into 550 full frames at 576x768, 550 face crops at 256x256, and the coordinates that map one back onto the other. At runtime the bottom half of the face crop is masked out, and the model is asked to reconstruct just that region from the audio. The result is composited back into the full frame.
That is why only the mouth moves, and why it is cheap: inference runs on half of a 256x256 crop, sixteen frames per batch, and not at all while nobody is speaking. The frame index ping-pongs forward and back rather than looping, so there is no visible cut at the seam.
Chat reaches it through the same SSE stream the web page uses. A local 7B model answers, and its output is split on punctuation and sent to speech synthesis clause by clause, so the avatar starts talking before the sentence has finished generating.
Measured: 25.00 fps sustained, 2.4 GB of a 23 GB A10G, 0% GPU while silent, ~1.4s from a viewer pressing send to the first word.
Chat, and what a viewer is allowed to say to a model
Messages arrive over one Server-Sent Events stream that also carries channel status, so a page holds a single connection rather than polling several endpoints.
Anything forwarded to the AI host is treated as speech, never as instruction. Control characters are stripped — without newlines a viewer cannot forge what looks like a system turn — the text is length-capped and framed as audience dialogue, and a shared pattern list drops the obvious attempts before they reach the model. The same list is used by the browser publisher, because a phrasing that fails on one surface should not simply work on the other.
Accounts, sessions and machine publishers
Sign-in is OAuth 2.0 with PKCE against Google and X. Sessions are rows in Postgres behind an HTTP-only cookie rather than JWTs, so signing out actually ends the session instead of waiting for an expiry.
A cookie is useless to a script, and SameSite rules mean it would not be sent cross-site anyway, so machine publishers use a bearer token instead. Only a browser session can mint one — a publish token cannot mint another — and the server keeps a digest, so a leaked backup yields nothing usable.
Source
Everything described here is in the repository on GitHub.