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 streaming.
Constraints
- Only the session host can bind, start, stop, or unbind a stream.
- Only one incoming stream is supported per session at a time.
Common use cases
- Broadcasting pre-recorded content into a live session
- Integrating external streaming software (OBS, vMix) as a session participant
- Streaming from hardware encoders or other RTMP sources
- 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
- Access to the Zoom Video SDK REST API to create stream ingestions
- RTMP streaming software (OBS, vMix, or a hardware encoder) available for configuration
Get the incoming live stream helper
After joining a session, obtain the IZoomVideoSDKIncomingLiveStreamHelper instance from the SDK.
IZoomVideoSDKIncomingLiveStreamHelper* incomingStreamHelper =
m_pVideoSDK->getIncomingLiveStreamHelper();
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 strStreamKeyID 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 |
For the full API reference, see Create a stream ingestion.
Step 2: Bind the stream
To associate the stream with the current session, call bindIncomingLiveStream with the stream_id from the REST API response as strStreamKeyID.
const zchar_t* streamKeyID = L"abc123def456"; // stream_id from REST API response
ZoomVideoSDKErrors err = incomingStreamHelper->bindIncomingLiveStream(streamKeyID);
if (err != ZoomVideoSDKErrors_Success)
{
// Handle bind error
}
The result is delivered asynchronously via the onBindIncomingLiveStreamResponse delegate callback.
Step 3: Configure and start your streaming software
After binding, configure your streaming software (for example, OBS Studio) with the credentials from the REST API response.
- Open OBS, go to Settings, then 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
To verify that the stream is ready, call getIncomingLiveStreamStatus(). The result is delivered asynchronously via onIncomingLiveStreamStatusResponse.
ZoomVideoSDKErrors err = incomingStreamHelper->getIncomingLiveStreamStatus();
if (err != ZoomVideoSDKErrors_Success)
{
// Handle error
}
The callback receives a list of IncomingLiveStreamStatus structs, one for each bound stream.
struct IncomingLiveStreamStatus {
const zchar_t* strStreamKeyID; // The stream key ID (stream_id from REST API)
bool isRTMPConnected; // Is streaming software connected to the Zoom platform?
bool isStreamPushed; // Has the video stream been pushed to the session?
};
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 status updates, implement onIncomingLiveStreamStatusResponse in your delegate and call getIncomingLiveStreamStatus() at appropriate points in your workflow rather than polling. For details, see Handle status callbacks.
Step 5: Start the stream as a virtual participant
Once isRTMPConnected is true in the status callback, call startIncomingLiveStream. The stream joins the session as a virtual participant named "Incoming livestream".
ZoomVideoSDKErrors err = incomingStreamHelper->startIncomingLiveStream(streamKeyID);
if (err != ZoomVideoSDKErrors_Success)
{
// Handle start error
}
The result is delivered via onStartIncomingLiveStreamResponse. After the callback reports success, the virtual participant is visible in the session and isStreamPushed will be true on the next status check.
Step 6: Subscribe to the stream
After the stream starts as a virtual participant, it arrives via the onUserJoin delegate callback like any other participant. To identify it, check IsIncomingLiveStreamUser() and then subscribe to its video pipe.
void onUserJoin(IZoomVideoSDKUserHelper* pUserHelper,
IVideoSDKVector<IZoomVideoSDKUser*>* userList)
{
if (!userList)
return;
for (int i = 0; i < userList->GetCount(); i++)
{
IZoomVideoSDKUser* user = userList->GetItem(i);
if (!user)
continue;
if (user->IsIncomingLiveStreamUser())
{
IZoomVideoSDKRawDataPipe* pVideoPipe = user->GetVideoPipe();
if (pVideoPipe)
{
pVideoPipe->subscribe(ZoomVideoSDKResolution_720P, pVideoCallbackHandler);
}
break;
}
}
}
Step 7: Stop and unbind
Stopping and unbinding are two separate steps with a required ordering.
- Stop the virtual participant from the session by calling
stopIncomingLiveStream. - Stop pushing in your streaming software (OBS, vMix, etc.). Wait until the RTMP connection drops before unbinding. Use
getIncomingLiveStreamStatus()and check theonIncomingLiveStreamStatusResponsecallback to confirmisRTMPConnectedisfalse. - Unbind to release server-side resources once the RTMP connection is fully released.
// Step 1: stop the virtual participant if the stream is active
ZoomVideoSDKErrors err = incomingStreamHelper->stopIncomingLiveStream(streamKeyID);
if (err != ZoomVideoSDKErrors_Success)
{
// Handle stop error
}
// Step 2: stop pushing in your streaming software, then call getIncomingLiveStreamStatus()
// and wait for onIncomingLiveStreamStatusResponse to confirm isRTMPConnected is false.
// Step 3: unbind once the RTMP connection has dropped
ZoomVideoSDKErrors unbindErr = incomingStreamHelper->unbindIncomingLiveStream(streamKeyID);
if (unbindErr != ZoomVideoSDKErrors_Success)
{
// Handle unbind error
}
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 onUnbindIncomingLiveStreamResponse reports success.
DELETE https://api.zoom.us/v2/videosdk/stream_ingestions/{streamId}
A successful deletion returns 204 No Content.
| 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 callbacks
The C++ SDK delivers all incoming live stream results through delegate callbacks on your IZoomVideoSDKDelegate implementation rather than through a push event. Call the corresponding SDK method and handle the result in the matching callback.
onBindIncomingLiveStreamResponse is called after bindIncomingLiveStream.
virtual void onBindIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID)
{
if (bSuccess)
{
// Stream successfully bound; proceed to configure streaming software
}
else
{
// Handle bind failure
}
}
onIncomingLiveStreamStatusResponse is called after getIncomingLiveStreamStatus. Use this to drive start/stop logic rather than polling.
virtual void onIncomingLiveStreamStatusResponse(
bool bSuccess,
IVideoSDKVector<IncomingLiveStreamStatus>* pStreamsStatusList)
{
if (bSuccess && pStreamsStatusList)
{
for (int i = 0; i < pStreamsStatusList->GetCount(); i++)
{
IncomingLiveStreamStatus status = pStreamsStatusList->GetItem(i);
if (status.isRTMPConnected && !status.isStreamPushed)
{
// Streaming software is connected; call startIncomingLiveStream
}
else if (!status.isRTMPConnected)
{
// RTMP connection dropped; safe to call unbindIncomingLiveStream
}
}
}
}
onStartIncomingLiveStreamResponse is called after startIncomingLiveStream.
virtual void onStartIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID)
{
if (bSuccess)
{
// Stream is now active as a virtual participant
}
else
{
// Handle start failure
}
}
onStopIncomingLiveStreamResponse is called after stopIncomingLiveStream.
virtual void onStopIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID)
{
if (bSuccess)
{
// Virtual participant removed; stop pushing in your streaming software
}
else
{
// Handle stop failure
}
}
onUnbindIncomingLiveStreamResponse is called after unbindIncomingLiveStream.
virtual void onUnbindIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID)
{
if (bSuccess)
{
// Stream unbound; now safe to delete the stream ingestion via REST API
}
else
{
// Handle unbind failure
}
}
Complete integration example
The following example ties together the full workflow: bind, configure streaming software, check status via callback, start the stream, subscribe on user join, and stop and unbind at the end of the session.
// stream_id obtained from POST /v2/videosdk/stream_ingestions
const zchar_t* streamKeyID = L"abc123def456";
// Obtain the helper after joining the session
IZoomVideoSDKIncomingLiveStreamHelper* incomingStreamHelper =
m_pVideoSDK->getIncomingLiveStreamHelper();
// --- Step 1: Bind the stream ---
ZoomVideoSDKErrors err = incomingStreamHelper->bindIncomingLiveStream(streamKeyID);
if (err != ZoomVideoSDKErrors_Success)
{
// Handle error
}
// --- Step 2: Configure OBS/vMix with stream_url and stream_key, then start streaming ---
// --- Step 3: Check status (result arrives in onIncomingLiveStreamStatusResponse) ---
incomingStreamHelper->getIncomingLiveStreamStatus();
// --- Delegate callbacks ---
void onBindIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID)
{
if (bSuccess)
{
// Bound; configure streaming software and begin pushing video
}
}
void onIncomingLiveStreamStatusResponse(
bool bSuccess,
IVideoSDKVector<IncomingLiveStreamStatus>* pStreamsStatusList)
{
if (!bSuccess || !pStreamsStatusList)
return;
for (int i = 0; i < pStreamsStatusList->GetCount(); i++)
{
IncomingLiveStreamStatus status = pStreamsStatusList->GetItem(i);
if (status.isRTMPConnected && !status.isStreamPushed)
{
// Step 4: Start the stream as a virtual participant
incomingStreamHelper->startIncomingLiveStream(status.strStreamKeyID);
}
else if (!status.isRTMPConnected)
{
// Streaming software has disconnected; safe to unbind
incomingStreamHelper->unbindIncomingLiveStream(status.strStreamKeyID);
}
}
}
void onStartIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID)
{
if (bSuccess)
{
// Virtual participant is now active; subscribe in onUserJoin
}
}
// Step 5: Subscribe to the stream when the virtual participant joins
void onUserJoin(IZoomVideoSDKUserHelper* pUserHelper,
IVideoSDKVector<IZoomVideoSDKUser*>* userList)
{
if (!userList)
return;
for (int i = 0; i < userList->GetCount(); i++)
{
IZoomVideoSDKUser* user = userList->GetItem(i);
if (!user)
continue;
if (user->IsIncomingLiveStreamUser())
{
IZoomVideoSDKRawDataPipe* pVideoPipe = user->GetVideoPipe();
if (pVideoPipe)
{
pVideoPipe->subscribe(ZoomVideoSDKResolution_720P, pVideoCallbackHandler);
}
break;
}
}
}
// Step 6: Stop and unbind at session end
void cleanup()
{
// Stop the virtual participant first
incomingStreamHelper->stopIncomingLiveStream(streamKeyID);
// Then stop pushing in your streaming software.
// Call getIncomingLiveStreamStatus() to confirm isRTMPConnected drops to false,
// then unbind in onIncomingLiveStreamStatusResponse above.
}
void onStopIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID)
{
if (bSuccess)
{
// Virtual participant removed; instruct user to stop streaming software,
// then poll status to confirm RTMP disconnection before unbinding
incomingStreamHelper->getIncomingLiveStreamStatus();
}
}
void onUnbindIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID)
{
if (bSuccess)
{
// All done; delete the stream ingestion via REST API from your server
}
}