Send raw data

To send custom raw frames in place of the Video SDK for iOS's default capture, register your own source on the ZoomVideoSDKSessionContext before you join the session. Each source type follows the same lifecycle:

  1. onInitialize / onMicInitialize / onShareSendStarted: the SDK hands you a sender. Store it; don't send frames yet.
  2. onStartSend / onMicStartSend: the SDK is ready to accept frames. Start your frame pump.
  3. While active, push frames continuously by calling the sender's send… method.
  4. onStopSend / onMicStopSend / onShareSendStopped: stop your frame pump.
  5. onUninitialized / onMicUninitialized: release the sender; the SDK has torn it down.

Sending a single frame from inside the initialize callback doesn't work. The sender isn't accepting frames yet, and you only get one frame instead of a continuous stream.

The examples below use placeholder helpers like captureNextFrame(), nextAudioChunk(), and captureShareFrame() for whatever frame source your app supplies: a hardware capture device, a media file decoder, a synthesized stream, and so on. The SDK does not provide these; they represent your app's source side of the pipeline.

Send raw video data

Implement ZoomVideoSDKVideoSource, store the sender from onInitialize, and drive a frame pump from onStartSend. Send each frame with sendVideoFrame on the ZoomVideoSDKVideoSender. Assign your source to the session context's externalVideoSourceDelegate before joining.

Add the code to MyVideoSource.swift.

class MyVideoSource: NSObject, ZoomVideoSDKVideoSource {
    private var sender: ZoomVideoSDKVideoSender?
    private var pumpTask: Task<Void, Never>?
    func onInitialize(_ rawDataSender: ZoomVideoSDKVideoSender, supportCapabilityArray: [Any], suggestCapability: ZoomVideoSDKVideoCapability) {
        // Store the sender; the SDK isn't ready for frames yet.
        sender = rawDataSender
    }
    func onPropertyChange(_ supportCapabilityArray: [Any], suggestCapability: ZoomVideoSDKVideoCapability) {
        // The session or device renegotiated; adjust your pump if needed.
    }
    func onStartSend() {
        // The SDK is ready for frames. Begin pumping on a background task.
        pumpTask = Task(priority: .userInitiated) {
            while !Task.isCancelled {
                let frame = captureNextFrame() // YUV I420 bytes from your source
                sender?.sendVideoFrame(frame.buffer,
                                       width: frame.width,
                                       height: frame.height,
                                       dataLength: frame.length,
                                       rotation: frame.rotation,
                                       format: frame.format)
                try? await Task.sleep(nanoseconds: 33_000_000) // ~30 fps
            }
        }
    }
    func onStopSend() {
        pumpTask?.cancel()
        pumpTask = nil
    }
    func onUninitialized() {
        sender = nil
    }
}
// Assign your source to the session context before joining.
let videoSource = MyVideoSource()
sessionContext.externalVideoSourceDelegate = videoSource

Add the interface to MyVideoSource.h.

#import <Foundation/Foundation.h>
#import <ZoomVideoSDK/ZoomVideoSDK.h>
NS_ASSUME_NONNULL_BEGIN
@interface MyVideoSource : NSObject <ZoomVideoSDKVideoSource>
@property (nonatomic, strong, nullable) ZoomVideoSDKVideoSender *sender;
@property (nonatomic, assign) BOOL running;
@end
NS_ASSUME_NONNULL_END

Add the implementation to MyVideoSource.m.

#import "MyVideoSource.h"
@implementation MyVideoSource
- (void)onInitialize:(ZoomVideoSDKVideoSender *)rawDataSender supportCapabilityArray:(NSArray *)supportCapabilityArray suggestCapability:(ZoomVideoSDKVideoCapability *)suggestCapability {
    // Store the sender; the SDK isn't ready for frames yet.
    self.sender = rawDataSender;
}
- (void)onPropertyChange:(NSArray *)supportCapabilityArray suggestCapability:(ZoomVideoSDKVideoCapability *)suggestCapability {
    // The session or device renegotiated; adjust your pump if needed.
}
- (void)onStartSend {
    // The SDK is ready for frames. Begin pumping on a background queue.
    self.running = YES;
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
        while (self.running) {
            Frame *frame = [self captureNextFrame]; // YUV I420 bytes from your source
            [self.sender sendVideoFrame:frame.buffer
                                  width:frame.width
                                 height:frame.height
                             dataLength:frame.length
                               rotation:frame.rotation
                                 format:frame.format];
            [NSThread sleepForTimeInterval:0.033]; // ~30 fps
        }
    });
}
- (void)onStopSend {
    self.running = NO;
}
- (void)onUninitialized {
    self.sender = nil;
}
@end

