Generated banner for Zoom Video SDK Windows
Generated logo for Zoom Video SDK Windows

Zoom Video SDK Windows

OpenAIOfficialCreated by ZoomDistributed by OpenAI

  • Skill
  • Tool integration
  • Software development
  • MIT
  • No issues found

From the author

Zoom Video SDK for Windows - C++ integration for video sessions, raw audio/video capture, screen sharing, recording, and real-time communication
— the description by Zoom

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The Zoom Video SDK for Windows provides a C++ library for building custom video applications on Windows platforms. It supports session management, raw audio and video capture or injection, screen sharing, cloud recording, and live transcription. The SDK offers two primary rendering approaches: the Canvas API for direct window rendering and a Raw Data Pipe for custom frame processing. It includes support for C# via a C++/CLI wrapper and requires specific implementation of Windows message loops and delegate callbacks to function correctly.

What it does

  • Enables joining and managing Zoom video sessions
  • Provides access to raw YUV420 video and PCM audio frames
  • Supports custom audio and video injection into sessions
  • Facilitates screen sharing and collaborative whiteboard features
  • Allows for cloud recording and RTMP live streaming
  • Integrates with .NET applications using C++/CLI

When to use it

  • When building custom video conferencing interfaces on Windows
  • When requiring raw access to audio or video streams for processing
  • When integrating Zoom functionality into existing C++ or C# desktop applications
  • When implementing custom screen sharing or virtual camera sources

Advantages

  • Direct integration with Windows HWND for video rendering
  • Flexible rendering options via Canvas API or Raw Data Pipe
  • Comprehensive support for session lifecycle and user events
  • Native C++ performance with C# interoperability

Usage examples

“How do I join a Zoom session in my C++ Windows application?”
Initialize the SDK object, add a delegate listener, and call joinSession with your session context, ensuring a Windows message pump is running to process callbacks.
“How can I render video in my application?”
Use the Canvas API to subscribe to a user's video directly to an HWND, or use the Raw Data Pipe to receive YUV420 frames for custom processing and rendering.
“Why are my SDK callbacks not firing?”
Ensure your application has a Windows message pump (using PeekMessage, TranslateMessage, and DispatchMessage) in the main loop to allow the SDK to deliver events.
“How do I handle remote user video subscriptions?”
Implement the onUserJoin and onUserVideoStatusChanged delegate methods to manually subscribe to remote users using their video canvas.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/openai/zoom-video-sdk-windows/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip zoom-video-sdk-windows.zip -d .agents/skills

Documentation by Zoom

Zoom Video SDK - Windows Development

Expert guidance for developing with the Zoom Video SDK on Windows. This SDK enables custom video applications, raw media capture/injection, cloud recording, live streaming, and real-time transcription on Windows platforms.

Official Documentation: https://developers.zoom.us/docs/video-sdk/windows/ API Reference: https://marketplacefront.zoom.us/sdk/custom/windows/ Sample Repository: https://github.com/zoom/videosdk-windows-rawdata-sample

New to Video SDK? Follow this path:

  1. SDK Architecture Pattern - Universal 3-step pattern for ANY feature

  2. Session Join Pattern - Complete working code to join a session

  3. Windows Message Loop - CRITICAL: Fix callbacks not firing

  4. Video Rendering - Display video with Canvas API

Reference:

Having issues?

Building a Custom UI?

SDK Overview

The Zoom Video SDK for Windows is a C++ library that provides:

  • Session Management: Join/leave video SDK sessions

  • Raw Data Access: Capture raw audio/video frames (YUV420, PCM)

  • Raw Data Injection: Send custom audio/video into sessions

  • Screen Sharing: Share screens or inject custom share sources

  • Cloud Recording: Record sessions to Zoom cloud

  • Live Streaming: Stream to RTMP endpoints (YouTube, etc.)

  • Chat & Commands: In-session messaging and command channels

  • Live Transcription: Real-time speech-to-text

  • Subsessions: Breakout room support

  • Whiteboard: Collaborative whiteboard features

  • Annotations: Screen share annotations

  • C# Integration: C++/CLI wrapper for .NET applications

Prerequisites

