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.
- After joining a meeting or webinar, get the
ZoomSDKMeetingProductionStudioControllerfrom the meeting service. - Check support for
isSupportPSModeand permission forcanStartPSMode, then register for events. - Call
startPSMode:with a video capability declaration including width, height, and format. This is the contract for all subsequentsendVideoFramecalls. - Before pushing any media, wait for
onStartPSModeResult:with YES, then wait foronStartSend:. - Push video frames and audio buffers only while
onStartSend:is active, usingZoomSDKPSSender. - Call
stopPSModewhen done, and release all resources inonStopSend.
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.
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
sendVideoFrameandsendAudioonly from this point. senderisnilif 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
stopPSModecall. - 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.NOmeans 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:andonStopSendfor 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
dataLengthmust be an even number.sampleRateis 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];
}