# 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`. ```swift class MyVideoSource: NSObject, ZoomVideoSDKVideoSource { private var sender: ZoomVideoSDKVideoSender? private var pumpTask: Task? 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`. ```objectivec #import #import NS_ASSUME_NONNULL_BEGIN @interface MyVideoSource : NSObject @property (nonatomic, strong, nullable) ZoomVideoSDKVideoSender *sender; @property (nonatomic, assign) BOOL running; @end NS_ASSUME_NONNULL_END ``` Add the implementation to `MyVideoSource.m`. ```objectivec #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`. ```objectivec // 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 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](#send-raw-video-data), which replaces the SDK's capture entirely. ```swift 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 ``` ```objectivec @interface MyPreProcessor : NSObject @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`. ```swift class MyVirtualMic: NSObject, ZoomVideoSDKVirtualAudioMic { private var sender: ZoomVideoSDKAudioSender? private var pumpTask: Task? 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`. ```objectivec #import #import NS_ASSUME_NONNULL_BEGIN @interface MyVirtualMic : NSObject @property (nonatomic, strong, nullable) ZoomVideoSDKAudioSender *sender; @property (nonatomic, assign) BOOL running; @end NS_ASSUME_NONNULL_END ``` Add the implementation to `MyVirtualMic.m`. ```objectivec #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`. ```objectivec // 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](/docs/video-sdk/ios/raw-data/receive-raw-data/#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`. ```swift class MyShareSource: NSObject, ZoomVideoSDKShareSource { private var sender: ZoomVideoSDKShareSender? private var pumpTask: Task? 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`. ```objectivec #import #import NS_ASSUME_NONNULL_BEGIN @interface MyShareSource : NSObject @property (nonatomic, strong, nullable) ZoomVideoSDKShareSender *sender; @property (nonatomic, assign) BOOL running; @end NS_ASSUME_NONNULL_END ``` Add the implementation to `MyShareSource.m`. ```objectivec #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. ```objectivec // 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](/docs/video-sdk/ios/share/).