System Requirements
  • OS: Windows 10 (1903 or later) or Windows 11

  • Architecture: x64 (recommended), x86, or ARM64

  • Visual Studio: 2019 or 2022 (Community, Professional, or Enterprise)

  • Windows SDK: 10.0.19041.0 or later

  • .NET Framework: 4.8 or later (for C# applications)

Visual Studio Workloads

Install these workloads via Visual Studio Installer:

  1. Desktop development with C++

    • MSVC v142 or v143 compiler

    • Windows 10/11 SDK

    • C++ CMake tools (optional)

  2. .NET desktop development (for C# applications)

    • .NET Framework 4.8 targeting pack

    • C++/CLI support

Quick Start

C++ Application
#include <windows.h>
#include "zoom_video_sdk_api.h"
#include "zoom_video_sdk_interface.h"
#include "zoom_video_sdk_delegate_interface.h"

USING_ZOOM_VIDEO_SDK_NAMESPACE

// 1. Create SDK object
IZoomVideoSDK* video_sdk_obj = CreateZoomVideoSDKObj();

// 2. Initialize
ZoomVideoSDKInitParams init_params;
init_params.domain = L"https://zoom.us";
init_params.enableLog = true;
init_params.logFilePrefix = L"zoom_win_video";
init_params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
init_params.shareRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
init_params.audioRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;

ZoomVideoSDKErrors err = video_sdk_obj->initialize(init_params);

// 3. Add event listener
video_sdk_obj->addListener(myDelegate);

// 4. Join session (IMPORTANT: set audioOption.connect = false)
ZoomVideoSDKSessionContext session_context;
session_context.sessionName = L"my-session";
session_context.userName = L"Windows User";
session_context.token = L"your-jwt-token";
session_context.videoOption.localVideoOn = false;
session_context.audioOption.connect = false;  // Connect audio after join
session_context.audioOption.mute = true;

IZoomVideoSDKSession* session = video_sdk_obj->joinSession(session_context);

// 5. CRITICAL: Add Windows message pump for callbacks to work
bool running = true;
while (running) {
    // Process Windows messages (required for SDK callbacks)
    MSG msg;
    while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    // Your application logic here
    Sleep(10);
}
C# Application
using ZoomVideoSDK;

var sdkManager = new ZoomSDKManager();
sdkManager.Initialize();
sdkManager.JoinSession("my-session", "jwt-token", "User Name", "");

Key Features

FeatureDescription
Session ManagementJoin, leave, and manage video sessions
Raw Video (YUV I420)Capture and inject raw video frames
Raw Audio (PCM)Capture and inject raw audio data
Screen SharingShare screens or custom content
Cloud RecordingRecord sessions to Zoom cloud
Live StreamingStream to RTMP endpoints
ChatSend/receive chat messages
Command ChannelCustom command messaging
Live TranscriptionReal-time speech-to-text
C# SupportFull .NET Framework integration

Sample Applications

Official Repository: https://github.com/zoom/videosdk-windows-rawdata-sample

SampleDescription
VSDK_SkeletonDemoMinimal session join - start here
VSDK_getRawVideoCapture YUV420 video frames
VSDK_getRawAudioCapture PCM audio
VSDK_sendRawVideoInject custom video (virtual camera)
VSDK_sendRawAudioInject custom audio (virtual mic)
VSDK_CloudRecordingCloud recording control
VSDK_CommandChannelCustom command messaging
VSDK_TranscriptionAndTranslationLive captions

See complete guide: Sample Applications Reference

Critical Gotchas and Best Practices

⚠️ CRITICAL: Windows Message Pump Required

The #1 issue that causes session joins to hang with no callbacks:

All Windows applications using the Zoom SDK MUST process Windows messages. The SDK uses Windows messages to deliver callbacks like onSessionJoin(), onError(), etc.

Problem: Without a message pump, joinSession() appears to succeed but callbacks never fire.

Solution: Add this to your main loop:

while (running) {
    // REQUIRED: Process Windows messages
    MSG msg;
    while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    // Your application logic
    Sleep(10);
}

Applies to:

  • Console applications (no automatic message pump)

  • Custom main loops

  • Applications that don't use standard WinMain/WndProc

GUI applications using WinMain with standard message loop already have this.

Audio Connection Strategy

Best Practice: Set audioOption.connect = false when joining, then connect audio in the onSessionJoin() callback.

// During join
session_context.audioOption.connect = false;  // Don't connect yet
session_context.audioOption.mute = true;

// In onSessionJoin() callback
void onSessionJoin() override {
    IZoomVideoSDKAudioHelper* audioHelper = video_sdk_obj->getAudioHelper();
    if (audioHelper) {
        audioHelper->startAudio();  // Connect now
    }
}

Why: This pattern is used in all official Zoom samples. It separates session join from audio initialization for better reliability and error handling.

All Delegate Callbacks Must Be Implemented

The IZoomVideoSDKDelegate interface has 70+ pure virtual methods. ALL must be implemented, even if empty:

// Required even if you don't use them
void onProxyDetectComplete() override {}
void onUserWhiteboardShareStatusChanged(IZoomVideoSDKUser*, IZoomVideoSDKWhiteboardHelper*) override {}
// ... etc

Tip: Check the SDK version's zoom_video_sdk_delegate_interface.h for the complete list. The interface changes between SDK versions.

Memory Mode for Raw Data

Always use heap mode for raw data memory:

init_params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
init_params.shareRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
init_params.audioRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;

Stack mode can cause issues with large video frames.

Thread Safety

SDK callbacks execute on SDK threads, not your main thread:

  • Don't perform heavy operations in callbacks

  • Don't call cleanup() from within callbacks

  • Use thread-safe queues for passing data to UI thread

  • Use mutexes when accessing shared state

Consult Official Samples First

When SDK behavior is unexpected, always check the official samples before troubleshooting:

Local samples:

  • C:\tempsdk\Zoom_VideoSDK_Windows_RawDataDemos\VSDK_SkeletonDemo\ (simplest)

  • C:\tempsdk\sdksamples\zoom-video-sdk-windows-2.4.12\Sample-Libs\x64\demo\

Official samples show correct patterns for:

  • Message pump implementation ✓

  • Audio connection strategy ✓

  • Error handling ✓

  • Memory management ✓

Video Rendering - Two Approaches

The Zoom SDK provides two different ways to render video. Choose based on your needs.

Best for: Standard applications, clean video quality, ease of implementation

The SDK renders video directly to your HWND. No YUV conversion needed.

// Subscribe to a user's video with Canvas API
IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas();
if (canvas) {
    ZoomVideoSDKErrors ret = canvas->subscribeWithView(
        hwnd,                                    // Your window handle
        ZoomVideoSDKVideoAspect_PanAndScan,     // Fit to window, may crop
        ZoomVideoSDKResolution_Auto              // Let SDK choose best resolution
    );

    if (ret == ZoomVideoSDKErrors_Success) {
        // SDK is now rendering directly to your window!
    }
}

// Unsubscribe when done
canvas->unSubscribeWithView(hwnd);

Advantages:

  • ✅ Best quality - SDK uses optimized, hardware-accelerated rendering

  • ✅ No artifacts - Professional video quality

  • ✅ Simple code - 3 lines to subscribe

  • ✅ Better performance - No CPU-intensive YUV conversion

  • ✅ Automatic scaling - SDK handles window resizing

  • ✅ Aspect ratio - Built-in aspect ratio handling

Example from official .NET sample:

// Self video preview
IZoomVideoSDKCanvas* canvas = myself->GetVideoCanvas();
canvas->subscribeWithView(selfVideoHwnd, aspect, resolution);

// Remote user video
IZoomVideoSDKCanvas* remoteCanvas = remoteUser->GetVideoCanvas();
remoteCanvas->subscribeWithView(remoteVideoHwnd, aspect, resolution);

Video Aspect Options:

  • ZoomVideoSDKVideoAspect_Original - Letterbox/pillarbox, no cropping

  • ZoomVideoSDKVideoAspect_FullFilled - Fill window, may crop edges

  • ZoomVideoSDKVideoAspect_PanAndScan - Smart crop to fill window

  • ZoomVideoSDKVideoAspect_LetterBox - Show full video with black bars

Resolution Options:

  • ZoomVideoSDKResolution_90P

  • ZoomVideoSDKResolution_180P

  • ZoomVideoSDKResolution_360P - Good balance

  • ZoomVideoSDKResolution_720P - HD quality

  • ZoomVideoSDKResolution_1080P

  • ZoomVideoSDKResolution_Auto - Let SDK decide (recommended)

🔧 Raw Data Pipe (Advanced Use Cases)

Best for: Custom video processing, effects, recording, computer vision

You receive raw YUV420 frames and handle rendering yourself.

// 1. Create a delegate to receive frames
class VideoRenderer : public IZoomVideoSDKRawDataPipeDelegate {
public:
    void onRawDataFrameReceived(YUVRawDataI420* data) override {
        int width = data->GetStreamWidth();
        int height = data->GetStreamHeight();

        char* yBuffer = data->GetYBuffer();
        char* uBuffer = data->GetUBuffer();
        char* vBuffer = data->GetVBuffer();

        // Convert YUV420 to RGB and render
        ConvertYUVToRGB(yBuffer, uBuffer, vBuffer, width, height);
        RenderToWindow(rgbBuffer, width, height);
    }

    void onRawDataStatusChanged(RawDataStatus status) override {
        // Handle video on/off
    }
};

// 2. Subscribe to raw data
IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe();
VideoRenderer* renderer = new VideoRenderer();
pipe->subscribe(ZoomVideoSDKResolution_720P, renderer);

YUV420 to RGB Conversion (ITU-R BT.601):

void ConvertYUV420ToRGB(char* yBuffer, char* uBuffer, char* vBuffer,
                        int width, int height) {
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            int yIndex = y * width + x;
            int uvIndex = (y / 2) * (width / 2) + (x / 2);

            int Y = (unsigned char)yBuffer[yIndex];
            int U = (unsigned char)uBuffer[uvIndex];
            int V = (unsigned char)vBuffer[uvIndex];

            // YUV to RGB conversion
            int C = Y - 16;
            int D = U - 128;
            int E = V - 128;

            int R = (298 * C + 409 * E + 128) >> 8;
            int G = (298 * C - 100 * D - 208 * E + 128) >> 8;
            int B = (298 * C + 516 * D + 128) >> 8;

            // Clamp to [0, 255]
            R = (R < 0) ? 0 : (R > 255) ? 255 : R;
            G = (G < 0) ? 0 : (G > 255) ? 255 : G;
            B = (B < 0) ? 0 : (B > 255) ? 255 : B;

            // Store RGB (BGR format for Windows)
            rgbBuffer[yIndex * 3 + 0] = (unsigned char)B;
            rgbBuffer[yIndex * 3 + 1] = (unsigned char)G;
            rgbBuffer[yIndex * 3 + 2] = (unsigned char)R;
        }
    }
}

Render with GDI:

void RenderToWindow(unsigned char* rgbBuffer, int width, int height) {
    HDC hdc = GetDC(hwnd);

    BITMAPINFO bmi = {};
    bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmi.bmiHeader.biWidth = width;
    bmi.bmiHeader.biHeight = -height;  // Negative for top-down
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 24;     // 24-bit RGB
    bmi.bmiHeader.biCompression = BI_RGB;

    RECT rect;
    GetClientRect(hwnd, &rect);

    StretchDIBits(hdc,
        0, 0, rect.right, rect.bottom,  // Destination
        0, 0, width, height,              // Source
        rgbBuffer, &bmi,
        DIB_RGB_COLORS, SRCCOPY);

    ReleaseDC(hwnd, hdc);
}

Disadvantages:

  • ⚠️ CPU intensive - YUV conversion can cause frame drops

  • ⚠️ Artifacts - Manual rendering may show tearing/artifacts

  • ⚠️ Complex - More code to maintain

  • ⚠️ Performance - Slower than Canvas API

Use Raw Data When:

  • Adding video filters/effects

  • Recording to custom formats

  • Computer vision processing

  • Custom compositing

  • Streaming to non-standard outputs

Self Video vs Remote Users

Self Video (your own camera):

Option A: Canvas API

IZoomVideoSDKSession* session = sdk->getSessionInfo();
IZoomVideoSDKUser* myself = session->getMyself();
IZoomVideoSDKCanvas* canvas = myself->GetVideoCanvas();
canvas->subscribeWithView(selfVideoHwnd, aspect, resolution);

Option B: Video Preview (for self only)

IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper();
videoHelper->startVideo();  // Start transmission

// For preview rendering
videoHelper->startVideoCanvasPreview(selfVideoHwnd, aspect, resolution);

Remote Users (other participants):

Canvas API (recommended):

// In onUserJoin callback
void onUserJoin(IZoomVideoSDKUserHelper*, IVideoSDKVector<IZoomVideoSDKUser*>* userList) {
    for (int i = 0; i < userList->GetCount(); i++) {
        IZoomVideoSDKUser* user = userList->GetItem(i);
        IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas();
        canvas->subscribeWithView(userVideoHwnd, aspect, resolution);
    }
}
Event-Driven Subscription Pattern

⚠️ CRITICAL: Video subscription must be event-driven and manual.

Key Events:

  1. onSessionJoin - Subscribe to self video

  2. onUserJoin - Subscribe to new remote users

  3. onUserVideoStatusChanged - Re-subscribe when video turns on/off

  4. onUserLeave - Unsubscribe and cleanup

Complete Pattern:

