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:
onInitialize/onMicInitialize/onShareSendStarted: the SDK hands you a sender. Store it; don't send frames yet.onStartSend/onMicStartSend: the SDK is ready to accept frames. Start your frame pump.- While active, push frames continuously by calling the sender's
send…method. onStopSend/onMicStopSend/onShareSendStopped: stop your frame pump.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.
| Parameter | Type | Meaning |
|---|---|---|
frameBuffer | char * | YUV I420 frame data laid out as Y plane, then U plane, then V plane. |
width | NSUInteger | Width of the source frame in pixels. |
height | NSUInteger | Height of the source frame in pixels. |
dataLength | NSUInteger | Total byte length of the buffer. For I420 this is width * height * 3 / 2. |
rotation | ZoomVideoSDKVideoRawDataRotation | Clockwise frame rotation: 0, 90, 180, or 270 degrees. |
format | ZoomVideoSDKFrameDataFormat | The 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 ofZoomVideoSDKVideoCapabilityobjects — every(resolution, fps)combination the session and device both support.suggestCapability: the SDK's suggestedZoomVideoSDKVideoCapability, 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.