Distribute WebRTC Video with MediaMTX and rstream
Add an on-demand MediaMTX backend to the adaptive rstream video platform, share one protected device uplink across viewers, and keep direct WebRTC as a selectable path.
The first guide in this series built the adaptive Go video producer. The second placed the same producer behind a Next.js product control plane. This guide adds multi-viewer distribution without introducing a second capture implementation or a second browser player.
The producer continues to expose one WHEP source through its outbound rstream tunnel. On the first viewer request, MediaMTX starts a lightweight Go adapter. The adapter obtains short-lived source and destination material from Next.js, pulls the producer through WHEP, repairs the shared source leg, and publishes a fresh H.264 track into MediaMTX. Later viewers reuse that publisher. When the last viewer leaves, MediaMTX stops the adapter and the source resource is deleted.
Direct WebRTC remains a first-class backend. It keeps the shortest path for one-to-one viewing and provides a useful diagnostic reference. Fan-out becomes valuable when several viewers need the same device: producer bandwidth stays close to one encoded stream instead of growing once per browser.
rstream examples / webrtc-videoOpen the shared producer, Next.js platform, and MediaMTX distributor.git clone https://github.com/rstreamlabs/rstream-examples.git
cd rstream-examples/webrtc-videoOne application with selectable distribution
The stable boundaries are deliberate.
producer/owns capture, encoding, TWCC/GCC adaptation, bounded pacing, NACK/RTX, optional FlexFEC, ICE recovery, and producer OpenMetrics.platform/owns device identity, user authorization, short-lived credentials, live tunnel state, and theVideoDistributorselection.shared/whep-client.tsis the browser session implementation used for direct producer and MediaMTX playback.distributor/contains MediaMTX and the optional on-demand adapter.
VIDEO_DISTRIBUTOR=direct returns the producer WHEP endpoint. VIDEO_DISTRIBUTOR=mediamtx returns the MediaMTX WHEP endpoint for the same device. The React player consumes the same discriminated payload in both cases, applies the same bounded ICE-restart and reconnect policy, and requests fresh authorization before replacing a failed resource.
Direct producer
Direct delivery has the lowest latency and the complete producer-to-viewer feedback loop: TWCC, NACK/RTX, optional FlexFEC, and the bounded producer pacer. It is the reference for one-to-one viewing and diagnosis. Its cost is linear device bandwidth: every additional viewer creates another uplink.
Native MediaMTX WHEP pull
Native pull gives MediaMTX one shared upstream without an adapter process. It suits an already-compatible, statically configured WHEP source when NACK and TWCC provide enough protection. It does not add the per-device resolver or source-side RTX/FlexFEC. MediaMTX 1.20 cannot negotiate this profile with the repository's strict producer, so it remains a qualified interoperability option rather than an rstream producer deployment.
MediaMTX with the rstream adapter
The adapter is the multi-viewer profile for an rstream device. One NACK/RTX/FlexFEC-protected source leg carries the stream, while MediaMTX gives every browser a fresh NACK/TWCC leg. Device uplink use stays close to one encoded stream and credentials are resolved on demand. This adds one WebRTC hop, one lightweight process while the path is active, and a second congestion domain to observe.
These profiles share the producer, platform contract, and browser player; only the distribution backend changes. Moving from direct viewing to fan-out does not create a second capture pipeline or product UI. The native profile remains available where it genuinely fits, but it never silently lowers the producer's validation rules.
The deployment decision is correspondingly small. Keep direct delivery for one viewer or the lowest latency. Use native pull for an already-compatible WHEP source after qualifying its narrower repair model. Select the adapter for the strict rstream producer whenever several viewers must share one protected device uplink.
Keep feedback local to each media leg
Direct playback has one congestion domain. Browser feedback reaches the producer, and the producer adjusts its encoder and pacer for that viewer's path.
Fan-out has two. The adapter receives and repairs the shared device stream. It then publishes a new RTP flow to MediaMTX, with new transport-wide sequence numbers and downstream feedback state. Each browser negotiates its own MediaMTX WebRTC session. Loss and delay on one viewer leg therefore remain distinguishable from the device uplink and cannot directly control the encoder shared by every viewer.
NACK, RTX, and FlexFEC are terminated on the source leg. The adapter reorders packets inside a 300 ms window, retries missing packets at a bounded cadence, expires missing media after one second, and forwards PLI/FIR recovery requests to the producer. It stops on overflow or peer failure rather than accumulating an unbounded live-stream backlog.
MediaMTX does not transcode this stream. That keeps the deployment lightweight and avoids concealing an expensive codec pipeline behind a configuration flag. Products that need resolution or codec ladders can add measured transcoding workers later without changing the producer, player, or VideoDistributor contract.
Respect the single-rendition boundary
The reference distributor carries one H.264 rendition. It is efficient when viewers can receive that rendition, but it is not a substitute for a bitrate ladder. Downstream TWCC terminates at MediaMTX. It describes the viewer leg, but the current single-rendition path has no alternate bitrate to select and does not send that estimate back to the producer encoder shared by every viewer.
This distinction changes the deployment gate. Use the direct backend when one viewer needs end-to-end encoder adaptation or the shortest path. Use the current fan-out backend only for a viewer population whose qualified capacity fits the source rendition. A deployment that must serve heterogeneous links needs measured multi-rendition selection or transcoding before it can claim the same continuity. Reducing queue limits, hiding drops, or weakening the playback gate does not solve that architectural constraint.
MediaMTX can carry multiple tracks from compatible publishers, and external
workers can produce additional renditions. Neither capability is enabled by
this reference because a source ladder needs its own encoder budget, selection
policy, observability, and congestion qualification. The VideoDistributor
boundary leaves room for that profile without changing the producer-facing
product API or the browser player.
Build the combined distributor
The distributor image contains the pinned MediaMTX binary and the Go adapter in one unprivileged container. The official MediaMTX image is distroless and cannot execute an external runOnDemand command; the supplied image adds only a shell, CA roots, the adapter, and a non-root runtime.
cd distributor
docker build -t rstream-video-distributor:local .It exposes three listeners.
| Listener | Purpose | Exposure |
|---|---|---|
8889/tcp | WHEP/WHIP HTTP surface | publish through an authenticated rstream HTTP tunnel |
8189/udp | WebRTC ICE media | advertise a reachable address or configure STUN/TURN |
9998/tcp | MediaMTX OpenMetrics | keep on loopback or a private monitoring network |
The included configuration disables RTSP, RTMP, HLS, SRT, MoQ, playback, the API, and pprof. Only the WebRTC and metrics surfaces required by this deployment remain enabled.
Configure Next.js and MediaMTX
Generate the MediaMTX access key and one named distributor identity from the platform directory.
cd ../platform
npm run mediamtx:key -- mediamtx-oneThe command emits distinct material for the two trust boundaries. The platform keeps the MediaMTX access private key and the distributor's public resolver key. The distributor keeps its resolver private key; MediaMTX receives the platform's public access keys through JWKS.
VIDEO_DISTRIBUTOR="mediamtx"
MEDIAMTX_TUNNEL_NAME="webrtc-video-mediamtx"
MEDIAMTX_JWT_PRIVATE_KEY_BASE64="..."
MEDIAMTX_JWT_ADDITIONAL_JWKS='{"keys":[]}'
MEDIAMTX_JWT_ISSUER="rstream-webrtc-video-platform"
MEDIAMTX_JWT_AUDIENCE="rstream-mediamtx"
MEDIAMTX_SOURCE_RESOLVER_JWKS='{"keys":[...]}'
MEDIAMTX_SOURCE_RESOLVER_ISSUER="rstream-video-distributor"
MEDIAMTX_SOURCE_RESOLVER_AUDIENCE="rstream-video-source-resolver"
MEDIAMTX_TOKEN_TTL_SECONDS="300"MediaMTX 1.20 refreshes a remote JWKS at most once per hour. A key rotation
therefore has two phases: publish the next public key through
MEDIAMTX_JWT_ADDITIONAL_JWKS while the current signer remains active, wait
for every MediaMTX instance to refresh, then switch the signer and retain the
old public key for at least the longest token lifetime plus one complete
refresh window. This order prevents a newly signed viewer token from reaching
an instance that has not learned its key.
Give the distributor the public platform endpoints and the private key for its
named resolver identity. MediaMTX accepts its normal MTX_... environment
overrides.
MTX_AUTHJWTJWKS="https://platform.example/api/video/distributor/jwks"
MTX_AUTHJWTISSUER="rstream-webrtc-video-platform"
MTX_AUTHJWTAUDIENCE="rstream-mediamtx"
MTX_WEBRTCALLOWORIGINS="https://platform.example"
MTX_WEBRTCADDITIONALHOSTS="media.example"
RSTREAM_SOURCE_RESOLVER_URL="https://platform.example/api/video/distributor/source"
RSTREAM_SOURCE_RESOLVER_INSTANCE_ID="mediamtx-one"
RSTREAM_SOURCE_RESOLVER_PRIVATE_KEY_BASE64="..."
RSTREAM_SOURCE_RESOLVER_ISSUER="rstream-video-distributor"
RSTREAM_SOURCE_RESOLVER_AUDIENCE="rstream-video-source-resolver"
RSTREAM_MEDIAMTX_URL="http://127.0.0.1:8889"
MTX_PATHDEFAULTS_MAXREADERS="8"Start the supplied single-service Compose deployment with 8189/udp reachable from its intended viewers. Keep WHEP/WHIP and metrics on loopback; the compose file also runs the combined container read-only, drops Linux capabilities, and enables no-new-privileges.
cp distributor.env.example distributor.env
# Replace every example origin, host, and secret before continuing.
docker compose up --build -d
docker compose psMediaMTX and the adapter still run in one container. MediaMTX creates the adapter only while a device path has demand, owns its signals, and reaps it during shutdown; Compose does not add another media service.
Publish the HTTP surface from the same host. The tunnel name must match MEDIAMTX_TUNNEL_NAME.
rstream forward 8889 \
--name webrtc-video-mediamtx \
--token-auth \
--label service=webrtc-video-distributorThe distributor tunnel carries the browser's WHEP requests. The adapter reaches the producer WHEP resource through its device tunnel and publishes WHIP to MediaMTX over loopback. WebRTC media still follows ICE. A publicly reachable 8189/udp address is the shortest path; NATed or restricted deployments should configure webrtcICEServers2 with their STUN/TURN policy. The platform also supplies TURN credentials to the browser, but that does not replace server-side MediaMTX candidate configuration.
Let demand own the source lifecycle
MediaMTX maps a viewer path such as devices/<device-id> to one runOnDemand process. The process receives that path through MTX_PATH; it never accepts a viewer-supplied source URL.
The lifecycle is short and deterministic.
- The first browser requests
devices/<device-id>/whep. - MediaMTX starts one adapter and waits up to 15 seconds for its publisher.
- The adapter signs a short-lived resolver request with its named Ed25519 identity.
- Next.js verifies the device, confirms its rstream tunnel is online, and returns short-lived source WHEP, destination WHIP, and TURN material.
- The adapter pulls, repairs, and publishes the source. Additional viewers share it.
- Five seconds after the final reader leaves, MediaMTX sends
SIGINT; the adapter cancels its workers and deletes both HTTP media resources.
runOnDemandRestart is disabled. Recovery stays inside the single adapter process owned by the active demand: a failed source or destination attempt closes both peer connections and HTTP resources, resolves fresh credentials, then retries after a jittered exponential delay bounded between one and 15 seconds. Thirty seconds of stable forwarding resets that delay, and MediaMTX SIGINT interrupts either forwarding or backoff immediately. This avoids an uncontrolled process restart loop while allowing an existing demand to recover when a device or path returns.
The player has a separate five-attempt reconnect budget with bounded jitter. Every full reconnect asks Next.js for fresh viewer authorization, and thirty seconds of stable playback resets that budget.
The supplied MediaMTX profile admits eight readers per device path. That is a measured reference boundary rather than an audience claim: at 8 Mbit/s, the qualification exercises one, four, and eight simultaneous viewers, rejects a ninth while the existing eight keep receiving media, and repeatedly replaces readers without creating a second producer session. Repeat the qualification on the deployment class before raising the limit. A per-path limit does not replace a fleet-wide CPU, memory, and egress admission policy.
Separate product, edge, and media authorization
The request crosses three different trust boundaries.
| Credential | Holder | Scope |
|---|---|---|
| rstream viewer token | browser | connect to one online distributor tunnel and one device WHEP path |
| MediaMTX read JWT | browser | read on one devices/<device-id> path |
| resolver identity | adapter | resolve one path and purpose as one registered distributor instance |
| rstream source token | adapter | connect to one online producer tunnel and its WHEP resource paths |
| MediaMTX publish JWT | adapter | publish on the same device path |
MediaMTX verifies read and publish JWTs from the platform JWKS endpoint. Issuer, audience, action, and exact path are checked. Each resolver call also contains the distributor instance, exact path, purpose, nonce, and a lifetime of at most 20 seconds. The platform verifies it against the instance public key, while the private key stays inside that distributor deployment. Browser credentials cannot publish, publisher credentials cannot read another path, and the adapter never receives the platform's rstream application secret. CORS is restricted to the product origin.
The browser needs both edge and media authorization on the distributor request. Its short-lived rstream token is carried as rstream.token; the engine validates its tunnel and path scope, removes it, and forwards the separate MediaMTX JWT in Authorization. This keeps the distributor tunnel protected without forcing both trust boundaries into one bearer or exposing the MediaMTX surface without edge policy.
Observe source and distribution independently
The producer exporter remains the source of truth for capture freshness, frame cadence, encoder output, TWCC/GCC estimate, pacer wire rate, queue residence, NACK/RTX, and FlexFEC on the device uplink. A collector beside the device scrapes its private OpenMetrics listener.
MediaMTX exposes path readiness, reader count, and inbound/outbound bytes on its private metrics listener.
GET http://127.0.0.1:9998/metrics?type=paths&path=devices/<device-id>The pair answers different operational questions. A stale producer frame timestamp identifies capture or encoding failure. Healthy producer output with a missing MediaMTX publisher isolates source resolution or the shared WebRTC leg. A ready path with rising outbound bytes and a single failing browser points to that viewer's ICE or access link.
The on-demand adapter writes bounded repair totals in its structured shutdown record. It separates recovery packets received from repairs delivered before the reorder window closes, and identifies late RTX/FlexFEC packets instead of reporting them as successful repairs. It deliberately does not start one metrics server per device: transient processes would collide on listeners and turn device identity into an unbounded metric dimension. Stable fleet identity belongs in collector target labels; session and viewer ids stay out of metric labels.
Use native WHEP pull where it fits
MediaMTX can pull another WHEP source without the adapter.
paths:
cameras/warehouse:
source: wheps://source.example/whep
whepBearerToken: source-token
sourceOnDemand: true
sourceOnDemandStartTimeout: 15s
sourceOnDemandCloseAfter: 5sThis profile is useful when the source accepts MediaMTX's offer, static source configuration is appropriate, and NACK/TWCC provide enough protection. It removes the adapter process and its custom media handling.
MediaMTX 1.20 native pull is not interchangeable with this repository's strict producer. Draft 04 requires rtcp-mux-only and one common msid on every active media section; the MediaMTX source offer contains neither, and its WHEP player does not complete a 406 counter-offer exchange. It also does not provide the adapter's source-side RTX/FlexFEC repair or dynamic per-device resolver. The integration test locks those differences so a future MediaMTX release can be requalified without guesswork. Silently accepting a non-conforming offer to force compatibility is not an accepted profile.
Qualify fan-out before deployment
Run the bounded unit, race, static-analysis, and real MediaMTX suites from the distributor directory.
make verify
make integration
make qualify-fanout OUT=/tmp/rstream-video-distributorThe integration suite does more than check process startup. It opens two simultaneous viewers and requires exactly one producer WHEP resource. The source and both viewer sessions must negotiate transport-wide congestion control, and the source must receive TWCC feedback. The suite then removes one H.264 RTP packet, repairs it with FlexFEC, and requires both viewers to receive the complete ordered range. A second run suppresses both the media packet and its first FEC packet; the bridge must request and receive RTX before delivering the same complete range. It also verifies a ready MediaMTX path, two readers, and positive inbound and outbound byte counters. Closing the viewers must produce one source DELETE; the next viewer must start a fresh source without stale state. A rejected first negotiation must release the path and allow the next demand to succeed.
Peer failure, cancellation, repeated close, stalled HTTP operations, invalid and weak ETags, expired counter-offers, queue bounds, bounded source retries, resolver authorization, JWT action and path separation, and native-pull capability drift have dedicated regression coverage. The container smoke test also requires a non-root runtime and a reachable private metrics listener.
The fan-out run uses a decoder-valid constrained-baseline H.264 GOP at approximately 8 Mbit/s rather than synthetic bytes labelled as video. Across three independent runs it checks constant device ingress, output proportional to reader count, complete and consistent payload delivery, one source lifecycle, source TWCC feedback, bounded CPU, memory, and process topology. It also treats any B-frame, RTP-loss, source-timeout, or adapter-failure diagnostic as a failed run. At saturation, the ninth reader must fail quickly without disturbing the eight active viewers; twelve leave-and-replacement cycles must then recover the capacity without restarting the producer source.
The finite browser runner validates each delivery mode through a real rstream context with edge authentication enabled.
RSTREAM_CONTEXT="<context>" \
RSTREAM_DISTRIBUTOR_MODE="mediamtx" \
qualification/end-to-end/run.sh /tmp/rstream-video-resultIt records first-frame timing, decoded frame rate, freezes, retransmission and FlexFEC evidence, producer OpenMetrics, traffic-control counters, source recovery, and balanced WHEP resources on one timeline. Capacity, delay, jitter, loss, and an explicit receiver playout target can be applied to the browser leg. A direct run must lower and recover its encoder target when capacity changes. The current one-rendition MediaMTX profile is a no-go when viewer capacity falls below the selected source rate: downstream feedback remains local and cannot create the missing lower rendition. That failed case is retained as an architectural boundary rather than hidden by a weaker playback threshold.
The runner identifies the exact temporary producer tunnel and project before minting a WHEP-path-scoped connect token. The token remains in process memory and the WHEP URL; result files record only that edge authentication was active. Use RSTREAM_DISTRIBUTOR_MODE=direct for the one-to-one reference. Disabling edge auth is useful for isolating local media behavior, but it does not satisfy the release authentication gate.
The deployment is a no-go if two viewers create two device sources, a missing packet escapes the repair window in the protected profile, a WHEP resource survives the final viewer, credentials authorize another path or action, any worker outlives cancellation, or the advertised ICE path is unreachable from the target network.
The standalone producer guide remains the detailed evidence for encoder adaptation, playback continuity, latency, and repair under controlled congestion. This guide adds the fan-out-specific gates rather than repeating the same media qualification under a different heading.
For a device fleet, regional MediaMTX selection, viewer policy, or media topology tailored to your product, contact us.