class MainFrame : public IZoomVideoSDKDelegate {
private:
    std::map<IZoomVideoSDKUser*, IZoomVideoSDKCanvas*> subscribedUsers_;
    HWND videoWindow_;

public:
    void onSessionJoin() override {
        // Start your own video
        IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper();
        videoHelper->startVideo();

        // Subscribe to self video
        IZoomVideoSDKUser* myself = sdk->getSessionInfo()->getMyself();
        SubscribeToUser(myself);
    }

    void onUserJoin(IZoomVideoSDKUserHelper*,
                    IVideoSDKVector<IZoomVideoSDKUser*>* userList) override {
        // Get current user to exclude self
        IZoomVideoSDKUser* myself = sdk->getSessionInfo()->getMyself();

        for (int i = 0; i < userList->GetCount(); i++) {
            IZoomVideoSDKUser* user = userList->GetItem(i);

            // IMPORTANT: Only subscribe to REMOTE users!
            if (user != myself) {
                SubscribeToUser(user);
            }
        }
    }

    void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper*,
                                  IVideoSDKVector<IZoomVideoSDKUser*>* userList) override {
        IZoomVideoSDKUser* myself = sdk->getSessionInfo()->getMyself();

        for (int i = 0; i < userList->GetCount(); i++) {
            IZoomVideoSDKUser* user = userList->GetItem(i);
            if (user != myself) {
                // Re-subscribe when video status changes
                SubscribeToUser(user);
            }
        }
    }

    void onUserLeave(IZoomVideoSDKUserHelper*,
                    IVideoSDKVector<IZoomVideoSDKUser*>* userList) override {
        for (int i = 0; i < userList->GetCount(); i++) {
            IZoomVideoSDKUser* user = userList->GetItem(i);
            UnsubscribeFromUser(user);
        }
    }

    void onSessionLeave() override {
        // Cleanup all subscriptions
        for (auto& pair : subscribedUsers_) {
            IZoomVideoSDKCanvas* canvas = pair.second;
            if (canvas) {
                canvas->unSubscribeWithView(videoWindow_);
            }
        }
        subscribedUsers_.clear();
    }

private:
    void SubscribeToUser(IZoomVideoSDKUser* user) {
        if (!user || subscribedUsers_.find(user) != subscribedUsers_.end())
            return;

        IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas();
        if (canvas) {
            ZoomVideoSDKErrors ret = canvas->subscribeWithView(
                videoWindow_,
                ZoomVideoSDKVideoAspect_PanAndScan,
                ZoomVideoSDKResolution_Auto
            );

            if (ret == ZoomVideoSDKErrors_Success) {
                subscribedUsers_[user] = canvas;
            }
        }
    }

    void UnsubscribeFromUser(IZoomVideoSDKUser* user) {
        auto it = subscribedUsers_.find(user);
        if (it != subscribedUsers_.end()) {
            IZoomVideoSDKCanvas* canvas = it->second;
            if (canvas) {
                canvas->unSubscribeWithView(videoWindow_);
            }
            subscribedUsers_.erase(it);
        }
    }
};

Key Points:

  • ✅ Subscribe in response to events (onUserJoin, onUserVideoStatusChanged)

  • ✅ Always exclude current user from remote subscriptions

  • ✅ Unsubscribe on onUserLeave

  • ✅ Clean up all subscriptions on onSessionLeave

  • ✅ Track subscriptions in a map for lifecycle management

⚠️ Screen Share Subscription (DIFFERENT from Video!)

CRITICAL: Screen share subscription uses IZoomVideoSDKShareAction from the callback, NOT user->GetShareCanvas()!

// WRONG - This won't work for remote screen shares!
user->GetShareCanvas()->subscribeWithView(hwnd, ...);

// CORRECT - Use IZoomVideoSDKShareAction from onUserShareStatusChanged callback
void onUserShareStatusChanged(IZoomVideoSDKShareHelper* pShareHelper,
                               IZoomVideoSDKUser* pUser,
                               IZoomVideoSDKShareAction* pShareAction) {
    if (!pShareAction) return;

    ZoomVideoSDKShareStatus status = pShareAction->getShareStatus();

    if (status == ZoomVideoSDKShareStatus_Start ||
        status == ZoomVideoSDKShareStatus_Resume) {
        // Subscribe to the share using Canvas API
        IZoomVideoSDKCanvas* shareCanvas = pShareAction->getShareCanvas();
        if (shareCanvas) {
            shareCanvas->subscribeWithView(shareWindow_,
                ZoomVideoSDKVideoAspect_Original);
        }
    }
    else if (status == ZoomVideoSDKShareStatus_Stop) {
        // Unsubscribe when share stops
        IZoomVideoSDKCanvas* shareCanvas = pShareAction->getShareCanvas();
        if (shareCanvas) {
            shareCanvas->unSubscribeWithView(shareWindow_);
        }
    }
}

Why is share different from video?

  • Video: Each user has one video stream → use user->GetVideoCanvas()

  • Share: A user can have multiple share actions (multi-share) → use IZoomVideoSDKShareAction* from callback

  • The IZoomVideoSDKShareAction object represents a specific share stream and contains the share status, type, and rendering interfaces

See also: Screen Share Subscription Example

Multi-User Video Layout

For multiple participants, you need one HWND per user:

// Create separate windows/panels for each user
HWND selfVideoWindow = CreateWindow(...);   // Your video
HWND user1Window = CreateWindow(...);       // User 1's video
HWND user2Window = CreateWindow(...);       // User 2's video

// Subscribe each user to their own window
myself->GetVideoCanvas()->subscribeWithView(selfVideoWindow, ...);
user1->GetVideoCanvas()->subscribeWithView(user1Window, ...);
user2->GetVideoCanvas()->subscribeWithView(user2Window, ...);

Layout Strategies:

  • Grid layout (2x2, 3x3)

  • Gallery view (scrollable)

  • Active speaker (large) + thumbnails

  • Picture-in-picture

Common Video Issues
IssueCauseSolution
Video not showingNot calling startVideo()Call videoHelper->startVideo() in onSessionJoin
Artifacts/tearingUsing Raw Data PipeSwitch to Canvas API
Poor performanceYUV conversion on UI threadUse Canvas API or move conversion to worker thread
Video freezesNot processing Windows messagesAdd message pump to main loop
Can't see selfSubscribing to wrong userUse session->getMyself() for self video
Seeing self in remote listNot excluding selfCheck if (user != myself) before subscribing

Complete Documentation Library

This skill includes comprehensive guides organized by category:

Core Concepts (Start Here!)
Complete Examples
UI Framework Integration
C++/CLI Wrapper Patterns (Wrapping ANY Native Library)
Troubleshooting
References
Most Critical Issues (From Real Debugging)
  1. Callbacks not firing → Missing Windows message loop (99% of issues)

  2. Video subscribe returns error 2 → Subscribing too early

  3. Abstract class errors → Missing virtual method implementations

Key Insight

Once you learn the 3-step pattern, you can implement ANY feature:

  1. Get singleton → 2. Implement delegate → 3. Subscribe & use

See: SDK Architecture Pattern

Resources


Need help? Start with SKILL.md for complete navigation.

Merged from video-sdk/windows/SKILL.md

Zoom Video SDK Windows - Complete Documentation Index

Quick Start Path

If you're new to the SDK, follow this order:

  1. Overview → windows.md

  2. Read the architecture pattern → concepts/sdk-architecture-pattern.md

    • Universal formula: Singleton → Delegate → Subscribe

    • Once you understand this, you can implement any feature

  3. Fix build errors → troubleshooting/build-errors.md

    • SDK header dependencies

    • Required include order

  4. Implement session join → examples/session-join-pattern.md

    • Complete working JWT + session join code

  5. Fix callback issues → troubleshooting/windows-message-loop.md

    • CRITICAL: Why callbacks don't fire without Windows message loop

  6. Implement video → examples/video-rendering.md

    • Canvas API (SDK-rendered) vs Raw Data Pipe

  7. Troubleshoot any issues → troubleshooting/common-issues.md

    • Quick diagnostic checklist

    • Error code tables


Documentation Structure

video-sdk/windows/
├── SKILL.md                           # Main skill overview
├── SKILL.md                           # This file - navigation guide
├── windows.md                          # Secondary overview doc (pointer-style)
│
├── concepts/                          # Core architectural patterns
│   ├── sdk-architecture-pattern.md   # Universal formula for ANY feature
│   ├── singleton-hierarchy.md        # 5-level navigation guide
│   └── canvas-vs-raw-data.md         # SDK-rendered vs self-rendered choice
│
├── examples/                          # Complete working code
│   ├── session-join-pattern.md       # JWT auth + session join
│   ├── video-rendering.md            # Canvas API video display
│   ├── screen-share-subscription.md  # View remote screen shares
│   ├── raw-video-capture.md          # YUV420 raw frame capture
│   ├── raw-audio-capture.md          # PCM audio capture
│   ├── send-raw-video.md             # Virtual camera (inject video)
│   ├── send-raw-audio.md             # Virtual mic (inject audio)
│   ├── cloud-recording.md            # Cloud recording control
│   ├── command-channel.md            # Custom command messaging
│   ├── transcription.md              # Live transcription/captions
│   └── dotnet-winforms/              # UI Framework integration
│       └── README.md                 # Win32, WinForms, WPF patterns
│                                     # C++/CLI wrapper patterns
│                                     # Production quality guidelines
│
├── troubleshooting/                   # Problem solving guides
│   ├── windows-message-loop.md       # CRITICAL - Why callbacks fail
│   ├── build-errors.md               # Header dependency fixes
│   └── common-issues.md              # Quick diagnostic workflow
│
└── references/                        # Reference documentation
    ├── windows-reference.md           # API hierarchy, methods, error codes
    ├── delegate-methods.md            # All 80+ callback methods
    └── samples.md                     # Official samples guide

