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:
onInitialize/onMicInitialize/onShareSendStarted: the SDK gives you a sender. Store it; don't send frames yet.onStartSend/onMicStartSend: the SDK is ready to accept frames. Start your frame pump.- While active, push frames continuously by calling
send…on the stored sender. onStopSend/onMicStopSend/onShareSendStopped: stop your frame pump.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.
| 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, 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.