Use production studio mode

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.
  2. Check support for isSupportPSMode and permission for canStartPSMode, then register for events.
  3. Call startPSMode: with a video capability declaration including width, height, and format. This is the contract for all subsequent sendVideoFrame calls.
  4. Before pushing any media, wait for onStartPSModeResult: with YES, then wait for onStartSend:.
  5. Push video frames and audio buffers only while onStartSend: is active, using ZoomSDKPSSender.
  6. 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.

APIMethodDescription
Register callback.delegate = selfMust set before calling startPSMode:.
Check supportisSupportPSModeWhether PS is available in this meeting or client. Check before showing any PS UI.
Check permissioncanStartPSModeWhether the current host or co-host may start PS.
Start PSstartPSMode:(cap)Declares the video capability contract. Asynchronous — result comes in onStartPSModeResult.
Stop PSstopPSModeInitiates teardown. Cleanup must happen in onStopSend.
Is PS startedisPSModeStartedWhether PS mode is currently active. Use for state checks, not as a send gate.
Get PS user IDgetPSUserIDThe 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.

let cap = ZoomSDKPSVideoSourceCapability()
cap.width = 1920
cap.height = 1080
cap.format = ZoomSDKFrameDataFormat_I420_FULL
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.

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
      }
  }
}
@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:.
func onStartPSModeResult(_ success: Bool) {
  guard success else {
      print("PS mode failed to start")
      return
  }
  // Accepted — wait for onStartSend: before sending any media
}
- (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.
func onStartSend(_ sender: ZoomSDKPSSender?) {
  guard let sender else { return }
  self.psSender = sender
  startCapturePipeline() // Begin periodic sendVideoFrame / sendAudio calls
}
- (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.

func onStopSend() {
    stopCapturePipeline() // Stop all sendVideoFrame / sendAudio calls first
    psSender = nil        // Then release sender
}
- (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.
func onPSUserStatusChanged(_ userId: UInt, isStart start: Bool) {
    if start {
        // userId is now publishing via PS
    } else {
        // userId stopped PS publishing
    }
}
// @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.

let e: ZoomSDKError = psSender.sendVideoFrame(
    buffer,
    width: 1920,
    height: 1080,
    frameLength: 1920 * 1080 * 3 / 2,  // I420
    format: .I420_FULL
)
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.
let e: ZoomSDKError = psSender.sendAudio(
    pcmBuffer,
    dataLength: 1920,      // must be even
    sampleRate: 48000,
    channel: .mono
)
ZoomSDKError e = [self.psSender sendAudio:pcmBuffer
                                dataLength:1920      // must be even
                                sampleRate:48000
                                channel:ZoomSDKAudioChannel_Mono];

Stop production studio mode

let err: ZoomSDKError = psCtrl.stopPSMode()
// All cleanup must happen in onStopSend — not here
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.

let actionController = meetingService.getMeetingActionController()
if let userInfo = actionController.getUser(byUserID: userId) {
    let isPS = userInfo.isProductionStudioUser()
    let parentId = userInfo.getProductionStudioParent()
}
ZoomSDKMeetingActionController *actionController = [meetingService getMeetingActionController];
ZoomSDKUserInfo *userInfo = [actionController getUserByUserID:userId];
if (userInfo) {
  BOOL isPS = [userInfo isProductionStudioUser];
  unsigned int parentId = [userInfo getProductionStudioParent];
}