By Use Case

I want to build a video app
  1. SDK Architecture Pattern - Understand the pattern

  2. Session Join Pattern - Join sessions

  3. Video Rendering - Display video

  4. Windows Message Loop - Fix callback issues

I'm getting build errors
  1. Build Errors Guide - SDK header dependencies

  2. Delegate Methods - Abstract class errors

  3. Common Issues - Linker errors

I'm getting runtime errors
  1. Windows Message Loop - Callbacks not firing

  2. Common Issues - Error code tables

I want to view screen shares
  1. Screen Share Subscription - DIFFERENT from video!

  2. SDK Architecture Pattern - Event-driven pattern

  3. Video Rendering - Compare with video subscription

I want to capture raw video/audio
  1. Canvas vs Raw Data - Choose your approach

  2. Raw Video Capture - YUV420 frame capture

  3. Raw Audio Capture - PCM audio capture

  4. SDK Architecture Pattern - Subscription pattern

I want to send custom video/audio (virtual camera/mic)
  1. Send Raw Video - Inject custom video frames

  2. Send Raw Audio - Inject custom audio

  3. SDK Architecture Pattern - External source pattern

I want to record sessions
  1. Cloud Recording - Start/stop cloud recording

  2. API Reference - Recording helper methods

I want to use live transcription
  1. Transcription - Enable live captions

  2. Delegate Methods - Transcription callbacks

I want custom messaging between participants
  1. Command Channel - Send custom commands

  2. API Reference - Command channel methods

I want to build a Win32 native app
  1. Win32 Integration - Direct SDK + Canvas API

  2. Video Rendering - Canvas API patterns

  3. Production Guidelines - Best practices

I want to build a WinForms (.NET) app
  1. WinForms Integration - C++/CLI wrapper + Raw Data

  2. C++/CLI Patterns - gcroot, Finalizer, LockBits

  3. Production Guidelines - IDisposable, thread safety

I want to build a WPF (.NET) app
  1. WPF Integration - C++/CLI + BitmapSource

  2. Bitmap Conversion - Freeze(), Dispatcher

  3. Production Guidelines - Performance optimization

I want to use C# / .NET Framework (general)
  1. .NET Integration Overview - Complete C++/CLI wrapper guide

  2. Raw Video Capture - YUV→RGB conversion patterns

  3. Session Join Pattern - SDK initialization flow

I want to wrap ANY native C++ library for .NET
  1. C++/CLI Wrapper Patterns - Complete 8-pattern guide

  2. Pattern 1: Basic Structure - Project setup + class layout

  3. Pattern 3: gcroot Callbacks - Native→Managed events

  4. Pattern 4: IDisposable - Cleanup pattern

  5. Common Errors - Troubleshooting

I want to implement a specific feature
  1. SDK Architecture Pattern - START HERE!

  2. Singleton Hierarchy - Navigate to the feature

  3. API Reference - Method signatures


Most Critical Documents

1. SDK Architecture Pattern (MASTER DOCUMENT)

concepts/sdk-architecture-pattern.md

The universal 3-step pattern:

  1. Get singleton (SDK, helpers, session, users)

  2. Implement delegate (event callbacks)

  3. Subscribe and use

2. Windows Message Loop (MOST COMMON ISSUE)

troubleshooting/windows-message-loop.md

99% of "callbacks not firing" issues are caused by missing Windows message loop.

3. Singleton Hierarchy (NAVIGATION MAP)

concepts/singleton-hierarchy.md

5-level deep navigation showing how to reach every feature.


Key Learnings

Critical Discoveries:
  1. Windows Message Loop is MANDATORY

  2. Subscribe in onUserVideoStatusChanged, NOT onUserJoin

    • Video may not be ready when user joins

    • Wait for video status change callback

    • See: Video Rendering

  3. Two Rendering Paths

    • Canvas API: SDK renders to your HWND (recommended)

    • Raw Data Pipe: You receive YUV frames (advanced)

    • See: Canvas vs Raw Data

  4. Helpers Control YOUR Streams Only

    • videoHelper->startVideo() starts YOUR camera

    • To see others, subscribe to their Canvas/Pipe

    • See: Singleton Hierarchy

  5. UI Framework Integration Differs by Platform

    • Win32: Direct SDK, Canvas API (SDK renders to HWND) - best performance

    • WinForms: C++/CLI wrapper, Raw Data Pipe, YUV→Bitmap, InvokeRequired

    • WPF: Same wrapper + Bitmap→BitmapSource, Dispatcher, Freeze()

    • See: UI Framework Integration

  6. C++/CLI Wrapper Patterns (for ANY native library → .NET)

    • void* pointers - hide native types from managed headers

    • gcroot<T^> - prevent GC from collecting managed references in native code

    • Finalizer + Destructor - ~Class() and !Class() for IDisposable cleanup

    • pin_ptr + Marshal::Copy - array/buffer conversion

    • LockBits - 100x faster than SetPixel for image manipulation

    • Thread marshaling - InvokeRequired (WinForms) / Dispatcher (WPF)

    • See: C++/CLI Wrapper Guide

  7. Audio Connection Timing

    • Set audioOption.connect = false during join

    • Call startAudio() in onSessionJoin callback

    • See: Production Guidelines


Quick Reference

"My code won't compile"

→ Build Errors Guide

"Callbacks never fire"

→ Windows Message Loop

"Video subscription returns error 2"

→ Video Rendering - Subscribe in onUserVideoStatusChanged

"Abstract class error"

→ Delegate Methods

"How do I implement [feature]?"

→ SDK Architecture Pattern

"How do I navigate to [controller]?"

→ Singleton Hierarchy

"What error code means what?"

→ Common Issues


Document Version

Based on Zoom Video SDK for Windows v2.x


Happy coding!

Remember: The SDK Architecture Pattern is your key to unlocking the entire SDK. Read it first!

Operations

  • RUNBOOK.md - 5-minute preflight and debugging checklist.

Files

examples/dotnet-winforms/guide.md

UI Integration Guide for Zoom Video SDK Windows

