Files
2026-07-13 12:20:06 +08:00

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


TL;DR: Subscribe to self in onSessionJoin, subscribe to remote users in onUserVideoStatusChanged (when status.isOn == true), unsubscribe in onUserLeave.