# 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. ```kotlin class MyVideoSource : ZoomVideoSDKVideoSource { private var sender: ZoomVideoSDKVideoSender? = null private var pumpJob: Job? = null override fun onInitialize( sender: ZoomVideoSDKVideoSender?, capabilityList: MutableList, 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, capability: ZoomVideoSDKVideoCapability? ) { // The session or device renegotiated; adjust your pump if needed. } } val context = ZoomVideoSDKSessionContext().apply { externalVideoSource = MyVideoSource() } ``` ```java 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 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 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. | Parameter | Type | Meaning | | ------------- | -------- | --------------------------------------------------------------------------- | | `buffer` | `byte[]` | YUV I420 frame data laid out as Y plane, then U plane, then V plane. | | `width` | `int` | Width of the source frame in pixels. | | `height` | `int` | Height of the source frame in pixels. | | `frameLength` | `int` | Total byte length of the buffer. For I420 this is `width * height * 3 / 2`. | | `rotation` | `int` | Clockwise rotation in degrees: `0`, `90`, `180`, or `270`. | ### Capability list `onInitialize` and `onPropertyChange` deliver two values. - `capabilityList: List`: 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](#send-raw-video), which replaces the SDK's capture entirely. ```kotlin val myPreProcessor = object : ZoomVideoSDKVideoSourcePreProcessor { override fun onPreProcessRawData(rawData: ZoomVideoSDKPreProcessRawData?) { // Modify rawData here before the frame is sent. } } val context = ZoomVideoSDKSessionContext().apply { preProcessor = myPreProcessor } ``` ```java 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`. ```kotlin 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() } ``` ```java 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](/docs/video-sdk/android/raw-data/receive-raw-data/#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`. ```kotlin 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 } } ``` ```java 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](/docs/video-sdk/android/share/share-camera-or-external-source/#share-an-external-source).