# 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. 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 () @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]; } ``` ---