# 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* 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* 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 #include #include #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 m_userWindows; std::map m_userCanvases; }; ``` ### VideoCanvasManager.cpp ```cpp #include "VideoCanvasManager.h" #include 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* 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* 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`.