# Use production studio mode

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

> The code on this page works with either the **default UI** or the **custom UI**.

Production studio (PS) mode lets an authorized client publish custom raw video and audio into the Zoom meeting pipeline through the Meeting SDK's `PSSender`, instead of - or in addition to - a normal camera or mic path. Only the host or co-host can start production studio mode using `canStartPSMode`.

1. After joining a meeting or webinar, get the `ZoomSDKMeetingProductionStudioController` from the meeting service.
1. Check support for `isSupportPSMode` and permission for `canStartPSMode`, then register for events.
1. Call `startPSMode:` with a video capability declaration including width, height, and format. This is the contract for all subsequent `sendVideoFrame` calls.
1. Before pushing any media, wait for `onStartPSModeResult:` with YES, then wait for `onStartSend:`.
1. Push video frames and audio buffers only while `onStartSend:` is active, using `ZoomSDKPSSender`.
1. Call `stopPSMode` when done, and release all resources in `onStopSend`.

## Get the controller

Get the controller from the meeting service after joining a meeting. If the SDK is not in a meeting or if production studio mode is unavailable, the return value is nil.

## Controller APIs

All APIs are available on `ZoomSDKMeetingProductionStudioController`.

| API               | Method              | Description                                                                                   |
| ----------------- | ------------------- | --------------------------------------------------------------------------------------------- |
| Register callback | `.delegate = self`  | Must set before calling `startPSMode:`.                                                       |
| Check support     | `isSupportPSMode`   | Whether PS is available in this meeting or client. Check before showing any PS UI.            |
| Check permission  | `canStartPSMode`    | Whether the current host or co-host may start PS.                                             |
| Start PS          | `startPSMode:(cap)` | Declares the video capability contract. Asynchronous — result comes in `onStartPSModeResult`. |
| Stop PS           | `stopPSMode`        | Initiates teardown. Cleanup must happen in onStopSend.                                        |
| Is PS started     | `isPSModeStarted`   | Whether PS mode is currently active. Use for state checks, not as a send gate.                |
| Get PS user ID    | `getPSUserID`       | The current PS user's user ID. Use for roster correlation.                                    |

### ZoomSDKPSVideoSourceCapability

`ZoomSDKPSVideoSourceCapability` is passed to `startPSMode:`. It declares the exact video format your app will send. All subsequent `sendVideoFrame` calls must match these values.

```swift
let cap = ZoomSDKPSVideoSourceCapability()
cap.width = 1920
cap.height = 1080
cap.format = ZoomSDKFrameDataFormat_I420_FULL
```

```objectivec
ZoomSDKPSVideoSourceCapability *cap = [ZoomSDKPSVideoSourceCapability new];
cap.width  = 1920;
cap.height = 1080;
cap.format = ZoomSDKFrameDataFormat_I420_FULL;
```

## Start production mode

Always check support and permission for production studio mode before calling `startPSMode:`. Set the delegate first.

```swift
class MyApp: ZoomSDKMeetingProductionStudioControllerDelegate {

  func beginProductionStudio(_ meetingService: ZoomSDKMeetingService) {
      guard let psCtrl = meetingService.getMeetingProductionStudioController() else { return }
      guard psCtrl.isSupportPSMode() else { return }
      guard psCtrl.canStartPSMode() else { return }

      psCtrl.delegate = self

      let cap = ZoomSDKPSVideoSourceCapability()
      cap.width = 1920
      cap.height = 1080
      cap.format = ZoomSDKFrameDataFormat_I420_FULL

      let err = psCtrl.startPSMode(cap)
      if err != ZoomSDKError_Success {
          // Invalid parameter or wrong state — do not expect callbacks
      }
  }
}
```

```objectivec
@interface MyApp () <ZoomSDKMeetingProductionStudioControllerDelegate>
@end

- (void)beginProductionStudio:(ZoomSDKMeetingService *)meetingService
{
    ZoomSDKMeetingProductionStudioController *psCtrl =
        [meetingService getMeetingProductionStudioController];
    if (!psCtrl) return;
    if (![psCtrl isSupportPSMode]) return;  // PS unavailable in this meeting
    if (![psCtrl canStartPSMode]) return;   // Not host/co-host

    psCtrl.delegate = self;                 // Register before starting

    ZoomSDKPSVideoSourceCapability *cap = [ZoomSDKPSVideoSourceCapability new];
    cap.width  = 1920;
    cap.height = 1080;
    cap.format = ZoomSDKFrameDataFormat_I420_FULL;

    ZoomSDKError err = [psCtrl startPSMode:cap];
    if (err != ZoomSDKError_Success) {
        // Invalid parameter or wrong state — do not expect callbacks
    }
}
```

## Callbacks

### onStartPSModeResult:(success)

Fires after `startPSMode:` is processed by the SDK.

-   NO - PS mode failed to start. Do not attempt to send media. Show an error if needed.
-   YES - PS mode accepted. Do not send yet. Wait for `onStartSend:`.

```swift
func onStartPSModeResult(_ success: Bool) {
  guard success else {
      print("PS mode failed to start")
      return
  }
  // Accepted — wait for onStartSend: before sending any media
}
```