Add the code where you build your ZoomVideoSDKSessionContext.

// Assign your source to the session context before joining.
MyVideoSource *videoSource = [[MyVideoSource alloc] init];
sessionContext.externalVideoSourceDelegate = videoSource;

sendVideoFrame parameters

sendVideoFrame takes the following parameters.

ParameterTypeMeaning
frameBufferchar *YUV I420 frame data laid out as Y plane, then U plane, then V plane.
widthNSUIntegerWidth of the source frame in pixels.
heightNSUIntegerHeight of the source frame in pixels.
dataLengthNSUIntegerTotal byte length of the buffer. For I420 this is width * height * 3 / 2.
rotationZoomVideoSDKVideoRawDataRotationClockwise frame rotation: 0, 90, 180, or 270 degrees.
formatZoomVideoSDKFrameDataFormatThe buffer layout (YUV I420).

Capability list

onInitialize and onPropertyChange both deliver two values that describe what the session and device can handle.

  • supportCapabilityArray: an array of ZoomVideoSDKVideoCapability objects — every (resolution, fps) combination the session and device both support.
  • suggestCapability: the SDK's suggested ZoomVideoSDKVideoCapability, derived from the session's maximum capability and the device's maximum capability.

Match your frame pump's resolution and frame rate to one of the entries in supportCapabilityArray.

Pre-process raw video data

To keep the SDK's built-in camera capture but transform frames before they go out (for example, to apply a custom filter or watermark), implement ZoomVideoSDKVideoSourcePreProcessor and assign it to the session context's preProcessorDelegate before joining. This is an alternative to Send raw video data, which replaces the SDK's capture entirely.

class MyPreProcessor: NSObject, ZoomVideoSDKVideoSourcePreProcessor {
    func onPreProcessRawData(_ rawData: ZoomVideoSDKPreProcessRawData) {
        // Modify rawData here before the frame is sent.
    }
}
// Assign the pre-processor to the session context before joining.
let preProcessor = MyPreProcessor()
sessionContext.preProcessorDelegate = preProcessor
@interface MyPreProcessor : NSObject <ZoomVideoSDKVideoSourcePreProcessor>
@end
@implementation MyPreProcessor
- (void)onPreProcessRawData:(ZoomVideoSDKPreProcessRawData *)rawData {
    // Modify rawData here before the frame is sent.
}
@end
// Assign the pre-processor to the session context before joining.
MyPreProcessor *preProcessor = [[MyPreProcessor alloc] init];
sessionContext.preProcessorDelegate = preProcessor;

Send raw audio data

Implement ZoomVideoSDKVirtualAudioMic, store the sender from onMicInitialize, and start your audio pump in onMicStartSend. Send audio with the send method on the ZoomVideoSDKAudioSender. The audio must be mono, 16-bit PCM, little-endian. Assign your microphone to the session context's virtualAudioMicDelegate before joining.

Add the code to MyVirtualMic.swift.

class MyVirtualMic: NSObject, ZoomVideoSDKVirtualAudioMic {
    private var sender: ZoomVideoSDKAudioSender?
    private var pumpTask: Task<Void, Never>?
    func onMicInitialize(_ rawDataSender: ZoomVideoSDKAudioSender) {
        // Store the sender; the SDK isn't ready for audio yet.
        sender = rawDataSender
    }
    func onMicStartSend() {
        // The SDK is ready for audio. Begin pumping on a background task.
        pumpTask = Task(priority: .userInitiated) {
            while !Task.isCancelled {
                let chunk = nextAudioChunk() // mono 16-bit PCM bytes from your source
                sender?.send(chunk.buffer, dataLength: chunk.length, sampleRate: chunk.sampleRate)
            }
        }
    }
    func onMicStopSend() {
        pumpTask?.cancel()
        pumpTask = nil
    }
    func onMicUninitialized() {
        sender = nil
    }
}
// Assign your microphone to the session context before joining.
let virtualMic = MyVirtualMic()
sessionContext.virtualAudioMicDelegate = virtualMic

Add the interface to MyVirtualMic.h.

