File: C:/Users/fred/.codex/.tmp/plugins/plugins/zoom/skills/video-sdk/windows/examples/video-rendering.md
# Video Rendering with Canvas API
Complete working code for subscribing to and displaying video using the Canvas API.
## Overview
The Canvas API lets the SDK render video directly to your window. This is the **recommended approach** for standard video applications.
```
┌─────────────────────────────────────────────────────────────────┐
│ VIDEO SUBSCRIPTION FLOW │
├─────────────────────────────────────────────────────────────────┤
│ onSessionJoin → Subscribe to self video │
│ onUserVideoStatusChanged → Subscribe to remote video (when ON)│
│ onUserLeave → Unsubscribe and cleanup │
└─────────────────────────────────────────────────────────────────┘
```
---
## Critical Rule
### Subscribe in onUserVideoStatusChanged, NOT onUserJoin
```cpp
// WRONG - Video may not be ready yet!
void onUserJoin(IZoomVideoSDKUserHelper* helper,
IVideoSDKVector<IZoomVideoSDKUser*>* userList) override {
for (int i = 0; i < userList->GetCount(); i++) {
IZoomVideoSDKUser* user = userList->GetItem(i);
// This returns Error 2 (Internal_Error) - video not ready!
user->GetVideoCanvas()->subscribeWithView(hwnd, aspect, resolution);
}
}
// CORRECT - Wait for video status change
void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper,
IVideoSDKVector<IZoomVideoSDKUser*>* userList) override {
IZoomVideoSDKUser* myself = g_sdk->getSessionInfo()->getMyself();
for (int i = 0; i < userList->GetCount(); i++) {
IZoomVideoSDKUser* user = userList->GetItem(i);
// Skip self (handled separately)
if (user == myself) continue;
// Check if video is actually on
ZoomVideoSDKVideoStatus status = user->GetVideoPipe()->getVideoStatus();
if (status.isOn) {
// NOW it's safe to subscribe
SubscribeToUser(user);
} else {
// Video turned off - unsubscribe
UnsubscribeFromUser(user);
}
}
}
```
---
## Complete Working Code
### VideoCanvasManager.h
```cpp
#pragma once
#include <windows.h>
#include <map>
#include <vector>
#include "zoom_video_sdk_interface.h"
USING_ZOOM_VIDEO_SDK_NAMESPACE
class VideoCanvasManager {
public:
VideoCanvasManager(HWND parentWindow, IZoomVideoSDK* sdk);
~VideoCanvasManager();
// Subscribe to self video
void SubscribeToSelf();
// Subscribe/unsubscribe to remote user
void SubscribeToUser(IZoomVideoSDKUser* user);
void UnsubscribeFromUser(IZoomVideoSDKUser* user);
// Cleanup
void UnsubscribeAll();
// Layout
void UpdateLayout();
private:
HWND CreateVideoWindow();
void DestroyVideoWindow(HWND hwnd);
IZoomVideoSDK* m_sdk;
HWND m_parentWindow;
// Self video
HWND m_selfVideoWindow;
IZoomVideoSDKCanvas* m_selfCanvas;
// Remote users: user -> window mapping
std::map<IZoomVideoSDKUser*, HWND> m_userWindows;
std::map<IZoomVideoSDKUser*, IZoomVideoSDKCanvas*> m_userCanvases;
};
```
### VideoCanvasManager.cpp
```cpp
#include "VideoCanvasManager.h"
#include <iostream>
VideoCanvasManager::VideoCanvasManager(HWND parentWindow, IZoomVideoSDK* sdk)
: m_parentWindow(parentWindow)
, m_sdk(sdk)
, m_selfVideoWindow(nullptr)
, m_selfCanvas(nullptr) {
}
VideoCanvasManager::~VideoCanvasManager() {
UnsubscribeAll();
}
HWND VideoCanvasManager::CreateVideoWindow() {
// Create a child window for video rendering
HWND hwnd = CreateWindowExW(
0,
L"STATIC", // Simple static window class
L"",
WS_CHILD | WS_VISIBLE | WS_BORDER,
0, 0, 320, 240, // Size will be set by UpdateLayout()
m_parentWindow,
nullptr,
GetModuleHandle(nullptr),
nullptr
);
return hwnd;
}
void VideoCanvasManager::DestroyVideoWindow(HWND hwnd) {
if (hwnd) {
DestroyWindow(hwnd);
}
}
void VideoCanvasManager::SubscribeToSelf() {
IZoomVideoSDKSession* session = m_sdk->getSessionInfo();
if (!session) return;
IZoomVideoSDKUser* myself = session->getMyself();
if (!myself) return;
// Create window for self video
if (!m_selfVideoWindow) {
m_selfVideoWindow = CreateVideoWindow();
}
// Subscribe
m_selfCanvas = myself->GetVideoCanvas();
if (m_selfCanvas) {
ZoomVideoSDKErrors err = m_selfCanvas->subscribeWithView(
m_selfVideoWindow,
ZoomVideoSDKVideoAspect_PanAndScan,
ZoomVideoSDKResolution_Auto
);
if (err == ZoomVideoSDKErrors_Success) {
std::cout << "Self video subscribed" << std::endl;
} else {
std::cout << "Self video subscribe failed: " << err << std::endl;
}
}
UpdateLayout();
}
void VideoCanvasManager::SubscribeToUser(IZoomVideoSDKUser* user) {
if (!user) return;
// Skip if already subscribed
if (m_userWindows.find(user) != m_userWindows.end()) {
return;
}
// Check if video is on
IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe();
if (!pipe || !pipe->getVideoStatus().isOn) {
return;
}
// Create window for this user
HWND hwnd = CreateVideoWindow();
m_userWindows[user] = hwnd;
// Subscribe to canvas
IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas();
if (canvas) {
ZoomVideoSDKErrors err = canvas->subscribeWithView(
hwnd,
ZoomVideoSDKVideoAspect_PanAndScan,
ZoomVideoSDKResolution_Auto
);
if (err == ZoomVideoSDKErrors_Success) {
m_userCanvases[user] = canvas;
std::wcout << L"Subscribed to: " << user->getUserName() << std::endl;
} else {
std::wcout << L"Subscribe failed for: " << user->getUserName()
<< L" error: " << err << std::endl;
// Cleanup on failure
DestroyVideoWindow(hwnd);
m_userWindows.erase(user);
}
}
UpdateLayout();
}
void VideoCanvasManager::UnsubscribeFromUser(IZoomVideoSDKUser* user) {
if (!user) return;
auto it = m_userWindows.find(user);
if (it == m_userWindows.end()) {
return;
}
HWND hwnd = it->second;
// Unsubscribe from canvas
auto canvasIt = m_userCanvases.find(user);
if (canvasIt != m_userCanvases.end()) {
IZoomVideoSDKCanvas* canvas = canvasIt->second;
if (canvas) {
canvas->unSubscribeWithView(hwnd);
}
m_userCanvases.erase(canvasIt);
}
// Destroy window
DestroyVideoWindow(hwnd);
m_userWindows.erase(it);
std::wcout << L"Unsubscribed from: " << user->getUserName() << std::endl;
UpdateLayout();
}
void VideoCanvasManager::UnsubscribeAll() {
// Unsubscribe self
if (m_selfCanvas && m_selfVideoWindow) {
m_selfCanvas->unSubscribeWithView(m_selfVideoWindow);
DestroyVideoWindow(m_selfVideoWindow);
m_selfVideoWindow = nullptr;
m_selfCanvas = nullptr;
}
// Unsubscribe all remote users
for (auto& pair : m_userCanvases) {
IZoomVideoSDKCanvas* canvas = pair.second;
IZoomVideoSDKUser* user = pair.first;
auto windowIt = m_userWindows.find(user);
if (windowIt != m_userWindows.end() && canvas) {
canvas->unSubscribeWithView(windowIt->second);
}
}
for (auto& pair : m_userWindows) {
DestroyVideoWindow(pair.second);
}
m_userCanvases.clear();
m_userWindows.clear();
}
void VideoCanvasManager::UpdateLayout() {
RECT parentRect;
GetClientRect(m_parentWindow, &parentRect);
int totalVideos = (m_selfVideoWindow ? 1 : 0) + m_userWindows.size();
if (totalVideos == 0) return;
// Calculate grid dimensions
int cols = (int)ceil(sqrt((double)totalVideos));
int rows = (int)ceil((double)totalVideos / cols);
int cellWidth = (parentRect.right - parentRect.left) / cols;
int cellHeight = (parentRect.bottom - parentRect.top) / rows;
int index = 0;
// Position self video (top-left)
if (m_selfVideoWindow) {
int row = index / cols;
int col = index % cols;
SetWindowPos(m_selfVideoWindow, NULL,
col * cellWidth, row * cellHeight,
cellWidth, cellHeight,
SWP_NOZORDER);
index++;
}
// Position remote videos
for (auto& pair : m_userWindows) {
HWND hwnd = pair.second;
int row = index / cols;
int col = index % cols;
SetWindowPos(hwnd, NULL,
col * cellWidth, row * cellHeight,
cellWidth, cellHeight,
SWP_NOZORDER);
index++;
}
}
```
### Using in Delegate
```cpp
class MyDelegate : public IZoomVideoSDKDelegate {
private:
VideoCanvasManager* m_videoManager;
public:
MyDelegate(HWND parentWindow, IZoomVideoSDK* sdk) {
m_videoManager = new VideoCanvasManager(parentWindow, sdk);
}
~MyDelegate() {
delete m_videoManager;
}
void onSessionJoin() override {
// Subscribe to self video
m_videoManager->SubscribeToSelf();
}
void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper,
IVideoSDKVector<IZoomVideoSDKUser*>* userList) override {
IZoomVideoSDKUser* myself = g_sdk->getSessionInfo()->getMyself();
for (int i = 0; i < userList->GetCount(); i++) {
IZoomVideoSDKUser* user = userList->GetItem(i);
if (user == myself) continue;
ZoomVideoSDKVideoStatus status = user->GetVideoPipe()->getVideoStatus();
if (status.isOn) {
m_videoManager->SubscribeToUser(user);
} else {
m_videoManager->UnsubscribeFromUser(user);
}
}
}
void onUserLeave(IZoomVideoSDKUserHelper* helper,
IVideoSDKVector<IZoomVideoSDKUser*>* userList) override {
for (int i = 0; i < userList->GetCount(); i++) {
m_videoManager->UnsubscribeFromUser(userList->GetItem(i));
}
}
void onSessionLeave() override {
m_videoManager->UnsubscribeAll();
}
// ... other callbacks
};
```
---
## Error Handling
### Subscribe Fail Callback
```cpp
void onVideoCanvasSubscribeFail(ZoomVideoSDKSubscribeFailReason reason,
IZoomVideoSDKUser* user, void* handle) override {
std::wcout << L"Subscribe failed for: " << user->getUserName()
<< L" reason: " << reason << std::endl;
switch (reason) {
case ZoomVideoSDKSubscribeFailReason_HasSubscribe1080POr720P:
std::cout << "Already have a 1080p/720p subscription" << std::endl;
break;
case ZoomVideoSDKSubscribeFailReason_HasSubscribeExceededLimit:
std::cout << "Subscription limit exceeded" << std::endl;
break;
case ZoomVideoSDKSubscribeFailReason_TooFrequentCall:
std::cout << "Calling too frequently - add Sleep(200)" << std::endl;
break;
}
}
```
### Subscribe Fail Reasons
| Code | Reason | Solution |
|------|--------|----------|
| 0 | None | - |
| 1 | HasSubscribe1080POr720P | Already have HD subscription |
| 2 | HasSubscribeTwo720P | Max 2x 720p subscriptions |
| 3 | HasSubscribeExceededLimit | Too many subscriptions |
| 4 | HasSubscribeTwoShare | Max 2 share subscriptions |
| 5 | HasSubscribeVideo1080POr720PAndOneShare | Limit reached |
| 6 | TooFrequentCall | Add `Sleep(200)` between calls |
---
## Aspect Ratio Options
| Option | Behavior | Use Case |
|--------|----------|----------|
| `ZoomVideoSDKVideoAspect_Original` | Letterbox/pillarbox | Show full video |
| `ZoomVideoSDKVideoAspect_FullFilled` | Fill, may crop | Full coverage |
| `ZoomVideoSDKVideoAspect_PanAndScan` | Smart crop | Balanced (recommended) |
| `ZoomVideoSDKVideoAspect_LetterBox` | Black bars | Preserve aspect |
---
## Resolution Options
| Option | Resolution | Use Case |
|--------|------------|----------|
| `ZoomVideoSDKResolution_90P` | 160x90 | Thumbnails |
| `ZoomVideoSDKResolution_180P` | 320x180 | Small previews |
| `ZoomVideoSDKResolution_360P` | 640x360 | Standard |
| `ZoomVideoSDKResolution_720P` | 1280x720 | HD |
| `ZoomVideoSDKResolution_1080P` | 1920x1080 | Full HD |
| `ZoomVideoSDKResolution_Auto` | SDK chooses | Recommended |
---
## Related Documentation
- [Canvas vs Raw Data](../concepts/canvas-vs-raw-data.md) - Rendering approach comparison
- [Raw Video Capture](raw-video-capture.md) - For custom processing
- [Singleton Hierarchy](../concepts/singleton-hierarchy.md) - Canvas/Pipe navigation
- [Common Issues](../troubleshooting/common-issues.md) - Error codes
---
**TL;DR**: Subscribe to self in `onSessionJoin`, subscribe to remote users in `onUserVideoStatusChanged` (when `status.isOn == true`), unsubscribe in `onUserLeave`.