Skip to content

Egress: HLS & RTMP Live Streaming ​

Broadcast a live room's media out of the platform: push it to RTMP endpoints (YouTube, Twitch, a custom mediamtx/nginx-rtmp), serve it as an HLS live playlist for large audiences, and/or record it to server disk for later download.

Egress runs server-side: one supervised FFmpeg pipeline per session mixes all participant audio and composites published video (screen share in the primary tile) into a single program. Membership changes are picked up with a short debounced restart of the pipeline (a brief output interruption is expected).

Requirements ​

  • ffmpeg on the server host (>= 5, with libx264 and aac)
  • UDP ports 42000-42100 for FFmpeg's RTP ingest (configurable; must not overlap the mediasoup RTC_MIN_PORT/RTC_MAX_PORT range)
  • Disk space for HLS segments when the HLS output is enabled

API ​

All endpoints require the platform API key (Authorization: Bearer <key>) and operate on the key's own project, like the rest of /v1.

Start an egress session ​

bash
curl -X POST https://api.example.com/v1/rooms/$ROOM_ID/egress \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rtmpEndpoints": ["rtmps://a.rtmp.youtube.com/live2/$KEY"],
    "hls": true
  }'

Response 201:

json
{
  "id": "egress-id",
  "roomId": "room-id",
  "outputs": { "rtmpEndpoints": ["rtmps://a.rtmp.youtube.com/live2/$KEY"], "hls": true, "record": false },
  "hlsUrl": "/egress/hls/egress-id/index.m3u8",
  "recordingUrl": null,
  "recordingSizeBytes": null,
  "startedAt": "...",
  "endedAt": null
}

Validation: one to three rtmp(s):// endpoints, hls: true, and/or record: true; at least one output is required. A second active session for the same room responds 409. Endpoint hosts on private/loopback addresses are rejected unless EGRESS_ALLOW_PRIVATE_TARGETS=true (development only - use it with a local mediamtx target).

Client-initiated start/stop ​

In-call participants can start and stop egress over the room socket - no API key in the browser. The SDK surface:

tsx
const controls = useEgressControls();
await controls.start({ record: true, hls: true });
await controls.stop();

Rules:

  • Outputs are limited to record and hls; RTMP endpoints stay server-side via /v1 (push URLs from a browser are an SSRF boundary)
  • The record output requires the start-recording capability and hls requires start-broadcast (delivered with the join acknowledgement; the host role carries both)
  • Only project rooms support egress; user-owned rooms are refused
  • One active session per room, exactly like the REST path: a second start answers a coded ALREADY_ACTIVE denial
  • Every room participant receives egress:status broadcasts on session transitions; useEgressState() mirrors them (isRecording, isLive)

Inspect, list, stop ​

bash
curl -H "Authorization: Bearer $API_KEY" https://api.example.com/v1/egress/$EGRESS_ID
curl -H "Authorization: Bearer $API_KEY" https://api.example.com/v1/rooms/$ROOM_ID/egress
curl -X POST -H "Authorization: Bearer $API_KEY" https://api.example.com/v1/egress/$EGRESS_ID/stop

A session moves through starting, live, stopping, and a terminal ended (reason stopped or room-ended) or failed (with an error reason). Ending the room always stops its egress. Webhook consumers receive egress.started, egress.stopped, and egress.failed alongside the room lifecycle events.

Watching the HLS output ​

While the session is live (and until the files are cleaned up afterwards), the playlist is served by the server:

text
https://api.example.com/egress/hls/$EGRESS_ID/index.m3u8

Open it in VLC, Safari, or any HLS-capable player. The playlist keeps a sliding window of recent 4-second segments; the final window remains available after the session ends.

Recording to server disk ​

Start a session with record: true (alone or alongside RTMP/HLS) and the composited program is written to server disk:

bash
curl -X POST https://api.example.com/v1/rooms/$ROOM_ID/egress \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"record": true}'

Files land under EGRESS_RECORDINGS_DIR/<egress-id>/. Each supervised pipeline restart writes a new numbered part file, so already-written material is never truncated; the restart gap (a couple of seconds) is the only loss window. When the session ends, the parts are losslessly remuxed (stream copy, no re-encode) into a single seekable recording.mp4 and the raw parts are removed. If the server crashes mid-session, the raw MPEG-TS parts remain on disk and stay downloadable.

Recordings API ​

All endpoints require the platform API key and are scoped to the key's project. Recordings grow at roughly the program bitrate (~1.1 GB/hour at the default 2.6 Mbps program); delete what you no longer need.

bash
# List recordings (newest first, optional ?roomId= filter)
curl -H "Authorization: Bearer $API_KEY" https://api.example.com/v1/recordings

# Download (HTTP Range supported; 206 Partial Content for byte ranges)
curl -H "Authorization: Bearer $API_KEY" -o recording.mp4 \
  https://api.example.com/v1/recordings/$EGRESS_ID/file

# Delete (removes the stored files)
curl -X DELETE -H "Authorization: Bearer $API_KEY" \
  https://api.example.com/v1/recordings/$EGRESS_ID

The session view (GET /v1/egress/$EGRESS_ID) carries recordingUrl and recordingSizeBytes for record sessions. When the finalized MP4 is ready (shortly after egress.stopped), the project's webhook endpoint receives egress.recording_ready with the download URL and byte size - no polling. Sessions that fail without finalization never emit it; their raw parts stay downloadable.

Environment variables ​

VariableDefaultPurpose
EGRESS_FFMPEG_PATHffmpegBinary used for egress pipelines
EGRESS_MEDIA_PORT_MIN / EGRESS_MEDIA_PORT_MAX42000 / 42100UDP range FFmpeg binds for RTP ingest
EGRESS_HLS_DIRsystem temp dirRoot directory for HLS segment trees
EGRESS_RECORDINGS_DIR<server cwd>/data/egress-recordingsRoot directory for session recordings
EGRESS_ALLOW_PRIVATE_TARGETSfalseDevelopment escape hatch for local RTMP targets

Local development with an RTMP target ​

Run mediamtx next to the stack and start egress against it with EGRESS_ALLOW_PRIVATE_TARGETS=true:

bash
docker run --rm -p 1935:1935 bluenviron/mediamtx:latest
# then start egress with "rtmpEndpoints": ["rtmp://127.0.0.1:1935/stream"]
# and watch with: ffplay rtmp://127.0.0.1:1935/stream