# Live stream (pull)

<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

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<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.

```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<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`.

```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<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
    }
}
```
