13 KiB
13 KiB
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
// 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
#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
#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
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
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 - Rendering approach comparison
- Raw Video Capture - For custom processing
- Singleton Hierarchy - Canvas/Pipe navigation
- Common Issues - Error codes
TL;DR: Subscribe to self in onSessionJoin, subscribe to remote users in onUserVideoStatusChanged (when status.isOn == true), unsubscribe in onUserLeave.