This guide covers three different UI approaches for integrating the Zoom Video SDK:

  1. Win32 (Native C++) - Direct SDK usage, no wrapper

  2. WinForms (C# .NET) - Requires C++/CLI wrapper

  3. WPF (C# .NET) - Requires C++/CLI wrapper + BitmapSource conversion

Quick Comparison

AspectWin32WinFormsWPF
LanguageC++C#C#
Wrapper RequiredNoYes (C++/CLI)Yes (C++/CLI)
Video RenderingCanvas API (SDK renders)Raw Data Pipe (you render)Raw Data Pipe + BitmapSource
PerformanceBestGoodGood (extra conversion)
ComplexityMediumMediumHigher
UI ThreadingWin32 message loopInvokeRequiredDispatcher

Option 1: Win32 (Native C++) - Direct SDK

No wrapper needed. The SDK is native C++, so Win32 apps use it directly.

Architecture
┌─────────────────┐     ┌─────────────────┐
│   Win32 Dialog  │────►│  Native C++ SDK │
│   (main.cpp)    │◄────│  (videosdk.dll) │
└─────────────────┘     └─────────────────┘
       HWND              Canvas API
Key Patterns
1. SDK Manager Class (Native C++)
// ZoomSDKManager.h
class ZoomSDKManager {
private:
    IZoomVideoSDK* m_pZoomSDK;
    IZoomVideoSDKSession* m_pSession;
    CustomZoomDelegate* m_pDelegate;

public:
    bool Initialize();
    bool JoinSession(const std::string& name, const std::string& token, ...);
    bool StartVideo();
    bool StartVideoPreview(HWND hwnd);  // Canvas API!
    bool SubscribeRemoteVideo(HWND hwnd, const std::string& userId);
};
2. Delegate Implementation (All 80+ Callbacks)
class CustomZoomDelegate : public IZoomVideoSDKDelegate {
private:
    ZoomSDKManager* m_pManager;

public:
    void onSessionJoin() override {
        m_pManager->OnSessionStatusChanged(SessionStatus::InSession, "Joined");
    }

    void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper,
                                  IVideoSDKVector<IZoomVideoSDKUser*>* userList) override {
        // Handle video status changes
    }

    // ... implement all 80+ callbacks
};
3. Video Rendering with Canvas API (SDK-Rendered)
// Start video preview - SDK renders directly to HWND
bool ZoomSDKManager::StartVideoPreview(HWND hwnd) {
    IZoomVideoSDKVideoHelper* videoHelper = m_pZoomSDK->getVideoHelper();

    // SDK renders directly to the window handle
    ZoomVideoSDKErrors ret = videoHelper->startVideoCanvasPreview(hwnd);
    return ret == ZoomVideoSDKErrors_Success;
}

// Subscribe to remote user's video
bool ZoomSDKManager::SubscribeRemoteVideo(HWND hwnd, const std::string& userId) {
    IZoomVideoSDKSession* session = m_pZoomSDK->getSessionInfo();
    IVideoSDKVector<IZoomVideoSDKUser*>* userList = session->getRemoteUsers();

    for (int i = 0; i < userList->GetCount(); i++) {
        IZoomVideoSDKUser* user = userList->GetItem(i);
        IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas();

        // SDK renders remote video directly to HWND
        canvas->subscribeWithView(hwnd, ZoomVideoSDKVideoAspect_Original, ZoomVideoSDKResolution_Auto);
    }
    return true;
}
4. Win32 Dialog with Video Panels
// main.cpp - Dialog procedure
INT_PTR CALLBACK MainDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) {
    switch (message) {
    case WM_INITDIALOG:
        InitializeZoomSDK();
        PopulateDeviceLists(hDlg);
        return TRUE;

    case WM_COMMAND:
        switch (LOWORD(wParam)) {
        case IDC_START_VIDEO:
            g_pSDKManager->StartVideo();

            // Get HWND of video panel control
            HWND selfVideoHwnd = GetDlgItem(hDlg, IDC_SELF_VIDEO);
            g_pSDKManager->StartVideoPreview(selfVideoHwnd);

            HWND remoteVideoHwnd = GetDlgItem(hDlg, IDC_REMOTE_VIDEO);
            g_pSDKManager->SubscribeRemoteVideo(remoteVideoHwnd, "");
            break;
        }
    }
    return FALSE;
}
Win32 Flow Summary
1. CreateZoomVideoSDKObj()
2. Initialize SDK with params
3. Create & register CustomZoomDelegate
4. Join session
5. On IDC_START_VIDEO click:
   - startVideo() → transmit your camera
   - startVideoCanvasPreview(selfHwnd) → see yourself
   - subscribeWithView(remoteHwnd) → see others
6. SDK renders directly to HWNDs
Sample Location
C:\tempsdk\videosdk-windows-dotnet-desktop-framework-quickstart\
  └── ZoomVideoSDK.Win32\
      ├── main.cpp              # Win32 dialog + event handlers
      ├── ZoomSDKManager.cpp    # SDK wrapper class
      ├── ZoomSDKManager.h      # Header with delegate
      └── main.rc               # Dialog resources

Option 2: WinForms (C# + C++/CLI Wrapper)

Requires C++/CLI bridge because Zoom SDK is native C++.

Architecture
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   C# WinForms   │────►│  C++/CLI Wrapper│────►│  Native C++ SDK │
│   (MainForm.cs) │◄────│  (ZoomSDKManager)│◄────│  (videosdk.dll) │
└─────────────────┘     └─────────────────┘     └─────────────────┘
     Events              gcroot<T^>               Callbacks
     Bitmap^             YUV→RGB                  YUVRawDataI420
Key Patterns
1. C++/CLI Wrapper Class
// ZoomSDKManager.h (C++/CLI)
public ref class ZoomSDKManager {
private:
    void* m_pVideoSDK;              // Hide native types
    void* m_pSessionHandler;        // Native callback handler

public:
    // Managed events for C# consumption
    event EventHandler<SessionStatusEventArgs^>^ SessionStatusChanged;
    event EventHandler<VideoFrameEventArgs^>^ PreviewVideoReceived;
    event EventHandler<VideoFrameEventArgs^>^ RemoteVideoReceived;

    bool Initialize();
    bool JoinSession(String^ name, String^ token, String^ user, String^ pw);
    bool StartVideo();
};
2. Native Callback → Managed Event (gcroot pattern)
// Native handler stores managed reference via gcroot
class VideoPreviewHandler : public IZoomVideoSDKRawDataPipeDelegate {
private:
    gcroot<ZoomSDKManager^> m_managedHandler;  // Prevents GC

public:
    VideoPreviewHandler(ZoomSDKManager^ handler) : m_managedHandler(handler) {}

    void onRawDataFrameReceived(YUVRawDataI420* data) override {
        ZoomSDKManager^ handler = static_cast<ZoomSDKManager^>(m_managedHandler);
        if (handler && data) {
            // Convert YUV to Bitmap
            Bitmap^ bitmap = handler->ConvertYUVToBitmap(
                data->GetYBuffer(), data->GetUBuffer(), data->GetVBuffer(),
                data->GetStreamWidth(), data->GetStreamHeight(), ...);

            // Fire managed event
            handler->OnPreviewVideoReceived(bitmap);
        }
    }
};
3. YUV→RGB Conversion (LockBits for Performance)
Bitmap^ ZoomSDKManager::ConvertYUVToBitmap(char* yBuffer, char* uBuffer, char* vBuffer,
                                           int width, int height, ...) {
    Bitmap^ bitmap = gcnew Bitmap(width, height, PixelFormat::Format24bppRgb);

    // Lock for direct memory access (100x faster than SetPixel)
    BitmapData^ data = bitmap->LockBits(rect, ImageLockMode::WriteOnly, ...);
    unsigned char* rgbPtr = (unsigned char*)data->Scan0.ToPointer();

    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            // YUV420 → RGB (ITU-R BT.601)
            int Y = yBuffer[y * yStride + x];
            int U = uBuffer[(y/2) * uStride + (x/2)];
            int V = vBuffer[(y/2) * vStride + (x/2)];

            int R = (298 * (Y-16) + 409 * (V-128) + 128) >> 8;
            int G = (298 * (Y-16) - 100 * (U-128) - 208 * (V-128) + 128) >> 8;
            int B = (298 * (Y-16) + 516 * (U-128) + 128) >> 8;

            // Write BGR (bitmap format)
            rgbPtr[y * stride + x * 3 + 0] = (byte)B;
            rgbPtr[y * stride + x * 3 + 1] = (byte)G;
            rgbPtr[y * stride + x * 3 + 2] = (byte)R;
        }
    }

    bitmap->UnlockBits(data);
    return bitmap;
}
4. C# Consumer (WinForms)
// MainForm.cs
public partial class MainForm : Form {
    private ZoomSDKInterop _zoomSDK;

    public MainForm() {
        InitializeComponent();
        InitializeZoomSDK();
    }

    private void InitializeZoomSDK() {
        _zoomSDK = new ZoomSDKInterop();

        // Subscribe to events
        _zoomSDK.SessionJoined += OnSessionJoined;
        _zoomSDK.PreviewVideoReceived += OnPreviewVideo;
        _zoomSDK.RemoteVideoReceived += OnRemoteVideo;

        _zoomSDK.Initialize();
    }

    private void OnPreviewVideo(object sender, VideoFrameEventArgs e) {
        // Must marshal to UI thread
        if (InvokeRequired) {
            BeginInvoke(new Action(() => OnPreviewVideo(sender, e)));
            return;
        }

        // Display bitmap in PictureBox
        _selfVideoPanel.Image?.Dispose();
        _selfVideoPanel.Image = e.Frame;
    }
}
WinForms Flow Summary
C# Layer:
1. new ZoomSDKInterop() → creates C++/CLI ZoomSDKManager
2. Subscribe to events (SessionJoined, PreviewVideoReceived, etc.)
3. _zoomSDK.Initialize() → SDK init
4. _zoomSDK.JoinSession(...) → join
5. _zoomSDK.StartVideo() → start camera + preview

C++/CLI Layer:
1. Creates native SDK via CreateZoomVideoSDKObj()
2. Creates VideoPreviewHandler with gcroot<ZoomSDKManager^>
3. Starts Raw Data Pipe subscription
4. onRawDataFrameReceived → YUV→RGB → fires PreviewVideoReceived event

C# Layer (UI Thread):
1. OnPreviewVideo receives Bitmap
2. Checks InvokeRequired for thread safety
3. Sets PictureBox.Image = bitmap
Sample Location
C:\tempsdk\videosdk-windows-dotnet-desktop-framework-quickstart\
  ├── ZoomVideoSDK.Wrapper\         # C++/CLI Bridge
  │   ├── ZoomSDKManager.h          # Managed class definition
  │   └── ZoomSDKManager.cpp        # Native ↔ Managed bridge
  │
  └── ZoomVideoSDK.WinForms\        # C# WinForms App
      ├── ZoomSDKInterop.cs         # High-level C# wrapper
      ├── MainForm.cs               # UI + event handlers
      └── Program.cs                # Entry point

