# Send raw data

<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

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<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()
}
```

```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<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.

| 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<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](#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).