```objectivec
- (void)onStartPSModeResult:(BOOL)success
{
    if (!success) {
        NSLog(@"PS mode failed to start");
        return;
    }
    // Accepted — wait for onStartSend: before sending any media
}
```

### onStartSend:(sender)

Fires when the SDK is ready to receive media. The sender object is valid only until `onStopSend` fires.

-   Save the sender and begin your capture/encode loop.
-   Call `sendVideoFrame` and `sendAudio` only from this point.
-   `sender` is `nil` if something went wrong - guard before using.

```swift
func onStartSend(_ sender: ZoomSDKPSSender?) {
  guard let sender else { return }
  self.psSender = sender
  startCapturePipeline() // Begin periodic sendVideoFrame / sendAudio calls
}
```

```objectivec
- (void)onStartSend:(ZoomSDKPSSender *)sender
{
    if (!sender) return;
    self.psSender = sender;
    [self startCapturePipeline]; // Begin periodic sendVideoFrame / sendAudio calls
}
```

### onStopSend

`onStopSend` fires when the SDK requires all sending to stop. This can be triggered by:

-   Your own `stopPSMode` call.
-   Meeting end or the local user leaving.
-   SDK internally stopping PS, such as for a role change.

Always stop sending and release the sender here, regardless of what caused the stop.

```swift
func onStopSend() {
    stopCapturePipeline() // Stop all sendVideoFrame / sendAudio calls first
    psSender = nil        // Then release sender
}
```

```objectivec
- (void)onStopSend
{
    [self stopCapturePipeline]; // Stop all sendVideoFrame / sendAudio calls first
    self.psSender = nil;        // Then release sender
}
```

### onPSUserStatusChanged:isStart: (optional)

`onPSUserStatusChanged:isStart` fires when a PS user in the meeting or webinar starts or stops publishing.

-   `userId` - the user ID of the PS participant.
-   `start YES` - that user began PS sending. `NO` means that they stopped.
-   Use for updating UI, such as showing a **Live** indicator for the PS user or for logging.
-   Does not affect your own send pipeline. Use `onStartSend:` and `onStopSend` for that.

```swift
func onPSUserStatusChanged(_ userId: UInt, isStart start: Bool) {
    if start {
        // userId is now publishing via PS
    } else {
        // userId stopped PS publishing
    }
}
```

```objectivec
// @optional in the protocol
- (void)onPSUserStatusChanged:(unsigned int)userId isStart:(BOOL)start
{
    if (start) {
        // userId is now publishing via PS
    } else {
        // userId stopped PS publishing
    }
}
```

## Send video and audio

Call `sendVideoFrame` and `sendAudio` only after `onStartSend:` and before `onStopSend` on a single producer thread.

### sendVideoFrame

Width, height, and format must exactly match the `ZoomSDKPSVideoSourceCapability` passed to `startPSMode:`. Mismatch returns `ZoomSDKError_InvalidParameter`.

```swift
let e: ZoomSDKError = psSender.sendVideoFrame(
    buffer,
    width: 1920,
    height: 1080,
    frameLength: 1920 * 1080 * 3 / 2,  // I420
    format: .I420_FULL
)
```

```objectivec
ZoomSDKError e = [
    self.psSender sendVideoFrame:buffer
    width:1920
    height:1080
    frameLength:1920 * 1080 * 3 / 2  // I420
    format:ZoomSDKFrameDataFormat_I420_FULL
];
```

### sendAudio

-   `dataLength` must be an even number.
-   `sampleRate` is 32000 or 48000. 48000 is recommended.

```swift
let e: ZoomSDKError = psSender.sendAudio(
    pcmBuffer,
    dataLength: 1920,      // must be even
    sampleRate: 48000,
    channel: .mono
)
```

```objectivec
ZoomSDKError e = [self.psSender sendAudio:pcmBuffer
                                dataLength:1920      // must be even
                                sampleRate:48000
                                channel:ZoomSDKAudioChannel_Mono];
```

## Stop production studio mode

```swift
let err: ZoomSDKError = psCtrl.stopPSMode()
// All cleanup must happen in onStopSend — not here
```

```objectivec
ZoomSDKError err = [psCtrl stopPSMode];
// All cleanup must happen in onStopSend — not here
```

`stopPSMode` initiates teardown asynchronously. Do not release the sender or stop your capture loop here. Wait for `onStopSend`.

## Use the participant identity

`isProductionStudioUser` and `getProductionStudioParent` describe who a participant is in the roster. Use them for UI labeling or debugging. They have no connection to the send pipeline.

```swift
let actionController = meetingService.getMeetingActionController()

if let userInfo = actionController.getUser(byUserID: userId) {
    let isPS = userInfo.isProductionStudioUser()
    let parentId = userInfo.getProductionStudioParent()
}
```

```objectivec
ZoomSDKMeetingActionController *actionController = [meetingService getMeetingActionController];

ZoomSDKUserInfo *userInfo = [actionController getUserByUserID:userId];
if (userInfo) {
  BOOL isPS = [userInfo isProductionStudioUser];
  unsigned int parentId = [userInfo getProductionStudioParent];
}
```

---