Option 3: WPF (C# + C++/CLI Wrapper)

Same C++/CLI wrapper as WinForms, but with additional WPF-specific handling.

Architecture
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│    C# WPF       │────►│  C# Interop     │────►│  C++/CLI Wrapper│────►│  Native C++ SDK │
│  (MainWindow)   │◄────│  (ZoomSDKInterop)│◄────│  (ZoomSDKManager)│◄────│  (videosdk.dll) │
└─────────────────┘     └─────────────────┘     └─────────────────┘     └─────────────────┘
     BitmapSource        Bitmap→BitmapSource     gcroot<T^>              Callbacks
     Dispatcher          Conversion              YUV→RGB                 YUVRawDataI420
Key Differences from WinForms
AspectWinFormsWPF
Video TypeSystem.Drawing.BitmapSystem.Windows.Media.Imaging.BitmapSource
UI ThreadInvokeRequired + BeginInvokeDispatcher.CheckAccess() + Dispatcher.BeginInvoke
Image ControlPictureBox.ImageImage.Source
Extra StepNoneBitmap → BitmapSource conversion
Key Patterns
1. WPF-Specific Event Args
// WPF uses BitmapSource instead of Bitmap
public class VideoFrameEventArgs : EventArgs {
    public BitmapSource Frame { get; set; }  // WPF type
    public string UserId { get; set; }
}
2. Bitmap → BitmapSource Conversion
// ZoomSDKInterop.cs (WPF version)
private BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap) {
    if (bitmap == null) return null;

    using (var memory = new MemoryStream()) {
        bitmap.Save(memory, System.Drawing.Imaging.ImageFormat.Png);
        memory.Position = 0;

        var bitmapImage = new BitmapImage();
        bitmapImage.BeginInit();
        bitmapImage.StreamSource = memory;
        bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
        bitmapImage.EndInit();
        bitmapImage.Freeze();  // Make thread-safe for WPF

        return bitmapImage;
    }
}

// Event handler bridges C++/CLI Bitmap to WPF BitmapSource
_sdkManager.PreviewVideoReceived += (sender, e) => {
    var wpfFrame = ConvertBitmapToBitmapSource(e.Frame);
    PreviewVideoReceived?.Invoke(this, new VideoFrameEventArgs(wpfFrame, "self"));
};
3. WPF Dispatcher for UI Thread
// MainWindow.xaml.cs
private void OnPreviewVideoReceived(object sender, VideoFrameEventArgs e) {
    // WPF uses Dispatcher instead of InvokeRequired
    if (!Dispatcher.CheckAccess()) {
        Dispatcher.BeginInvoke(new Action<object, VideoFrameEventArgs>(OnPreviewVideoReceived), sender, e);
        return;
    }

    // Frame throttling (~30fps)
    if (DateTime.Now - _lastPreviewVideoUpdate < _videoUpdateInterval) return;
    _lastPreviewVideoUpdate = DateTime.Now;

    if (e.Frame != null) {
        SelfVideoImage.Source = e.Frame;  // WPF Image control
    }
}
4. Alternative: WriteableBitmap (Higher Performance)

For better performance, you can write directly to WriteableBitmap:

private BitmapSource CreateErrorBitmapSource(int width, int height, string message) {
    var writeableBitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgr24, null);
    writeableBitmap.Lock();

    unsafe {
        byte* backBuffer = (byte*)writeableBitmap.BackBuffer;
        int stride = writeableBitmap.BackBufferStride;

        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                byte* pixel = backBuffer + y * stride + x * 3;
                pixel[0] = 0;    // Blue
                pixel[1] = 0;    // Green
                pixel[2] = 255;  // Red
            }
        }
    }

    writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
    writeableBitmap.Unlock();
    writeableBitmap.Freeze();  // Thread-safe

    return writeableBitmap;
}
WPF Flow Summary
Same as WinForms, with these differences:

C# WPF Interop Layer:
1. Receives Bitmap from C++/CLI wrapper
2. Converts Bitmap → BitmapSource (PNG stream or WriteableBitmap)
3. Calls Freeze() to make cross-thread safe
4. Fires WPF-compatible event

MainWindow (UI Thread):
1. OnPreviewVideoReceived receives BitmapSource
2. Checks Dispatcher.CheckAccess() for thread safety
3. Sets Image.Source = bitmapSource
Sample Location
C:\tempsdk\videosdk-windows-dotnet-desktop-framework-quickstart\
  ├── ZoomVideoSDK.Wrapper\         # C++/CLI Bridge (shared with WinForms)
  │   ├── ZoomSDKManager.h
  │   └── ZoomSDKManager.cpp
  │
  └── ZoomVideoSDK.WPF\             # C# WPF App
      ├── ZoomSDKInterop.cs         # WPF-specific interop (BitmapSource)
      ├── MainWindow.xaml           # XAML layout
      ├── MainWindow.xaml.cs        # Code-behind with Dispatcher
      └── App.xaml                  # Application entry

Decision Matrix

If you need...UseWhy
Best performanceWin32Canvas API, SDK renders directly
C++ codebaseWin32No interop overhead
Existing WinForms appWinForms + C++/CLINatural integration
Modern .NET UIWPF + C++/CLIXAML, data binding
Cross-platform .NETConsider AvaloniaWPF-like but cross-platform

C++/CLI Wrapper Patterns (For .NET Integration)

This section teaches general C++/CLI wrapping patterns applicable to ANY native C++ library.

When to Use C++/CLI
ScenarioSolution
Native C++ library → C# appC++/CLI wrapper (this guide)
C library → C# appP/Invoke (simpler, no wrapper needed)
COM library → C# appCOM Interop
.NET library → C++ appReverse P/Invoke or COM
Project Setup
  1. Create C++/CLI Class Library:

    • Visual Studio → New Project → "CLR Class Library (.NET Framework)"

    • Or add /clr to existing C++ project

  2. Project Properties:

    Configuration Properties → General:
    - Common Language Runtime Support: /clr
    - .NET Target Framework: v4.8
    
    C/C++ → General:
    - Additional Include Directories: path\to\native\sdk\include
    
    Linker → General:
    - Additional Library Directories: path\to\native\sdk\lib
    
    Linker → Input:
    - Additional Dependencies: native_sdk.lib
  3. File Structure:

    MyWrapper/
    ├── MyWrapper.h         # Managed ref class definition
    ├── MyWrapper.cpp       # Implementation
    ├── NativeCallbacks.h   # Native callback classes with gcroot
    └── Stdafx.h            # Precompiled header

Pattern 1: Basic Wrapper Structure

Goal: Expose native C++ class to C#

// MyWrapper.h (C++/CLI)
#pragma once
#include <msclr\marshal_cppstd.h>  // For string conversion

using namespace System;
using namespace System::Runtime::InteropServices;

namespace MyLibraryWrapper {

    // Forward declare native types (hide from C#)
    class NativeClass;  // Don't #include native headers here!

    public ref class ManagedWrapper {
    private:
        NativeClass* m_pNative;  // Raw pointer to native object
        bool m_disposed;

    public:
        ManagedWrapper();
        ~ManagedWrapper();       // Destructor (IDisposable.Dispose)
        !ManagedWrapper();       // Finalizer (destructor fallback)

        // Managed methods that wrap native calls
        bool Initialize();
        void DoSomething(String^ param);
        String^ GetResult();
    };
}
// MyWrapper.cpp
#include "stdafx.h"
#include "MyWrapper.h"
#include "native_sdk.h"  // Include native headers in .cpp only!

namespace MyLibraryWrapper {

    ManagedWrapper::ManagedWrapper() : m_pNative(nullptr), m_disposed(false) {
        m_pNative = new NativeClass();
    }

    ManagedWrapper::~ManagedWrapper() {
        this->!ManagedWrapper();  // Call finalizer
        m_disposed = true;
    }

    ManagedWrapper::!ManagedWrapper() {
        if (m_pNative) {
            delete m_pNative;
            m_pNative = nullptr;
        }
    }

    bool ManagedWrapper::Initialize() {
        if (!m_pNative) return false;
        return m_pNative->init() == 0;  // Native returns 0 for success
    }

    void ManagedWrapper::DoSomething(String^ param) {
        if (!m_pNative) return;

        // Convert managed String^ to native std::wstring
        std::wstring nativeParam = msclr::interop::marshal_as<std::wstring>(param);
        m_pNative->doSomething(nativeParam.c_str());
    }

    String^ ManagedWrapper::GetResult() {
        if (!m_pNative) return nullptr;

        // Convert native wchar_t* to managed String^
        const wchar_t* result = m_pNative->getResult();
        return result ? gcnew String(result) : nullptr;
    }
}

Pattern 2: Opaque void* Pointers

Goal: Hide native types from managed headers (prevents header dependency leaks)

// In .h file - use void* to hide native types
private:
    void* m_pNativeSDK;     // Actually INativeSDK*
    void* m_pNativeSession; // Actually INativeSession*

