UI Integration Guide for Zoom Video SDK Windows
This guide covers three different UI approaches for integrating the Zoom Video SDK:
- Win32 (Native C++) - Direct SDK usage, no wrapper
- WinForms (C# .NET) - Requires C++/CLI wrapper
- WPF (C# .NET) - Requires C++/CLI wrapper + BitmapSource conversion
Quick Comparison
| Aspect | Win32 | WinForms | WPF |
|---|---|---|---|
| Language | C++ | C# | C# |
| Wrapper Required | No | Yes (C++/CLI) | Yes (C++/CLI) |
| Video Rendering | Canvas API (SDK renders) | Raw Data Pipe (you render) | Raw Data Pipe + BitmapSource |
| Performance | Best | Good | Good (extra conversion) |
| Complexity | Medium | Medium | Higher |
| UI Threading | Win32 message loop | InvokeRequired |
Dispatcher |
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
| Aspect | WinForms | WPF |
|---|---|---|
| Video Type | System.Drawing.Bitmap |
System.Windows.Media.Imaging.BitmapSource |
| UI Thread | InvokeRequired + BeginInvoke |
Dispatcher.CheckAccess() + Dispatcher.BeginInvoke |
| Image Control | PictureBox.Image |
Image.Source |
| Extra Step | None | Bitmap → 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... | Use | Why |
|---|---|---|
| Best performance | Win32 | Canvas API, SDK renders directly |
| C++ codebase | Win32 | No interop overhead |
| Existing WinForms app | WinForms + C++/CLI | Natural integration |
| Modern .NET UI | WPF + C++/CLI | XAML, data binding |
| Cross-platform .NET | Consider Avalonia | WPF-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
| Scenario | Solution |
|---|---|
| Native C++ library → C# app | C++/CLI wrapper (this guide) |
| C library → C# app | P/Invoke (simpler, no wrapper needed) |
| COM library → C# app | COM Interop |
| .NET library → C++ app | Reverse P/Invoke or COM |
Project Setup
-
Create C++/CLI Class Library:
- Visual Studio → New Project → "CLR Class Library (.NET Framework)"
- Or add
/clrto existing C++ project
-
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 -
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
| Error | Cause | Fix |
|---|---|---|
LNK2020: unresolved token |
Missing native .lib | Add to Linker → Input → Additional Dependencies |
C3767: candidate function not accessible |
Exposing native types in public API | Use void* or wrap in managed class |
AccessViolationException in callback |
GC moved managed object | Use gcroot<T^> in native handler |
ObjectDisposedException |
Using wrapper after Dispose | Check m_disposed flag |
BadImageFormatException |
x86/x64 mismatch | Match Platform Target to native SDK |
Production Quality Review
This section provides a detailed assessment of each sample against production standards.
Overall Assessment Summary
| Sample | Quality | Issues | Production Ready |
|---|---|---|---|
| Win32 | Good | 4 minor | Yes (with fixes) |
| C++/CLI Wrapper | Good | 4 minor | Yes (with fixes) |
| WinForms | Good | 3 minor | Yes (with fixes) |
| WPF | Good | 4 minor | Yes (with fixes) |
Win32 Sample (ZoomVideoSDK.Win32)
What's Good
| Check | Status | Notes |
|---|---|---|
| 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
| Severity | Issue | Location | Recommendation |
|---|---|---|---|
| ⚠️ Medium | audioOption.connect = true during join |
Line 489 | Set to false, call startAudio() in onSessionJoin callback |
| ⚠️ Low | Global pointer without thread safety | Line 22 main.cpp | Use mutex or make thread-local |
| ⚠️ Low | Arbitrary Sleep(100) after init |
Lines 366-367 | Use callback-based readiness check |
| ⚠️ Low | No WM_DESTROY handler | main.cpp | Add cleanup in WM_DESTROY |
Recommended Fix: Audio Connection
// 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
| Check | Status | Notes |
|---|---|---|
| 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
| Severity | Issue | Location | Recommendation |
|---|---|---|---|
| ⚠️ Medium | audioOption.connect = true |
Line 343 | Same fix as Win32 |
| ⚠️ Medium | unSubscribe(nullptr) |
Line 987 | Pass actual delegate used for subscription |
| ⚠️ Medium | RemoteVideoHandler memory leak | Line 947 | Store handler, delete in UnsubscribeFromUserVideo |
| ⚠️ Low | Simplified stride calculation | Lines 1162, 1212 | Use data->GetYStride(), GetUStride(), GetVStride() |
Recommended Fix: Handler Memory Leak
// 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
| Check | Status | Notes |
|---|---|---|
| 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
| Severity | Issue | Location | Recommendation |
|---|---|---|---|
| ⚠️ Medium | Missing IDisposable pattern | ZoomSDKInterop.cs | Add ~ZoomSDKInterop() destructor |
| ⚠️ Low | Null check after Cleanup | Line 468-469 | Add _sdkManager != null check before events |
| ⚠️ Info | Duplicate YUV conversion | Lines 369-443 | Remove - wrapper already handles this |
Recommended Fix: IDisposable
// 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
| Check | Status | Notes |
|---|---|---|
| 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
| Severity | Issue | Location | Recommendation |
|---|---|---|---|
| ⚠️ Medium | MemoryStream PNG conversion | Lines 384-395 | Use Imaging.CreateBitmapSourceFromHBitmap for speed |
| ⚠️ Medium | Missing IDisposable | ZoomSDKInterop.cs | Same fix as WinForms |
| ⚠️ Low | Duplicate YUV conversion | Lines 406-480 | Remove - wrapper handles this |
| ⚠️ Info | WriteableBitmap error bitmap | Lines 493-527 | No text drawn - consider DrawingContext |
Recommended Fix: Faster Bitmap Conversion
// 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
Related Documentation
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