Send raw data

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()
}
public class MyVideoSource implements ZoomVideoSDKVideoSource {
    private ZoomVideoSDKVideoSender sender;
    private final ExecutorService pump = Executors.newSingleThreadExecutor();
    private volatile boolean running;
    @Override
    public void onInitialize(
        ZoomVideoSDKVideoSender sender,
        List<ZoomVideoSDKVideoCapability> capabilityList,
        ZoomVideoSDKVideoCapability capability
    ) {
        // Store the sender; the SDK isn't ready for frames yet.
        this.sender = sender;
    }
    @Override
    public void onStartSend() {
        running = true;
        pump.execute(() -> {
            while (running) {
                Frame frame = captureNextFrame(); // YUV I420 bytes from your source
                if (sender != null) {
                    sender.sendVideoFrame(
                        frame.buffer,
                        frame.width,
                        frame.height,
                        frame.length,
                        frame.rotation
                    );
                }
                try {
                    Thread.sleep(33L); // ~30 fps
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
    }
    @Override
    public void onStopSend() {
        running = false;
    }
    @Override
    public void onUninitialized() {
        sender = null;
        pump.shutdown();
    }
    @Override
    public void onPropertyChange(
        List<ZoomVideoSDKVideoCapability> capabilityList,
        ZoomVideoSDKVideoCapability capability
    ) {
        // The session or device renegotiated; adjust your pump if needed.
    }
}
ZoomVideoSDKSessionContext context = new ZoomVideoSDKSessionContext();
context.externalVideoSource = new 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
}
ZoomVideoSDKVideoSourcePreProcessor preProcessor = new ZoomVideoSDKVideoSourcePreProcessor() {
    @Override
    public void onPreProcessRawData(ZoomVideoSDKPreProcessRawData rawData) {
        // Modify rawData here before the frame is sent.
    }
};
ZoomVideoSDKSessionContext context = new ZoomVideoSDKSessionContext();
context.preProcessor = preProcessor;

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()
}
public class MyVirtualMic implements ZoomVideoSDKVirtualAudioMic {
    private ZoomVideoSDKAudioRawDataSender sender;
    private final ExecutorService pump = Executors.newSingleThreadExecutor();
    private volatile boolean running;
    @Override
    public void onMicInitialize(ZoomVideoSDKAudioRawDataSender sender) {
        this.sender = sender;
    }
    @Override
    public void onMicStartSend() {
        running = true;
        pump.execute(() -> {
            while (running) {
                AudioChunk chunk = nextAudioChunk(); // PCM bytes from your source
                if (sender != null) {
                    sender.send(chunk.bytes, chunk.length, chunk.sampleRate);
                }
            }
        });
    }
    @Override
    public void onMicStopSend() {
        running = false;
    }
    @Override
    public void onMicUninitialized() {
        sender = null;
        pump.shutdown();
    }
}
ZoomVideoSDKSessionContext context = new ZoomVideoSDKSessionContext();
context.virtualAudioMic = new 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
    }
}
public class MyShareSource implements ZoomVideoSDKShareSource {
    private ZoomVideoSDKShareSender sender;
    private final ExecutorService pump = Executors.newSingleThreadExecutor();
    private volatile boolean running;
    @Override
    public void onShareSendStarted(ZoomVideoSDKShareSender sender) {
        this.sender = sender;
        running = true;
        pump.execute(() -> {
            while (running) {
                ShareFrame frame = captureShareFrame(); // raw share frame from your source
                if (this.sender != null) {
                    this.sender.sendShareFrame(
                        frame.buffer,
                        frame.width,
                        frame.height,
                        frame.length,
                        frame.format
                    );
                }
                try {
                    Thread.sleep(33L);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
    }
    @Override
    public void onShareSendStopped() {
        running = false;
        sender = null;
    }
}

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