Skill 126 · Zoom Meeting SDK Windows
Subchapter 126.15
examples/raw-video-capture.mdMarkdown24 KBView on GitHub
This guide shows how to capture raw video data from Zoom meetings using the Windows Meeting SDK. Raw video data is provided in YUV420 (I420) format for video and PCM format for audio.
There are two ways to access raw data in the Zoom SDK:
| Method | Permission Source | How to Get Permission | Use Case |
|---|---|---|---|
| Raw Recording | Local recording permission | Host/co-host OR request from host | Capture data with “recording” consent dialog |
| Raw Streaming | Live streaming permission | Must be licensed Pro/Business/Education/Enterprise | Capture data with “streaming” consent dialog |
Key differences:
.mp4) for the SDK user. Host can still cloud record.SDK Version Requirements:
The meeting must have local recording enabled, AND you must meet one of:
You must meet all of:
You can skip the “request permission from host” step by using OAuth tokens:
For recording:
Meeting_token:read:admin:local_recording (admin) or Meeting_token:read:local_recording (user)GET /meetings/{meetingId}/jointoken/local_recordingapp_privilege_token join parameterFor streaming:
Meeting_token:read:admin:live_streaming (admin) or Meeting_token:read:live_streaming (user)GET /meetings/{meetingId}/jointoken/live_streamingapp_privilege_token join parameterBefore capturing video, you must:
MEETING_STATUS_INMEETING)See Authentication Pattern for this setup.
CRITICAL: You MUST call StartRawRecording() before you can capture video data.
void OnInMeeting() {
std::cout << "[VIDEO] In meeting! Starting video capture..." << std::endl;
// Get recording controller
IMeetingRecordingController* recordingCtrl =
meetingService->GetMeetingRecordingController();
if (!recordingCtrl) {
std::cerr << "[VIDEO] ERROR: Failed to get recording controller" << std::endl;
return;
}
// Check if we can start recording
SDKError canStart = recordingCtrl->CanStartRecording(false, 0);
if (canStart != SDKERR_SUCCESS) {
std::cerr << "[VIDEO] Cannot start recording: " << canStart << std::endl;
return;
}
// Start raw recording (this enables raw data capture)
SDKError err = recordingCtrl->StartRawRecording();
if (err != SDKERR_SUCCESS) {
std::cerr << "[VIDEO] StartRawRecording failed: " << err << std::endl;
return;
}
std::cout << "[VIDEO] Raw recording started!" << std::endl;
// Wait a moment for recording to initialize
std::this_thread::sleep_for(std::chrono::milliseconds(500));
// Continue to Step 3...
}Important notes:
StartRawRecording() does NOT create a recording file on diskSDKERR_WRONG_USAGE, you may lack permissionsAlternative: Use Raw Streaming instead:
// If you have streaming permission instead of recording permission
IMeetingLiveStreamController* streamCtrl = meetingService->GetMeetingLiveStreamController();
SDKError err = streamCtrl->StartRawLiveStream();
if (err != SDKERR_SUCCESS) {
std::cerr << "[VIDEO] StartRawLiveStream failed: " << err << std::endl;
return;
}
std::cout << "[VIDEO] Raw streaming started!" << std::endl;
// Same subscription process applies after thisRequesting permission at runtime:
// If you're not the host, request permission
IMeetingLiveStreamController* streamCtrl = meetingService->GetMeetingLiveStreamController();
streamCtrl->RequestRawLiveStream(L"My AI Bot", L"https://example.com/description");
// Listen for onRawLiveStreamPrivilegeChanged callback to know when grantedYou need to know which user’s video you want to capture:
// Get participants controller
IMeetingParticipantsController* participantsCtrl =
meetingService->GetMeetingParticipantsController();
if (!participantsCtrl) {
std::cerr << "[VIDEO] Failed to get participants controller" << std::endl;
return;
}
// Get list of all participants
IList<unsigned int>* participantList = participantsCtrl->GetParticipantsList();
if (!participantList || participantList->GetCount() == 0) {
std::cerr << "[VIDEO] No participants in meeting" << std::endl;
return;
}
// Get first participant's user ID
uint32_t userId = participantList->GetItem(0);
std::cout << "[VIDEO] Found participant: " << userId << std::endl;
// You can also get the user's name:
IUserInfo* userInfo = participantsCtrl->GetUserByUserID(userId);
if (userInfo) {
const wchar_t* userName = userInfo->GetUserName();
std::wcout << L"[VIDEO] User name: " << userName << std::endl;
}How to capture specific users:
participantList and subscribe to eachIMeetingVideoController()->GetActiveVideoUserID()IMeetingParticipantsController()->GetMySelfUser()->GetUserID()GetUserName()This class receives the video frames:
ZoomSDKRendererDelegate.h:
#pragma once
#include <windows.h>
#include <cstdint>
#include <rawdata/rawdata_renderer_interface.h>
#include <zoom_sdk_raw_data_def.h>
#include <fstream>
#include <iostream>
using namespace ZOOM_SDK_NAMESPACE;
class ZoomSDKRendererDelegate : public IZoomSDKRendererDelegate {
public:
ZoomSDKRendererDelegate();
~ZoomSDKRendererDelegate();
// Called when a new video frame arrives
void onRawDataFrameReceived(YUVRawDataI420* data) override;
// Called when raw data status changes
void onRawDataStatusChanged(RawDataStatus status) override;
// Called before renderer is destroyed
void onRendererBeDestroyed() override;
private:
void SaveToRawYUVFile(YUVRawDataI420* data);
int frameCount;
};ZoomSDKRendererDelegate.cpp:
#include "ZoomSDKRendererDelegate.h"
ZoomSDKRendererDelegate::ZoomSDKRendererDelegate() : frameCount(0) {
std::cout << "[VIDEO] Renderer delegate created" << std::endl;
}
ZoomSDKRendererDelegate::~ZoomSDKRendererDelegate() {
std::cout << "[VIDEO] Renderer destroyed. Total frames: " << frameCount << std::endl;
}
void ZoomSDKRendererDelegate::onRawDataFrameReceived(YUVRawDataI420* data) {
if (!data) return;
// Get frame dimensions
int width = data->GetStreamWidth();
int height = data->GetStreamHeight();
// Get rotation (0, 90, 180, 270 degrees)
int rotation = data->GetRotation();
// Log every 30 frames (every ~1 second at 30fps)
if (frameCount % 30 == 0) {
std::cout << "[VIDEO] Frame " << frameCount
<< " - " << width << "x" << height
<< " (rotation: " << rotation << "°)" << std::endl;
}
// Process the frame (save to file, encode, analyze, etc.)
SaveToRawYUVFile(data);
frameCount++;
}
void ZoomSDKRendererDelegate::onRawDataStatusChanged(RawDataStatus status) {
switch (status) {
case RawData_On:
std::cout << "[VIDEO] Raw data ON" << std::endl;
break;
case RawData_Off:
std::cout << "[VIDEO] Raw data OFF" << std::endl;
break;
default:
std::cout << "[VIDEO] Status: " << status << std::endl;
break;
}
}
void ZoomSDKRendererDelegate::onRendererBeDestroyed() {
std::cout << "[VIDEO] Renderer being destroyed" << std::endl;
}
void ZoomSDKRendererDelegate::SaveToRawYUVFile(YUVRawDataI420* data) {
// Open output file in append binary mode
std::ofstream outputFile("output.yuv", std::ios::out | std::ios::binary | std::ios::app);
if (!outputFile.is_open()) {
std::cerr << "[VIDEO] Error opening output.yuv" << std::endl;
return;
}
// YUV420 format: Y plane + U plane + V plane
size_t ySize = data->GetStreamWidth() * data->GetStreamHeight();
size_t uvSize = ySize / 4; // U and V are quarter size each
// Write planes in order: Y, U, V
outputFile.write(data->GetYBuffer(), ySize);
outputFile.write(data->GetUBuffer(), uvSize);
outputFile.write(data->GetVBuffer(), uvSize);
outputFile.close();
}// Create renderer helper and delegate
IZoomSDKVideoSource* videoHelper = nullptr;
ZoomSDKRendererDelegate* videoDelegate = new ZoomSDKRendererDelegate();
SDKError err = createRenderer(&videoHelper, videoDelegate);
if (err != SDKERR_SUCCESS || !videoHelper) {
std::cerr << "[VIDEO] Failed to create renderer: " << err << std::endl;
delete videoDelegate;
return;
}
// Set desired resolution (affects CPU/bandwidth usage)
videoHelper->setRawDataResolution(ZoomSDKResolution_720P);
// Other options: ZoomSDKResolution_90P, 180P, 360P, 720P, 1080P
// Subscribe to user's video stream
err = videoHelper->subscribe(userId, RAW_DATA_TYPE_VIDEO);
if (err != SDKERR_SUCCESS) {
std::cerr << "[VIDEO] Subscribe failed: " << err << std::endl;
return;
}
std::cout << "[VIDEO] Successfully subscribed! Frames will arrive in onRawDataFrameReceived()" << std::endl;Important notes:
createRenderer() (global function), not CreateRenderer() or newonRawDataFrameReceived()YUV420 separates image into:
For a 1920x1080 frame:
Total bytes: 1920 * 1080 * 1.5 = 3,110,400 bytes
Y plane: [0 to 2,073,599] (1920 * 1080 = 2,073,600 bytes)
U plane: [2,073,600 to 2,592,639] (960 * 540 = 518,400 bytes)
V plane: [2,592,640 to 3,110,399] (960 * 540 = 518,400 bytes)Formula:
size_t ySize = width * height;
size_t uSize = (width / 2) * (height / 2) = width * height / 4;
size_t vSize = (width / 2) * (height / 2) = width * height / 4;
size_t totalSize = ySize + uSize + vSize = width * height * 1.5;void ProcessFrame(YUVRawDataI420* data) {
int width = data->GetStreamWidth(); // e.g., 1920
int height = data->GetStreamHeight(); // e.g., 1080
// Get pointers to each plane
const char* yBuffer = data->GetYBuffer(); // Brightness
const char* uBuffer = data->GetUBuffer(); // Blue-difference
const char* vBuffer = data->GetVBuffer(); // Red-difference
// Calculate sizes
size_t ySize = width * height;
size_t uvSize = (width / 2) * (height / 2);
// Example: Access pixel at (x, y)
int x = 100, y = 50;
// Y value (full resolution)
unsigned char yValue = yBuffer[y * width + x];
// U and V values (subsampled 2x2)
unsigned char uValue = uBuffer[(y/2) * (width/2) + (x/2)];
unsigned char vValue = vBuffer[(y/2) * (width/2) + (x/2)];
std::cout << "Pixel (" << x << "," << y << "): "
<< "Y=" << (int)yValue << " "
<< "U=" << (int)uValue << " "
<< "V=" << (int)vValue << std::endl;
}void YUVtoRGB(unsigned char y, unsigned char u, unsigned char v,
unsigned char& r, unsigned char& g, unsigned char& b) {
int c = y - 16;
int d = u - 128;
int e = v - 128;
int red = (298 * c + 409 * e + 128) >> 8;
int green = (298 * c - 100 * d - 208 * e + 128) >> 8;
int blue = (298 * c + 516 * d + 128) >> 8;
// Clamp to [0, 255]
r = (red < 0) ? 0 : (red > 255) ? 255 : red;
g = (green < 0) ? 0 : (green > 255) ? 255 : green;
b = (blue < 0) ? 0 : (blue > 255) ? 255 : blue;
}void SaveToRawYUVFile(YUVRawDataI420* data) {
std::ofstream file("output.yuv", std::ios::binary | std::ios::app);
size_t ySize = data->GetStreamWidth() * data->GetStreamHeight();
size_t uvSize = ySize / 4;
file.write(data->GetYBuffer(), ySize);
file.write(data->GetUBuffer(), uvSize);
file.write(data->GetVBuffer(), uvSize);
file.close();
}Play back with ffplay:
ffplay -f rawvideo -pixel_format yuv420p -video_size 1920x1080 -framerate 30 output.yuv#include <opencv2/opencv.hpp>
void SaveAsPNG(YUVRawDataI420* data, int frameNumber) {
int width = data->GetStreamWidth();
int height = data->GetStreamHeight();
// Create YUV Mat
cv::Mat yuvImage(height + height/2, width, CV_8UC1);
// Copy Y plane
memcpy(yuvImage.data, data->GetYBuffer(), width * height);
// Copy U plane
memcpy(yuvImage.data + width * height, data->GetUBuffer(), width * height / 4);
// Copy V plane
memcpy(yuvImage.data + width * height * 5/4, data->GetVBuffer(), width * height / 4);
// Convert to BGR
cv::Mat bgrImage;
cv::cvtColor(yuvImage, bgrImage, cv::COLOR_YUV2BGR_I420);
// Save as PNG
std::string filename = "frame_" + std::to_string(frameNumber) + ".png";
cv::imwrite(filename, bgrImage);
}Video streams may be rotated (0°, 90°, 180°, 270°):
void ProcessFrame(YUVRawDataI420* data) {
int rotation = data->GetRotation(); // 0, 90, 180, or 270
if (rotation == 0) {
// Normal orientation, no rotation needed
SaveFrame(data);
} else {
// Need to rotate frame before displaying/processing
RotateFrame(data, rotation);
SaveFrame(data);
}
}OpenCV rotation:
cv::Mat RotateImage(cv::Mat& image, int rotation) {
cv::Mat rotated;
switch (rotation) {
case 90:
cv::rotate(image, rotated, cv::ROTATE_90_CLOCKWISE);
break;
case 180:
cv::rotate(image, rotated, cv::ROTATE_180);
break;
case 270:
cv::rotate(image, rotated, cv::ROTATE_90_COUNTERCLOCKWISE);
break;
default:
rotated = image;
break;
}
return rotated;
}Video typically arrives at:
Process frames quickly to avoid dropping:
void onRawDataFrameReceived(YUVRawDataI420* data) {
// Fast: Just save to buffer/queue
frameQueue.push(data); // Process in separate thread
// Slow: Heavy processing here
ConvertToRGB(data); // May drop frames!
EncodeToH264(data);
SaveToDisk(data);
}Per frame:
At 30 fps:
Use buffering and separate processing threads for high performance.
// Lower resolution = less CPU/bandwidth
videoHelper->setRawDataResolution(ZoomSDKResolution_360P); // Good for analysis
// Higher resolution = better quality
videoHelper->setRawDataResolution(ZoomSDKResolution_1080P); // Good for recording// Unsubscribe from video stream
if (videoHelper) {
videoHelper->unSubscribe(userId, RAW_DATA_TYPE_VIDEO);
}
// Stop raw recording
if (recordingCtrl) {
recordingCtrl->StopRawRecording();
}// Renderer delegate is automatically deleted by SDK
// Don't call delete on videoDelegate yourself!
// Just null out the pointer
videoHelper = nullptr;
videoDelegate = nullptr;Audio is available in PCM format through separate callbacks:
class ZoomAudioRawDataDelegate : public IZoomSDKAudioRawDataDelegate {
public:
// Called for each participant's audio (no support for telephone participants)
void onOneWayAudioRawDataReceived(AudioRawData* data_, uint32_t node_id) override {
// Process individual participant's audio
std::cout << "[AUDIO] Received audio from user: " << node_id << std::endl;
}
// Called for mixed meeting audio (what all participants hear)
void onMixedAudioRawDataReceived(AudioRawData* data_) override {
// Process mixed audio from all participants
SavePCMData(data_);
}
};
// Subscribe to audio
IZoomSDKAudioRawDataHelper* audioHelper = GetAudioRawdataHelper();
ZoomAudioRawDataDelegate* audioDelegate = new ZoomAudioRawDataDelegate();
audioHelper->subscribe(audioDelegate);Two audio callbacks:
onOneWayAudioRawDataReceived: Individual participant audio (excludes phone participants)onMixedAudioRawDataReceived: Combined meeting audio (what you’d hear in the meeting)You can also capture screen share data (separate from video):
// Subscribe to share data instead of video
videoHelper->subscribe(userId, RAW_DATA_TYPE_SHARE);The same IZoomSDKRendererDelegate::onRawDataFrameReceived() callback is used, but you’ll receive share content instead of camera video.
Meeting hosts with a raw streaming token can enable alpha channel mode, which provides a mask to remove participant backgrounds. This is useful for rendering meeting participants in custom virtual environments.
IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController();
// Check if alpha channel mode can be enabled
if (videoCtrl->CanEnableAlphaChannelMode()) {
// Enable alpha channel mode
SDKError err = videoCtrl->EnableAlphaChannelMode(true);
if (err == SDKERR_SUCCESS) {
std::cout << "[ALPHA] Alpha channel mode enabled" << std::endl;
}
}
// Check if currently enabled
bool isEnabled = videoCtrl->IsAlphaChannelModeEnabled();
std::cout << "[ALPHA] Alpha mode active: " << (isEnabled ? "yes" : "no") << std::endl;Implement IMeetingVideoCtrlEvent callback:
class VideoCtrlEventListener : public IMeetingVideoCtrlEvent {
public:
void onVideoAlphaChannelStatusChanged(bool isAlphaModeOn) override {
std::cout << "[ALPHA] Alpha channel mode: "
<< (isAlphaModeOn ? "ON" : "OFF") << std::endl;
}
// ... other IMeetingVideoCtrlEvent methods
};When alpha channel mode is enabled, YUVRawDataI420 has additional methods:
void onRawDataFrameReceived(YUVRawDataI420* data) override {
// Get standard YUV data
char* yBuffer = data->GetYBuffer();
char* uBuffer = data->GetUBuffer();
char* vBuffer = data->GetVBuffer();
// Get alpha mask (only available when alpha mode is enabled)
char* alphaBuffer = data->GetAlphaBuffer();
unsigned int alphaLen = data->GetAlphaBufferLen();
if (alphaBuffer != nullptr && alphaLen > 0) {
// Alpha mask is available!
// Use it to remove background from video frame
ProcessWithAlphaMask(data, alphaBuffer, alphaLen);
} else {
// No alpha data (alpha mode not enabled or not available)
ProcessNormalFrame(data);
}
}The alpha buffer contains a grayscale mask where:
From official documentation:
Do not perform heavy operations from within the raw data callbacks. This may cause delayed or lost data.
Recommended pattern:
void onRawDataFrameReceived(YUVRawDataI420* data) {
// Fast: Copy to queue and return immediately
Frame copy;
copy.width = data->GetStreamWidth();
copy.height = data->GetStreamHeight();
memcpy(copy.buffer, data->GetYBuffer(), data->GetBufferLen());
frameQueue.enqueue(copy); // Let separate thread process
// DON'T: Heavy processing here
// EncodeH264(data); // Blocks callback!
// SaveToNetwork(data); // Too slow!
}Checklist:
StartRawRecording() or StartRawLiveStream() BEFORE subscribingRAW_DATA_TYPE_VIDEO)onRawDataFrameReceived() correctlyTest: Subscribe to your own video stream first (easier to control)
Checklist:
width * height * 1.5 bytesTest: Save raw YUV and play with ffplay to verify format
Solutions:
setRawDataResolution(ZoomSDKResolution_360P)See the full working implementation in:
C:\tempsdk\zoom-windows-sdk-sample\src\
├── ZoomSDKRendererDelegate.h
├── ZoomSDKRendererDelegate.cpp
└── main.cpp (OnInMeeting function)Key files to reference:
OnInMeeting()Last Updated: Based on Zoom Windows Meeting SDK v6.7.2.26830
This file