// In .cpp file - cast back to real types
bool ManagedWrapper::JoinSession() {
    INativeSDK* sdk = static_cast<INativeSDK*>(m_pNativeSDK);
    INativeSession* session = sdk->joinSession(...);
    m_pNativeSession = static_cast<void*>(session);
    return session != nullptr;
}

Why: Native SDK headers often have complex dependencies. Using void* means you only need to #include native headers in the .cpp file, not the .h file. This prevents compile errors in consuming C# projects.


Pattern 3: gcroot<T^> for Native→Managed Callbacks

Goal: Native code needs to call back into managed code

// NativeCallbacks.h
#pragma once
#include <vcclr.h>  // For gcroot

// Forward declare the managed class
namespace MyLibraryWrapper { ref class ManagedWrapper; }

// Native class that implements SDK callback interface
class NativeEventHandler : public INativeEventListener {
private:
    gcroot<MyLibraryWrapper::ManagedWrapper^> m_managed;  // GC-safe ref

public:
    NativeEventHandler(MyLibraryWrapper::ManagedWrapper^ wrapper)
        : m_managed(wrapper) {}

    // Native callback (called by SDK on background thread)
    void onEvent(int eventCode, const wchar_t* message) override {
        // Get managed reference (prevents GC during callback)
        MyLibraryWrapper::ManagedWrapper^ wrapper = m_managed;
        if (wrapper) {
            wrapper->FireManagedEvent(eventCode, gcnew String(message));
        }
    }

    void onDataReceived(const unsigned char* data, int length) override {
        MyLibraryWrapper::ManagedWrapper^ wrapper = m_managed;
        if (wrapper) {
            // Copy native data to managed array
            array<Byte>^ managedData = gcnew array<Byte>(length);
            Marshal::Copy(IntPtr((void*)data), managedData, 0, length);
            wrapper->FireDataEvent(managedData);
        }
    }
};
// In ManagedWrapper.h - add events
public ref class ManagedWrapper {
public:
    // Managed events for C# consumption
    event EventHandler<EventArgs^>^ SomethingHappened;
    event EventHandler<DataEventArgs^>^ DataReceived;

internal:
    // Called by native callback handler
    void FireManagedEvent(int code, String^ message);
    void FireDataEvent(array<Byte>^ data);
};

Critical: gcroot<T^> prevents the .NET garbage collector from moving/collecting the managed object while native code holds a reference. Without it, callbacks will crash.


Pattern 4: Destructor + Finalizer (IDisposable)

Goal: Guarantee native resource cleanup

public ref class ManagedWrapper {
private:
    NativeClass* m_pNative;
    NativeEventHandler* m_pHandler;  // Must also be cleaned up
    bool m_disposed;

public:
    // Destructor - called by Dispose() or 'using' statement
    ~ManagedWrapper() {
        if (!m_disposed) {
            this->!ManagedWrapper();  // Call finalizer logic
            m_disposed = true;
            GC::SuppressFinalize(this);  // No need for finalizer now
        }
    }

    // Finalizer - called by GC if Dispose wasn't called
    !ManagedWrapper() {
        // Clean up in reverse order of creation
        if (m_pHandler) {
            delete m_pHandler;
            m_pHandler = nullptr;
        }
        if (m_pNative) {
            m_pNative->shutdown();  // SDK cleanup
            delete m_pNative;
            m_pNative = nullptr;
        }
    }
};

C# Usage:

// Option 1: Explicit dispose
var wrapper = new ManagedWrapper();
try {
    wrapper.Initialize();
    // use wrapper...
} finally {
    wrapper.Dispose();  // Calls ~ManagedWrapper()
}

// Option 2: using statement (preferred)
using (var wrapper = new ManagedWrapper()) {
    wrapper.Initialize();
    // use wrapper...
}  // Dispose() called automatically

Pattern 5: String Conversion

Goal: Convert between managed String^ and native strings

#include <msclr\marshal_cppstd.h>

// Managed String^ → Native std::wstring
void SetName(String^ name) {
    std::wstring nativeName = msclr::interop::marshal_as<std::wstring>(name);
    m_pNative->setName(nativeName.c_str());
}

// Managed String^ → Native std::string (UTF-8)
void SetNameUtf8(String^ name) {
    std::string nativeName = msclr::interop::marshal_as<std::string>(name);
    m_pNative->setNameUtf8(nativeName.c_str());
}

// Native wchar_t* → Managed String^
String^ GetName() {
    const wchar_t* name = m_pNative->getName();
    return name ? gcnew String(name) : nullptr;
}

// Native char* (UTF-8) → Managed String^
String^ GetNameUtf8() {
    const char* name = m_pNative->getNameUtf8();
    return name ? gcnew String(name, 0, strlen(name), System::Text::Encoding::UTF8) : nullptr;
}

Pattern 6: Array/Buffer Conversion

Goal: Pass binary data between managed and native code

// Managed array → Native buffer
void SendData(array<Byte>^ data) {
    if (data == nullptr || data->Length == 0) return;

    // Pin the managed array (prevents GC from moving it)
    pin_ptr<Byte> pinned = &data[0];
    unsigned char* nativePtr = pinned;

    m_pNative->sendData(nativePtr, data->Length);
}
// pinned automatically unpins when out of scope

// Native buffer → Managed array
array<Byte>^ ReceiveData() {
    unsigned char* buffer = nullptr;
    int length = 0;

    m_pNative->receiveData(&buffer, &length);

    if (!buffer || length <= 0) return nullptr;

    array<Byte>^ result = gcnew array<Byte>(length);
    Marshal::Copy(IntPtr(buffer), result, 0, length);

    m_pNative->freeBuffer(buffer);  // SDK may require this
    return result;
}

Pattern 7: Thread Marshaling (Native Thread → UI Thread)

Goal: Fire events safely when native callbacks occur on background threads

// In native callback handler
void NativeEventHandler::onVideoFrame(YUVData* frame) {
    ManagedWrapper^ wrapper = m_managed;
    if (wrapper) {
        // Convert frame to managed Bitmap (still on native thread)
        Bitmap^ bitmap = wrapper->ConvertYUVToBitmap(frame);

        // Fire event - consumer must marshal to UI thread
        wrapper->FireVideoFrameEvent(bitmap);
    }
}

C# Consumer (WinForms):

wrapper.VideoFrameReceived += (sender, e) => {
    if (InvokeRequired) {
        BeginInvoke(new Action(() => pictureBox.Image = e.Frame));
    } else {
        pictureBox.Image = e.Frame;
    }
};

C# Consumer (WPF):

wrapper.VideoFrameReceived += (sender, e) => {
    if (!Dispatcher.CheckAccess()) {
        Dispatcher.BeginInvoke(new Action(() => image.Source = e.Frame));
    } else {
        image.Source = e.Frame;
    }
};

Pattern 8: LockBits for Fast Image Manipulation

Goal: Convert image formats efficiently (e.g., YUV→RGB)

Bitmap^ ConvertYUVToBitmap(unsigned char* yuvData, int width, int height) {
    Bitmap^ bitmap = gcnew Bitmap(width, height, PixelFormat::Format24bppRgb);

    // Lock bitmap memory for direct access (100x faster than SetPixel)
    Rectangle rect(0, 0, width, height);
    BitmapData^ data = bitmap->LockBits(rect, ImageLockMode::WriteOnly,
                                         PixelFormat::Format24bppRgb);

    unsigned char* rgbPtr = (unsigned char*)data->Scan0.ToPointer();
    int stride = data->Stride;  // May include padding!

    // YUV420 to RGB conversion
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            int yIndex = y * width + x;
            int uvIndex = (y / 2) * (width / 2) + (x / 2);

            int Y = yuvData[yIndex];
            int U = yuvData[width * height + uvIndex];
            int V = yuvData[width * height + (width/2)*(height/2) + uvIndex];

            // BT.601 conversion
            int C = Y - 16, D = U - 128, E = V - 128;
            int R = (298*C + 409*E + 128) >> 8;
            int G = (298*C - 100*D - 208*E + 128) >> 8;
            int B = (298*C + 516*D + 128) >> 8;

            // Clamp and write BGR (bitmap format)
            unsigned char* pixel = rgbPtr + y * stride + x * 3;
            pixel[0] = (B < 0) ? 0 : (B > 255) ? 255 : B;
            pixel[1] = (G < 0) ? 0 : (G > 255) ? 255 : G;
            pixel[2] = (R < 0) ? 0 : (R > 255) ? 255 : R;
        }
    }

    bitmap->UnlockBits(data);
    return bitmap;
}

Complete Wrapper Checklist

When wrapping ANY native C++ library:

[ ] Project: C++/CLI Class Library with /clr enabled
[ ] Headers: Native includes in .cpp only, void* in .h
[ ] Lifecycle: Destructor (~) + Finalizer (!) pattern
[ ] Callbacks: gcroot<T^> in native handler classes
[ ] Strings: marshal_as<std::wstring> or gcnew String()
[ ] Arrays: pin_ptr for managed→native, Marshal::Copy for native→managed
[ ] Threading: Document which thread callbacks fire on
[ ] Cleanup: Delete native objects in reverse order of creation
[ ] Events: C# events for callbacks, document threading requirements

Common Wrapper Errors
ErrorCauseFix
LNK2020: unresolved tokenMissing native .libAdd to Linker → Input → Additional Dependencies
C3767: candidate function not accessibleExposing native types in public APIUse void* or wrap in managed class
AccessViolationException in callbackGC moved managed objectUse gcroot<T^> in native handler
ObjectDisposedExceptionUsing wrapper after DisposeCheck m_disposed flag
BadImageFormatExceptionx86/x64 mismatchMatch Platform Target to native SDK

Production Quality Review

This section provides a detailed assessment of each sample against production standards.

Overall Assessment Summary
SampleQualityIssuesProduction Ready
Win32Good4 minorYes (with fixes)
C++/CLI WrapperGood4 minorYes (with fixes)
WinFormsGood3 minorYes (with fixes)
WPFGood4 minorYes (with fixes)

Win32 Sample (ZoomVideoSDK.Win32)
What's Good
CheckStatusNotes
All 80+ delegate callbacks✅Lines 17-358 in ZoomSDKManager.cpp
Null checks on SDK pointers✅Consistent throughout
Exception handling✅try/catch blocks on all SDK calls
Cleanup sequence✅leave → removeListener → cleanup → destroy
Canvas API usage✅startVideoCanvasPreview, subscribeWithView
Device enumeration✅Proper SDK interfaces
UTF-8 string conversion✅WideCharToMultiByte used correctly
Issues Found
SeverityIssueLocationRecommendation
⚠️ MediumaudioOption.connect = true during joinLine 489Set to false, call startAudio() in onSessionJoin callback
⚠️ LowGlobal pointer without thread safetyLine 22 main.cppUse mutex or make thread-local
⚠️ LowArbitrary Sleep(100) after initLines 366-367Use callback-based readiness check
⚠️ LowNo WM_DESTROY handlermain.cppAdd cleanup in WM_DESTROY
// BEFORE (current - problematic)
sessionContext.audioOption.connect = true;  // May miss audio callbacks

// AFTER (recommended)
sessionContext.audioOption.connect = false;

// Then in onSessionJoin callback:
void CustomZoomDelegate::onSessionJoin() {
    if (m_pManager) {
        // Now safe to connect audio
        IZoomVideoSDKAudioHelper* audioHelper = m_pManager->GetSDK()->getAudioHelper();
        audioHelper->startAudio();
    }
}

C++/CLI Wrapper (ZoomVideoSDK.Wrapper)
What's Good
CheckStatusNotes
gcroot<T^> usage✅Lines 25, 40-41, 57 - prevents GC collection
Finalizer + Destructor pattern✅Lines 245-254 - proper C++/CLI cleanup
LockBits for YUV conversion✅Lines 831-880 - 100x faster than SetPixel
All 80+ delegate callbacks✅SimpleNativeHandler implements all
Error handling✅Status messages for all operations
Video preview cleanup✅StopVideoPreview cleans up handler
Issues Found
SeverityIssueLocationRecommendation
⚠️ MediumaudioOption.connect = trueLine 343Same fix as Win32
⚠️ MediumunSubscribe(nullptr)Line 987Pass actual delegate used for subscription
⚠️ MediumRemoteVideoHandler memory leakLine 947Store handler, delete in UnsubscribeFromUserVideo
⚠️ LowSimplified stride calculationLines 1162, 1212Use data->GetYStride(), GetUStride(), GetVStride()
// ZoomSDKManager.h - Add member to track handlers
private:
    std::map<String^, RemoteVideoHandler*> m_remoteHandlers;

// In SubscribeToUserVideo
RemoteVideoHandler* remoteHandler = new RemoteVideoHandler(this, userId);
m_remoteHandlers[userId] = remoteHandler;  // Store for later cleanup

// In UnsubscribeFromUserVideo
if (m_remoteHandlers.ContainsKey(userId)) {
    RemoteVideoHandler* handler = m_remoteHandlers[userId];
    videoPipe->unSubscribe(handler);  // Pass actual handler
    delete handler;
    m_remoteHandlers.Remove(userId);
}

WinForms Sample (ZoomVideoSDK.WinForms)
What's Good
CheckStatusNotes
Thread marshaling✅InvokeRequired + BeginInvoke throughout
Frame rate throttling✅30fps limit prevents UI overload
Bitmap disposal✅.Image?.Dispose() before assignment
Form closing cleanup✅LeaveSession + Cleanup in OnFormClosing
JWT token decoder✅Proper Base64 URL-safe handling
Exception handling✅All operations wrapped in try/catch
Issues Found
SeverityIssueLocationRecommendation
⚠️ MediumMissing IDisposable patternZoomSDKInterop.csAdd ~ZoomSDKInterop() destructor
⚠️ LowNull check after CleanupLine 468-469Add _sdkManager != null check before events
⚠️ InfoDuplicate YUV conversionLines 369-443Remove - wrapper already handles this
// ZoomSDKInterop.cs
public class ZoomSDKInterop : IDisposable
{
    private bool _disposed = false;

    ~ZoomSDKInterop()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                Cleanup();
            }
            _disposed = true;
        }
    }
}

WPF Sample (ZoomVideoSDK.WPF)
What's Good
CheckStatusNotes
Dispatcher thread safety✅CheckAccess() + BeginInvoke
BitmapImage.Freeze()✅Cross-thread safety for WPF
Frame rate throttling✅30fps limit
Window close cleanup✅LeaveSession + Cleanup in OnClosed
BitmapCacheOption.OnLoad✅Proper stream handling
Issues Found
SeverityIssueLocationRecommendation
⚠️ MediumMemoryStream PNG conversionLines 384-395Use Imaging.CreateBitmapSourceFromHBitmap for speed
⚠️ MediumMissing IDisposableZoomSDKInterop.csSame fix as WinForms
⚠️ LowDuplicate YUV conversionLines 406-480Remove - wrapper handles this
⚠️ InfoWriteableBitmap error bitmapLines 493-527No text drawn - consider DrawingContext
// Faster alternative to MemoryStream PNG encoding
[DllImport("gdi32.dll")]
private static extern bool DeleteObject(IntPtr hObject);

private BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap)
{
    if (bitmap == null) return null;

    IntPtr hBitmap = bitmap.GetHbitmap();
    try
    {
        var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
            hBitmap,
            IntPtr.Zero,
            Int32Rect.Empty,
            BitmapSizeOptions.FromEmptyOptions());
        source.Freeze();
        return source;
    }
    finally
    {
        DeleteObject(hBitmap);  // Prevent GDI handle leak
    }
}

Cross-Sample Issues

These issues appear in multiple samples:

1. Audio Connection Timing

Affected: Win32, C++/CLI Wrapper

// Current (all samples)
sessionContext.audioOption.connect = true;

// Recommended per Zoom docs
sessionContext.audioOption.connect = false;
// Then call startAudio() in onSessionJoin callback

Why: Setting connect = true during join may cause audio to connect before the session is fully established, potentially missing early audio callbacks.

2. Video Subscription Timing

Pattern followed correctly: All samples wait for onUserJoin or onUserVideoStatusChanged before subscribing to video.

3. Resource Cleanup

Pattern followed correctly: All samples implement cleanup in form/window closing handlers.


Production Checklist

Use this checklist before deploying:

[ ] Audio: Set audioOption.connect = false, start in onSessionJoin
[ ] Video: Subscribe only after onUserJoin/onUserVideoStatusChanged
[ ] Memory: Track and cleanup all native handlers
[ ] Threading: Marshal all UI updates to main thread
[ ] Disposal: Implement IDisposable with destructor/finalizer
[ ] Frame Rate: Throttle video at 30fps to prevent UI lock
[ ] Error Handling: Try/catch all SDK calls
[ ] Cleanup: LeaveSession before Cleanup on app close

Sample Locations

All samples are in:

C:\tempsdk\videosdk-windows-dotnet-desktop-framework-quickstart\
├── ZoomVideoSDK.Win32\      # Native Win32 + Canvas API
├── ZoomVideoSDK.Wrapper\    # C++/CLI Bridge (shared)
├── ZoomVideoSDK.WinForms\   # C# WinForms + Raw Data
└── ZoomVideoSDK.WPF\        # C# WPF + BitmapSource

Requirements and compatibility

Written for: OpenAI Codex

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
Zoom
Publisher
OpenAI (Official)
Distributed by
OpenAI
Source
openai/plugins · plugins/zoom/skills/video-sdk/windows · commit d420d13
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from plugins/zoom/LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Zoom or OpenAI.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download zoom-video-sdk-windows.zip