Send raw data

View as Markdown

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

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

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

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)

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