#import <Foundation/Foundation.h>
#import <ZoomVideoSDK/ZoomVideoSDK.h>
NS_ASSUME_NONNULL_BEGIN
@interface MyVirtualMic : NSObject <ZoomVideoSDKVirtualAudioMic>
@property (nonatomic, strong, nullable) ZoomVideoSDKAudioSender *sender;
@property (nonatomic, assign) BOOL running;
@end
NS_ASSUME_NONNULL_END

Add the implementation to MyVirtualMic.m.

#import "MyVirtualMic.h"
@implementation MyVirtualMic
- (void)onMicInitialize:(ZoomVideoSDKAudioSender *)rawDataSender {
    // Store the sender; the SDK isn't ready for audio yet.
    self.sender = rawDataSender;
}
- (void)onMicStartSend {
    // The SDK is ready for audio. Begin pumping on a background queue.
    self.running = YES;
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
        while (self.running) {
            AudioChunk *chunk = [self nextAudioChunk]; // mono 16-bit PCM bytes from your source
            [self.sender send:chunk.buffer dataLength:chunk.length sampleRate:chunk.sampleRate];
        }
    });
}
- (void)onMicStopSend {
    self.running = NO;
}
- (void)onMicUninitialized {
    self.sender = nil;
}
@end

Add the code where you build your ZoomVideoSDKSessionContext.

// Assign your microphone to the session context before joining.
MyVirtualMic *virtualMic = [[MyVirtualMic alloc] init];
sessionContext.virtualAudioMicDelegate = virtualMic;

To process incoming audio through a virtual speaker, see Receive raw audio for virtual speaker.

Send raw share data

Implement ZoomVideoSDKShareSource. The SDK invokes onShareSendStarted once it has a sender ready; pump share frames from there until onShareSendStopped. Register the source by passing it to startSharingExternalSource on the ZoomVideoSDKShareHelper.

Add the code to MyShareSource.swift.

class MyShareSource: NSObject, ZoomVideoSDKShareSource {
    private var sender: ZoomVideoSDKShareSender?
    private var pumpTask: Task<Void, Never>?
    func onShareSendStarted(_ rawDataSender: ZoomVideoSDKShareSender?) {
        sender = rawDataSender
        // sendShareFrame sends one frame, so pump frames on a background task.
        pumpTask = Task(priority: .userInitiated) {
            while !Task.isCancelled {
                let frame = captureShareFrame() // raw share frame from your source
                sender?.sendShareFrame(frame.buffer,
                                       width: frame.width,
                                       height: frame.height,
                                       frameLength: frame.length,
                                       format: frame.format)
                try? await Task.sleep(nanoseconds: 33_000_000)
            }
        }
    }
    func onShareSendStopped() {
        pumpTask?.cancel()
        pumpTask = nil
        sender = nil
    }
}
// Register the share source with the SDK.
let shareSource = MyShareSource()
ZoomVideoSDK.shareInstance()?.getShareHelper()?.startSharingExternalSource(shareSource, andAudioSource: nil, isPlaying: false)

Add the interface to MyShareSource.h.

#import <Foundation/Foundation.h>
#import <ZoomVideoSDK/ZoomVideoSDK.h>
NS_ASSUME_NONNULL_BEGIN
@interface MyShareSource : NSObject <ZoomVideoSDKShareSource>
@property (nonatomic, strong, nullable) ZoomVideoSDKShareSender *sender;
@property (nonatomic, assign) BOOL running;
@end
NS_ASSUME_NONNULL_END

Add the implementation to MyShareSource.m.

#import "MyShareSource.h"
@implementation MyShareSource
- (void)onShareSendStarted:(ZoomVideoSDKShareSender *)rawDataSender {
    self.sender = rawDataSender;
    self.running = YES;
    // sendShareFrame sends one frame, so pump frames on a background queue.
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
        while (self.running) {
            ShareFrame *frame = [self captureShareFrame]; // raw share frame from your source
            [self.sender sendShareFrame:frame.buffer
                                  width:frame.width
                                 height:frame.height
                            frameLength:frame.length
                                 format:frame.format];
            [NSThread sleepForTimeInterval:0.033];
        }
    });
}
- (void)onShareSendStopped {
    self.running = NO;
    self.sender = nil;
}
@end

Add the code where you start sharing.

// Register the share source with the SDK.
MyShareSource *shareSource = [[MyShareSource alloc] init];
[[[ZoomVideoSDK shareInstance] getShareHelper] startSharingExternalSource:shareSource andAudioSource:nil isPlaying:NO];

For sharing a screen or a single UIView without supplying raw frames, see Core features.