Send raw data

View as Markdown

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

  1. onInitialize / onMicInitialize / onShareSendStarted: the SDK gives 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 send… on the stored sender.
  4. onStopSend / onMicStopSend / onShareSendStopped: stop your frame pump.
  5. onUninitialized / onMicUninitialized: release the sender; the SDK has torn it down.

Sending one frame from inside the initialize callback (as you'd see in a naive implementation) doesn't work. The sender isn't accepting frames yet, and you only get one chance instead of a continuous stream.

The examples below use placeholder helpers like captureNextFrame(), nextAudioChunk(), and captureShareFrame() for whatever frame source your app is supplying: 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

Implement ZoomVideoSDKVideoSource, then drive a frame pump from onStartSend. The SDK reports a list of supported capabilities and a suggested capability. Pick a resolution and frame rate from those before pumping.

class MyVideoSource : ZoomVideoSDKVideoSource {
    private var sender: ZoomVideoSDKVideoSender? = null
    private var pumpJob: Job? = null

    override fun onInitialize(
        sender: ZoomVideoSDKVideoSender?,
        capabilityList: MutableList<ZoomVideoSDKVideoCapability>,
        capability: ZoomVideoSDKVideoCapability?
    ) {
        // Store the sender; the SDK isn't ready for frames yet.
        this.sender = sender
        // capabilityList is every (resolution, fps) the session+device supports.
        // capability is the SDK's suggested choice.
    }

    override fun onStartSend() {
        // Begin pumping frames on a background thread.
        pumpJob = CoroutineScope(Dispatchers.Default).launch {
            while (isActive) {
                val frame = captureNextFrame() // YUV I420 bytes from your source
                sender?.sendVideoFrame(
                    frame.buffer,
                    frame.width,
                    frame.height,
                    frame.length,
                    frame.rotation
                )
                delay(33L) // ~30 fps
            }
        }
    }

    override fun onStopSend() {
        pumpJob?.cancel()
        pumpJob = null
    }

    override fun onUninitialized() {
        sender = null
    }

    override fun onPropertyChange(
        capabilityList: MutableList<ZoomVideoSDKVideoCapability>,
        capability: ZoomVideoSDKVideoCapability?
    ) {
        // The session or device renegotiated; adjust your pump if needed.
    }
}

val context = ZoomVideoSDKSessionContext().apply {
    externalVideoSource = MyVideoSource()
}

sendVideoFrame parameters

sender.sendVideoFrame(buffer, width, height, frameLength, rotation) takes the following parameters.

ParameterTypeMeaning
bufferbyte[]YUV I420 frame data laid out as Y plane, then U plane, then V plane.
widthintWidth of the source frame in pixels.
heightintHeight of the source frame in pixels.
frameLengthintTotal byte length of the buffer. For I420 this is width * height * 3 / 2.
rotationintClockwise rotation in degrees: 0, 90, 180, or 270.

Capability list

onInitialize and onPropertyChange deliver two values.

  • capabilityList: List<ZoomVideoSDKVideoCapability>: every (resolution, fps) combination the session and device both support.
  • capability: ZoomVideoSDKVideoCapability: the SDK's suggested choice, based on the session's max capability and the device's max capability.

Match your frame pump's resolution and rate to one of the entries in capabilityList; the SDK drops frames that don't match.

Pre-process raw video

If you want 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 preProcessor on ZoomVideoSDKSessionContext before joining the session. This is an alternative to Send raw video, which replaces the SDK's capture entirely.

val myPreProcessor = object : ZoomVideoSDKVideoSourcePreProcessor {
    override fun onPreProcessRawData(rawData: ZoomVideoSDKPreProcessRawData?) {
        // Modify rawData here before the frame is sent.
    }
}

val context = ZoomVideoSDKSessionContext().apply {
    preProcessor = myPreProcessor
}

Send raw audio

Implement ZoomVideoSDKVirtualAudioMic and store the sender. Start your audio pump in onMicStartSend.

class MyVirtualMic : ZoomVideoSDKVirtualAudioMic {
    private var sender: ZoomVideoSDKAudioRawDataSender? = null
    private var pumpJob: Job? = null

    override fun onMicInitialize(sender: ZoomVideoSDKAudioRawDataSender?) {
        this.sender = sender
    }

    override fun onMicStartSend() {
        pumpJob = CoroutineScope(Dispatchers.Default).launch {
            while (isActive) {
                val chunk = nextAudioChunk() // PCM bytes from your source
                sender?.send(chunk.bytes, chunk.length, chunk.sampleRate)
            }
        }
    }

    override fun onMicStopSend() {
        pumpJob?.cancel()
        pumpJob = null
    }

    override fun onMicUninitialized() {
        sender = null
    }
}

val context = ZoomVideoSDKSessionContext().apply {
    virtualAudioMic = MyVirtualMic()
}

To listen for incoming audio through a virtual speaker, see Receive audio through a 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.

class MyShareSource : ZoomVideoSDKShareSource {
    private var sender: ZoomVideoSDKShareSender? = null
    private var pumpJob: Job? = null

    override fun onShareSendStarted(sender: ZoomVideoSDKShareSender?) {
        this.sender = sender
        pumpJob = CoroutineScope(Dispatchers.Default).launch {
            while (isActive) {
                val frame = captureShareFrame() // raw share frame from your source
                sender?.sendShareFrame(
                    frame.buffer,
                    frame.width,
                    frame.height,
                    frame.length,
                    frame.format
                )
                delay(33L)
            }
        }
    }

    override fun onShareSendStopped() {
        pumpJob?.cancel()
        pumpJob = null
        sender = null
    }
}

To register the share source with the SDK, pass it to startSharingExternalSource on ZoomVideoSDKShareHelper. See Share a camera or external source.