Live stream (pull)
The incoming live stream feature lets you ingest an external RTMP stream (from OBS, vMix, hardware encoders, or other software) into a Video SDK session as a special virtual participant. Other session participants can subscribe to its video and audio just like any other participant.
To broadcast a session out to an RTMP endpoint instead, see Live stream (push).
Constraints
- Only the session host can bind, start, stop, or unbind a stream.
- Only one incoming stream is supported per session at a time.
- Cannot be used from within a breakout room.
Common use cases
- Broadcasting pre-recorded content into a live session
- Integrating external streaming software (OBS, vMix) as a session participant
- Creating hybrid events with both live participants and streamed content
Prerequisites
- A Zoom Video SDK account with the incoming live stream feature enabled
- Host privileges in the active session
- RTMP streaming software (OBS, vMix, or a hardware encoder) available for configuration
Get the incoming live stream client
After joining a session, retrieve the feature client:
const incomingLiveStream = client.getIncomingLiveStreamClient();
Step 1: Create a stream ingestion via the REST API
Before calling any SDK method, use the Zoom REST API to create a stream ingestion. This returns the credentials your streaming software needs.
POST https://api.zoom.us/v2/videosdk/stream_ingestions
Request body
{
"session_id": "your_session_id",
"stream_name": "My Incoming Stream"
}
Response
{
"stream_id": "abc123def456",
"stream_url": "rtmp://stream.zoom.us/live",
"stream_key": "sk_your_stream_key"
}
| Field | Purpose |
|---|---|
stream_id | Pass as streamId to all SDK methods |
stream_url | Configure as the RTMP server URL in your streaming software |
stream_key | Configure as the stream key in your streaming software |
See Create a stream ingestion in the API reference.
Step 2: Bind the stream
Call bindIncomingLiveStream with the stream_id from the REST API response to associate the stream with the current session:
const streamId = "abc123def456"; // stream_id from REST API response
await incomingLiveStream.bindIncomingLiveStream(streamId);
Note
If another stream is already RTMP-connected, binding a different stream ID will fail. Unbind the existing stream first.
Step 3: Configure and start your streaming software
After binding, configure your streaming software (e.g., OBS Studio) with the credentials from the REST API response:
- Open OBS → Settings → Stream
- Set Server to the
stream_urlvalue - Set Stream Key to the
stream_keyvalue - Click Start Streaming
The stream must be actively pushing video before you can call startIncomingLiveStream.
Step 4: Check stream status
Use getIncomingLiveStreamStatus() to verify the stream is ready. When a stream is bound but not yet RTMP-connected, each call also triggers an on-demand status refresh from the server.
const status = incomingLiveStream.getIncomingLiveStreamStatus();
// { isRTMPConnected: boolean, isStreamPushed: boolean, streamId: string }
if (status.isRTMPConnected && !status.isStreamPushed) {
// Streaming software is connected, ready to call startIncomingLiveStream
}
isRTMPConnected | isStreamPushed | Meaning |
|---|---|---|
false | false | Streaming software not yet connected. Check your RTMP URL and stream key |
true | false | Streaming software is connected and pushing, but the host has not yet called startIncomingLiveStream |
true | true | Stream is active as a virtual participant in the session |
For real-time updates instead of polling, listen to the incoming-live-stream-status event.
Step 5: Start the stream as a virtual participant
Once isRTMPConnected is true, call startIncomingLiveStream. The stream joins the session as a virtual participant named "Incoming livestream".
await incomingLiveStream.startIncomingLiveStream(streamId);
// Resolves when isStreamPushed becomes true
The promise resolves only after the server confirms the stream is actively pushing (isStreamPushed: true).
Step 6: Subscribe to the stream
Once started, the stream appears in the participant list like any other user. Identify it using the user-added event and the participant's isRtmpUser flag:
client.on("user-added", (participants) => {
const streamUser = participants.find((p) => p.isRtmpUser);
if (streamUser) {
client
.getMediaStream()
.attachVideo(streamUser.userId, VideoQuality.Video_720P)
.then((userVideo) => {
document
.querySelector("video-player-container")
.appendChild(userVideo);
});
}
});
Step 7: Stop and unbind
Stopping and unbinding are two separate steps with a required prerequisite:
- Stop - removes the stream as a virtual participant from the session (if active).
- Stop pushing in your streaming software -
unbindIncomingLiveStreamrequiresisRTMPConnectedto befalse. Listen to theincoming-live-stream-statusevent and wait until the streaming software has disconnected before calling unbind. - Unbind - releases server-side resources once the RTMP connection is fully released.
// Step 1: stop the virtual participant if stream is active
const { isStreamPushed } = incomingLiveStream.getIncomingLiveStreamStatus();
if (isStreamPushed) {
await incomingLiveStream.stopIncomingLiveStream(streamId);
}
// Step 2: stop pushing in your streaming software (OBS, vMix, etc.)
// Then listen for the RTMP connection to drop before unbinding
// Step 3: unbind once isRTMPConnected becomes false
client.on("incoming-live-stream-status", async (payload) => {
if (!payload.isRTMPConnected) {
await incomingLiveStream.unbindIncomingLiveStream(payload.streamId);
}
});
Note
Calling
unbindIncomingLiveStreamwhileisRTMPConnectedis stilltruewill reject with an error. Always ensure the streaming software has stopped pushing first.
Step 8: Delete the stream ingestion via the REST API
After unbinding, delete the stream ingestion from your server. Each Video SDK account has a limit on the number of stream ingestions, so cleaning up unused ones prevents hitting that ceiling.
Call the following endpoint from your backend after unbindIncomingLiveStream resolves:
DELETE https://api.zoom.us/v2/videosdk/stream_ingestions/{streamId}
A successful deletion returns 204 No Content.
Important
The stream ingestion must be fully unbound before deletion. Attempting to delete an in-use ingestion returns error
34015. Only call this endpoint after the SDK'sunbindIncomingLiveStreampromise has resolved.
| Error Code | Meaning |
|---|---|
34015 | Stream ingestion is still in use; unbind it first |
34012 | Failed to delete the stream ingestion |
34001 | Stream ingestion does not exist |
Handle status events
Listen to incoming-live-stream-status for real-time status changes during the session, useful for driving start/stop logic reactively instead of polling:
client.on("incoming-live-stream-status", (payload) => {
const { isRTMPConnected, isStreamPushed, streamId } = payload;
if (isRTMPConnected && !isStreamPushed) {
// Streaming software is connected and pushing, start as a virtual participant
incomingLiveStream.startIncomingLiveStream(streamId);
} else if (!isRTMPConnected) {
// Streaming software has stopped pushing, safe to unbind
incomingLiveStream.unbindIncomingLiveStream(streamId);
}
});
Complete integration example
const streamId = "abc123def456"; // stream_id from POST /v2/videosdk/stream_ingestions
// Retrieve client after joining the session
const incomingLiveStream = client.getIncomingLiveStreamClient();
// 1. Bind the stream
await incomingLiveStream.bindIncomingLiveStream(streamId);
// 2. Configure OBS/vMix with stream_url + stream_key, then start streaming
// 3. React to status changes
client.on("incoming-live-stream-status", async (payload) => {
const { isRTMPConnected, isStreamPushed, streamId } = payload;
if (isRTMPConnected && !isStreamPushed) {
await incomingLiveStream.startIncomingLiveStream(streamId);
console.log("Incoming stream is now a virtual participant");
}
});
// 4. Subscribe to the stream's video when the participant joins
client.on("user-added", (participants) => {
const streamUser = participants.find((p) => p.isRtmpUser);
if (streamUser) {
client
.getMediaStream()
.attachVideo(streamUser.userId, VideoQuality.Video_720P)
.then((userVideo) => {
document
.querySelector("video-player-container")
.appendChild(userVideo);
});
}
});
// 5. On session end, stop the virtual participant if active
// unbind is triggered via incoming-live-stream-status once isRTMPConnected becomes false
async function cleanup() {
const { isStreamPushed } = incomingLiveStream.getIncomingLiveStreamStatus();
if (isStreamPushed) {
await incomingLiveStream.stopIncomingLiveStream(streamId);
}
// Ask the user to stop pushing in their streaming software.
// unbindIncomingLiveStream will be called by the incoming-live-stream-status handler above
// once isRTMPConnected becomes false.
}