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 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"
}
FieldPurpose
stream_idPass as strStreamKeyID to all SDK methods
stream_urlConfigure as the RTMP server URL in your streaming software
stream_keyConfigure 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.

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 (for example, OBS Studio) with the credentials from the REST API response.

  1. Open OBS, go to Settings, then Stream.
  2. Set Server to the stream_url value.
  3. Set Stream Key to the stream_key value.
  4. Click Start Streaming.

The stream must be actively pushing video before you can call startIncomingLiveStream.

Note

Keep your streaming software pushing for as long as the stream should stay live. If it stops pushing, the virtual participant drops from the session.

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?
};
isRTMPConnectedisStreamPushedMeaning
falsefalseStreaming software not yet connected. Check your RTMP URL and stream key.
truefalseStreaming software is connected and pushing, but the host has not yet called startIncomingLiveStream.
truetrueStream 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.

  1. Stop the virtual participant from the session by calling stopIncomingLiveStream.
  2. Stop pushing in your streaming software (OBS, vMix, etc.). Wait until the RTMP connection drops before unbinding. Use getIncomingLiveStreamStatus() and check the onIncomingLiveStreamStatusResponse callback to confirm isRTMPConnected is false.
  3. 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
}

Note

Calling unbindIncomingLiveStream while isRTMPConnected is still true will fail. Always ensure the streaming software has stopped pushing first and that onIncomingLiveStreamStatusResponse has confirmed the RTMP connection is closed.

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.

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 onUnbindIncomingLiveStreamResponse has confirmed success.

Error CodeMeaning
34015Stream ingestion is still in use; unbind it first
34012Failed to delete the stream ingestion
34001Stream 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
    }
}