# 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](/docs/video-sdk/windows/live-stream/). ## 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. ```cpp 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. ```bash POST https://api.zoom.us/v2/videosdk/stream_ingestions ``` ### Request body ```json { "session_id": "your_session_id", "stream_name": "My Incoming Stream" } ``` ### Response ```json { "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](/docs/api/video-sdk/#tag/sessions/post/videosdk/stream_ingestions). ## 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`. ```cpp 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`. ```cpp ZoomVideoSDKErrors err = incomingStreamHelper->getIncomingLiveStreamStatus(); if (err != ZoomVideoSDKErrors_Success) { // Handle error } ``` The callback receives a list of `IncomingLiveStreamStatus` structs, one for each bound stream. ```cpp 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](#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"**. ```cpp 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. ```cpp void onUserJoin(IZoomVideoSDKUserHelper* pUserHelper, IVideoSDKVector* 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. ```cpp // 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. ```bash 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 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`. ```cpp 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. ```cpp virtual void onIncomingLiveStreamStatusResponse( bool bSuccess, IVideoSDKVector* 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`. ```cpp 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`. ```cpp 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`. ```cpp 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. ```cpp // 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* 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* 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 } } ```