Skill 126 · Zoom Meeting SDK Windows
Subchapter 126.4
concepts/custom-ui-architecture.mdMarkdown9 KBView on GitHub
Skill: Zoom Meeting SDK (Windows)
Category: Concepts :
Custom UI mode lets you create your OWN meeting window instead of the SDK’s default meeting UI. The SDK renders video into your window using Direct3D, but you control all layout, window management, and UI elements.
This is NOT “HWND hijacking.” The SDK creates child windows inside your parent window and renders into those using its own D3D pipeline. Your window and WndProc remain untouched.
Set ENABLE_CUSTOMIZED_UI_FLAG during SDK initialization:
InitParam initParam;
initParam.strWebDomain = L"https://zoom.us";
initParam.emLanguageID = LANGUAGE_English;
// CRITICAL: Enable Custom UI mode
initParam.obConfigOpts.optionalFeatures = ENABLE_CUSTOMIZED_UI_FLAG;
SDKError err = InitSDK(initParam);Without this flag, the SDK creates its own default meeting window. With it, the SDK creates NO UI — you must provide everything.
Your Window (WS_OVERLAPPEDWINDOW) — you own this, your WndProc
|
+-- [SDK Child HWND] — created internally by CreateVideoContainer()
| - SDK owns the WndProc
| - D3D11 swap chain bound to this child HWND
| - Composites all video onto one surface
|
+-- VideoElement: Active (logical RECT region, NOT a window)
+-- VideoElement: Normal0 (logical RECT region, NOT a window)
+-- VideoElement: Normal1 (logical RECT region, NOT a window)
+-- ...
|
+-- [SDK Child HWND for Share] — created by CreateShareRender()
| - Separate D3D surface for screen share content
| - Requires HandleWindowsMoveMsg() for DWM resyncCreateVideoContainer(hParentWnd, rc) — SDK creates a child HWND inside your parent. You never see or manage this child HWND directly.
Video elements are NOT separate windows — they are logical render regions within a single D3D surface. SetPos(RECT) tells the SDK’s compositor where to place each video texture within the container.
Your app does ZERO rendering — no WM_PAINT, no GDI calls, no BitBlt. The SDK handles 100% of video drawing internally.
The SDK supports multiple rendering backends, configurable via InitParam.renderOpts.videoRenderMode:
enum ZoomSDKVideoRenderMode {
ZoomSDKVideoRenderMode_None = 0, // Auto (default)
ZoomSDKVideoRenderMode_Auto,
ZoomSDKVideoRenderMode_D3D11EnableFLIP, // D3D11 with DXGI flip model (best)
ZoomSDKVideoRenderMode_D3D11, // D3D11 standard
ZoomSDKVideoRenderMode_D3D9, // D3D9 fallback
ZoomSDKVideoRenderMode_GDI, // GDI software fallback (VMs)
};Hierarchy: D3D11 FLIP > D3D11 > D3D9 > GDI
The D3D11 FLIP model uses DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL — this requires a dedicated child HWND (further confirming the child window architecture).
The SDK’s child HWND has its own WndProc that intercepts input messages. Your parent window’s WndProc never sees mouse/keyboard events that land on the video area. The SDK forwards them back to you through callbacks:
Forwarded messages:
WM_MOUSEMOVE, WM_MOUSEENTER, WM_MOUSELEAVE,
WM_LBUTTONDOWN, WM_LBUTTONUP, WM_RBUTTONUP,
WM_LBUTTONDBLCLK, WM_KEYDOWNIf you need to handle clicks on the video (e.g., click a participant to select them), you must handle them in onWindowMsgNotification, not in your parent WndProc.
// On ICustomizedShareRender only
virtual SDKError HandleWindowsMoveMsg() = 0;When using Direct3D, the swap chain’s presentation is tied to the window’s screen position via DWM (Desktop Window Manager) composition. When the parent window moves:
HandleWindowsMoveMsg() tells the SDK to force re-present at the new coordinates.
This only exists on ICustomizedShareRender, not on ICustomizedVideoContainer — the video container likely handles this internally or uses the FLIP model which doesn’t have this issue.
The ICustomizedUIMgr interface has a HasLicense() method. The official SDK demo checks it as a hard gate:
SDKError err = m_pCustomUIMgr->HasLicense();
if (err != SDKERR_SUCCESS) {
// Demo aborts here
}In practice, modern SDK licenses may include Custom UI by default. It’s safe to log a warning but continue if it fails — the SDK will return errors on actual API calls if the license is truly missing.
CreateCustomizedUIMgr(&pMgr) — global, creates the managerpMgr->SetEvent(&listener) — register for destroy notificationspMgr->CreateVideoContainer(&pContainer, hParentWnd, rc) — creates the rendering surfacepContainer->SetEvent(&containerListener) — register for layout/render eventspContainer->Show() / SetBkColor() — configure appearancepContainer->CreateVideoElement(&pElement, type) — create render slotspContainer->DestroyAllVideoElement() — remove all render slotspMgr->DestroyVideoContainer(pContainer) — destroy the rendering surfacepMgr->DestroyShareRender(pShareRender) — destroy share render if createdDestroyCustomizedUIMgr(pMgr) — global cleanupStart() to begin, Stop() to pauseIVideoRenderElement* pElement = nullptr;
pContainer->CreateVideoElement(&pElement, VideoRenderElement_ACTIVE);
IActiveVideoRenderElement* pActive = dynamic_cast<IActiveVideoRenderElement*>(pElement);
pActive->SetPos(rect);
pActive->Show();
pActive->Start();Subscribe(userId) to bind it to a userSetResolution()IVideoRenderElement* pElement = nullptr;
pContainer->CreateVideoElement(&pElement, VideoRenderElement_NORMAL);
INormalVideoRenderElement* pNormal = dynamic_cast<INormalVideoRenderElement*>(pElement);
pNormal->Subscribe(userId);
pNormal->SetResolution(VideoRenderResolution_360p);
pNormal->SetPos(rect);
pNormal->Show();Video element positions are RECTs relative to the container’s client area, not screen coordinates.
When the container receives onLayoutNotification(RECT wnd_client_rect), you should recalculate and re-apply all element positions:
void OnLayoutNotification(RECT clientRect) {
int width = clientRect.right - clientRect.left;
int height = clientRect.bottom - clientRect.top;
// Active speaker: top 70%
RECT activeRect = { 0, 0, width, (int)(height * 0.7) };
pActiveElement->SetPos(activeRect);
// Gallery: bottom 30%, evenly split horizontally
int galleryTop = (int)(height * 0.7);
int elemWidth = width / galleryCount;
for (int i = 0; i < galleryCount; i++) {
RECT r = { i * elemWidth, galleryTop, (i+1) * elemWidth, height };
normalElements[i]->SetPos(r);
}
}The share render is a separate SDK child window for displaying screen shares:
pMgr->CreateShareRender(&pShareRender, hParentWnd, rc);
pShareRender->SetEvent(&shareListener);
pShareRender->Hide(); // Hidden until someone shares
// When sharing starts (via onSharingSourceNotification):
pShareRender->SetShareSourceID(shareSourceID);
pShareRender->Show();
pShareRender->SetViewMode(CSM_FULLFILL); // or CSM_LETTER_BOXzVideoUI.dll, zVideoApp.dll, zVideoAppFrame.dll — core video renderingavcodec_zm-59.dll, avutil_zm-57.dll, swscale_zm-6.dll — FFmpeg decodersclDNN64.dll, mkldnn.dll — Intel DNN for AI features (background blur, etc.)See also: