
Build Zoom Video Sdk App
- 1.4k installs
- 23.1k repo stars
- Updated July 28, 2026
- anthropics/knowledge-work-plugins
build-zoom-video-sdk-app is an agent skill for reference skill for zoom video sdk. use after routing to a custom-session workflow when the user needs full control over the video experience rather than an actual zoom meet
About
The build-zoom-video-sdk-app skill is designed for reference skill for Zoom Video SDK. Use after routing to a custom-session workflow when the user needs full control over the video experience rather than an actual Zoom meeting. /build-zoom-video-sdk-app Background reference for fully custom video-session products. Prefer plan-zoom-product first when the boundary between Meeting SDK and Video SDK is still unclear. Invoke when the user asks about build zoom video sdk app or related SKILL.md workflows.
- Do not switch to REST meeting endpoints for Video SDK join flows.
- Video SDK does not use Meeting IDs, join_url, or Meeting SDK join payload fields (meetingNumber, passWord).
- Zoom Video SDK credentials from Marketplace.
- SDK Key and Secret.
- Web development environment.
Build Zoom Video Sdk App by the numbers
- 1,410 all-time installs (skills.sh)
- +81 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #261 of 1,896 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
build-zoom-video-sdk-app capabilities & compatibility
- Capabilities
- do not switch to rest meeting endpoints for vide · video sdk does not use meeting ids, join_url, or · zoom video sdk credentials from marketplace · sdk key and secret
- Use cases
- frontend
What build-zoom-video-sdk-app says it does
Reference skill for Zoom Video SDK. Use after routing to a custom-session workflow when the user needs full control over the video experience rather than an actual Zoom meeting.
Reference skill for Zoom Video SDK. Use after routing to a custom-session workflow when the user needs full control over the video experience rather than an act
npx skills add https://github.com/anthropics/knowledge-work-plugins --skill build-zoom-video-sdk-appAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.4k |
|---|---|
| repo stars | ★ 23.1k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | anthropics/knowledge-work-plugins ↗ |
How do I reference skill for zoom video sdk. use after routing to a custom-session workflow when the user needs full control over the video experience rather than an actual zoom meeting?
Reference skill for Zoom Video SDK. Use after routing to a custom-session workflow when the user needs full control over the video experience rather than an actual Zoom meeting.
Who is it for?
Developers using build zoom video sdk app workflows documented in SKILL.md.
Skip if: Skip when the task falls outside build-zoom-video-sdk-app scope or needs a different stack.
When should I use this skill?
User asks about build zoom video sdk app or related SKILL.md workflows.
What you get
Completed build-zoom-video-sdk-app workflow with documented commands, files, and expected deliverables.
- Android Video SDK integration scaffold
- Session join and media control flow
Files
Zoom Video SDK (Android)
Use this skill when building Android apps with custom real-time video sessions.
Start Here
1. android.md 2. concepts/lifecycle-workflow.md 3. concepts/architecture.md 4. examples/session-join-pattern.md 5. scenarios/high-level-scenarios.md 6. references/android-reference-map.md 7. references/environment-variables.md 8. references/versioning-and-compatibility.md 9. troubleshooting/common-issues.md
Key Sources
- Docs: https://developers.zoom.us/docs/video-sdk/android/
- API reference: https://marketplacefront.zoom.us/sdk/custom/android/index.html
- Broader guide: ../SKILL.md
Operations
- RUNBOOK.md - 5-minute preflight and debugging checklist.
Android Video SDK Overview
What this platform skill is for
- Building fully custom Android video session UI (not Zoom Meeting UI)
- Managing join/leave and participant state via Video SDK events
- Handling camera, mic, share, chat, command, and optional raw data paths
Primary implementation path
1. Backend generates short-lived Video SDK token using Video SDK Key/Secret. 2. Android initializes SDK and joins a session by sessionName + token. 3. App binds SDK events to UI state (user join/leave, video/audio/share changes). 4. App starts/stops media explicitly and cleans up SDK resources on leave.
Prerequisites
- Android Studio + supported Gradle/AGP stack
- Video SDK Android package (
mobilertc.aar) - Backend token endpoint for Video SDK JWT generation
- Camera/microphone permissions flow and runtime handling
Important notes
- Video SDK session auth is token-based and server-generated.
- Do not use Meeting SDK payload fields (
meetingNumber,passWord) in Video SDK flows. - Keep token generation and key/secret handling server-side only.
Source links
- Docs: https://developers.zoom.us/docs/video-sdk/android/
- API reference: https://marketplacefront.zoom.us/sdk/custom/android/index.html
Android Architecture Concept
flowchart LR
UI[Android UI Layer] --> VM[Session ViewModel / Controller]
VM --> SDK[Zoom Video SDK Android]
VM --> API[Token API]
API --> Signer[Server-side JWT Signer]
Signer --> Market[Video SDK App Credentials]
SDK --> Events[Participant/Media Events]
Events --> UIDesign guidance
- Keep token creation strictly backend-side.
- Keep SDK calls in a session controller or ViewModel boundary.
- Drive UI from SDK event streams to avoid stale participant state.
- Treat join/start-media/leave as explicit state transitions.
Android Lifecycle Workflow
flowchart TD
A[Fetch token from backend] --> B[Initialize Video SDK]
B --> C[Join session]
C --> D[Bind event listeners]
D --> E[Start local media]
E --> F[Handle remote user/media events]
F --> G[Leave session]
G --> H[Cleanup listeners and media]Operational sequence
1. Request token from backend using app auth context. 2. Initialize SDK and register core listeners. 3. Join session with session name/topic, display name, and token. 4. Start local camera/mic only after successful join. 5. Render remote users when events indicate media state changes. 6. On leave/disconnect, unsubscribe listeners and release resources.
Android Session Join Pattern
suspend fun joinVideoSession(sessionName: String, userName: String) {
val token = tokenApi.getVideoSdkToken(sessionName, userName)
val initResult = videoSdk.initialize(initParams)
check(initResult.isSuccess) { "SDK init failed" }
videoSdk.addListener(sessionListener)
val joinResult = videoSdk.joinSession(
sessionName = sessionName,
userName = userName,
token = token
)
check(joinResult.isSuccess) { "Join failed" }
videoHelper.startVideo()
audioHelper.startAudio()
}Notes
- Start local media after join success.
- Keep camera/mic permissions and denial handling explicit.
Android Reference Map
Docs anchors
- Getting started and integration: https://developers.zoom.us/docs/video-sdk/android/
- API surface index: https://marketplacefront.zoom.us/sdk/custom/android/index.html
API areas to focus on
- Session lifecycle and join context
- Audio/video helpers
- Participant/user helpers
- Share/chat/command channels
- Raw data interfaces and delegates
Crawl summary
- Reference pages crawled: 650
- Docs pages crawled: 23 (22 markdown files persisted)
Android Environment Variables
| Variable | Required | Used for | Where to find |
|---|---|---|---|
ZOOM_VIDEO_SDK_KEY | Yes | Video SDK credential pair | Zoom Marketplace -> Video SDK app -> App Credentials |
ZOOM_VIDEO_SDK_SECRET | Yes (server only) | Token/JWT signing | Zoom Marketplace -> Video SDK app -> App Credentials |
VIDEO_SDK_TOKEN_ENDPOINT | Yes | Android app token fetch URL | Your backend deployment config |
VIDEO_SDK_SESSION_NAME | Runtime | Session/topic id | Generated by your app workflow |
VIDEO_SDK_USER_NAME | Runtime | Display name in session | Generated from app user profile |
Runtime-only values
VIDEO_SDK_TOKENshould be short-lived and generated server-side.
Android Versioning and Compatibility
Package evidence
- SDK package:
zoom-video-sdk-android-2.5.0.zip - Internal version:
v2.5.0 (37500) - Package includes
mobilertc.aarand sample modules.
Compatibility notes
- Keep app and backend token logic aligned with the same Video SDK release family.
- Expect method additions/renames across releases; pin SDK version per release train.
- Revalidate proguard/R8 rules and permissions whenever upgrading.
Contradictions or drift to watch
- Changelog points to external support portal pages, not in-package detailed notes.
- Crawl of docs subpages may miss dynamic pages; confirm against official docs at build time.
Video SDK Android 5-Minute Preflight Runbook
Use this before deep debugging.
Skill Doc Standard Note
- Skill entrypoint is
SKILL.md. - This runbook is an operational convention (recommended), not a required skill file.
- SDK/API names can drift by version; validate current names against docs/raw-docs before release.
1) Confirm Integration Surface
- Confirm this is a Video SDK custom session flow for Android (not Meeting SDK).
- Verify UI/state are driven by session events, not meeting semantics.
- Wrapper platforms require JS/native bridge synchronization checks.
2) Confirm Required Credentials
- Video SDK app credentials (SDK Key/Secret) stored server-side.
- Backend-generated session JWT token.
- Session fields (
sessionName,userName, role type) resolved before join.
3) Confirm Lifecycle Order
1. Initialize SDK client/context and register event listeners. 2. Generate/fetch session token from backend. 3. Join session and establish media streams. 4. Handle participant/media/control events during active session.
4) Confirm Event/State Handling
- Keep participant state keyed by user/session IDs.
- Reconcile subscribe/unsubscribe transitions for video/audio/share streams.
- Treat reconnect and device-change events as first-class state transitions.
5) Confirm Cleanup + Upgrade Posture
- Leave/end session and release helper/client resources.
- Remove listeners to avoid duplicate callbacks on rejoin.
- Re-check SDK version compatibility before deployment updates.
6) Quick Probes
- Token issuance and join flow succeed once end-to-end.
- Audio/video publish-subscribe operations complete with expected callbacks.
- Leave/rejoin works without leaked listener or stream state.
7) Fast Decision Tree
- Join fails immediately -> invalid/expired token or session field mismatch.
- Media state stuck -> listener binding/order issue or permission/device problem.
- Inconsistent behavior after update -> wrapper/native SDK version mismatch.
8) Source Checkpoints
Official docs
- https://developers.zoom.us/docs/video-sdk/android/
- https://marketplacefront.zoom.us/sdk/custom/android/index.html
Raw docs in repo
tools/zoom-crawler/raw-docs/developers.zoom.us/docs/video-sdk/android/tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/video-sdk/android/
Android High-Level Scenarios
1. Telehealth mobile room
- Patient and provider join by appointment topic.
- App renders role-specific controls (mute/video/share).
2. Creator live session app
- Host/co-host moderation controls with hand-raise queue.
- Event-driven participant tile updates.
3. Support escalation with media
- Customer support app escalates from chat to live video.
- Session context and metadata carried from CRM record.
4. Field operations collaboration
- On-site staff share camera feed to remote experts.
- Capture optional snapshots/logging using event timeline.
5. Education small-group Android client
- Dynamic layouts for active speaker and breakout-like flows.
- Local recording policy and consent state surfaced in UI.
Android Common Issues
Token invalid / join fails
- Verify token issued by backend with correct Video SDK key/secret.
- Confirm session name, role claims, and expiration window.
Local video/audio not starting
- Check runtime permissions and OS-level privacy controls.
- Ensure start media calls happen after join success.
Remote tiles not updating
- Validate event listener registration order.
- Drive tile updates from participant/media events, not static snapshots.
Build problems after SDK upgrade
- Recheck dependency conflicts and packaging options.
- Revisit keep rules and ABI packaging configuration.
High-Level Scenarios
1. Custom mobile collaboration room
- Teams join named sessions in branded Flutter UI.
- Use chat, command channel, and share helpers for collaboration.
2. Telehealth or support session app
- Session token issued by backend per appointment.
- App controls audio/video with strict permission and consent flows.
3. Live class / cohort experience
- Instructor-hosted session with participant management.
- Optional transcription and cloud recording control.
4. Event companion app
- Lightweight mobile client for live events.
- Uses live stream/session status and quality telemetry for UX adaptation.
Lifecycle Workflow
Recommended execution flow for Flutter Video SDK integrations:
1. Initialize SDK with InitConfig. 2. Register event listener(s) and app state handlers. 3. Join session with signed token. 4. Start audio/video/share flows through helpers. 5. Handle participant/session events and quality telemetry. 6. Leave session and run cleanup.
Sequence Diagram
Flutter App
-> initSdk(initConfig)
-> register ZoomVideoSdkEventListener
-> joinSession(joinConfig)
-> use audio/video/share/chat helpers
-> handle EventType callbacks
-> leaveSession
-> cleanupSDK Architecture Pattern
Flutter wrapper exposes helper-centric APIs and event constants.
Layers
- Core platform wrapper (
ZoomVideoSdk, platform channel). - Session object and user/session state models.
- Domain helpers: audio/video/chat/share/recording/live transcription/phone/subsession.
- Event channel via
ZoomVideoSdkEventListenerandEventTypeconstants.
Pattern
1. Resolve helper/session object. 2. Invoke async method. 3. Process event callback and state transitions. 4. Update UI/store from event payload.
Design guidance
- Keep event-to-state mapping centralized.
- Treat SDK enums/errors as versioned contracts.
- Isolate helper calls behind adapter methods for easier upgrades.
Event Handling Pattern
Bind the SDK event listener early and route events through one reducer/state manager.
Pattern
1. Register listener before or immediately after join. 2. Map EventType values to handlers. 3. Keep handler side-effects minimal and predictable.
Typical events to prioritize
- session join/leave
- user join/leave/video/audio status
- share status
- error and subscribe-fail events
- chat/command channel events
Minimum realtime media UX checklist
For practical 2-device validation, include these controls and views:
- join / leave session
- local mic mute-unmute
- local video on-off
- camera switch
- speaker toggle
- remote participant video tiles
- event log panel (timestamped)
Remote media rendering pattern
- On session join, fetch current users and render local preview + remote tiles.
- On
onUserJoinandonUserLeave, refresh participant list and rerender. - On
onUserVideoStatusChangedandonUserAudioStatusChanged, update UI state from events (avoid optimistic-only state). - Keep one code path for participant list refresh to avoid state drift.
Session Join Pattern
Flow
1. Backend signs Video SDK session token. 2. App creates JoinSessionConfig. 3. App calls joinSession. 4. UI reacts to session/user event callbacks.
Minimal shape
final joinConfig = JoinSessionConfig(
sessionName: 'my-session',
token: '<VIDEO_SDK_JWT>',
userName: 'Mobile User',
audioOptions: {'connect': true, 'mute': true},
videoOptions: {'localVideoOn': true},
);
await zoom.joinSession(joinConfig);Setup Guide
0. Flutter and Android tooling baseline (Windows)
Install and verify Flutter before SDK wiring.
# install location example
git clone https://github.com/flutter/flutter.git -b stable --depth 1 C:\users\dreamtcs\tools\flutter
# verify
C:\users\dreamtcs\tools\flutter\bin\flutter.bat --version
C:\users\dreamtcs\tools\flutter\bin\flutter.bat doctor -vIf Flutter is not on PATH in your shell, use the full flutter.bat path for all commands.
1. Install package
dependencies:
flutter_zoom_videosdk: ^<version>flutter pub get2. Initialize SDK
final zoom = ZoomVideoSdk();
await zoom.initSdk(InitConfig(
domain: 'zoom.us',
enableLog: true,
));If init fails without a clear error string, wrap initSdk with PlatformException handling and surface code/message/details in UI logs.
3. Core prerequisites
- Flutter and Dart toolchain compatible with wrapper version.
- iOS/Android native setup aligned with package expectations.
- Backend service for Video SDK JWT generation.
4. Android host app requirements
- Set
minSdkto at least28in the app module. - Add runtime permissions for camera and mic (plus Bluetooth connect where applicable).
- If Java compile fails with
ZoomVideoSDKDelegate not found, add the Zoom Android artifacts in the app module dependencies:
dependencies {
implementation("us.zoom.videosdk:zoomvideosdk-core:2.3.10")
implementation("us.zoom.videosdk:zoomvideosdk-videoeffects:2.3.10")
implementation("us.zoom.videosdk:zoomvideosdk-annotation:2.3.10")
implementation("us.zoom.videosdk:zoomvideosdk-whiteboard:2.3.10")
implementation("us.zoom.videosdk:zoomvideosdk-broadcast-streaming:2.3.10")
}5. ADB device setup (physical Android recommended)
When emulator startup is unstable, run on a real phone:
# pairing (from Wireless debugging)
adb pair <ip:pair-port> <pair-code>
# connect (from mDNS connect port)
adb connect <ip:connect-port>
adb devices -lThen run the app:
flutter run -d <device-id> --debug --no-resident6. Security baseline
- Never embed SDK secret in app bundle.
- Issue short-lived session tokens server-side.
- Validate all session join parameters before SDK call.
Flutter Reference Index
Primary sources used:
raw-docs/developers.zoom.us/docs/video-sdk/flutter/*.mdraw-docs/marketplacefront.zoom.us/sdk/video-sdk/flutter/index.mdraw-docs/marketplacefront.zoom.us/sdk/video-sdk/flutter/...(crawled API reference set)- local package archive analysis (flutter Video SDK zip)
Reference API base:
- https://marketplacefront.zoom.us/sdk/custom/flutter/index.html
Module Map
Core
native_zoom_videosdk(session join/leave/init)native_zoom_videosdk_event_listener(EventType constants)
Media and session helpers
- audio/video/share helpers
- chat helper and command channel
- user/session helpers
- recording/live stream/live transcription helpers
- subsession, whiteboard, virtual background, phone/CRC helpers
Models / enums
- status enums (recording, live stream, network, device)
- error enums and failure reasons
- session quality/statistics objects
Official Sources
Primary sources used for this skill:
- Zoom docs: https://developers.zoom.us/docs/video-sdk/flutter/
- Zoom Flutter API reference: https://marketplacefront.zoom.us/sdk/custom/flutter/index.html
- Zoom Flutter quickstart repo: https://github.com/zoom/videosdk-flutter-quickstart
- Local package archive analysis (Flutter Video SDK zip)
Crawled snapshots:
skills/raw-docs/developers.zoom.us/docs/video-sdk/flutter/skills/raw-docs/marketplacefront.zoom.us/sdk/video-sdk/flutter/
Video SDK Flutter 5-Minute Preflight Runbook
Use this before deep debugging.
Skill Doc Standard Note
- Skill entrypoint is
SKILL.md. - This runbook is an operational convention (recommended), not a required skill file.
- SDK/API names can drift by version; validate current names against docs/raw-docs before release.
1) Confirm Integration Surface
- Confirm this is a Video SDK custom session flow for Flutter (not Meeting SDK).
- Verify UI/state are driven by session events, not meeting semantics.
- Wrapper platforms require JS/native bridge synchronization checks.
2) Confirm Required Credentials
- Video SDK app credentials (SDK Key/Secret) stored server-side.
- Backend-generated session JWT token.
- Session fields (
sessionName,userName, role type) resolved before join.
3) Confirm Lifecycle Order
1. Initialize SDK client/context and register event listeners. 2. Generate/fetch session token from backend. 3. Join session and establish media streams. 4. Handle participant/media/control events during active session.
4) Confirm Event/State Handling
- Keep participant state keyed by user/session IDs.
- Reconcile subscribe/unsubscribe transitions for video/audio/share streams.
- Treat reconnect and device-change events as first-class state transitions.
5) Confirm Cleanup + Upgrade Posture
- Leave/end session and release helper/client resources.
- Remove listeners to avoid duplicate callbacks on rejoin.
- Re-check SDK version compatibility before deployment updates.
6) Quick Probes
- Token issuance and join flow succeed once end-to-end.
- Audio/video publish-subscribe operations complete with expected callbacks.
- Leave/rejoin works without leaked listener or stream state.
7) Fast Decision Tree
- Join fails immediately -> invalid/expired token or session field mismatch.
- Media state stuck -> listener binding/order issue or permission/device problem.
- Inconsistent behavior after update -> wrapper/native SDK version mismatch.
8) Source Checkpoints
Official docs
- https://developers.zoom.us/docs/video-sdk/flutter/
- https://marketplacefront.zoom.us/sdk/custom/flutter/index.html
Raw docs in repo
raw-docs/developers.zoom.us/docs/video-sdk/flutter/raw-docs/marketplacefront.zoom.us/sdk/video-sdk/flutter/
Common Issues
Join fails or stalls
- Validate JWT token generation and expiry.
- Ensure
sessionNameand join config fields are valid. - Verify SDK init completed before
joinSession.
SDK initialization fails
- Confirm
domain: "zoom.us"is used inInitConfig. - Request runtime permissions before init/join (camera, microphone, Bluetooth connect on newer Android).
- Catch
PlatformExceptionfrominitSdkand logcode,message, anddetails. - If UI shows contradictory status (for example success text treated as failure), normalize success checks against both SDK constants and returned success strings.
Event callbacks not handled consistently
- Register event listener before critical actions.
- Avoid scattered listeners in multiple widgets.
- Centralize callback dispatch into one state path.
Media controls appear inconsistent
- Check permission states (camera/mic/storage where needed).
- Re-check helper availability after session reconnect.
- Use event-driven status updates, not optimistic UI assumptions.
Platform-specific issues
- Confirm iOS/Android native setup and package versions match.
- Rebuild clean when plugin/native versions change.
Android compile error: ZoomVideoSDKDelegate not found
Symptom:
- Build fails in generated plugin registration with missing
us.zoom.sdk.ZoomVideoSDKDelegate.
Fix:
- Add Zoom Video SDK Android dependencies in the host app module (
android/app/build.gradle.ktsor Gradle Groovy equivalent), even when using local plugin path dependency. - Rebuild with
flutter clean,flutter pub get, andflutter build apk --debug.
ADB connection issues (physical device)
- If
adb devicesis empty, verify USB/Wireless debugging is enabled and phone trusts host. - For wireless debugging, complete both steps:
adb pairthenadb connect. - If connection drops, rerun
adb connect <ip:connect-port>and verify withadb devices -l.
Deprecated and Contradictions
Observed from freshly crawled docs/reference plus local package archive:
1. Prompt reference was missing/invalid
- Input prompt had
reference: o. - Effective Flutter reference source was discovered from docs links and crawled from
sdk/custom/flutter/index.html.
2. Version metadata mismatch in package archive
- Archive filename indicates
2.4.0. pubspec.yamland changelog content show2.3.10references.
Action: treat archive naming and package metadata independently; verify actual package version fields before rollout.
3. Docs/package age drift indicators
- Some docs mention package versions and setup patterns that may lag current wrappers.
- API reference is very large and highly version-sensitive (helpers, enums, errors).
Action: pin tested versions and maintain an internal compatibility matrix.
4. Crawl completeness note
- Reference crawl completed with one page-level error among a very large set.
- Use targeted recrawl for missing pages if a specific symbol cannot be found.
Version Drift
Flutter wrapper, native SDKs, and docs can drift independently.
Upgrade checklist
1. Compare wrapper version metadata and changelog with actual package content. 2. Re-validate API enum names and helper method signatures. 3. Re-test lifecycle: init -> join -> media -> leave -> cleanup. 4. Re-check event constants and error handling mappings. 5. Re-run smoke tests for chat/share/recording/transcription if used.
iOS Architecture Concept
flowchart LR
UI[SwiftUI/UIKit] --> Store[Session Store / Coordinator]
Store --> SDK[Zoom Video SDK iOS]
Store --> TokenAPI[Token API]
TokenAPI --> Signer[Server JWT Signer]
SDK --> Delegates[SDK Delegate Callbacks]
Delegates --> StoreDesign guidance
- Use a coordinator/store layer to isolate SDK-specific logic.
- Keep join/start/leave actions explicit and serial.
- Render participant tiles from delegate-driven state only.
iOS Lifecycle Workflow
flowchart TD
A[Request token] --> B[SDK init]
B --> C[Register delegates]
C --> D[Join session]
D --> E[Start/stop local media]
E --> F[Render remote state from events]
F --> G[Leave and cleanup]Operational sequence
1. Request token from backend. 2. Initialize Video SDK and attach delegates. 3. Join session with session name/topic and display name. 4. Start local media after join success callback. 5. Handle participant and media callbacks as the source of truth. 6. Cleanup delegates and session resources on exit.
iOS Session Join Pattern
func joinSession(sessionName: String, userName: String) async throws {
let token = try await tokenClient.fetchVideoToken(sessionName: sessionName, userName: userName)
try videoSDK.initialize(with: initParams)
videoSDK.delegate = self
try videoSDK.joinSession(
sessionName: sessionName,
userName: userName,
token: token
)
try videoSDK.audioHelper.startAudio()
try videoSDK.videoHelper.startVideo()
}Notes
- Trigger media start after successful join callback when possible.
- Keep token expiry handling (refresh/rejoin) explicit.
iOS Video SDK Overview
What this platform skill is for
- Building custom iOS video experiences with UIKit or SwiftUI
- Managing session state with tokenized join and event callbacks
- Supporting camera, mic, share, chat, and optional advanced media flows
Primary implementation path
1. Backend generates short-lived Video SDK token. 2. App initializes Video SDK and registers delegates. 3. App joins session with user identity + token. 4. App maps participant/media delegate events to UI state. 5. App handles leave/disconnect with explicit cleanup.
Prerequisites
- iOS project with Video SDK binary integration
- Backend service for token generation
- Permissions handling for camera/mic
Important notes
- Keep SDK key/secret on backend only.
- Prefer a deterministic session state machine to avoid UI desync.
Source links
- Docs: https://developers.zoom.us/docs/video-sdk/ios/
- API reference: https://marketplacefront.zoom.us/sdk/custom/ios/annotated.html
iOS Environment Variables
| Variable | Required | Used for | Where to find |
|---|---|---|---|
ZOOM_VIDEO_SDK_KEY | Yes | Video SDK app credential pair | Zoom Marketplace -> Video SDK app -> App Credentials |
ZOOM_VIDEO_SDK_SECRET | Yes (server only) | JWT signing for Video SDK token | Zoom Marketplace -> Video SDK app -> App Credentials |
VIDEO_SDK_TOKEN_ENDPOINT | Yes | Token fetch endpoint used by iOS app | Your backend config |
VIDEO_SDK_SESSION_NAME | Runtime | Session/topic identifier | Generated by application workflow |
VIDEO_SDK_USER_NAME | Runtime | Session display name | Derived from app identity/profile |
Runtime-only values
VIDEO_SDK_TOKENis generated backend-side and sent to app for join.
iOS Reference Map
Docs anchors
- Integration docs: https://developers.zoom.us/docs/video-sdk/ios/
- API class index: https://marketplacefront.zoom.us/sdk/custom/ios/annotated.html
API areas to focus on
- Session lifecycle + context models
- Delegate callbacks for participant/media state
- Audio/video/share helpers
- Chat/command and advanced control surfaces
Crawl summary
- Reference pages crawled: 324
- Docs pages crawled: 23 (22 markdown files persisted)
iOS Versioning and Compatibility
Package evidence
- SDK package:
zoom-video-sdk-iOS-2.5.0.zip - Internal version file:
v2.5.0(33006) - Changelog is externally hosted via developer support portal.
Compatibility notes
- Keep iOS SDK and backend token claims aligned to same release train.
- Recheck Swift/ObjC bridge and framework embedding settings on upgrade.
- Audit deprecated APIs from the iOS reference when moving minor/major versions.
Contradictions or drift to watch
- Changelog content is not bundled in package; release details are external.
- Copyright footer differs across packages (iOS bundle includes 2025).
Video SDK iOS 5-Minute Preflight Runbook
Use this before deep debugging.
Skill Doc Standard Note
- Skill entrypoint is
SKILL.md. - This runbook is an operational convention (recommended), not a required skill file.
- SDK/API names can drift by version; validate current names against docs/raw-docs before release.
1) Confirm Integration Surface
- Confirm this is a Video SDK custom session flow for iOS (not Meeting SDK).
- Verify UI/state are driven by session events, not meeting semantics.
- Wrapper platforms require JS/native bridge synchronization checks.
2) Confirm Required Credentials
- Video SDK app credentials (SDK Key/Secret) stored server-side.
- Backend-generated session JWT token.
- Session fields (
sessionName,userName, role type) resolved before join.
3) Confirm Lifecycle Order
1. Initialize SDK client/context and register event listeners. 2. Generate/fetch session token from backend. 3. Join session and establish media streams. 4. Handle participant/media/control events during active session.
4) Confirm Event/State Handling
- Keep participant state keyed by user/session IDs.
- Reconcile subscribe/unsubscribe transitions for video/audio/share streams.
- Treat reconnect and device-change events as first-class state transitions.
5) Confirm Cleanup + Upgrade Posture
- Leave/end session and release helper/client resources.
- Remove listeners to avoid duplicate callbacks on rejoin.
- Re-check SDK version compatibility before deployment updates.
6) Quick Probes
- Token issuance and join flow succeed once end-to-end.
- Audio/video publish-subscribe operations complete with expected callbacks.
- Leave/rejoin works without leaked listener or stream state.
7) Fast Decision Tree
- Join fails immediately -> invalid/expired token or session field mismatch.
- Media state stuck -> listener binding/order issue or permission/device problem.
- Inconsistent behavior after update -> wrapper/native SDK version mismatch.
8) Source Checkpoints
Official docs
- https://developers.zoom.us/docs/video-sdk/ios/
- https://marketplacefront.zoom.us/sdk/custom/ios/annotated.html
Raw docs in repo
tools/zoom-crawler/raw-docs/developers.zoom.us/docs/video-sdk/ios/tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/video-sdk/ios/
iOS High-Level Scenarios
1. Concierge mobile sessions
- Branded iOS consultation rooms with role-based controls.
- Tight UX around reconnect and network transitions.
2. Premium creator rooms
- Host/mod controls, audience visibility rules, and hand raise queue.
- Event-driven participant list and speaker highlights.
3. Mobile field diagnostics
- Real-time camera streams to expert reviewer.
- Optional metadata tagging on key media events.
4. iOS-first support escalation
- Transition from text support to live video in-app.
- Persist case context while session state changes.
5. Training cohorts
- Session templates for cohort classes and facilitator controls.
- Multi-scene layout handling with predictable lifecycle cleanup.
iOS Common Issues
Join fails with token/auth error
- Validate token issuer, expiry, and Video SDK key pairing.
- Ensure backend and app use the same environment/project credentials.
Media controls appear stuck
- Verify delegate callback sequencing and app state transitions.
- Confirm camera/mic permission prompts were accepted.
Participant list/video tiles desync
- Rebuild UI from delegate events, not cached arrays only.
- Handle reconnect and app foreground/background transitions.
Build/runtime binary issues
- Confirm framework embedding/signing setup.
- Reconcile architecture slices and deployment target requirements.
Raw Data - The ONLY Rendering Option on Linux
Linux vs Windows/Mac: Key Difference
CRITICAL: Unlike Windows and macOS, Linux SDK does NOT have Canvas API.
| Platform | Canvas API | Raw Data Pipe |
|---|---|---|
| Windows | ✅ Yes (SDK renders to HWND) | ✅ Yes (YUV420 frames) |
| macOS | ✅ Yes (SDK renders to NSView) | ✅ Yes (YUV420 frames) |
| Linux | ❌ NO | ✅ ONLY OPTION (YUV420 frames) |
What this means: On Linux, you MUST use the Raw Data Pipe and implement your own rendering. There is no built-in rendering like on Windows/Mac.
---
What is Raw Data?
Raw data is uncompressed, unprocessed media data:
- Video: YUV420 (I420) format - separate Y, U, V planes
- Audio: PCM 16-bit format - raw audio samples
- Share: YUV420 format (same as video)
YUV420 (I420) Format
Y Plane (Luminance - full resolution):
[Y Y Y Y Y Y Y Y]
[Y Y Y Y Y Y Y Y]
[Y Y Y Y Y Y Y Y]
[Y Y Y Y Y Y Y Y]
U Plane (Chrominance - 1/4 resolution):
[U U U U]
[U U U U]
V Plane (Chrominance - 1/4 resolution):
[V V V V]
[V V V V]Why YUV420?
- Efficient: 12 bits per pixel (vs 24 for RGB)
- Standard: Used by video codecs (H.264, VP8, VP9)
- Human vision: Less sensitive to color than brightness
PCM Audio Format
PCM 16-bit Mono (32kHz):
Sample Rate: 32000 Hz
Bit Depth: 16 bits per sample
Channels: 1 (mono)
Buffer: char* array of signed 16-bit integers---
Raw Data Pipe Architecture
Receive Pattern
// 1. Implement delegate
class VideoRenderer : public IZoomVideoSDKRawDataPipeDelegate {
public:
void onRawDataFrameReceived(YUVRawDataI420* data) override {
// Receive YUV420 frame
int width = data->GetStreamWidth();
int height = data->GetStreamHeight();
char* yBuffer = data->GetYBuffer();
char* uBuffer = data->GetUBuffer();
char* vBuffer = data->GetVBuffer();
// Convert YUV to RGB
unsigned char* rgbBuffer = ConvertYUVToRGB(yBuffer, uBuffer, vBuffer, width, height);
// Render with Qt/GTK/SDL/OpenGL
RenderFrame(rgbBuffer, width, height);
}
void onRawDataStatusChanged(RawDataStatus status) override {
if (status == RawData_On) {
printf("Video started\n");
} else {
printf("Video stopped\n");
}
}
};
// 2. Subscribe to user's video
IZoomVideoSDKUser* user = /* from callback */;
IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe();
pipe->subscribe(ZoomVideoSDKResolution_720P, new VideoRenderer());Send Pattern (Virtual Devices)
// 1. Implement virtual video source
class VirtualCamera : public IZoomVideoSDKVideoSource {
IZoomVideoSDKVideoSender* sender_;
public:
void onInitialize(IZoomVideoSDKVideoSender* sender,
IVideoSDKVector<VideoSourceCapability>* caps,
VideoSourceCapability& suggest) override {
sender_ = sender;
}
void onStartSend() override {
// Start sending frames
while (sending_) {
// Load or generate YUV420 frame
char* yBuffer = /* Y plane */;
char* uBuffer = /* U plane */;
char* vBuffer = /* V plane */;
sender_->sendVideoFrame(yBuffer, uBuffer, vBuffer,
width, height, 0, rotation);
std::this_thread::sleep_for(std::chrono::milliseconds(33)); // ~30 FPS
}
}
void onStopSend() override {
sending_ = false;
}
void onPropertyChange(...) override {}
void onUninitialized() override { sender_ = nullptr; }
private:
bool sending_ = false;
};
// 2. Set before joining
session_context.externalVideoSource = new VirtualCamera();---
YUV to RGB Conversion
Required for rendering: Most UI frameworks expect RGB/RGBA, not YUV.
ITU-R BT.601 Formula
void ConvertYUV420ToRGB(char* yBuffer, char* uBuffer, char* vBuffer,
int width, int height, unsigned char* rgbBuffer) {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int yIndex = y * width + x;
int uvIndex = (y / 2) * (width / 2) + (x / 2);
int Y = (unsigned char)yBuffer[yIndex];
int U = (unsigned char)uBuffer[uvIndex];
int V = (unsigned char)vBuffer[uvIndex];
// YUV to RGB conversion
int C = Y - 16;
int D = U - 128;
int E = V - 128;
int R = (298 * C + 409 * E + 128) >> 8;
int G = (298 * C - 100 * D - 208 * E + 128) >> 8;
int B = (298 * C + 516 * D + 128) >> 8;
// Clamp to [0, 255]
R = std::max(0, std::min(255, R));
G = std::max(0, std::min(255, G));
B = std::max(0, std::min(255, B));
// Store RGB
int rgbIndex = yIndex * 3;
rgbBuffer[rgbIndex + 0] = (unsigned char)R;
rgbBuffer[rgbIndex + 1] = (unsigned char)G;
rgbBuffer[rgbIndex + 2] = (unsigned char)B;
}
}
}Optimized with libyuv (Recommended)
#include <libyuv/convert.h>
void ConvertYUV420ToRGB(char* yBuffer, char* uBuffer, char* vBuffer,
int width, int height, unsigned char* rgbBuffer) {
libyuv::I420ToRGB24(
(const uint8_t*)yBuffer, width,
(const uint8_t*)uBuffer, width / 2,
(const uint8_t*)vBuffer, width / 2,
rgbBuffer, width * 3,
width, height
);
}Install libyuv:
sudo apt install -y libyuv-dev---
Rendering Options
Option 1: Qt (Recommended for Cross-Platform)
class QtVideoWidget : public QWidget, public IZoomVideoSDKRawDataPipeDelegate {
Q_OBJECT
signals:
void frameReceived(QImage frame);
public:
QtVideoWidget(QWidget* parent = nullptr) : QWidget(parent) {
connect(this, &QtVideoWidget::frameReceived,
this, &QtVideoWidget::updateFrame);
}
void onRawDataFrameReceived(YUVRawDataI420* data) override {
int width = data->GetStreamWidth();
int height = data->GetStreamHeight();
// Convert YUV to RGB
unsigned char* rgbBuffer = new unsigned char[width * height * 3];
ConvertYUV420ToRGB(data->GetYBuffer(), data->GetUBuffer(), data->GetVBuffer(),
width, height, rgbBuffer);
// Create QImage (takes ownership of buffer)
QImage img(rgbBuffer, width, height, width * 3, QImage::Format_RGB888,
[](void* ptr) { delete[] (unsigned char*)ptr; }, rgbBuffer);
// Emit signal (thread-safe)
emit frameReceived(img.copy());
}
void onRawDataStatusChanged(RawDataStatus status) override {}
private slots:
void updateFrame(QImage img) {
pixmap_ = QPixmap::fromImage(img);
update(); // Trigger repaint
}
protected:
void paintEvent(QPaintEvent*) override {
if (!pixmap_.isNull()) {
QPainter painter(this);
painter.drawPixmap(rect(), pixmap_);
}
}
private:
QPixmap pixmap_;
};Option 2: GTK with Cairo
class GtkVideoRenderer : public IZoomVideoSDKRawDataPipeDelegate {
public:
GtkVideoRenderer(GtkWidget* drawingArea) : drawing_area_(drawingArea) {
g_signal_connect(drawing_area_, "draw", G_CALLBACK(on_draw_static), this);
}
void onRawDataFrameReceived(YUVRawDataI420* data) override {
int width = data->GetStreamWidth();
int height = data->GetStreamHeight();
// Allocate RGB buffer
std::lock_guard<std::mutex> lock(mutex_);
if (rgb_buffer_) delete[] rgb_buffer_;
rgb_buffer_ = new unsigned char[width * height * 3];
width_ = width;
height_ = height;
// Convert YUV to RGB
ConvertYUV420ToRGB(data->GetYBuffer(), data->GetUBuffer(), data->GetVBuffer(),
width, height, rgb_buffer_);
// Trigger redraw on main thread
g_idle_add([](gpointer user_data) {
gtk_widget_queue_draw((GtkWidget*)user_data);
return G_SOURCE_REMOVE;
}, drawing_area_);
}
void onRawDataStatusChanged(RawDataStatus status) override {}
private:
static gboolean on_draw_static(GtkWidget* widget, cairo_t* cr, gpointer user_data) {
return ((GtkVideoRenderer*)user_data)->on_draw(widget, cr);
}
gboolean on_draw(GtkWidget* widget, cairo_t* cr) {
std::lock_guard<std::mutex> lock(mutex_);
if (!rgb_buffer_) return FALSE;
// Create Cairo surface from RGB buffer
cairo_surface_t* surface = cairo_image_surface_create_for_data(
rgb_buffer_,
CAIRO_FORMAT_RGB24,
width_, height_,
cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, width_)
);
// Scale to widget size
int widget_width = gtk_widget_get_allocated_width(widget);
int widget_height = gtk_widget_get_allocated_height(widget);
cairo_scale(cr,
(double)widget_width / width_,
(double)widget_height / height_);
// Draw
cairo_set_source_surface(cr, surface, 0, 0);
cairo_paint(cr);
cairo_surface_destroy(surface);
return TRUE;
}
GtkWidget* drawing_area_;
unsigned char* rgb_buffer_ = nullptr;
int width_ = 0;
int height_ = 0;
std::mutex mutex_;
};Option 3: SDL2 (Lightweight)
class SDL2VideoRenderer : public IZoomVideoSDKRawDataPipeDelegate {
public:
SDL2VideoRenderer() {
SDL_Init(SDL_INIT_VIDEO);
window_ = SDL_CreateWindow("Zoom Video",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
1280, 720, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
renderer_ = SDL_CreateRenderer(window_, -1, SDL_RENDERER_ACCELERATED);
}
~SDL2VideoRenderer() {
if (texture_) SDL_DestroyTexture(texture_);
if (renderer_) SDL_DestroyRenderer(renderer_);
if (window_) SDL_DestroyWindow(window_);
SDL_Quit();
}
void onRawDataFrameReceived(YUVRawDataI420* data) override {
int width = data->GetStreamWidth();
int height = data->GetStreamHeight();
// Create texture if needed
if (!texture_ || width_ != width || height_ != height) {
if (texture_) SDL_DestroyTexture(texture_);
texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_IYUV,
SDL_TEXTUREACCESS_STREAMING, width, height);
width_ = width;
height_ = height;
}
// Update texture with YUV data (no conversion needed!)
SDL_UpdateYUVTexture(texture_, nullptr,
(Uint8*)data->GetYBuffer(), width,
(Uint8*)data->GetUBuffer(), width / 2,
(Uint8*)data->GetVBuffer(), width / 2);
// Render
SDL_RenderClear(renderer_);
SDL_RenderCopy(renderer_, texture_, nullptr, nullptr);
SDL_RenderPresent(renderer_);
}
void onRawDataStatusChanged(RawDataStatus status) override {}
private:
SDL_Window* window_ = nullptr;
SDL_Renderer* renderer_ = nullptr;
SDL_Texture* texture_ = nullptr;
int width_ = 0;
int height_ = 0;
};Advantage: SDL2 supports YUV textures natively - no RGB conversion needed!
Option 4: OpenGL (High Performance)
class OpenGLVideoRenderer : public IZoomVideoSDKRawDataPipeDelegate {
public:
void onRawDataFrameReceived(YUVRawDataI420* data) override {
// Upload YUV planes as textures
glBindTexture(GL_TEXTURE_2D, yTexture_);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE,
width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE,
data->GetYBuffer());
glBindTexture(GL_TEXTURE_2D, uTexture_);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE,
width/2, height/2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE,
data->GetUBuffer());
glBindTexture(GL_TEXTURE_2D, vTexture_);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE,
width/2, height/2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE,
data->GetVBuffer());
// Use shader to convert YUV to RGB on GPU
RenderWithYUVShader();
}
void onRawDataStatusChanged(RawDataStatus status) override {}
private:
GLuint yTexture_, uTexture_, vTexture_;
};---
Performance Considerations
CPU vs GPU Conversion
| Method | Performance | Complexity |
|---|---|---|
| Manual CPU | Slowest | Simple |
| libyuv (SIMD) | Fast | Simple |
| SDL2 YUV texture | Very Fast | Simple |
| OpenGL shader | Fastest | Complex |
Recommendation:
- Simple apps: libyuv
- Cross-platform: Qt + libyuv
- Performance-critical: SDL2 or OpenGL
Memory Management
CRITICAL: YUV frames can be large. Use heap mode and manage memory carefully.
// Initialize with heap mode
init_params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
init_params.shareRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
// Reference counting for async processing
void onRawDataFrameReceived(YUVRawDataI420* data) override {
if (data->CanAddRef()) {
data->AddRef();
// Queue for background processing
processing_queue_.push(data);
// Later, after processing
data->Release();
}
}Frame Rate Control
class ThrottledRenderer : public IZoomVideoSDKRawDataPipeDelegate {
auto last_frame_ = std::chrono::steady_clock::now();
const int target_fps_ = 30;
void onRawDataFrameReceived(YUVRawDataI420* data) override {
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - last_frame_);
// Throttle to target FPS
if (elapsed.count() < (1000 / target_fps_)) {
return; // Skip frame
}
last_frame_ = now;
RenderFrame(data);
}
};---
Audio Raw Data
Receive Audio
// In IZoomVideoSDKDelegate
void onMixedAudioRawDataReceived(AudioRawData* data) override {
char* buffer = data->GetBuffer(); // PCM 16-bit
unsigned int len = data->GetBufferLen(); // Bytes
unsigned int sampleRate = data->GetSampleRate(); // Hz
unsigned int channels = data->GetChannelNum(); // 1=mono, 2=stereo
// Play or save audio
PlayAudio(buffer, len, sampleRate, channels);
}
void onOneWayAudioRawDataReceived(AudioRawData* data, IZoomVideoSDKUser* user) override {
// Per-user audio
}Send Audio (Virtual Mic)
class VirtualMic : public IZoomVideoSDKVirtualAudioMic {
IZoomVideoSDKAudioSender* sender_;
void onMicInitialize(IZoomVideoSDKAudioSender* sender) override {
sender_ = sender;
}
void onMicStartSend() override {
// Load PCM audio file
char* audioBuffer = LoadPCMAudio("audio.pcm");
int length = GetAudioLength();
int sampleRate = 32000;
// Send in chunks
int chunkSize = sampleRate * 2 / 100; // 10ms chunks (16-bit = 2 bytes)
for (int offset = 0; offset < length; offset += chunkSize) {
sender_->Send(audioBuffer + offset, chunkSize, sampleRate);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
void onMicStopSend() override {}
void onMicUninitialized() override { sender_ = nullptr; }
};---
Summary
| Feature | Linux SDK |
|---|---|
| Canvas API | ❌ Not available |
| Raw Data Pipe | ✅ ONLY rendering option |
| YUV to RGB | ✅ Required for most UIs |
| Qt Integration | ✅ Recommended |
| GTK Integration | ✅ Supported |
| SDL2 | ✅ Best performance |
| OpenGL | ✅ Maximum performance |
| Virtual Devices | ✅ For custom media injection |
| Memory Mode | ✅ Always use Heap |
Key Takeaway: Linux requires manual rendering. Choose your rendering framework (Qt, GTK, SDL2, OpenGL) and implement YUV to RGB conversion.
---
See Also
- [SDK Architecture Pattern](sdk-architecture-pattern.md) - Universal pattern
- [Raw Video Capture](../examples/raw-video-capture.md) - Complete capture example
- [Raw Audio Capture](../examples/raw-audio-capture.md) - Audio capture example
- [Qt/GTK Integration](../examples/qt-gtk-integration.md) - UI framework integration
- [Virtual Audio/Video](../examples/virtual-audio-video.md) - Custom media injection
SDK Architecture Pattern - Universal Formula
The 3-Step Pattern for ANY Feature
Once you understand this pattern, you can implement ANY Zoom Video SDK feature. The SDK follows a consistent architectural pattern across all features.
Pattern Overview
1. Get Singleton/Helper → 2. Implement Delegate → 3. Subscribe & UseThis pattern applies to:
- Video subscription
- Audio processing
- Screen sharing
- Raw data capture
- Raw data injection (virtual devices)
- Recording
- Live streaming
- Transcription
- Commands
- Chat
---
Step 1: Get Singleton/Helper
The SDK uses a singleton hierarchy to access features:
// Main SDK singleton
IZoomVideoSDK* video_sdk_obj = CreateZoomVideoSDKObj();
// Get helpers for specific features
IZoomVideoSDKAudioHelper* audio = video_sdk_obj->getAudioHelper();
IZoomVideoSDKVideoHelper* video = video_sdk_obj->getVideoHelper();
IZoomVideoSDKShareHelper* share = video_sdk_obj->getShareHelper();
IZoomVideoSDKChatHelper* chat = video_sdk_obj->getChatHelper();
IZoomVideoSDKRecordingHelper* rec = video_sdk_obj->getRecordingHelper();
IZoomVideoSDKLiveStreamHelper* stream = video_sdk_obj->getLiveStreamHelper();
IZoomVideoSDKLiveTranscriptionHelper* transcription = video_sdk_obj->getLiveTranscriptionHelper();Key Insight: Helpers control YOUR streams/actions. To receive others' data, you subscribe to their pipes/canvas.
---
Step 2: Implement Delegate
The SDK communicates via callbacks. You implement delegate interfaces to receive events.
Main Event Delegate
class MyDelegate : public IZoomVideoSDKDelegate {
public:
// Session events
virtual void onSessionJoin() override { /* Session joined */ }
virtual void onSessionLeave() override { /* Session left */ }
virtual void onError(ZoomVideoSDKErrors errorCode, int detailErrorCode) override { /* Error occurred */ }
// User events
virtual void onUserJoin(IZoomVideoSDKUserHelper*, IVideoSDKVector<IZoomVideoSDKUser*>*) override { /* Users joined */ }
virtual void onUserLeave(IZoomVideoSDKUserHelper*, IVideoSDKVector<IZoomVideoSDKUser*>*) override { /* Users left */ }
// Video events
virtual void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper*, IVideoSDKVector<IZoomVideoSDKUser*>*) override { /* Video status changed */ }
// Audio events
virtual void onUserAudioStatusChanged(IZoomVideoSDKAudioHelper*, IVideoSDKVector<IZoomVideoSDKUser*>*) override { /* Audio status changed */ }
virtual void onMixedAudioRawDataReceived(AudioRawData* data_) override { /* Mixed audio data */ }
virtual void onOneWayAudioRawDataReceived(AudioRawData* data_, IZoomVideoSDKUser* pUser) override { /* Per-user audio */ }
// Share events
virtual void onUserShareStatusChanged(IZoomVideoSDKShareHelper*, IZoomVideoSDKUser*, IZoomVideoSDKShareAction*) override { /* Share status changed */ }
// ... many more callbacks (see linux-reference.md for complete list)
};
// Register delegate
video_sdk_obj->addListener(new MyDelegate());Raw Data Delegates
For raw video/share data:
class VideoDelegate : public IZoomVideoSDKRawDataPipeDelegate {
public:
virtual void onRawDataFrameReceived(YUVRawDataI420* data) override {
// Process YUV420 frame
int width = data->GetStreamWidth();
int height = data->GetStreamHeight();
char* yBuffer = data->GetYBuffer();
char* uBuffer = data->GetUBuffer();
char* vBuffer = data->GetVBuffer();
}
virtual void onRawDataStatusChanged(RawDataStatus status) override {
// Video on/off
}
};For virtual audio mic:
class VirtualMic : public IZoomVideoSDKVirtualAudioMic {
public:
virtual void onMicInitialize(IZoomVideoSDKAudioSender* sender) override {
audio_sender_ = sender; // Store for sending
}
virtual void onMicStartSend() override {
// Start sending audio frames
}
virtual void onMicStopSend() override { /* Stop sending */ }
virtual void onMicUninitialized() override { audio_sender_ = nullptr; }
private:
IZoomVideoSDKAudioSender* audio_sender_ = nullptr;
};For virtual video source:
class VirtualVideo : public IZoomVideoSDKVideoSource {
public:
virtual void onInitialize(IZoomVideoSDKVideoSender* sender,
IVideoSDKVector<VideoSourceCapability>* caps,
VideoSourceCapability& suggest) override {
video_sender_ = sender; // Store for sending
}
virtual void onStartSend() override {
// Start sending video frames
}
virtual void onStopSend() override { /* Stop sending */ }
virtual void onPropertyChange(...) override { /* Resolution changed */ }
virtual void onUninitialized() override { video_sender_ = nullptr; }
private:
IZoomVideoSDKVideoSender* video_sender_ = nullptr;
};---
Step 3: Subscribe & Use
Pattern A: Control YOUR streams (via Helpers)
// Start YOUR audio
IZoomVideoSDKAudioHelper* audio = video_sdk_obj->getAudioHelper();
audio->startAudio();
audio->muteAudio(true);
// Start YOUR video
IZoomVideoSDKVideoHelper* video = video_sdk_obj->getVideoHelper();
video->startVideo();
video->stopVideo();
// Start YOUR screen share
IZoomVideoSDKShareHelper* share = video_sdk_obj->getShareHelper();
share->startShare();
share->stopShare();Pattern B: Receive data from OTHERS (via Pipes)
// Subscribe to remote user's video
IZoomVideoSDKUser* user = /* get from onUserJoin */;
IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe();
pipe->subscribe(ZoomVideoSDKResolution_720P, videoDelegate);
// Subscribe to remote user's share
IZoomVideoSDKRawDataPipe* sharePipe = user->GetSharePipe();
sharePipe->subscribe(ZoomVideoSDKResolution_720P, shareDelegate);Pattern C: Inject custom data (via Virtual Devices)
// Set virtual mic BEFORE joining
session_context.virtualAudioMic = new VirtualMic();
session_context.audioOption.connect = true;
session_context.audioOption.mute = false;
// Set virtual video source BEFORE joining
session_context.externalVideoSource = new VirtualVideo();
// Join session
video_sdk_obj->joinSession(session_context);
// Send data in onStartSend callbacks
// For audio: audio_sender_->Send(data, len, sampleRate);
// For video: video_sender_->sendVideoFrame(y, u, v, w, h, 0, rotation);---
Common Patterns
Pattern: Session Join
// 1. Get SDK singleton
IZoomVideoSDK* sdk = CreateZoomVideoSDKObj();
// 2. Implement delegate
sdk->addListener(new MyDelegate());
// 3. Configure and join
ZoomVideoSDKSessionContext ctx;
ctx.sessionName = "my-session";
ctx.userName = "Linux Bot";
ctx.token = "jwt-token";
ctx.audioOption.connect = true;
ctx.audioOption.mute = false;
ctx.videoOption.localVideoOn = false;
// For headless: add virtual speaker
ctx.virtualAudioSpeaker = new VirtualSpeaker();
IZoomVideoSDKSession* session = sdk->joinSession(ctx);Pattern: Raw Video Capture
// 1. Create delegate
class VideoCapture : public IZoomVideoSDKRawDataPipeDelegate {
void onRawDataFrameReceived(YUVRawDataI420* data) override {
// Save to file or process
}
};
// 2. In onUserJoin or onUserVideoStatusChanged
IZoomVideoSDKUser* user = /* from callback */;
IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe();
pipe->subscribe(ZoomVideoSDKResolution_720P, new VideoCapture());Pattern: Virtual Audio Injection
// 1. Implement virtual mic
class MyMic : public IZoomVideoSDKVirtualAudioMic {
IZoomVideoSDKAudioSender* sender_;
void onMicInitialize(IZoomVideoSDKAudioSender* sender) override {
sender_ = sender;
}
void onMicStartSend() override {
// Load PCM audio and send
char* audioData = LoadPCMAudio();
sender_->Send(audioData, length, 32000);
}
};
// 2. Set before joining
session_context.virtualAudioMic = new MyMic();Pattern: Cloud Recording
// 1. Get recording helper
IZoomVideoSDKRecordingHelper* rec = sdk->getRecordingHelper();
// 2. Check permissions
if (rec->canStartRecording() == ZoomVideoSDKErrors_Success) {
// 3. Start recording
rec->startCloudRecording();
}
// Listen in delegate
void onCloudRecordingStatus(RecordingStatus status,
IZoomVideoSDKRecordingConsentHandler* handler) override {
if (status == Recording_Start) {
printf("Recording started\n");
}
}---
Linux-Specific Patterns
Pattern: Headless Linux (Docker/WSL)
Problem: No physical audio devices.
Solution: Use virtual audio speaker and mic.
// Virtual speaker for receiving audio
class MySpeaker : public IZoomVideoSDKVirtualAudioSpeaker {
void onVirtualSpeakerMixedAudioReceived(AudioRawData* data) override {
// Process or discard audio
}
void onVirtualSpeakerOneWayAudioReceived(AudioRawData* data, IZoomVideoSDKUser* user) override {
// Per-user audio
}
void onVirtualSpeakerSharedAudioReceived(AudioRawData* data) override {
// Share audio
}
};
// Virtual mic for sending audio
class MyMic : public IZoomVideoSDKVirtualAudioMic {
IZoomVideoSDKAudioSender* sender_;
void onMicInitialize(IZoomVideoSDKAudioSender* sender) override {
sender_ = sender;
}
void onMicStartSend() override {
// Send PCM audio
}
void onMicStopSend() override {}
void onMicUninitialized() override { sender_ = nullptr; }
};
// Apply before joining
session_context.virtualAudioSpeaker = new MySpeaker();
session_context.virtualAudioMic = new MyMic();
session_context.audioOption.connect = true;Pattern: Qt/GTK UI Integration
Qt Pattern:
// Use Qt signals/slots with SDK callbacks
class QtVideoRenderer : public QWidget, public IZoomVideoSDKRawDataPipeDelegate {
Q_OBJECT
signals:
void frameReceived(QImage frame);
public:
void onRawDataFrameReceived(YUVRawDataI420* data) override {
// Convert YUV to RGB
QImage img = ConvertYUVToRGB(data);
// Emit signal (thread-safe)
emit frameReceived(img);
}
};
// In Qt widget
connect(renderer, &QtVideoRenderer::frameReceived,
this, [this](QImage img) {
// Update UI on main thread
videoLabel->setPixmap(QPixmap::fromImage(img));
});GTK Pattern:
// Use Glib main loop for thread safety
class GtkVideoRenderer : public IZoomVideoSDKRawDataPipeDelegate {
void onRawDataFrameReceived(YUVRawDataI420* data) override {
// Marshal to main thread
g_idle_add([](gpointer user_data) {
YUVRawDataI420* data = (YUVRawDataI420*)user_data;
// Update GTK UI safely
return G_SOURCE_REMOVE;
}, data);
}
};---
Key Insights
1. Helpers vs Pipes
| Component | Purpose | Example |
|---|---|---|
| Helpers | Control YOUR streams | videoHelper->startVideo() starts YOUR camera |
| Pipes | Receive OTHERS' streams | user->GetVideoPipe()->subscribe() receives their video |
2. Virtual Devices for Injection
To send custom audio/video, use virtual devices:
IZoomVideoSDKVirtualAudioMic- Send custom audioIZoomVideoSDKVirtualAudioSpeaker- Receive audio (headless)IZoomVideoSDKVideoSource- Send custom videoIZoomVideoSDKShareSource- Send custom share
Set these before joining session.
3. Memory Modes
Always use heap mode for raw data:
init_params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
init_params.shareRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
init_params.audioRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;4. Qt5 Dependencies
SDK requires Qt5 libraries (bundled in SDK package):
- Copy from SDK
samples/qt_libs/Qt/lib/ - Create symlinks for versioned libraries
- See Qt Dependencies Guide
5. PulseAudio for Audio
Linux requires PulseAudio for raw audio features:
sudo apt install -y pulseaudio
mkdir -p ~/.config
echo "[General]" > ~/.config/zoomus.conf
echo "system.audio.type=default" >> ~/.config/zoomus.conf---
Complete Example
#include "zoom_video_sdk_api.h"
#include "zoom_video_sdk_interface.h"
#include "zoom_video_sdk_delegate_interface.h"
USING_ZOOM_VIDEO_SDK_NAMESPACE
class BotDelegate : public IZoomVideoSDKDelegate {
void onSessionJoin() override {
printf("Joined session!\n");
// Start audio
IZoomVideoSDKAudioHelper* audio = video_sdk_obj->getAudioHelper();
audio->startAudio();
audio->subscribe(); // For raw audio callbacks
}
void onUserJoin(IZoomVideoSDKUserHelper*, IVideoSDKVector<IZoomVideoSDKUser*>* users) override {
for (int i = 0; i < users->GetCount(); i++) {
IZoomVideoSDKUser* user = users->GetItem(i);
// Subscribe to video
IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe();
pipe->subscribe(ZoomVideoSDKResolution_720P, videoDelegate);
}
}
void onMixedAudioRawDataReceived(AudioRawData* data) override {
// Process audio
char* buffer = data->GetBuffer();
unsigned int len = data->GetBufferLen();
unsigned int sampleRate = data->GetSampleRate();
}
// ... implement all other required callbacks
};
int main() {
// 1. Create SDK
IZoomVideoSDK* sdk = CreateZoomVideoSDKObj();
// 2. Initialize
ZoomVideoSDKInitParams init_params;
init_params.domain = "https://zoom.us";
init_params.enableLog = true;
init_params.logFilePrefix = "bot";
init_params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
init_params.shareRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
init_params.audioRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
sdk->initialize(init_params);
// 3. Add delegate
sdk->addListener(new BotDelegate());
// 4. Join session
ZoomVideoSDKSessionContext ctx;
ctx.sessionName = "my-session";
ctx.userName = "Linux Bot";
ctx.token = "jwt-token";
ctx.audioOption.connect = true;
ctx.audioOption.mute = false;
ctx.videoOption.localVideoOn = false;
// For headless
ctx.virtualAudioSpeaker = new VirtualSpeaker();
IZoomVideoSDKSession* session = sdk->joinSession(ctx);
// Keep running
while (running) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// Cleanup
sdk->leaveSession(false);
sdk->cleanup();
DestroyZoomVideoSDKObj();
return 0;
}---
Next Steps
- [Singleton Hierarchy](singleton-hierarchy.md) - Navigate the 5-level SDK structure
- [Session Join Pattern](../examples/session-join-pattern.md) - Complete working session join
- [Raw Video Capture](../examples/raw-video-capture.md) - Capture YUV420 frames
- [Raw Audio Capture](../examples/raw-audio-capture.md) - Capture PCM audio
- [Virtual Audio/Video](../examples/virtual-audio-video.md) - Inject custom media
Singleton Hierarchy - Complete SDK Navigation Map
5-Level SDK Navigation
The Zoom Video SDK for Linux follows a hierarchical singleton pattern. Understanding this hierarchy is the key to navigating the entire SDK.
Level 1: SDK Singleton
└─ IZoomVideoSDK (CreateZoomVideoSDKObj)
│
├─ Level 2: Session Info
│ └─ IZoomVideoSDKSession (via getSessionInfo)
│ └─ Level 3: Users
│ └─ IZoomVideoSDKUser[] (via getMyself, getAllUsers, getRemoteUsers)
│ └─ Level 4: User Pipes
│ ├─ IZoomVideoSDKRawDataPipe (via GetVideoPipe)
│ └─ IZoomVideoSDKRawDataPipe (via GetSharePipe)
│
└─ Level 2: Feature Helpers
├─ IZoomVideoSDKAudioHelper (getAudioHelper)
├─ IZoomVideoSDKVideoHelper (getVideoHelper)
├─ IZoomVideoSDKShareHelper (getShareHelper)
├─ IZoomVideoSDKChatHelper (getChatHelper)
├─ IZoomVideoSDKRecordingHelper (getRecordingHelper)
├─ IZoomVideoSDKLiveStreamHelper (getLiveStreamHelper)
├─ IZoomVideoSDKLiveTranscriptionHelper (getLiveTranscriptionHelper)
├─ IZoomVideoSDKCmdChannel (getCmdChannel)
├─ IZoomVideoSDKPhoneHelper (getPhoneHelper)
├─ IZoomVideoSDKCRCHelper (getCRCHelper)
├─ IZoomVideoSDKWhiteboardHelper (getWhiteboardHelper)
├─ IZoomVideoSDKSubSessionHelper (getSubSessionHelper)
└─ Settings Helpers
├─ IZoomVideoSDKAudioSettingHelper (getAudioSettingHelper)
├─ IZoomVideoSDKVideoSettingHelper (getVideoSettingHelper)
└─ IZoomVideoSDKShareSettingHelper (getShareSettingHelper)---
Level 1: SDK Singleton
Entry Point: This is where everything starts.
// Create SDK object
IZoomVideoSDK* video_sdk_obj = CreateZoomVideoSDKObj();
// Initialize
ZoomVideoSDKInitParams init_params;
init_params.domain = "https://zoom.us";
init_params.enableLog = true;
init_params.logFilePrefix = "bot";
init_params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
init_params.shareRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
init_params.audioRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
video_sdk_obj->initialize(init_params);
// Join session
ZoomVideoSDKSessionContext ctx;
ctx.sessionName = "my-session";
ctx.userName = "Bot";
ctx.token = "jwt-token";
IZoomVideoSDKSession* session = video_sdk_obj->joinSession(ctx);
// Cleanup
video_sdk_obj->leaveSession(false);
video_sdk_obj->cleanup();
DestroyZoomVideoSDKObj();Key Methods:
initialize()- Must be called before any other SDK operationsjoinSession()- Join or create a sessionleaveSession(bool endSession)- Leave session (host can end for all)cleanup()- Release SDK resourcesaddListener()/removeListener()- Register event callbacksisInSession()- Check if currently in a sessiongetSessionInfo()- Access Level 2: Sessionget*Helper()- Access Level 2: Feature Helpers
---
Level 2A: Session Info
Purpose: Access session-level information and users.
IZoomVideoSDKSession* session = video_sdk_obj->getSessionInfo();Key Methods:
// Current user
IZoomVideoSDKUser* myself = session->getMyself();
// All users (including self)
IVideoSDKVector<IZoomVideoSDKUser*>* all = session->getAllUsers();
// Remote users only (excluding self)
IVideoSDKVector<IZoomVideoSDKUser*>* remote = session->getRemoteUsers();
// Session info
const char* sessionName = session->getSessionName();
const char* sessionID = session->getSessionID();
const char* sessionPassword = session->getSessionPassword();
const char* sessionHost = session->getSessionHost();---
Level 2B: Feature Helpers
Feature helpers control YOUR streams and actions. They do NOT control other users' streams.
Audio Helper
Purpose: Control YOUR audio (mic, speaker, mute).
IZoomVideoSDKAudioHelper* audio = video_sdk_obj->getAudioHelper();
// Start/stop your audio
audio->startAudio();
audio->stopAudio();
// Mute/unmute yourself
audio->muteAudio(true);
audio->unmuteAudio();
// Subscribe to raw audio callbacks
audio->subscribe();
audio->unSubscribe();
// Device management
IVideoSDKVector<IZoomVideoSDKMicDevice*>* mics = audio->getMicList();
IVideoSDKVector<IZoomVideoSDKSpeakerDevice*>* speakers = audio->getSpeakerList();
audio->selectMic(deviceID);
audio->selectSpeaker(deviceID);Key Insight: To receive others' audio, subscribe via subscribe() and implement audio callbacks in IZoomVideoSDKDelegate.
Video Helper
Purpose: Control YOUR video (camera).
IZoomVideoSDKVideoHelper* video = video_sdk_obj->getVideoHelper();
// Start/stop your video
video->startVideo();
video->stopVideo();
// Camera management
IVideoSDKVector<IZoomVideoSDKCameraDevice*>* cameras = video->getCameraList();
video->selectCamera(deviceID);
// Video settings
video->rotateMyVideo(90); // 0, 90, 180, 270Key Insight: To receive others' video, subscribe to their GetVideoPipe().
Share Helper
Purpose: Control YOUR screen share.
IZoomVideoSDKShareHelper* share = video_sdk_obj->getShareHelper();
// Start/stop screen share
share->startShare();
share->stopShare();
// Custom share source
IZoomVideoSDKShareSource* source = new MyShareSource();
share->startSharingExternalSource(source);
// Share status
bool isSharing = share->isSharingOut();
bool canShare = share->isSharingOut() == false;Key Insight: To receive others' share, subscribe to their GetSharePipe().
Chat Helper
Purpose: Send/receive chat messages.
IZoomVideoSDKChatHelper* chat = video_sdk_obj->getChatHelper();
// Send messages
chat->sendChatToAll("Hello everyone!");
chat->sendChatToUser(user, "Private message");
// Delete messages (host only)
chat->deleteChatMessage(messageID);
// Check privileges
ZoomVideoSDKChatPrivilegeType priv = chat->getChatPrivilege();Receive: Implement onChatNewMessageNotify() in delegate.
Recording Helper
Purpose: Cloud recording control.
IZoomVideoSDKRecordingHelper* rec = video_sdk_obj->getRecordingHelper();
// Check permissions
ZoomVideoSDKErrors canRecord = rec->canStartRecording();
if (canRecord == ZoomVideoSDKErrors_Success) {
// Start recording
rec->startCloudRecording();
}
// Stop recording
rec->stopCloudRecording();
// Pause/resume
rec->pauseCloudRecording();
rec->resumeCloudRecording();
// Check status
bool isRecording = rec->isCloudRecording();Receive status: Implement onCloudRecordingStatus() in delegate.
Live Stream Helper
Purpose: RTMP live streaming.
IZoomVideoSDKLiveStreamHelper* stream = video_sdk_obj->getLiveStreamHelper();
// Check permissions
ZoomVideoSDKErrors canStream = stream->canStartLiveStream();
if (canStream == ZoomVideoSDKErrors_Success) {
// Start live stream
stream->startLiveStream("rtmp://...", "stream-key", "broadcast-url");
}
// Stop stream
stream->stopLiveStream();
// Check status
bool isStreaming = stream->isInLiveStreamingMode();Receive status: Implement onLiveStreamStatusChanged() in delegate.
Live Transcription Helper
Purpose: Real-time speech-to-text.
IZoomVideoSDKLiveTranscriptionHelper* trans = video_sdk_obj->getLiveTranscriptionHelper();
// Check permissions
bool canStart = trans->canStartLiveTranscription();
if (canStart) {
// Start transcription
trans->startLiveTranscription();
}
// Stop transcription
trans->stopLiveTranscription();
// Set language
IVideoSDKVector<ILiveTranscriptionLanguage*>* langs = trans->getAvailableSpokenLanguages();
trans->setSpokenLanguage(langs->GetItem(0)->getLTTLanguageID());Receive messages: Implement onLiveTranscriptionMsgInfoReceived() in delegate.
Command Channel
Purpose: Custom command messaging between participants.
IZoomVideoSDKCmdChannel* cmd = video_sdk_obj->getCmdChannel();
// Send command to user
cmd->sendCommand(user, "custom-command-data");Receive: Implement onCommandReceived() in delegate.
Phone Helper
Purpose: PSTN dial-out.
IZoomVideoSDKPhoneHelper* phone = video_sdk_obj->getPhoneHelper();
// Get supported countries
IVideoSDKVector<IZoomVideoSDKPhoneSupportCountryInfo*>* countries = phone->getSupportCountryInfo();
// Invite by phone
phone->inviteByPhone(countryCode, phoneNumber, displayName);
// Cancel invitation
phone->cancelInviteByPhone(success_callback, fail_callback);Settings Helpers
Purpose: Configure audio/video/share settings.
// Audio settings
IZoomVideoSDKAudioSettingHelper* audioSettings = video_sdk_obj->getAudioSettingHelper();
audioSettings->setMicVolume(volume);
audioSettings->setSpeakerVolume(volume);
// Video settings
IZoomVideoSDKVideoSettingHelper* videoSettings = video_sdk_obj->getVideoSettingHelper();
videoSettings->setOriginalSizeMode(true);
// Share settings
IZoomVideoSDKShareSettingHelper* shareSettings = video_sdk_obj->getShareSettingHelper();---
Level 3: Users
Purpose: Access individual user objects.
// Get current user
IZoomVideoSDKUser* myself = video_sdk_obj->getSessionInfo()->getMyself();
// Get all users
IVideoSDKVector<IZoomVideoSDKUser*>* users = video_sdk_obj->getSessionInfo()->getAllUsers();
// Iterate users
for (int i = 0; i < users->GetCount(); i++) {
IZoomVideoSDKUser* user = users->GetItem(i);
const char* name = user->getUserName();
bool isHost = user->isHost();
bool isManager = user->isManager();
}User Methods:
// User info
const char* getUserName();
const char* getUserGuid();
bool isHost();
bool isManager();
// Video status
IZoomVideoSDKRawDataPipe* GetVideoPipe(); // Level 4
// Audio status
IZoomVideoSDKAudioStatus* getAudioStatus();
// Share status
IZoomVideoSDKRawDataPipe* GetSharePipe(); // Level 4---
Level 4: User Pipes
Purpose: Subscribe to raw video/share data from specific users.
Video Pipe
IZoomVideoSDKUser* user = /* get from session */;
IZoomVideoSDKRawDataPipe* videoPipe = user->GetVideoPipe();
// Subscribe to user's video
class VideoDelegate : public IZoomVideoSDKRawDataPipeDelegate {
void onRawDataFrameReceived(YUVRawDataI420* data) override {
// Process YUV420 frame
}
void onRawDataStatusChanged(RawDataStatus status) override {
// Video on/off
}
};
videoPipe->subscribe(ZoomVideoSDKResolution_720P, new VideoDelegate());
// Unsubscribe
videoPipe->unSubscribe();Share Pipe
IZoomVideoSDKUser* user = /* get from session */;
IZoomVideoSDKRawDataPipe* sharePipe = user->GetSharePipe();
// Subscribe to user's share (same pattern as video)
sharePipe->subscribe(ZoomVideoSDKResolution_1080P, new ShareDelegate());---
Navigation Patterns
Pattern: Feature → Helper
"I want to do X" → Find the helper that controls X.
| Want to... | Navigate to... |
|---|---|
| Start my audio | getAudioHelper()->startAudio() |
| Start my video | getVideoHelper()->startVideo() |
| Share my screen | getShareHelper()->startShare() |
| Send chat | getChatHelper()->sendChatToAll() |
| Start recording | getRecordingHelper()->startCloudRecording() |
| Start live stream | getLiveStreamHelper()->startLiveStream() |
| Start transcription | getLiveTranscriptionHelper()->startLiveTranscription() |
Pattern: Receive Data → Pipes
"I want to receive X from others" → Subscribe to user pipes or implement delegate callbacks.
| Want to receive... | Navigate to... |
|---|---|
| Others' video (raw) | user->GetVideoPipe()->subscribe() |
| Others' share (raw) | user->GetSharePipe()->subscribe() |
| Mixed audio (raw) | getAudioHelper()->subscribe() + onMixedAudioRawDataReceived() |
| Per-user audio (raw) | getAudioHelper()->subscribe() + onOneWayAudioRawDataReceived() |
| Chat messages | Implement onChatNewMessageNotify() |
| Commands | Implement onCommandReceived() |
Pattern: Virtual Devices → Session Context
"I want to inject custom data" → Set virtual devices in session context BEFORE joining.
| Want to inject... | Navigate to... |
|---|---|
| Custom audio (mic) | session_context.virtualAudioMic = new MyMic() |
| Custom video | session_context.externalVideoSource = new MyVideo() |
| Virtual speaker (headless) | session_context.virtualAudioSpeaker = new MySpeaker() |
| Custom share | getShareHelper()->startSharingExternalSource(source) |
---
Quick Reference
Get SDK Object
IZoomVideoSDK* sdk = CreateZoomVideoSDKObj();Get Session
IZoomVideoSDKSession* session = sdk->getSessionInfo();Get Users
IZoomVideoSDKUser* myself = session->getMyself();
IVideoSDKVector<IZoomVideoSDKUser*>* all = session->getAllUsers();
IVideoSDKVector<IZoomVideoSDKUser*>* remote = session->getRemoteUsers();Get Helpers
IZoomVideoSDKAudioHelper* audio = sdk->getAudioHelper();
IZoomVideoSDKVideoHelper* video = sdk->getVideoHelper();
IZoomVideoSDKShareHelper* share = sdk->getShareHelper();
IZoomVideoSDKChatHelper* chat = sdk->getChatHelper();
IZoomVideoSDKRecordingHelper* rec = sdk->getRecordingHelper();
IZoomVideoSDKLiveStreamHelper* stream = sdk->getLiveStreamHelper();
IZoomVideoSDKLiveTranscriptionHelper* trans = sdk->getLiveTranscriptionHelper();
IZoomVideoSDKCmdChannel* cmd = sdk->getCmdChannel();Get Pipes
IZoomVideoSDKRawDataPipe* videoPipe = user->GetVideoPipe();
IZoomVideoSDKRawDataPipe* sharePipe = user->GetSharePipe();---
Complete Navigation Example
// Level 1: SDK Singleton
IZoomVideoSDK* sdk = CreateZoomVideoSDKObj();
sdk->initialize(init_params);
sdk->joinSession(session_context);
// Level 2A: Session Info
IZoomVideoSDKSession* session = sdk->getSessionInfo();
// Level 3: Users
IZoomVideoSDKUser* myself = session->getMyself();
IVideoSDKVector<IZoomVideoSDKUser*>* remote = session->getRemoteUsers();
// Level 4: Subscribe to remote user's video
IZoomVideoSDKUser* remoteUser = remote->GetItem(0);
IZoomVideoSDKRawDataPipe* videoPipe = remoteUser->GetVideoPipe();
videoPipe->subscribe(ZoomVideoSDKResolution_720P, videoDelegate);
// Level 2B: Control my audio
IZoomVideoSDKAudioHelper* audio = sdk->getAudioHelper();
audio->startAudio();
audio->subscribe(); // For raw audio callbacks
// Level 2B: Control my video
IZoomVideoSDKVideoHelper* video = sdk->getVideoHelper();
video->startVideo();
// Level 2B: Send chat
IZoomVideoSDKChatHelper* chat = sdk->getChatHelper();
chat->sendChatToAll("Hello!");
// Level 2B: Start recording
IZoomVideoSDKRecordingHelper* rec = sdk->getRecordingHelper();
if (rec->canStartRecording() == ZoomVideoSDKErrors_Success) {
rec->startCloudRecording();
}---
See Also
- [SDK Architecture Pattern](sdk-architecture-pattern.md) - Universal 3-step pattern
- [API Reference](../references/linux-reference.md) - Complete API documentation
- [Session Join Pattern](../examples/session-join-pattern.md) - Working session join code
Chat (Linux)
Use chat when you need:
- bot commands
- status updates
- control channel for media pipelines
Use this pattern when chat acts as a lightweight control or status channel around your media workflow.
Cloud Recording (Video SDK vs Meetings)
Clarify with customers:
- Meeting cloud recordings are managed via Meeting product settings and REST APIs.
- Video SDK sessions have a different lifecycle; some "recording" requests are actually "I want media output".
Use this note when someone asks for "recording" but the real requirement is media output or export behavior in a Video SDK session.
Command Channel
Complete working code for custom command messaging between participants on Linux.
Official Sample: videosdk-linux-raw-recording-sample
---
Overview
The command channel enables custom data exchange between participants within the same session. Use cases:
- Application-specific signaling
- Session transfer / waiting room coordination
- Real-time collaboration data
- Custom control messages
+-------------------------------------------------------------------+
| COMMAND CHANNEL FLOW (Linux) |
+-------------------------------------------------------------------+
| Sender: |
| getCmdChannel() -> sendCommand(nullptr, msg) [broadcast] |
| getCmdChannel() -> sendCommand(user, msg) [targeted] |
| |
| Receiver: |
| onCommandReceived(sender, command) callback |
| |
| IMPORTANT: Command channel is SESSION-SCOPED. |
| It does NOT span across different sessions. |
+-------------------------------------------------------------------+Key differences from Windows: On Linux, strings are const char* (UTF-8), not const wchar_t* (wide strings). See Windows Command Channel for comparison.
---
Limitations
| Limit | Value |
|---|---|
| Max message rate | 60 messages/second |
| Max message size | ~1KB recommended |
| Reliability | Best effort (not guaranteed) |
| Scope | Same session only |
Note: Commands are not persisted - late joiners won't receive previous commands.
---
Threading Requirement
ALL SDK calls — including getCmdChannel() and sendCommand() — must be made from the GLib main thread. Calling SDK methods from a std::thread or any background thread returns ZoomVideoSDKErrors_Internal_Error (error code 2).
Use g_idle_add() to schedule SDK calls from background threads. See Common Issues for details.
---
Complete Working Code
CommandHandler.h
#ifndef COMMAND_HANDLER_H
#define COMMAND_HANDLER_H
#include "zoom_video_sdk_api.h"
#include "zoom_video_sdk_interface.h"
#include <glib.h>
#include <string>
#include <functional>
USING_ZOOM_VIDEO_SDK_NAMESPACE
class CommandHandler {
public:
CommandHandler(IZoomVideoSDK* sdk);
// Send commands (MUST be called from GLib main thread)
bool SendToAll(const std::string& command);
bool SendToUser(IZoomVideoSDKUser* user, const std::string& command);
// Schedule send from a background thread (thread-safe)
void SendToAllFromBackground(const std::string& command);
// Connection status
bool IsConnected() const { return m_connected; }
// Callbacks from delegate
void OnCommandReceived(IZoomVideoSDKUser* sender, const char* command);
void OnConnectResult(bool success);
// Set message handler
using MessageCallback = std::function<void(IZoomVideoSDKUser*, const std::string&)>;
void SetMessageHandler(MessageCallback callback) { m_callback = callback; }
private:
IZoomVideoSDK* m_sdk;
IZoomVideoSDKCmdChannel* m_cmdChannel;
bool m_connected;
MessageCallback m_callback;
};
#endif // COMMAND_HANDLER_HCommandHandler.cpp
#include "CommandHandler.h"
#include <cstdio>
// Context struct for g_idle_add() — used to schedule SDK calls from background threads
struct SendCmdContext {
IZoomVideoSDK* sdk;
std::string cmd;
};
// Runs on the GLib main thread — safe to call SDK methods here
static gboolean sendCommandOnMainThread(gpointer data) {
auto* ctx = static_cast<SendCmdContext*>(data);
IZoomVideoSDKCmdChannel* ch = ctx->sdk->getCmdChannel();
if (ch) {
ZoomVideoSDKErrors err = ch->sendCommand(nullptr, ctx->cmd.c_str());
if (err != ZoomVideoSDKErrors_Success) {
printf("[CMD] Send failed: %d\n", err);
}
}
delete ctx;
return G_SOURCE_REMOVE; // One-shot — do not repeat
}
CommandHandler::CommandHandler(IZoomVideoSDK* sdk)
: m_sdk(sdk)
, m_cmdChannel(nullptr)
, m_connected(false) {
}
bool CommandHandler::SendToAll(const std::string& command) {
if (!m_cmdChannel) {
m_cmdChannel = m_sdk->getCmdChannel();
}
if (!m_cmdChannel) {
printf("[CMD] Command channel not available\n");
return false;
}
ZoomVideoSDKErrors err = m_cmdChannel->sendCommand(nullptr, command.c_str());
if (err == ZoomVideoSDKErrors_Success) {
printf("[CMD] Sent to all: %s\n", command.c_str());
return true;
}
printf("[CMD] Send failed: %d\n", err);
return false;
}
bool CommandHandler::SendToUser(IZoomVideoSDKUser* user, const std::string& command) {
if (!user) return false;
if (!m_cmdChannel) {
m_cmdChannel = m_sdk->getCmdChannel();
}
if (!m_cmdChannel) {
return false;
}
ZoomVideoSDKErrors err = m_cmdChannel->sendCommand(user, command.c_str());
if (err == ZoomVideoSDKErrors_Success) {
printf("[CMD] Sent to %s: %s\n", user->getUserName(), command.c_str());
return true;
}
printf("[CMD] Send failed: %d\n", err);
return false;
}
void CommandHandler::SendToAllFromBackground(const std::string& command) {
// Thread-safe: g_idle_add queues work onto the GLib main loop
auto* ctx = new SendCmdContext{m_sdk, command};
g_idle_add(sendCommandOnMainThread, ctx);
}
void CommandHandler::OnCommandReceived(IZoomVideoSDKUser* sender, const char* command) {
if (!sender || !command) return;
std::string cmdStr(command);
printf("[CMD] From %s: %s\n", sender->getUserName(), cmdStr.c_str());
if (m_callback) {
m_callback(sender, cmdStr);
}
}
void CommandHandler::OnConnectResult(bool success) {
m_connected = success;
printf("[CMD] Command channel %s\n", success ? "connected" : "failed");
}Using in Delegate
class BotDelegate : public IZoomVideoSDKDelegate {
private:
CommandHandler* m_cmdHandler;
public:
BotDelegate(IZoomVideoSDK* sdk) {
m_cmdHandler = new CommandHandler(sdk);
m_cmdHandler->SetMessageHandler([this](IZoomVideoSDKUser* sender,
const std::string& cmd) {
HandleCommand(sender, cmd);
});
}
void onCommandChannelConnectResult(bool isSuccess) override {
m_cmdHandler->OnConnectResult(isSuccess);
if (isSuccess) {
// Channel ready — safe to send commands now
m_cmdHandler->SendToAll("{\"type\":\"hello\"}");
}
}
void onCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* strCmd) override {
m_cmdHandler->OnCommandReceived(sender, strCmd);
}
// ... other delegate methods ...
private:
void HandleCommand(IZoomVideoSDKUser* sender, const std::string& cmd) {
// Parse JSON commands
if (cmd.find("\"type\":\"ping\"") != std::string::npos) {
m_cmdHandler->SendToUser(sender, "{\"type\":\"pong\"}");
}
}
};---
Sending from a Background Thread
If you need to trigger a command from a polling thread, HTTP handler, or any non-main thread, use SendToAllFromBackground() which internally uses g_idle_add():
// From a background polling thread:
void pollingThread(CommandHandler* cmdHandler) {
while (running) {
std::string data = fetchDataFromServer();
if (!data.empty()) {
// Thread-safe — schedules on GLib main thread
cmdHandler->SendToAllFromBackground(data);
}
std::this_thread::sleep_for(std::chrono::seconds(3));
}
}Do NOT call `sendCommand()` directly from background threads — it returns error code 2 (Internal_Error).
---
Command Channel Lifecycle
1. Call joinSession() — the command channel connects automatically 2. onCommandChannelConnectResult(true) fires when ready 3. Send commands with sendCommand(nullptr, msg) (broadcast) or sendCommand(user, msg) (targeted) 4. Receive commands via onCommandReceived(sender, command) callback 5. Channel disconnects when you leave the session
Session-scoped: The command channel only works between participants in the same session. It does NOT span across different sessions.
---
Common Issues
Commands Not Received
Cause: Channel not connected yet
Fix: Wait for onCommandChannelConnectResult(true) before sending:
void onCommandChannelConnectResult(bool isSuccess) override {
if (isSuccess) {
// NOW safe to send commands
}
}Error 2 (Internal_Error) on sendCommand
Cause: Calling SDK from a background thread
Fix: Use g_idle_add() to schedule on the GLib main thread (see SendToAllFromBackground above).
Targeted Send Fails
Cause: User pointer may be stale if user disconnected
Fix: Use broadcast (sendCommand(nullptr, msg)) which is more reliable:
// More reliable — broadcast to all
cmdChannel->sendCommand(nullptr, msg.c_str());
// Risky — user pointer may be stale
cmdChannel->sendCommand(userPtr, msg.c_str());---
Related Documentation
- Session Join Pattern - Session setup with GLib main loop
- Common Issues - Threading and GLib requirements
- Windows Command Channel - Windows equivalent (uses wchar_t)
- Web Command Channel - Web SDK equivalent
- Authorization - JWT roleType for host/co-host
Live Streaming (Linux)
Typical pattern:
- capture raw audio/video
- mux/encode as needed
- push to RTMP/WebRTC destination
Use this pattern when you need to relay captured session media to an external live-streaming destination.
Qt/GTK Integration (Linux)
For native UI apps:
- integrate SDK init/join into your app lifecycle
- render video into your window surface
- handle threading carefully
Use this pattern when you are embedding the Linux SDK into an existing native desktop UI.
Raw Audio Capture (Linux)
Use raw audio capture when you need:
- transcription
- diarization
- audio analytics
Use this pattern when you need a minimal starting point for audio capture and downstream analysis.
Raw Video Capture (Linux)
Use raw video capture when you need frames for:
- recording/transcoding
- computer vision
- streaming to a third party
Use this pattern when you need a minimal starting point for frame capture and downstream processing.
Session Join Pattern - Complete Working Example
Overview
This guide provides a complete, working example of joining a Zoom Video SDK session on Linux, including JWT generation, session configuration, event handling, and cleanup.
Prerequisites
# System dependencies
sudo apt update
sudo apt install -y build-essential gcc cmake libglib2.0-dev liblzma-dev \
libxcb-image0 libxcb-keysyms1 libxcb-xfixes0 libxcb-xkb1 libxcb-shape0 \
libxcb-shm0 libxcb-randr0 libxcb-xtest0 libgbm1 libxtst6 libgl1 libnss3 \
libasound2 libpulse0
# For headless Linux
sudo apt install -y pulseaudio
mkdir -p ~/.config
echo "[General]" > ~/.config/zoomus.conf
echo "system.audio.type=default" >> ~/.config/zoomus.conf
# Create log directory
mkdir -p ~/.zoom/logsJWT Token Generation
CRITICAL: You need a JWT token to join sessions. Generate from your SDK credentials.
Using Python
import jwt
import time
def generate_video_sdk_jwt(sdk_key, sdk_secret, session_name, role_type=1, session_key="", user_identity=""):
iat = int(time.time()) - 30
exp = iat + 60 * 60 * 2 # 2 hours
payload = {
"app_key": sdk_key,
"iat": iat,
"exp": exp,
"tpc": session_name,
"role_type": role_type, # 0=participant, 1=host
}
if session_key:
payload["session_key"] = session_key
if user_identity:
payload["user_identity"] = user_identity
token = jwt.encode(payload, sdk_secret, algorithm="HS256")
return token
# Usage
SDK_KEY = "YOUR_SDK_KEY"
SDK_SECRET = "YOUR_SDK_SECRET"
SESSION_NAME = "my-test-session"
jwt_token = generate_video_sdk_jwt(SDK_KEY, SDK_SECRET, SESSION_NAME, role_type=1)
print(f"JWT Token: {jwt_token}")Using Node.js
const jwt = require('jsonwebtoken');
function generateVideoSDKJWT(sdkKey, sdkSecret, sessionName, roleType = 1) {
const iat = Math.floor(Date.now() / 1000) - 30;
const exp = iat + 60 * 60 * 2; // 2 hours
const payload = {
app_key: sdkKey,
iat: iat,
exp: exp,
tpc: sessionName,
role_type: roleType // 0=participant, 1=host
};
return jwt.sign(payload, sdkSecret);
}
const SDK_KEY = "YOUR_SDK_KEY";
const SDK_SECRET = "YOUR_SDK_SECRET";
const SESSION_NAME = "my-test-session";
const jwtToken = generateVideoSDKJWT(SDK_KEY, SDK_SECRET, SESSION_NAME, 1);
console.log(`JWT Token: ${jwtToken}`);Complete C++ Implementation
Header File: BotDelegate.h
#ifndef BOT_DELEGATE_H
#define BOT_DELEGATE_H
#include "zoom_video_sdk_api.h"
#include "zoom_video_sdk_interface.h"
#include "zoom_video_sdk_delegate_interface.h"
#include <stdio.h>
#include <atomic>
USING_ZOOM_VIDEO_SDK_NAMESPACE
class BotDelegate : public IZoomVideoSDKDelegate {
public:
BotDelegate() : running_(true) {}
bool isRunning() const { return running_; }
void stop() { running_ = false; }
// Session events
virtual void onSessionJoin() override;
virtual void onSessionLeave() override;
virtual void onError(ZoomVideoSDKErrors errorCode, int detailErrorCode) override;
// User events
virtual void onUserJoin(IZoomVideoSDKUserHelper* pUserHelper,
IVideoSDKVector<IZoomVideoSDKUser*>* userList) override;
virtual void onUserLeave(IZoomVideoSDKUserHelper* pUserHelper,
IVideoSDKVector<IZoomVideoSDKUser*>* userList) override;
virtual void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* pVideoHelper,
IVideoSDKVector<IZoomVideoSDKUser*>* userList) override;
virtual void onUserAudioStatusChanged(IZoomVideoSDKAudioHelper* pAudioHelper,
IVideoSDKVector<IZoomVideoSDKUser*>* userList) override;
// Password events
virtual void onSessionNeedPassword(IZoomVideoSDKPasswordHandler* handler) override;
virtual void onSessionPasswordWrong(IZoomVideoSDKPasswordHandler* handler) override;
// Host/manager events
virtual void onUserHostChanged(IZoomVideoSDKUserHelper* pUserHelper,
IZoomVideoSDKUser* pUser) override;
virtual void onUserManagerChanged(IZoomVideoSDKUser* pUser) override;
virtual void onUserNameChanged(IZoomVideoSDKUser* pUser) override;
// Audio raw data (optional)
virtual void onMixedAudioRawDataReceived(AudioRawData* data_) override;
virtual void onOneWayAudioRawDataReceived(AudioRawData* data_,
IZoomVideoSDKUser* pUser) override;
// Minimal stubs for required callbacks
virtual void onUserShareStatusChanged(IZoomVideoSDKShareHelper*, IZoomVideoSDKUser*,
IZoomVideoSDKShareAction*) override {}
virtual void onLiveStreamStatusChanged(IZoomVideoSDKLiveStreamHelper*,
ZoomVideoSDKLiveStreamStatus) override {}
virtual void onCloudRecordingStatus(RecordingStatus, IZoomVideoSDKRecordingConsentHandler*) override {}
virtual void onHostAskUnmute() override {}
virtual void onUserActiveAudioChanged(IZoomVideoSDKAudioHelper*, IVideoSDKVector<IZoomVideoSDKUser*>*) override {}
virtual void onSessionNeedPassword(IZoomVideoSDKPasswordHandler*) override {}
virtual void onSessionPasswordWrong(IZoomVideoSDKPasswordHandler*) override {}
virtual void onMixedAudioRawDataReceived(AudioRawData*) override {}
virtual void onOneWayAudioRawDataReceived(AudioRawData*, IZoomVideoSDKUser*) override {}
virtual void onShareAudioRawDataReceived(AudioRawData*) override {}
virtual void onUserRecordingConsent(IZoomVideoSDKUser*) override {}
virtual void onCommandReceived(IZoomVideoSDKUser*, const zchar_t*) override {}
virtual void onCommandChannelConnectResult(bool) override {}
virtual void onChatNewMessageNotify(IZoomVideoSDKChatHelper*, IZoomVideoSDKChatMessage*) override {}
virtual void onChatMsgDeleteNotification(IZoomVideoSDKChatHelper*, const zchar_t*,
ZoomVideoSDKChatMessageDeleteType) override {}
virtual void onShareContentChanged(IZoomVideoSDKShareHelper*, IZoomVideoSDKUser*,
IZoomVideoSDKShareAction*) override {}
virtual void onLiveTranscriptionStatus(ZoomVideoSDKLiveTranscriptionStatus) override {}
virtual void onLiveTranscriptionMsgReceived(const zchar_t*, IZoomVideoSDKUser*,
ZoomVideoSDKLiveTranscriptionOperationType) override {}
virtual void onLiveTranscriptionMsgInfoReceived(ILiveTranscriptionMessageInfo*) override {}
virtual void onLiveTranscriptionMsgError(ILiveTranscriptionLanguage*,
ILiveTranscriptionLanguage*) override {}
virtual void onOriginalLanguageMsgReceived(ILiveTranscriptionMessageInfo*) override {}
virtual void onInviteByPhoneStatus(PhoneStatus, PhoneFailedReason) override {}
virtual void onCalloutJoinSuccess(IZoomVideoSDKUser*, const zchar_t*) override {}
virtual void onCameraControlRequestResult(IZoomVideoSDKUser*, bool) override {}
virtual void onCameraControlRequestReceived(IZoomVideoSDKUser*, ZoomVideoSDKCameraControlRequestType,
IZoomVideoSDKCameraControlRequestHandler*) override {}
virtual void onProxyDetectComplete() override {}
virtual void onProxySettingNotification(IZoomVideoSDKProxySettingHandler*) override {}
virtual void onSSLCertVerifiedFailNotification(IZoomVideoSDKSSLCertificateInfo*) override {}
virtual void onVideoAlphaChannelStatusChanged(bool) override {}
virtual void onMultiCameraStreamStatusChanged(ZoomVideoSDKMultiCameraStreamStatus,
IZoomVideoSDKUser*, IZoomVideoSDKRawDataPipe*) override {}
virtual void onUserVideoNetworkStatusChanged(ZoomVideoSDKNetworkStatus, IZoomVideoSDKUser*) override {}
virtual void onChatPrivilegeChanged(IZoomVideoSDKChatHelper*, ZoomVideoSDKChatPrivilegeType) override {}
virtual void onVideoCanvasSubscribeFail(ZoomVideoSDKSubscribeFailReason, IZoomVideoSDKUser*, void*) override {}
virtual void onShareCanvasSubscribeFail(ZoomVideoSDKSubscribeFailReason, IZoomVideoSDKUser*, void*) override {}
private:
std::atomic<bool> running_;
};
#endif // BOT_DELEGATE_HImplementation File: BotDelegate.cpp
#include "BotDelegate.h"
void BotDelegate::onSessionJoin() {
printf("[EVENT] Session joined successfully!\n");
// Get session info
IZoomVideoSDKSession* session = video_sdk_obj->getSessionInfo();
if (session) {
printf(" Session Name: %s\n", session->getSessionName());
printf(" Session ID: %s\n", session->getSessionID());
// Get myself
IZoomVideoSDKUser* myself = session->getMyself();
if (myself) {
printf(" My Name: %s\n", myself->getUserName());
printf(" Is Host: %s\n", myself->isHost() ? "Yes" : "No");
}
}
// Start audio
IZoomVideoSDKAudioHelper* audio = video_sdk_obj->getAudioHelper();
if (audio) {
ZoomVideoSDKErrors err = audio->startAudio();
if (err == ZoomVideoSDKErrors_Success) {
printf(" Audio started\n");
// Subscribe to raw audio (optional)
audio->subscribe();
} else {
printf(" Failed to start audio: %d\n", err);
}
}
}
void BotDelegate::onSessionLeave() {
printf("[EVENT] Session left\n");
running_ = false;
}
void BotDelegate::onError(ZoomVideoSDKErrors errorCode, int detailErrorCode) {
printf("[ERROR] Error occurred: %d, Detail: %d\n", errorCode, detailErrorCode);
// Common errors
switch (errorCode) {
case ZoomVideoSDKErrors_Auth_Error:
printf(" Authentication failed - check JWT token\n");
break;
case ZoomVideoSDKErrors_Auth_Wrong_Key_or_Secret:
printf(" Wrong SDK key or secret\n");
break;
case ZoomVideoSDKErrors_Session_Join_Failed:
printf(" Failed to join session\n");
break;
case ZoomVideoSDKErrors_Session_Need_Password:
printf(" Session requires password\n");
break;
case ZoomVideoSDKErrors_Session_Password_Wrong:
printf(" Wrong session password\n");
break;
default:
break;
}
}
void BotDelegate::onUserJoin(IZoomVideoSDKUserHelper* pUserHelper,
IVideoSDKVector<IZoomVideoSDKUser*>* userList) {
if (!userList) return;
int count = userList->GetCount();
printf("[EVENT] %d user(s) joined\n", count);
for (int i = 0; i < count; i++) {
IZoomVideoSDKUser* user = userList->GetItem(i);
if (user) {
printf(" User: %s\n", user->getUserName());
}
}
}
void BotDelegate::onUserLeave(IZoomVideoSDKUserHelper* pUserHelper,
IVideoSDKVector<IZoomVideoSDKUser*>* userList) {
if (!userList) return;
int count = userList->GetCount();
printf("[EVENT] %d user(s) left\n", count);
for (int i = 0; i < count; i++) {
IZoomVideoSDKUser* user = userList->GetItem(i);
if (user) {
printf(" User: %s\n", user->getUserName());
}
}
}
void BotDelegate::onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* pVideoHelper,
IVideoSDKVector<IZoomVideoSDKUser*>* userList) {
if (!userList) return;
int count = userList->GetCount();
for (int i = 0; i < count; i++) {
IZoomVideoSDKUser* user = userList->GetItem(i);
if (user) {
printf("[EVENT] Video status changed for: %s\n", user->getUserName());
// Subscribe to video here if needed
}
}
}
void BotDelegate::onUserAudioStatusChanged(IZoomVideoSDKAudioHelper* pAudioHelper,
IVideoSDKVector<IZoomVideoSDKUser*>* userList) {
if (!userList) return;
int count = userList->GetCount();
for (int i = 0; i < count; i++) {
IZoomVideoSDKUser* user = userList->GetItem(i);
if (user) {
IZoomVideoSDKAudioStatus* audioStatus = user->getAudioStatus();
if (audioStatus) {
printf("[EVENT] Audio status for %s: Muted=%s\n",
user->getUserName(),
audioStatus->isMuted() ? "Yes" : "No");
}
}
}
}
void BotDelegate::onSessionNeedPassword(IZoomVideoSDKPasswordHandler* handler) {
printf("[EVENT] Session requires password\n");
// Provide password if available
// handler->inputSessionPassword("password");
// Or leave without password
// handler->leaveSessionIgnorePassword();
}
void BotDelegate::onSessionPasswordWrong(IZoomVideoSDKPasswordHandler* handler) {
printf("[EVENT] Wrong session password\n");
// Retry with correct password or leave
// handler->inputSessionPassword("correct-password");
// handler->leaveSessionIgnorePassword();
}
void BotDelegate::onUserHostChanged(IZoomVideoSDKUserHelper* pUserHelper,
IZoomVideoSDKUser* pUser) {
if (pUser) {
printf("[EVENT] Host changed to: %s\n", pUser->getUserName());
}
}
void BotDelegate::onUserManagerChanged(IZoomVideoSDKUser* pUser) {
if (pUser) {
printf("[EVENT] Manager changed: %s (Is Manager: %s)\n",
pUser->getUserName(),
pUser->isManager() ? "Yes" : "No");
}
}
void BotDelegate::onUserNameChanged(IZoomVideoSDKUser* pUser) {
if (pUser) {
printf("[EVENT] User name changed to: %s\n", pUser->getUserName());
}
}
void BotDelegate::onMixedAudioRawDataReceived(AudioRawData* data_) {
// Process mixed audio (all participants)
// char* buffer = data_->GetBuffer();
// unsigned int len = data_->GetBufferLen();
// unsigned int sampleRate = data_->GetSampleRate();
}
void BotDelegate::onOneWayAudioRawDataReceived(AudioRawData* data_,
IZoomVideoSDKUser* pUser) {
// Process per-user audio
// if (pUser) {
// printf("Audio from: %s\n", pUser->getUserName());
// }
}Main File: main.cpp
IMPORTANT: The SDK internally uses Qt/GLib for event dispatching. You MUST use a GLib main loop — a while (running) { sleep(); } loop will NOT dispatch SDK events, and callbacks like onSessionJoin will never fire. See Common Issues for details.
#include "BotDelegate.h"
#include <glib.h>
#include <thread>
#include <chrono>
#include <signal.h>
IZoomVideoSDK* video_sdk_obj = nullptr;
BotDelegate* delegate = nullptr;
static GMainLoop* g_loop = nullptr;
// GLib timeout callback - checks if bot should stop
static gboolean glib_timeout_callback(gpointer data) {
BotDelegate* del = static_cast<BotDelegate*>(data);
if (!del->isRunning()) {
g_main_loop_quit(g_loop);
return FALSE; // Remove this timeout source
}
return TRUE; // Keep checking
}
void signalHandler(int signum) {
printf("\nReceived signal %d, cleaning up...\n", signum);
if (delegate) {
delegate->stop();
}
if (g_loop) {
g_main_loop_quit(g_loop);
}
}
int main(int argc, char* argv[]) {
// Check arguments
if (argc < 4) {
printf("Usage: %s <session_name> <user_name> <jwt_token> [session_password]\n", argv[0]);
return 1;
}
const char* sessionName = argv[1];
const char* userName = argv[2];
const char* jwtToken = argv[3];
const char* sessionPassword = (argc >= 5) ? argv[4] : "";
// Setup signal handlers
signal(SIGINT, signalHandler);
signal(SIGTERM, signalHandler);
printf("Zoom Video SDK Bot\n");
printf("==================\n");
printf("Session: %s\n", sessionName);
printf("User: %s\n", userName);
printf("\n");
// 1. Create SDK object
video_sdk_obj = CreateZoomVideoSDKObj();
if (!video_sdk_obj) {
printf("Failed to create SDK object\n");
return 1;
}
// 2. Initialize SDK
ZoomVideoSDKInitParams init_params;
init_params.domain = "https://zoom.us";
init_params.enableLog = true;
init_params.logFilePrefix = "bot";
init_params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
init_params.shareRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
init_params.audioRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;
init_params.enableIndirectRawdata = false;
ZoomVideoSDKErrors err = video_sdk_obj->initialize(init_params);
if (err != ZoomVideoSDKErrors_Success) {
printf("Failed to initialize SDK: %d\n", err);
DestroyZoomVideoSDKObj();
return 1;
}
printf("SDK initialized\n");
// 3. Add delegate
delegate = new BotDelegate();
video_sdk_obj->addListener(delegate);
// 4. Configure session context
ZoomVideoSDKSessionContext session_context;
session_context.sessionName = sessionName;
session_context.sessionPassword = sessionPassword;
session_context.userName = userName;
session_context.token = jwtToken;
session_context.sessionIdleTimeoutMins = 40;
session_context.autoLoadMutliStream = true;
session_context.videoOption.localVideoOn = false; // Headless bot
session_context.audioOption.connect = true;
session_context.audioOption.mute = false;
// For headless Linux: Virtual audio speaker
// Uncomment if you have implemented VirtualSpeaker class
// session_context.virtualAudioSpeaker = new VirtualSpeaker();
// 5. Join session
printf("Joining session...\n");
IZoomVideoSDKSession* session = video_sdk_obj->joinSession(session_context);
if (!session) {
printf("Failed to join session\n");
video_sdk_obj->cleanup();
DestroyZoomVideoSDKObj();
delete delegate;
return 1;
}
// 6. GLib main loop - REQUIRED for SDK event dispatching
// A while/sleep loop does NOT work — SDK callbacks will never fire without GLib.
printf("Bot is running. Press Ctrl+C to exit.\n\n");
g_loop = g_main_loop_new(NULL, FALSE);
g_timeout_add(100, glib_timeout_callback, delegate);
g_main_loop_run(g_loop); // Blocks here, SDK events dispatch on this thread
g_main_loop_unref(g_loop);
// 7. Cleanup
printf("\nCleaning up...\n");
if (video_sdk_obj->isInSession()) {
video_sdk_obj->leaveSession(false);
// Wait a bit for leave to complete
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
video_sdk_obj->cleanup();
DestroyZoomVideoSDKObj();
delete delegate;
printf("Goodbye!\n");
return 0;
}CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(ZoomVideoSDKBot VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(PkgConfig REQUIRED)
pkg_check_modules(GLIB REQUIRED glib-2.0)
# SDK paths
include_directories(
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/include/zoom_video_sdk
${GLIB_INCLUDE_DIRS}
)
link_directories(${CMAKE_SOURCE_DIR}/lib/zoom_video_sdk)
# Source files
set(SOURCES
src/main.cpp
src/BotDelegate.cpp
)
add_executable(${PROJECT_NAME} ${SOURCES})
target_link_libraries(${PROJECT_NAME}
videosdk
${GLIB_LIBRARIES}
pthread
)
set_target_properties(${PROJECT_NAME} PROPERTIES
BUILD_RPATH "${CMAKE_SOURCE_DIR}/lib/zoom_video_sdk"
INSTALL_RPATH "${CMAKE_SOURCE_DIR}/lib/zoom_video_sdk"
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin
)Build and Run
# 1. Extract SDK
tar -xf zoom-video-sdk-linux_x86_64.tar.xz
cd zoom-video-sdk-linux_x86_64
# 2. Copy Qt5 libraries and create symlinks
cp -r samples/qt_libs/Qt/lib/* lib/
cd lib
for lib in libQt5*.so.5; do
ln -sf $lib ${lib%.5}
done
cd ..
# 3. Build
mkdir build && cd build
cmake ..
make
# 4. Generate JWT token (use Python script above)
JWT_TOKEN="your.jwt.token.here"
# 5. Run
cd ../bin
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../lib/zoom_video_sdk
./ZoomVideoSDKBot "test-session" "Linux Bot" "$JWT_TOKEN"Testing
# Test with password
./ZoomVideoSDKBot "test-session" "Bot" "$JWT_TOKEN" "password123"
# Test as host (role_type=1 in JWT)
./ZoomVideoSDKBot "host-session" "Host Bot" "$HOST_JWT_TOKEN"
# Test as participant (role_type=0 in JWT)
./ZoomVideoSDKBot "join-session" "Participant Bot" "$PARTICIPANT_JWT_TOKEN"Common Issues
Issue: Callbacks not firing
Solution: You MUST use a GLib main loop (g_main_loop_run). A while/sleep loop does not dispatch SDK events. See the main.cpp example above and Common Issues.
Issue: "Failed to join session"
Causes: 1. Invalid JWT token 2. Session name doesn't match JWT tpc claim 3. Wrong SDK credentials 4. Expired token
Solution: Verify JWT payload and regenerate token.
Issue: "Auth failed"
Solution: Check SDK_KEY and SDK_SECRET in JWT generation.
Issue: No audio on headless Linux
Solution: Use virtual audio speaker:
session_context.virtualAudioSpeaker = new VirtualSpeaker();---
See Also
- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Universal pattern
- [Raw Audio Capture](raw-audio-capture.md) - Capture audio
- [Raw Video Capture](raw-video-capture.md) - Capture video
- [Virtual Audio/Video](virtual-audio-video.md) - Custom media injection
- [Command Channel](command-channel.md) - Custom command messaging with threading
Transcription (Linux)
Typical pattern:
- capture raw audio per user (if supported)
- feed to ASR
- optionally post results back via chat or external UI
Use this pattern when you need a simple speech-to-text pipeline on top of captured session audio.
Virtual Audio/Video (Injection) (Linux)
Use injection when you need a bot participant that:
- plays a pre-recorded clip into the session
- generates audio/video programmatically
Use this pattern when your bot needs to inject generated or prerecorded media into the session.
Video SDK Linux 5-Minute Preflight Runbook
Use this before deep debugging.
Skill Doc Standard Note
- Skill entrypoint is
SKILL.md. - This runbook is an operational convention (recommended), not a required skill file.
- SDK/API names can drift by version; validate current names against docs/raw-docs before release.
1) Confirm Integration Surface
- Confirm this is a Video SDK custom session flow for Linux (not Meeting SDK).
- Verify UI/state are driven by session events, not meeting semantics.
- Wrapper platforms require JS/native bridge synchronization checks.
2) Confirm Required Credentials
- Video SDK app credentials (SDK Key/Secret) stored server-side.
- Backend-generated session JWT token.
- Session fields (
sessionName,userName, role type) resolved before join.
3) Confirm Lifecycle Order
1. Initialize SDK client/context and register event listeners. 2. Generate/fetch session token from backend. 3. Join session and establish media streams. 4. Handle participant/media/control events during active session.
4) Confirm Event/State Handling
- Keep participant state keyed by user/session IDs.
- Reconcile subscribe/unsubscribe transitions for video/audio/share streams.
- Treat reconnect and device-change events as first-class state transitions.
5) Confirm Cleanup + Upgrade Posture
- Leave/end session and release helper/client resources.
- Remove listeners to avoid duplicate callbacks on rejoin.
- Re-check SDK version compatibility before deployment updates.
6) Quick Probes
- Token issuance and join flow succeed once end-to-end.
- Audio/video publish-subscribe operations complete with expected callbacks.
- Leave/rejoin works without leaked listener or stream state.
7) Fast Decision Tree
- Join fails immediately -> invalid/expired token or session field mismatch.
- Media state stuck -> listener binding/order issue or permission/device problem.
- Inconsistent behavior after update -> wrapper/native SDK version mismatch.
8) Source Checkpoints
Official docs
- https://developers.zoom.us/docs/video-sdk/linux/
- https://marketplacefront.zoom.us/sdk/custom/linux/
Raw docs in repo
raw-docs/developers.zoom.us/docs/video-sdk/linux/raw-docs/marketplacefront.zoom.us/sdk/video-sdk/linux/
Common Build Errors
CMake Errors
Error: "Could not find glib-2.0"
sudo apt install -y libglib2.0-devError: "CMake version too old"
# Install latest CMake
wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc | gpg --dearmor - | sudo tee /etc/apt/trusted.gpg.d/kitware.gpg
sudo apt-add-repository 'deb https://apt.kitware.com/ubuntu/ focal main'
sudo apt update
sudo apt install -y cmakeLinker Errors
Error: "undefined reference to `CreateZoomVideoSDKObj'"
Cause: Not linking libvideosdk.so
Solution:
link_directories(${CMAKE_SOURCE_DIR}/lib/zoom_video_sdk)
target_link_libraries(${PROJECT_NAME} videosdk)Error: Missing Qt symbols
Solution: Link Qt5 libraries:
target_link_libraries(${PROJECT_NAME}
videosdk
Qt5Core Qt5Gui Qt5Network Qt5Qml Qt5Quick
)Header Include Errors
Error: "zoom_video_sdk_api.h: No such file or directory"
Solution:
include_directories(
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/include/zoom_video_sdk
)Error: "helpers/zoom_video_sdk_*.h: No such file"
Solution: Include both paths:
include_directories(
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/include/zoom_video_sdk # For helpers/ relative includes
)Runtime Errors
Error: "libvideosdk.so: cannot open shared object file"
Solution:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/lib/zoom_video_sdkOr set RPATH:
set_target_properties(${PROJECT_NAME} PROPERTIES
BUILD_RPATH "${CMAKE_SOURCE_DIR}/lib/zoom_video_sdk"
INSTALL_RPATH "${CMAKE_SOURCE_DIR}/lib/zoom_video_sdk"
)Compiler Errors
Error: "ISO C++17 does not allow dynamic exception specifications"
Solution: SDK requires C++17:
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)Error: "pure virtual method called"
Cause: Missing IZoomVideoSDKDelegate method implementations.
Solution: Implement ALL delegate methods (even empty stubs).
Common Issues and Solutions
Session Join Issues
Issue: "Failed to join session" (no error details)
Causes: 1. Invalid JWT token 2. Session name doesn't match JWT tpc claim 3. Wrong SDK credentials 4. Expired token
Solution:
# Regenerate JWT token
import jwt
import time
payload = {
"app_key": SDK_KEY,
"iat": int(time.time()) - 30,
"exp": int(time.time()) + 7200,
"tpc": "exact-session-name", # MUST match sessionName
"role_type": 1 # 0=participant, 1=host
}
token = jwt.encode(payload, SDK_SECRET, algorithm="HS256")Issue: "Authentication failed"
Solution: Verify SDK_KEY and SDK_SECRET in JWT generation.
Issue: Session requires password
Solution:
session_context.sessionPassword = "password";Audio Issues
Issue: No audio callbacks
Cause: PulseAudio not configured.
Solution: See PulseAudio Setup.
Issue: Audio on headless Linux
Solution: Use virtual audio:
session_context.virtualAudioSpeaker = new VirtualSpeaker();
session_context.virtualAudioMic = new VirtualMic();Video Issues
Issue: "Linux has no Canvas API!"
Solution: Use Raw Data Pipe. See Raw Data vs Canvas.
Issue: Video frames not received
Cause: Not subscribing to user's video pipe.
Solution:
void onUserVideoStatusChanged(..., IVideoSDKVector<IZoomVideoSDKUser*>* userList) override {
for (int i = 0; i < userList->GetCount(); i++) {
IZoomVideoSDKUser* user = userList->GetItem(i);
IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe();
pipe->subscribe(ZoomVideoSDKResolution_720P, videoDelegate);
}
}Build Issues
Issue: "libQt5Core.so.5: not found"
Solution: See Qt Dependencies.
Issue: Undefined reference to SDK symbols
Solution: See Build Errors.
Memory Issues
Issue: Crashes with large video frames
Solution: Use heap memory mode:
init_params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap;SDK Init Error 7 (Invalid_Parameter)
sdk->initialize(initParams) returns error code 7 when:
1. Domain is wrong - Must be "https://zoom.us" (with protocol). Plain "zoom.us" causes error 7. 2. PulseAudio is not running - Headless Linux requires PulseAudio. See PulseAudio Setup. 3. Missing `~/.config/zoomus.conf` - Must exist with content:
[General]
system.audio.type=defaultGLib Main Loop Required
The SDK internally uses Qt/GLib for event dispatching. A while (running) { sleep(500ms); } loop does NOT work — onSessionJoin and all other delegate callbacks will never fire.
You MUST use a GLib main loop:
#include <glib.h>
static GMainLoop* loop = nullptr;
gboolean timeout_callback(gpointer data) {
return TRUE;
}
loop = g_main_loop_new(NULL, FALSE);
g_timeout_add(100, timeout_callback, loop);
g_main_loop_run(loop);
if (loop) g_main_loop_quit(loop);See Session Join Pattern for the complete working example.
All SDK Calls Must Be Made from the Main Thread
ALL Zoom Video SDK API calls must be called from the GLib main thread. Calling SDK methods from a std::thread or any background thread returns ZoomVideoSDKErrors_Internal_Error (error code 2).
Use g_idle_add() to schedule SDK calls from background threads:
struct CallContext {
IZoomVideoSDK* sdk;
std::string data;
};
static gboolean executeOnMainThread(gpointer data) {
auto* ctx = static_cast<CallContext*>(data);
// Make SDK calls here — this runs on the GLib main thread
delete ctx;
return G_SOURCE_REMOVE;
}
// From a background thread:
auto* ctx = new CallContext{sdk_, someData};
g_idle_add(executeOnMainThread, ctx);g_idle_add() is thread-safe — it queues work onto the GLib main loop. See Command Channel for a real-world example.
Quick Diagnostic Checklist
- [ ] PulseAudio installed and configured
- [ ] ~/.config/zoomus.conf exists
- [ ] Qt5 libraries copied from SDK
- [ ] Qt5 symlinks created
- [ ] LD_LIBRARY_PATH set correctly
- [ ] JWT token valid and not expired
- [ ] Session name matches JWT
tpcclaim - [ ] All delegate methods implemented
- [ ] Using heap memory mode
- [ ] Subscribing in correct callbacks
- [ ] Domain set to "https://zoom.us" (with protocol)
- [ ] Using GLib main loop (not while/sleep loop)
- [ ] SDK calls made from main thread only (use g_idle_add from background threads)
Error Codes
| Code | Name | Meaning |
|---|---|---|
| 0 | Success | Operation succeeded |
| 1001 | Auth_Error | Authentication failed |
| 1003 | Auth_Wrong_Token | Invalid JWT |
| 1004 | Auth_Expired_Token | JWT expired |
| 3001 | Session_Join_Failed | Failed to join |
| 3008 | Session_Need_Password | Password required |
| 3009 | Session_Password_Wrong | Wrong password |
| 7 | Invalid_Parameter | Wrong domain, missing PulseAudio, or missing zoomus.conf |
| 2 | Internal_Error | SDK method called from wrong thread (use g_idle_add) |
Getting Help
1. Check Official Docs 2. Search Dev Forum 3. Review GitHub Samples
PulseAudio Setup for Linux
Why PulseAudio is Required
The Zoom Video SDK for Linux requires PulseAudio for raw audio functions. Without it, audio raw data callbacks will not work.
Installation
sudo apt update
sudo apt install -y pulseaudioConfiguration
1. Create Configuration File
mkdir -p ~/.config
cat > ~/.config/zoomus.conf << 'CONFIG'
[General]
system.audio.type=default
CONFIG2. Start PulseAudio (if not running)
pulseaudio --check || pulseaudio --startDocker/Headless Setup
For Docker or headless environments:
# Install PulseAudio
apt-get update && apt-get install -y pulseaudio
# Create virtual devices
pactl load-module module-null-sink sink_name=virtual_speaker
pactl load-module module-null-source source_name=virtual_mic
# Configure Zoom
mkdir -p ~/.config
echo "[General]" > ~/.config/zoomus.conf
echo "system.audio.type=default" >> ~/.config/zoomus.confBetter Approach: Use virtual audio speaker/mic in SDK:
session_context.virtualAudioSpeaker = new VirtualSpeaker();
session_context.virtualAudioMic = new VirtualMic();Verification
# Check PulseAudio is running
pulseaudio --check && echo "Running" || echo "Not running"
# List audio devices
pactl list sinks short
pactl list sources short
# Test config
cat ~/.config/zoomus.confCommon Issues
Issue: PulseAudio not starting
# Kill existing instance
pulseaudio --kill
# Start fresh
pulseaudio --start
# Check status
pulseaudio --checkIssue: Permission denied
# Add user to audio group
sudo usermod -aG audio $USER
# Logout and login againIssue: No audio in Docker
Solution: Use virtual audio devices in SDK instead of system audio.
Qt5 Dependencies Setup
Critical: Use Bundled Qt5, NOT System Qt5
IMPORTANT: The Zoom SDK requires specific Qt5 libraries bundled with the SDK. Do NOT install system Qt5.
Setup Steps
1. Extract SDK
tar -xf zoom-video-sdk-linux_x86_64.tar.xz
cd zoom-video-sdk-linux_x86_642. Copy Qt5 Libraries
# Qt5 libs are in SDK samples
cp -r samples/qt_libs/Qt/lib/* lib/zoom_video_sdk/3. Create Symlinks
cd lib/zoom_video_sdk
# Create unversioned symlinks
for lib in libQt5*.so.5; do
ln -sf $lib ${lib%.5}
done
# Verify
ls -la libQt5*.soShould see:
libQt5Core.so -> libQt5Core.so.5
libQt5Core.so.5
libQt5Gui.so -> libQt5Gui.so.5
libQt5Gui.so.5
...4. Set Library Path
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/lib/zoom_video_sdkCMakeLists.txt Configuration
# Link SDK libraries
link_directories(${CMAKE_SOURCE_DIR}/lib/zoom_video_sdk)
target_link_libraries(${PROJECT_NAME}
videosdk
Qt5Core Qt5Gui Qt5Network Qt5Qml Qt5Quick
)
# Set RPATH
set_target_properties(${PROJECT_NAME} PROPERTIES
BUILD_RPATH "${CMAKE_SOURCE_DIR}/lib/zoom_video_sdk"
INSTALL_RPATH "${CMAKE_SOURCE_DIR}/lib/zoom_video_sdk"
)Required Qt5 Libraries
- libQt5Core.so.5
- libQt5Gui.so.5
- libQt5Network.so.5
- libQt5Qml.so.5
- libQt5Quick.so.5
Common Issues
Issue: "libQt5Core.so.5: cannot open shared object file"
Solution:
# Check library path
echo $LD_LIBRARY_PATH
# Add SDK lib directory
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/lib/zoom_video_sdk
# Or set in CMake RPATH (recommended)Issue: "version `Qt_5.15' not found"
Cause: System Qt5 conflicting with SDK Qt5.
Solution: 1. Remove system Qt5 from path 2. Ensure SDK Qt5 libraries are found first 3. Set RPATH correctly in CMake
Issue: Missing symlinks
Solution:
cd lib/zoom_video_sdk
for lib in libQt5*.so.5; do ln -sf $lib ${lib%.5}; donemacOS Architecture Concept
flowchart LR
UI[AppKit/SwiftUI Views] --> Coord[Session Coordinator]
Coord --> SDK[Zoom Video SDK macOS]
Coord --> TokenAPI[Token API]
SDK --> Events[Delegate/Event Stream]
Events --> Coord
Coord --> Render[View/Window Render State]Design guidance
- Centralize SDK access in a coordinator/service boundary.
- Separate render state from transport/session state.
- Treat join, share, and leave as explicit transitions.
macOS Lifecycle Workflow
flowchart TD
A[Fetch token] --> B[Initialize SDK]
B --> C[Attach delegates]
C --> D[Join session]
D --> E[Start camera/mic/share]
E --> F[Process participant and media events]
F --> G[Leave session]
G --> H[Cleanup windows and SDK resources]Operational sequence
1. Request token from backend. 2. Initialize SDK and delegate/event bridge. 3. Join session with user identity. 4. Start media after join confirmation. 5. Handle remote participant/media updates and view lifecycle. 6. Stop media and release resources on leave.
macOS Session Join Pattern
func joinSession(sessionName: String, userName: String) async throws {
let token = try await tokenService.fetchVideoToken(sessionName: sessionName, userName: userName)
try videoSDK.initialize(with: initParams)
videoSDK.delegate = self
try videoSDK.joinSession(
sessionName: sessionName,
userName: userName,
token: token
)
try videoSDK.videoHelper.startVideo()
try videoSDK.audioHelper.startAudio()
}Notes
- Keep media start/stop tied to session callbacks.
- Handle desktop device switching and permission denials cleanly.
macOS Video SDK Overview
What this platform skill is for
- Building custom macOS desktop video experiences
- Managing richer desktop device, windowing, and render scenarios
- Integrating session controls with native app architectures
Primary implementation path
1. Backend issues short-lived Video SDK token. 2. macOS app initializes SDK and event/delegate bridge. 3. App joins session and activates media controls. 4. App maps participant/media events to desktop windows/views. 5. App handles leave, shutdown, and resource cleanup safely.
Prerequisites
- Xcode macOS app setup with SDK frameworks
- Token backend service
- Audio/video/screen permissions and entitlement checks
Important notes
- Keep app-level session state management explicit.
- Validate entitlement and privacy prompts on clean machines.
Source links
- Docs: https://developers.zoom.us/docs/video-sdk/macos/
- API reference: https://marketplacefront.zoom.us/sdk/custom/macos/annotated.html
macOS Environment Variables
| Variable | Required | Used for | Where to find |
|---|---|---|---|
ZOOM_VIDEO_SDK_KEY | Yes | Video SDK app credential pair | Zoom Marketplace -> Video SDK app -> App Credentials |
ZOOM_VIDEO_SDK_SECRET | Yes (server only) | JWT signing for Video SDK token | Zoom Marketplace -> Video SDK app -> App Credentials |
VIDEO_SDK_TOKEN_ENDPOINT | Yes | Desktop app token fetch URL | Your backend deployment config |
VIDEO_SDK_SESSION_NAME | Runtime | Session/topic identifier | Generated by your app workflow |
VIDEO_SDK_USER_NAME | Runtime | Display name in session | Application user profile |
Runtime-only values
VIDEO_SDK_TOKENis server-generated and short-lived.
macOS Reference Map
Docs anchors
- Integration docs: https://developers.zoom.us/docs/video-sdk/macos/
- API class index: https://marketplacefront.zoom.us/sdk/custom/macos/annotated.html
API areas to focus on
- Session context and lifecycle
- Video/audio/share helpers
- Delegate/event callback contracts
- Chat/command and auxiliary helpers
Crawl summary
- Reference pages crawled: 242
- Docs pages crawled: 21 (20 markdown files persisted)
Related skills
FAQ
What does build-zoom-video-sdk-app do?
Reference skill for Zoom Video SDK. Use after routing to a custom-session workflow when the user needs full control over the video experience rather than an actual Zoom meeting.
When should I use build-zoom-video-sdk-app?
User asks about build zoom video sdk app or related SKILL.md workflows.
Is build-zoom-video-sdk-app safe to install?
Review the Security Audits panel on this page before installing in production.