Media Push (Push to CDN)
rtmp-converters lifecycle, output addresses, legacy entry_points migration and callbacks.
Media Push pushes the audio and video of a channel to an RTMP address (a CDN, a live-streaming platform, or your own transcoder). This page describes the current implementation (rtmp-converters), its lifecycle and state semantics, and how Agora's legacy entry_points (FLS) model relates to it and migrates onto it.
Concepts
| Concept | Meaning |
|---|---|
| converter | A push task: it pushes the mixed stream of one channel to one rtmpUrl. It has an id, a state, and creation/update timestamps |
| region | A region prefix (such as cn-shanghai), placed at the very front of the path |
| idleTimeout | Seconds after which the task is reclaimed automatically when there are no viewers or no publishers |
| transcodeOptions / rawOptions | Transcoding parameters; when omitted, the original parameters are passed through untouched |
The mapping between a converter and our internal Egress task is 1:1: every converter owns one real room_composite egress task.
Creating a converter
curl -s -X POST "$NIMEET_API_BASE/cn-shanghai/v1/projects/$APP_ID/rtmp-converters?regionHintIp=$REGION_HINT_IP" \
-H "Authorization: Basic $NIMEET_CUSTOMER_BASIC" \
-H 'Content-Type: application/json' \
-d '{
"converter": {
"name": "weekly-live",
"rtmpUrl": "rtmp://live.example.com/live/weekly",
"idleTimeout": 300,
"jitterBufferSizeMs": 500
}
}'The request body has exactly one field, converter (required), which contains:
| Field | Type | Description | |
|---|---|---|---|
name | string \ | null | Business name, makes queries easier |
rtmpUrl | string | Push address; when it carries an authentication string it is used as-is and redacted in logs | |
idleTimeout | integer | Idle reclamation seconds | |
jitterBufferSizeMs | integer | Jitter buffer; lower it when chasing low latency for live | |
transcodeOptions | object | Transcoding parameters (resolution / bitrate / frame rate) | |
rawOptions | object | Raw parameters (used when not transcoding) |
converter.id / state / createTs in the response are what later operations rely on.
Lifecycle
create convert(POST) → state: connecting → running → (stop / idle reclamation) stopped
│
└─ failure → failed (the error is in the error field of the keepalive query)| Operation | Endpoint | Description |
|---|---|---|
| Create | POST /{region}/v1/projects/{appId}/rtmp-converters | Starts pushing immediately |
| Query state | GET /{region}/v1/projects/{appId}/rtmp-converters/{converterId} | Returns state, rtmpUrl, idleTimeout… |
| Update | PATCH /{region}/v1/projects/{appId}/rtmp-converters/{converterId} | Can change rtmpUrl / transcoding parameters; pass sequence for optimistic concurrency |
| List | GET /v1/projects/{appId}/rtmp-converters | Supports cursor and channel filters |
| Destroy | DELETE /{region}/v1/projects/{appId}/rtmp-converters/{converterId} | Stops the push and releases the egress task |
Signaling and keepalive
- There is no heartbeat API: a converter keeps pushing while the channel has participants; if nobody is present for
idleTimeout, it is reclaimed automatically; - Both reclamation and failure emit a callback with event code
202(streaming_state_changed), see Callback event reference; - When a
PATCHcarryingsequencetargets an id that has been updated concurrently, it returns409;GETagain and then retry.
Relationship with the legacy entry_points (FLS) model
| Dimension | entry_points (legacy) | rtmp-converters (current) |
|---|---|---|
| Resource model | Create an entry point first, then attach "push this stream name" beneath it | One converter = one push target |
| Path | /v1/projects/{appid}/fls/* | /{region}/v1/projects/{appId}/rtmp-converters* |
| Multiple targets | Multiple streams under one entry point | Multiple converters |
| Agora status | API sunset | Current documentation |
| Our stance | Compatibility implementation, not recommended | Recommended |
Migration mapping: "one entry point + multiple stream names" corresponds to "multiple converters". The hls switch of the legacy API is handled by the output side (the CDN) in Media Push — we no longer generate an HLS manifest. If you need HLS, use bypass streaming (Legacy).
Playback address
After a successful push, viewers play the HLS/FLV address that corresponds to rtmpUrl through the CDN — that address is decided by your CDN, not generated by us. Our responsibility ends at "pushing the stream to rtmpUrl".
Quotas and errors
| Scenario | Response |
|---|---|
| Service not enabled | 403 service_not_enabled |
| Concurrent converters exceeded | 429 streaming_quota_exceeded (see Usage and quotas) |
| Duplicate converter name in the same project | 409 streaming_invalid_state, and details.existingConverterId points at the existing task (names are unique; duplicates are not allowed) |
Missing transcodeOptions.rtcChannel (or rawOptions) | 400 invalid_parameter: you must say which channel to pull the stream from |
| Unreachable RTMP address | Creation succeeds and state reflects the failure reason in the keepalive query; the create call does not block waiting for the push handshake |
End-to-end example
node run-all.mjs --only=1818-media-push-lifecycle.mjs walks through "create → query state → update → destroy" and asserts the envelope and the state at every step (it performs writes, so it needs --all or NIMEET_EXAMPLES_MUTATING=1).