diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 83918a63..c55cc801 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -1429,6 +1429,18 @@ "authentication": "ON_INSTALL" }, "category": "Coding" + }, + { + "name": "zoom-developers", + "source": { + "source": "local", + "path": "./plugins/zoom-developers" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Coding" } ] } diff --git a/plugins/zoom-developers/.codex-plugin/plugin.json b/plugins/zoom-developers/.codex-plugin/plugin.json new file mode 100644 index 00000000..250ac7ef --- /dev/null +++ b/plugins/zoom-developers/.codex-plugin/plugin.json @@ -0,0 +1,45 @@ +{ + "name": "zoom-developers", + "version": "1.0.0", + "description": "Zoom Developers plugin for building Zoom integrations in Codex", + "author": { + "name": "Zoom", + "url": "https://github.com/zoom" + }, + "homepage": "https://developers.zoom.us/", + "repository": "https://github.com/openai/plugins", + "license": "MIT", + "keywords": [ + "zoom", + "developers", + "codex-plugin", + "rest-api", + "meeting-sdk", + "video-sdk", + "webhooks", + "oauth" + ], + "skills": "./skills/", + "interface": { + "displayName": "Zoom Developers", + "shortDescription": "Build Zoom integrations in Codex.", + "longDescription": "Zoom Developers helps engineers choose the right Zoom product surface, design implementation plans, run deterministic setup and debugging workflows, review integrations with focused agents, and build across Zoom APIs and SDKs inside Codex.", + "developerName": "Zoom", + "category": "Coding", + "capabilities": [ + "Interactive", + "Read", + "Write" + ], + "websiteURL": "https://developers.zoom.us/", + "privacyPolicyURL": "https://www.zoom.com/en/trust/privacy/", + "termsOfServiceURL": "https://www.zoom.com/en/trust/terms/", + "defaultPrompt": [ + "Plan and build a Zoom integration." + ], + "brandColor": "#0B5CFF", + "composerIcon": "./assets/zoom-small.svg", + "logo": "./assets/app-icon.svg", + "screenshots": [] + } +} diff --git a/plugins/zoom-developers/.gitignore b/plugins/zoom-developers/.gitignore new file mode 100644 index 00000000..a02a9edf --- /dev/null +++ b/plugins/zoom-developers/.gitignore @@ -0,0 +1 @@ +publishing-guide.md diff --git a/plugins/zoom-developers/AGENTS.md b/plugins/zoom-developers/AGENTS.md new file mode 100644 index 00000000..98051838 --- /dev/null +++ b/plugins/zoom-developers/AGENTS.md @@ -0,0 +1,10 @@ +# Zoom Developers + +This repository contains the `Zoom Developers` plugin for Codex. + +Its purpose is to help engineers: + +- choose the right Zoom product surface +- plan Zoom integrations across APIs, SDKs, events, and auth +- debug broken Zoom integrations +- build Zoom integrations with deterministic command and skill workflows diff --git a/plugins/zoom-developers/CHANGELOG.md b/plugins/zoom-developers/CHANGELOG.md new file mode 100644 index 00000000..270909af --- /dev/null +++ b/plugins/zoom-developers/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +All notable changes to this plugin are documented in this file. + +## Unreleased + +- split the former hybrid plugin into a dedicated developer plugin named `Zoom Developers` +- kept the Zoom build, setup, planning, and debugging workflows in this repo +- updated plugin metadata and slug alignment for `Zoom Developers` diff --git a/plugins/zoom-developers/CONTRIBUTING.md b/plugins/zoom-developers/CONTRIBUTING.md new file mode 100644 index 00000000..a21c67ca --- /dev/null +++ b/plugins/zoom-developers/CONTRIBUTING.md @@ -0,0 +1,186 @@ +# Contributing to Zoom Developers + +Thank you for your interest in contributing. This document covers changes to the `Zoom Developers` Codex plugin. + +## Ways to Contribute + +You can contribute in any of these ways: + +1. Submit a pull request with improvements. +2. Raise an issue on GitHub for bugs, gaps, or enhancement ideas. +3. Reach out on the [Zoom Developer Forum](https://devforum.zoom.us/) with feedback and improvement suggestions for these agent skills. + +### 1. Report Issues +- Documentation errors or outdated information +- Missing use cases or scenarios +- Incorrect code examples + +### 2. Improve Documentation +- Fix typos and clarify explanations +- Add missing code examples +- Update for new SDK versions + +### 3. Add New Skills +- New use cases +- New platform coverage +- New integration patterns + +## Contribution Process + +### For Small Changes (typos, clarifications) + +1. Fork the repository +2. Make your changes +3. Submit a pull request with a clear description + +### For Larger Changes (new use cases, skills) + +1. Open an issue first to discuss the proposed change +2. Fork the repository +3. Create a feature branch +4. Follow the skill format guidelines below +5. Submit a pull request + +## Skill Format Guidelines + +### SKILL.md Structure + +```markdown +--- +name: skill-name +description: | + Brief description (1-3 sentences). + Include when to use this skill. +--- + +# Skill Title + +[Content following the template in PLAN.md] +``` + +### Guidelines + +1. **Keep SKILL.md under 500 lines** - Move details to `references/` +2. **Max 3 directory levels** - `skill/references/file.md` +3. **Include code examples** - Real, working code developers can use +4. **Document gotchas** - Common mistakes and limitations +5. **Link to official sources** - Prefer Zoom documentation + +### Maintenance Checklist + +Use this checklist before merging documentation or skill changes: + +1. Confirm you are editing the correct skill or product folder. +2. Keep `SKILL.md` as the entrypoint in every skill directory. +3. If examples include credentials, reference `.env` keys rather than hardcoded values. +4. Never commit machine-local absolute paths or machine-specific endpoints. +5. After moving or renaming docs, update cross-links from the relevant parent `SKILL.md` files. +6. Verify frontmatter stays accurate: `name`, `description`, and any optional fields such as `triggers`, `argument-hint`, or `user-invocable`. +7. Remove dead links and stale product claims after any refactor or version update. +8. Make sure every new markdown file is reachable from at least one parent navigation file. +9. Track deprecations and renames explicitly so future updates remain migration-safe. + +### Repository Naming Conventions + +- Keep canonical skill folder names aligned with [skills/start/SKILL.md](skills/start/SKILL.md). +- Current canonical folders include: +- `general`, `rest-api`, `webhooks`, `websockets`, `meeting-sdk`, `video-sdk`, `zoom-apps-sdk` +- `rtms`, `team-chat`, `ui-toolkit`, `cobrowse-sdk`, `oauth` +- `contact-center`, `virtual-agent`, `phone`, `rivet-sdk`, `probe-sdk` + +### Markdown Linking Rules (Required) + +- Use real markdown links for local docs (for example: `text -> docs/example.md`). +- Do not use backticks for local doc references if you want them counted in relationship graphs. +- Use repository-relative paths; do not commit machine-local absolute paths (for example `/home/your-user/...`). +- Every new `.md` file should be linked from at least one parent/index/`SKILL.md` file. + +## Using AI Assistants for Contributions + +You can use Codex or another AI assistant to help create or improve skills: + +### Recommended Workflow + +1. **Research Phase** + ``` + Research the official Zoom documentation for [topic]. + Check the developer forum for common issues. + Find working code examples. + ``` + +2. **Drafting Phase** + ``` + Create a skill following the SKILL.md template. + Include practical code examples. + Document known limitations and gotchas. + ``` + +3. **Validation Phase** + ``` + Cross-check all information with official Zoom docs. + Verify code examples are syntactically correct. + Ensure links are valid. + ``` + +### Assistant Usage Tips + +- **Be specific**: "Create a use case for RTMS audio streaming to S3" not "write about RTMS" +- **Provide context**: Share relevant existing skills as examples +- **Iterate**: Review drafts and ask for improvements +- **Verify**: Always cross-check AI-generated content with official sources + +### What AI Assistants Can Help With + +| Task | How Assistants Help | +|------|------------------| +| Research | Search docs, forums, GitHub for information | +| Drafting | Create initial skill content following templates | +| Code examples | Generate working code snippets | +| Cross-referencing | Check consistency across skills | +| Formatting | Ensure markdown is correct | + +### What Requires Human Review + +| Task | Why Human Review | +|------|------------------| +| Technical accuracy | AI may hallucinate APIs or features | +| Real-world gotchas | Comes from actual development experience | +| Business logic | Zoom-specific requirements and policies | +| Security practices | Must be verified against official guidance | + +## Quality Standards + +### Do + +- Verify all claims with official documentation +- Include working, tested code examples +- Document known limitations prominently +- Link to official resources +- Keep examples simple and practical +- Check that moved or renamed docs still have inbound links +- Remove outdated guidance that no longer matches the current plugin structure + +### Don't + +- Include unverified information +- Speculate about undocumented behavior +- Copy proprietary code without permission +- Include outdated or deprecated APIs without noting it +- Over-engineer examples + +## Code of Conduct + +- Be respectful and constructive +- Focus on improving the documentation +- Credit sources appropriately +- Follow Zoom's developer terms of service + +## Questions? + +- Open a GitHub issue for questions about contributing +- Check existing issues before creating new ones +- Join the [Zoom Developer Forum](https://devforum.zoom.us/) for Zoom-specific questions, feedback, and improvement requests for these agent skills + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/plugins/zoom-developers/LICENSE b/plugins/zoom-developers/LICENSE new file mode 100644 index 00000000..d95e1f8a --- /dev/null +++ b/plugins/zoom-developers/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Zoom Video Communications, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/zoom-developers/README.md b/plugins/zoom-developers/README.md new file mode 100644 index 00000000..fc78a68d --- /dev/null +++ b/plugins/zoom-developers/README.md @@ -0,0 +1,134 @@ +# Zoom Developers + +Zoom Developers is a Codex plugin for planning, building, and debugging Zoom integrations. It helps engineers choose the right Zoom product surface, shape implementation plans, debug failures, and route into Zoom API, SDK, webhook, bot, and automation references without reading the full Zoom doc tree first. + +## Plugin Shape + +This repository is packaged as a Codex plugin: + +- plugin manifest: [`.codex-plugin/plugin.json`](.codex-plugin/plugin.json) +- marketplace catalog entry: [`../../.agents/plugins/marketplace.json`](../../.agents/plugins/marketplace.json) +- deterministic command workflows: [`commands/`](commands/) +- focused reviewer agents: [`agents/`](agents/) +- reusable workflows and references: [`skills/`](skills/) + +This plugin contains local developer guidance, commands, skills, and reviewer agents. + +## Using In Codex + +Codex can use this plugin through command, skill, and reviewer-agent surfaces: + +- install the plugin from `/plugins` +- mention the plugin as `@Zoom Developers` if the UI exposes it +- use slash commands for deterministic flows such as `/setup-zoom-oauth`, `/debug-zoom-auth`, `/debug-zoom-webhook`, and `/zoom-integration-doctor` +- invoke a bundled skill explicitly with `$skill-name`, for example `$start` or `$setup-zoom-oauth` +- describe the task naturally and let Codex route into the right skill + +This plugin is for developer guidance and implementation workflows only. + +If a newly installed skill does not trigger in the current thread, start a new Codex session so the plugin metadata reloads. + +## Command Workflows + +Use the bundled slash commands when you want a deterministic flow rather than open-ended routing: + +| Command | Description | +|---|---| +| [`/plan-zoom-product`](commands/plan-zoom-product.md) | Choose the right Zoom product surface for a use case and explain the tradeoffs clearly | +| [`/plan-zoom-integration`](commands/plan-zoom-integration.md) | Turn a Zoom product idea into a practical build plan with auth, architecture, and milestones | +| [`/debug-zoom`](commands/debug-zoom.md) | Triage a broken Zoom integration when the failing layer is not yet obvious | +| [`/setup-zoom-oauth`](commands/setup-zoom-oauth.md) | Inspect the repo, choose the right Zoom OAuth flow, and wire the auth path cleanly | +| [`/setup-zoom-webhooks`](commands/setup-zoom-webhooks.md) | Implement or correct a Zoom webhook receiver with validation, signature checks, and reliable delivery handling | +| [`/setup-zoom-websockets`](commands/setup-zoom-websockets.md) | Implement or correct a Zoom WebSocket event stream with connection lifecycle and reconnect handling | +| [`/debug-zoom-auth`](commands/debug-zoom-auth.md) | Isolate OAuth, SDK auth, or token lifecycle failures and propose the minimal fix | +| [`/debug-zoom-webhook`](commands/debug-zoom-webhook.md) | Triage webhook registration, signature validation, delivery, and handler issues | +| [`/zoom-integration-doctor`](commands/zoom-integration-doctor.md) | Run a read-first integration audit across auth, SDK/API choice, and eventing | + +## Build Commands + +Use the bundled build commands when you want Codex to drive a specific Zoom implementation path: + +| Command | Description | +|---|---| +| [`/build-zoom-rest-api-app`](commands/build-zoom-rest-api-app.md) | Implement a Zoom REST API integration with the right resources, auth path, and verification loop | +| [`/build-zoom-apps-sdk-app`](commands/build-zoom-apps-sdk-app.md) | Implement a Zoom Apps SDK app that runs inside the Zoom client with the right running context and auth path | +| [`/build-zoom-meeting-app`](commands/build-zoom-meeting-app.md) | Implement an embedded or managed Zoom meeting flow in the current codebase | +| [`/build-zoom-meeting-sdk-app`](commands/build-zoom-meeting-sdk-app.md) | Implement a Zoom Meeting SDK integration with the right platform-specific join or start flow | +| [`/build-zoom-video-sdk-app`](commands/build-zoom-video-sdk-app.md) | Implement a custom Zoom Video SDK session workflow | +| [`/build-zoom-ui-toolkit-app`](commands/build-zoom-ui-toolkit-app.md) | Implement a Zoom Video SDK UI Toolkit integration for a prebuilt web session UI | +| [`/build-zoom-cobrowse-app`](commands/build-zoom-cobrowse-app.md) | Implement a Zoom Cobrowse integration with session lifecycle, privacy controls, and support workflow wiring | +| [`/build-zoom-rivet-app`](commands/build-zoom-rivet-app.md) | Implement a server-side Zoom integration with Rivet modules for auth, APIs, and webhooks | +| [`/build-zoom-probe-flow`](commands/build-zoom-probe-flow.md) | Implement readiness checks with Zoom Probe SDK before users join meetings or sessions | +| [`/build-zoom-rtms-app`](commands/build-zoom-rtms-app.md) | Implement a Zoom RTMS workflow for live media, transcript, or event-stream processing | +| [`/build-zoom-scribe-app`](commands/build-zoom-scribe-app.md) | Implement a Zoom Scribe transcription pipeline for uploaded or stored media | +| [`/build-zoom-bot`](commands/build-zoom-bot.md) | Implement a Zoom meeting bot, recorder, or real-time media workflow | +| [`/build-zoom-team-chat-app`](commands/build-zoom-team-chat-app.md) | Implement a Zoom Team Chat integration or chatbot flow | +| [`/build-zoom-phone-integration`](commands/build-zoom-phone-integration.md) | Implement a Zoom Phone integration around APIs, Smart Embed, or events | +| [`/build-zoom-contact-center-app`](commands/build-zoom-contact-center-app.md) | Implement a Zoom Contact Center integration for web, mobile, or backend workflows | +| [`/build-zoom-virtual-agent`](commands/build-zoom-virtual-agent.md) | Implement a Zoom Virtual Agent integration for web or mobile wrappers | + +## Reviewer Agents + +The plugin also bundles focused reviewer agents for specialist analysis: + +| Agent | Description | +|---|---| +| [`zoom-oauth-scope-auditor`](agents/zoom-oauth-scope-auditor.md) | Review scope sets for least privilege, missing scopes, and cross-surface mistakes | +| [`zoom-integration-reviewer`](agents/zoom-integration-reviewer.md) | Review a Zoom integration end to end for architectural, auth, webhook, and SDK risks | + +## Primary Workflows + +Codex can invoke skills implicitly from task descriptions, or explicitly by mentioning a skill such as `$start` or `$setup-zoom-oauth`. + +| Skill | Description | +|---|---| +| [`start`](skills/start/SKILL.md) | Start with a Zoom app idea and route to the right product and build path | +| [`setup-zoom-oauth`](skills/setup-zoom-oauth/SKILL.md) | Choose the auth model, scopes, and redirect flow for a Zoom app | +| [`build-zoom-meeting-app`](skills/build-zoom-meeting-app/SKILL.md) | Build an embedded or managed Zoom meeting flow | +| [`build-zoom-bot`](skills/build-zoom-bot/SKILL.md) | Build bots, recorders, and real-time meeting processors | +| [`debug-zoom`](skills/debug-zoom/SKILL.md) | Triage a broken Zoom integration and isolate the failing layer | +| [`build-zoom-rest-api-app`](skills/rest-api/SKILL.md) | Route into Zoom REST endpoints, scopes, and resource patterns | +| [`build-zoom-meeting-sdk-app`](skills/meeting-sdk/SKILL.md) | Route into embedded Zoom meeting implementation details | +| [`build-zoom-video-sdk-app`](skills/video-sdk/SKILL.md) | Route into custom video-session implementation details | +| [`build-zoom-rtms-app`](skills/rtms/SKILL.md) | Route into Zoom RTMS for live media, transcript, and event-stream workflows | +| [`setup-zoom-webhooks`](skills/webhooks/SKILL.md) | Set up Zoom webhook subscriptions, signature verification, and handlers | +| [`setup-zoom-websockets`](skills/websockets/SKILL.md) | Set up Zoom WebSocket event delivery when it fits better than webhooks | +| [`build-zoom-team-chat-app`](skills/team-chat/SKILL.md) | Build Team Chat user or chatbot integrations | +| [`build-zoom-phone-integration`](skills/phone/SKILL.md) | Build Zoom Phone integrations around Smart Embed, APIs, and events | +| [`build-zoom-contact-center-app`](skills/contact-center/SKILL.md) | Build Contact Center app, web, or native integrations | +| [`build-zoom-virtual-agent`](skills/virtual-agent/SKILL.md) | Build Virtual Agent web or mobile wrapper integrations | + +## Supporting References + +The plugin keeps the Zoom product-specific reference library under `skills/`. These are supporting references, not the primary entry surface: + +- [`skills/general/`](skills/general/) +- [`skills/rest-api/`](skills/rest-api/) +- [`skills/meeting-sdk/`](skills/meeting-sdk/) +- [`skills/video-sdk/`](skills/video-sdk/) +- [`skills/webhooks/`](skills/webhooks/) +- [`skills/websockets/`](skills/websockets/) +- [`skills/rtms/`](skills/rtms/) +- [`skills/oauth/`](skills/oauth/) + +## Example Prompts + +```text +Use $start to plan an internal meeting assistant that extracts action items and stores summaries. +``` + +```text +Run /build-zoom-meeting-app to add a Zoom join flow to this React app with the right auth and server-side pieces. +``` + +```text +Run /debug-zoom-webhook to diagnose why Zoom events reach the endpoint but signature validation fails. +``` + +```text +Run /plan-zoom-product for an internal meeting assistant that needs summaries, action items, and follow-up docs so we pick the right Zoom surface first. +``` + +## Related Plugin + +If your goal is to connect Codex to live Zoom meeting data, use the separate `Zoom` plugin instead of this one. diff --git a/plugins/zoom-developers/agents/openai.yaml b/plugins/zoom-developers/agents/openai.yaml new file mode 100644 index 00000000..5a2f2133 --- /dev/null +++ b/plugins/zoom-developers/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "Zoom Developers" + short_description: "Use Zoom Developers reviewer agents for scope audits and integration reviews" + icon_small: "./assets/zoom-small.svg" + icon_large: "./assets/app-icon.svg" + default_prompt: "Use Zoom Developers reviewer agents to audit scopes and review Zoom integrations for architectural and implementation issues." diff --git a/plugins/zoom-developers/agents/zoom-integration-reviewer.md b/plugins/zoom-developers/agents/zoom-integration-reviewer.md new file mode 100644 index 00000000..1c70666f --- /dev/null +++ b/plugins/zoom-developers/agents/zoom-integration-reviewer.md @@ -0,0 +1,15 @@ +You are the Zoom Integration Reviewer for this plugin. + +Purpose: +- Review a Zoom integration end to end for architectural fit, auth correctness, eventing reliability, and SDK or API misuse. + +Rules: +- Prioritize behavioral risk and production failure modes over style. +- Flag wrong product-surface choices, broken token lifecycle assumptions, webhook verification bugs, and unsupported SDK or API assumptions first. +- Report findings before summaries. +- If evidence is incomplete, say what is missing instead of guessing. + +Output format: +1. Findings ordered by severity +2. Open questions or missing evidence +3. Recommended next steps diff --git a/plugins/zoom-developers/agents/zoom-oauth-scope-auditor.md b/plugins/zoom-developers/agents/zoom-oauth-scope-auditor.md new file mode 100644 index 00000000..cc34f4f4 --- /dev/null +++ b/plugins/zoom-developers/agents/zoom-oauth-scope-auditor.md @@ -0,0 +1,16 @@ +You are the Zoom OAuth Scope Auditor for this plugin. + +Purpose: +- Review requested, configured, and documented Zoom scopes for least privilege and correctness. +- Catch missing scopes, over-broad scopes, and scope sets that mix the wrong Zoom product surfaces. + +Rules: +- Prefer the narrowest documented scope set that supports the requested workflow. +- Distinguish REST API scopes, SDK-related auth requirements, and product-specific granular scopes. +- Do not invent scopes. If evidence is missing, say what is missing. +- Treat classic broad scopes as suspect when the workflow clearly uses a published granular alternative. + +Output format: +1. Findings ordered by severity +2. Missing evidence or assumptions +3. Recommended scope set by surface diff --git a/plugins/zoom-developers/assets/app-icon.svg b/plugins/zoom-developers/assets/app-icon.svg new file mode 100644 index 00000000..0e66c135 --- /dev/null +++ b/plugins/zoom-developers/assets/app-icon.svg @@ -0,0 +1,5 @@ + + Zoom + + + diff --git a/plugins/zoom-developers/assets/zoom-small.svg b/plugins/zoom-developers/assets/zoom-small.svg new file mode 100644 index 00000000..9a983167 --- /dev/null +++ b/plugins/zoom-developers/assets/zoom-small.svg @@ -0,0 +1,5 @@ + + Zoom + + + diff --git a/plugins/zoom-developers/commands/_conventions.md b/plugins/zoom-developers/commands/_conventions.md new file mode 100644 index 00000000..8da26a66 --- /dev/null +++ b/plugins/zoom-developers/commands/_conventions.md @@ -0,0 +1,70 @@ +# Command Conventions + +Every slash command in this plugin follows a consistent structure so Codex produces reliable, verifiable Zoom workflow results. Every non-underscore file in `commands/` must include all required sections below. + +## Required Sections + +### 1. Preflight + +Check prerequisites before doing any work: + +- Inspect the current repo for the relevant Zoom surface, framework, and existing integration code. +- Confirm required local tools are available before relying on them. +- Note missing credentials, env vars, callback routes, or endpoint URLs before attempting setup or debugging. +- Flag dirty working tree or risky production-impacting changes when they matter to the requested workflow. + +Preflight failures should produce clear next actions. Do not silently skip them. + +### 2. Plan + +State what will happen before execution: + +- List the files, commands, and external checks the workflow will use. +- Call out any destructive or production-impacting step and require explicit user confirmation. +- If there are multiple viable paths, state which one was chosen and why. + +### 3. Commands + +The operational core. Follow these conventions: + +- Prefer read-first inspection before making code or config changes. +- Never print secret values. Show env var names, scope names, file paths, and metadata only. +- Use structured output when available. +- Prefer the narrowest Zoom surface that actually solves the task. +- Do not claim OAuth, webhook, or SDK setup succeeded without checking the relevant evidence. + +### 4. Verification + +After execution, confirm the outcome: + +- Re-read the changed files or live state. +- Verify the exact auth, signature, endpoint, or config path that was modified. +- Surface errors, warnings, and unresolved blockers explicitly. + +### 5. Summary + +Present a concise result block: + +```text +## Result +- Action: what was done +- Status: success | partial | failed +- Details: key files, env vars, endpoints, or findings +``` + +### 6. Next Steps + +Suggest the logical follow-up: + +- setup flows -> test auth or live API connectivity +- debug flows -> rerun the failing path with the fix applied +- review flows -> apply the recommended fixes or narrow the scope + +## File Naming + +- Command files live in `commands/` and end in `.md`. +- Files prefixed with `_` are conventions or meta docs, not user-facing commands. + +## Frontmatter + +Every command file must include YAML frontmatter with at least a `description` field. diff --git a/plugins/zoom-developers/commands/build-zoom-apps-sdk-app.md b/plugins/zoom-developers/commands/build-zoom-apps-sdk-app.md new file mode 100644 index 00000000..993f4571 --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-apps-sdk-app.md @@ -0,0 +1,54 @@ +--- +description: Implement a Zoom Apps SDK app that runs inside the Zoom client with the right running context, auth path, and backend integration. +--- + +# Build Zoom Apps SDK App + +Use this command when the repo should build an app that runs inside the Zoom client rather than embedding Zoom in an external app. + +## Preflight + +1. Inspect the codebase for existing Zoom Apps SDK usage, frontend framework, backend APIs, and auth handling. +2. Confirm the requirement is truly an in-client Zoom app and not a Meeting SDK or Video SDK workflow. +3. Identify the required running context: meeting, webinar, main client, phone, collaborate mode, immersive mode, camera mode, or another supported context. +4. Check for required Marketplace app settings, allowed domains, redirect URIs, and in-client OAuth requirements without printing secrets. + +## Plan + +Before making changes: + +- state the target running context and user flow +- list the files that will be changed +- state the auth model, backend responsibilities, and any capability gating +- state how the app will be verified locally or in Zoom client testing + +## Commands + +1. Add or correct Zoom Apps SDK initialization in the correct frontend surface. +2. Configure capability checks and gate features by running context instead of assuming global support. +3. Add the minimum backend or API wiring needed for the app’s first useful flow. +4. Wire in-client OAuth or supporting backend auth only where the chosen workflow requires it. +5. Keep domain allowlists, callback paths, and app configuration aligned with the implementation. +6. Avoid mixing external embed assumptions into an in-client Zoom app architecture. + +## Verification + +1. Re-read the client initialization, capability checks, and backend integration code after changes. +2. Run local build or tests where available. +3. Verify the app load path, running-context assumptions, and first useful user action. +4. State any remaining blocker such as allowlist gaps, missing Zoom client context, or app configuration mismatch. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom Apps SDK app +- Status: success | partial | failed +- Details: running context, files changed, auth path, verification run +``` + +## Next Steps + +- Test the app inside the intended Zoom client context. +- Add advanced client capabilities only after the base app loads reliably. +- If the requirement is to run outside Zoom client, switch to the appropriate meeting, video, or REST flow. diff --git a/plugins/zoom-developers/commands/build-zoom-bot.md b/plugins/zoom-developers/commands/build-zoom-bot.md new file mode 100644 index 00000000..ccb924ed --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-bot.md @@ -0,0 +1,53 @@ +--- +description: Implement a Zoom meeting bot, recorder, or real-time media workflow in the current codebase. +--- + +# Build Zoom Bot + +Use this command when the repo needs to join meetings programmatically, capture media, process transcripts, or automate meeting-time behavior. + +## Preflight + +1. Inspect the codebase for existing bot, worker, Meeting SDK, RTMS, or media-processing code. +2. Identify whether the bot is joining meetings, consuming live media, processing recordings, or combining multiple paths. +3. Confirm the execution environment and deployment model for the bot runtime. +4. Check for required credentials, meeting join inputs, and media dependencies without printing secrets. + +## Plan + +Before making changes: + +- state the bot goal and the Zoom surfaces involved +- list the files that will be changed +- state the auth path, runtime responsibilities, and media or transcript outputs +- state how the first working bot path will be verified + +## Commands + +1. Add or correct the bot entry path, meeting join or attach flow, and downstream processing pipeline. +2. Keep the first pass narrow: one working automation path before optional orchestration or analytics layers. +3. Add the minimal server, worker, or scheduler wiring required for the workflow. +4. Reuse existing logging, queueing, and secrets patterns where possible. +5. Do not claim recording or transcript automation works until the path is checked end to end. + +## Verification + +1. Re-read the bot runtime, auth helpers, and processing code that changed. +2. Run local build, tests, or static validation where available. +3. Verify the join pipeline, media handling assumptions, or downstream artifact path. +4. State any remaining blocker such as runtime environment constraints, account requirements, or missing meeting inputs. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom bot workflow +- Status: success | partial | failed +- Details: runtime, files changed, auth path, verification run +``` + +## Next Steps + +- Test with a real meeting in a controlled environment. +- Add storage, analytics, or downstream integrations only after the base bot loop works. +- If the requirement is post-meeting retrieval rather than live participation, consider REST APIs or recording workflows instead. diff --git a/plugins/zoom-developers/commands/build-zoom-cobrowse-app.md b/plugins/zoom-developers/commands/build-zoom-cobrowse-app.md new file mode 100644 index 00000000..c6d7e48a --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-cobrowse-app.md @@ -0,0 +1,53 @@ +--- +description: Implement a Zoom Cobrowse integration with session lifecycle, privacy controls, and support workflow wiring. +--- + +# Build Zoom Cobrowse App + +Use this command when the repo needs a browser co-browsing experience for support or guided assistance workflows. + +## Preflight + +1. Inspect the codebase for existing support tooling, session initialization code, frontend embeds, and auth or token generation helpers. +2. Confirm the workflow is truly browser co-browsing rather than Contact Center, Virtual Agent, or a general screen-sharing feature. +3. Identify the role model: customer page, agent console, session initiation path, and handoff mechanism. +4. Check whether required tokens, allowed origins, and privacy requirements are already represented without printing secrets. + +## Plan + +Before making changes: + +- state the support workflow and role model +- list the files that will be changed +- state the auth path, session lifecycle responsibilities, and privacy controls in scope +- state how the base Cobrowse flow will be verified + +## Commands + +1. Add or correct the Cobrowse SDK initialization and base session lifecycle in the proper frontend and backend layers. +2. Implement the smallest working start or join flow before adding advanced annotations or remote-assist behavior. +3. Add privacy masking and blocked-element handling early when required by the workflow. +4. Reuse existing session management, logging, and support tooling patterns where possible. +5. Avoid pushing unrelated Contact Center or Virtual Agent abstractions into a Cobrowse-specific flow. + +## Verification + +1. Re-read the session initialization, auth path, and privacy or event-handling code after changes. +2. Run local build or tests where available. +3. Verify the start or join flow, session lifecycle, and privacy assumptions. +4. State any remaining blocker such as token generation, allowed-origin config, CORS or CSP, or browser constraints. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom Cobrowse integration +- Status: success | partial | failed +- Details: files changed, auth path, session lifecycle, verification run +``` + +## Next Steps + +- Test one real support session end to end. +- Add remote assist or richer collaboration only after the base session path works. +- If the product choice is still uncertain, compare against Contact Center or Virtual Agent before expanding scope. diff --git a/plugins/zoom-developers/commands/build-zoom-contact-center-app.md b/plugins/zoom-developers/commands/build-zoom-contact-center-app.md new file mode 100644 index 00000000..9351db4e --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-contact-center-app.md @@ -0,0 +1,53 @@ +--- +description: Implement a Zoom Contact Center integration for web, mobile, or backend workflows with the right auth and event handling. +--- + +# Build Zoom Contact Center App + +Use this command when the repo needs Zoom Contact Center web, mobile, or backend integration work. + +## Preflight + +1. Inspect the codebase for existing Contact Center code, SDK usage, OAuth handling, and event consumers. +2. Identify the target platform and workflow: web embed, native wrapper, engagement handling, campaign flow, or backend orchestration. +3. Confirm the owning runtime and UI surface. +4. Check for the required auth, SDK, or endpoint configuration without printing secrets. + +## Plan + +Before making changes: + +- state the target platform and Contact Center workflow +- list the files that will be changed +- state the auth path, event path, and interaction model +- state how the initial integration will be verified + +## Commands + +1. Add or correct the Contact Center integration at the proper platform layer. +2. Reuse existing app structure and avoid generic abstractions that hide platform-specific requirements. +3. Keep the first pass scoped to one concrete engagement or routing workflow. +4. Add the minimum auth, event, and UI wiring required for a working path. +5. Keep secrets and account-specific identifiers out of output. + +## Verification + +1. Re-read the platform integration files and supporting auth or event code that changed. +2. Run local build or tests where available. +3. Verify the configured workflow path and handler shape. +4. State any remaining blocker such as missing SDK setup, app configuration, or account prerequisites. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom Contact Center workflow +- Status: success | partial | failed +- Details: platform, files changed, auth path, verification run +``` + +## Next Steps + +- Test the target engagement flow in the intended environment. +- Expand to more event or campaign cases only after the base path works. +- If auth or delivery is failing, use the narrower setup or debug commands next. diff --git a/plugins/zoom-developers/commands/build-zoom-meeting-app.md b/plugins/zoom-developers/commands/build-zoom-meeting-app.md new file mode 100644 index 00000000..f40089cd --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-meeting-app.md @@ -0,0 +1,53 @@ +--- +description: Implement an embedded or managed Zoom meeting flow in the current codebase with the right SDK and auth path. +--- + +# Build Zoom Meeting App + +Use this command when the repo needs to embed or launch real Zoom meetings with the Meeting SDK or adjacent server-side join flow. + +## Preflight + +1. Inspect the codebase for existing Zoom meeting code, frontend framework, backend support, and auth handling. +2. Confirm the requested experience is a real Zoom meeting and not a custom video experience that belongs on Video SDK. +3. Identify the target platform: web, mobile, desktop, or multi-platform. +4. Check whether the codebase already has the required server-side token or signature path without printing secrets. + +## Plan + +Before making changes: + +- state the target platform and Meeting SDK surface +- list the files that will be changed +- state the join or start flow, auth path, and required backend pieces +- state how the meeting flow will be verified locally + +## Commands + +1. Inspect current app structure and place the meeting integration in the correct UI and server layers. +2. Add or correct Meeting SDK initialization, join or start flow, and backend auth support. +3. Keep the implementation aligned with the target platform’s established patterns instead of forcing a generic abstraction. +4. Add only the minimum UI, env wiring, and server code needed for a working meeting flow. +5. Keep secrets, tokens, and meeting credentials out of output and logs. + +## Verification + +1. Re-read the meeting client code and any server auth helpers that changed. +2. Run the relevant local build or tests. +3. Verify the UI path reaches the expected join or start call shape. +4. State any remaining blocker such as missing credentials, callback setup, domain config, or SDK prerequisites. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom meeting app flow +- Status: success | partial | failed +- Details: platform, files changed, auth path, verification run +``` + +## Next Steps + +- Test the join or start flow with a real meeting. +- Add webhook or bot-side follow-up only if the product needs it. +- If the requirement is actually custom media control, switch to `/build-zoom-video-sdk-app`. diff --git a/plugins/zoom-developers/commands/build-zoom-meeting-sdk-app.md b/plugins/zoom-developers/commands/build-zoom-meeting-sdk-app.md new file mode 100644 index 00000000..e6ebc74d --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-meeting-sdk-app.md @@ -0,0 +1,53 @@ +--- +description: Implement a Zoom Meeting SDK integration with the right platform-specific join or start flow, auth path, and verification loop. +--- + +# Build Zoom Meeting SDK App + +Use this command when the repo needs to join, start, or embed real Zoom meetings through the Meeting SDK rather than a broader meeting workflow. + +## Preflight + +1. Inspect the codebase for existing Meeting SDK usage, target platform code, backend signature helpers, and meeting join UI. +2. Confirm the requirement is a real Zoom meeting and not a custom video session or an in-client Zoom app. +3. Identify the target platform: web, Android, iOS, macOS, Windows, Electron, React Native, Unreal, or Linux bot. +4. Check whether the required auth and meeting inputs exist without printing secrets: meeting number, password path, role, SDK auth material, and host start requirements. + +## Plan + +Before making changes: + +- state the target Meeting SDK platform and user flow +- list the files that will be changed +- state the auth path, join or start flow, and any required backend pieces +- state how the implementation will be verified locally + +## Commands + +1. Add or correct Meeting SDK initialization at the appropriate client layer for the target platform. +2. Implement the smallest working join or start flow before adding advanced meeting controls or raw data features. +3. Add or correct the backend auth path needed for the chosen platform. +4. Keep platform-specific behavior explicit instead of forcing a generic abstraction over materially different SDKs. +5. Avoid leaking meeting credentials, signatures, or tokens in logs or command output. + +## Verification + +1. Re-read the client Meeting SDK code and supporting backend auth code after changes. +2. Run the relevant local build or tests where available. +3. Verify the join or start call shape, auth path, and expected meeting inputs. +4. State any remaining blocker such as missing credentials, waiting room constraints, unsupported environment, or platform setup gaps. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom Meeting SDK integration +- Status: success | partial | failed +- Details: platform, files changed, auth path, verification run +``` + +## Next Steps + +- Test the flow with a real meeting on the target platform. +- Add meeting controls or bot-like behavior only after the base join path works. +- If the requirement changes to a custom session product, switch to `/build-zoom-video-sdk-app`. diff --git a/plugins/zoom-developers/commands/build-zoom-phone-integration.md b/plugins/zoom-developers/commands/build-zoom-phone-integration.md new file mode 100644 index 00000000..afc61897 --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-phone-integration.md @@ -0,0 +1,53 @@ +--- +description: Implement a Zoom Phone integration around APIs, Smart Embed, calling flows, and event handling. +--- + +# Build Zoom Phone Integration + +Use this command when the repo needs Zoom Phone APIs, embedded phone experiences, CTI-style workflows, or call-related automation. + +## Preflight + +1. Inspect the codebase for existing phone integration code, OAuth handling, embed surfaces, and event consumers. +2. Identify whether the workflow is Smart Embed, direct API usage, call automation, CRM integration, or another phone-specific path. +3. Confirm the runtime and UI surface that will own the phone workflow. +4. Check for the required auth and event configuration without printing secrets. + +## Plan + +Before making changes: + +- state the Phone workflow being implemented +- list the files that will be changed +- state the auth path, event path, and user interaction surface +- state how the initial phone flow will be verified + +## Commands + +1. Add or correct the Zoom Phone integration at the appropriate UI or backend layer. +2. Reuse existing routing, event, and API abstractions where possible. +3. Keep the first pass focused on one concrete calling or phone-data workflow. +4. Add the minimum configuration, auth handling, and event logic required for a working path. +5. Avoid blending general meeting assumptions into phone-specific workflows. + +## Verification + +1. Re-read the phone integration code and supporting auth or event code that changed. +2. Run local build or tests where available. +3. Verify the API, embed, or event path shape. +4. State any remaining blocker such as missing account setup, OAuth scope gaps, or runtime prerequisites. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom Phone integration +- Status: success | partial | failed +- Details: files changed, auth path, event path, verification run +``` + +## Next Steps + +- Exercise one real call-related workflow end to end. +- Add CRM or analytics follow-up only after the base integration works. +- If auth is incomplete, run `/setup-zoom-oauth`. diff --git a/plugins/zoom-developers/commands/build-zoom-probe-flow.md b/plugins/zoom-developers/commands/build-zoom-probe-flow.md new file mode 100644 index 00000000..1a62e513 --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-probe-flow.md @@ -0,0 +1,53 @@ +--- +description: Implement readiness checks with Zoom Probe SDK before users join meetings or sessions. +--- + +# Build Zoom Probe Flow + +Use this command when the app needs pre-join diagnostics for browser, device, network, or environment readiness before Meeting SDK or Video SDK sessions. + +## Preflight + +1. Inspect the codebase for existing pre-join screens, media-permission checks, diagnostics code, and meeting or video join flows. +2. Confirm the requirement is readiness gating before join, not a general support diagnostics page unrelated to Zoom session entry. +3. Identify which checks matter for this workflow: browser support, camera, microphone, speaker, network, CPU, or diagnostic reports. +4. Check how failed checks should be handled: block join, warn the user, or capture telemetry. + +## Plan + +Before making changes: + +- state the target join flow and the readiness checks in scope +- list the files that will be changed +- state the gating behavior and any telemetry or support output +- state how the probe flow will be verified + +## Commands + +1. Add or correct the Probe SDK integration at the pre-join layer of the app. +2. Run diagnostics before the Meeting SDK or Video SDK join path rather than during unrelated app initialization. +3. Keep probe results separate from tokens and avoid collecting unnecessary device details. +4. Add the minimum UI or state handling needed to communicate readiness and block or allow join appropriately. +5. Reuse existing telemetry or support-reporting patterns where possible. + +## Verification + +1. Re-read the diagnostics code, gating logic, and any reporting path after changes. +2. Run local build or tests where available. +3. Verify the probe checks run before join and influence the flow as intended. +4. State any remaining blocker such as HTTPS requirements, browser limitations, permission handling, or unsupported environments. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom Probe readiness flow +- Status: success | partial | failed +- Details: files changed, checks added, gating behavior, verification run +``` + +## Next Steps + +- Test the flow under both passing and failing device or network conditions. +- Tune the threshold between warn and block after observing real user behavior. +- If the app also needs the actual meeting or session join flow, wire the probe output into the corresponding build command next. diff --git a/plugins/zoom-developers/commands/build-zoom-rest-api-app.md b/plugins/zoom-developers/commands/build-zoom-rest-api-app.md new file mode 100644 index 00000000..4d733c65 --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-rest-api-app.md @@ -0,0 +1,53 @@ +--- +description: Implement a Zoom REST API integration with the right resources, auth path, and verification loop. +--- + +# Build Zoom REST API App + +Use this command when the repo should call Zoom REST endpoints directly for meetings, users, recordings, docs-adjacent workflows, or account resources. + +## Preflight + +1. Inspect the codebase for existing Zoom API usage, env vars, HTTP client wrappers, and server routes. +2. Confirm the requested workflow is actually a REST API problem and not better served by Meeting SDK, Video SDK, or Webhooks. +3. Identify the runtime that will own the Zoom API calls and token lifecycle. +4. Check whether the required OAuth values and scope configuration already exist without printing secrets. + +## Plan + +Before making changes: + +- state the exact Zoom resource or endpoint family to implement +- list the files that will be created or edited +- state the auth model, token owner, and required scopes +- state how the new API flow will be verified + +## Commands + +1. Inspect the existing project structure and add the Zoom API integration at the narrowest appropriate layer. +2. Reuse existing HTTP client, config, and error-handling patterns where possible. +3. Add or correct Zoom OAuth token usage, API calls, request validation, and response shaping. +4. Keep secrets out of logs and avoid leaking access tokens in output. +5. Add minimal docs or examples only where they materially help the next developer use the integration. + +## Verification + +1. Re-read the edited files and confirm the endpoint, auth path, and error handling are coherent. +2. Run the relevant local tests or build if available. +3. If safe credentials already exist, exercise the narrowest live or mocked API path to confirm request shape. +4. State any remaining blocker such as missing scopes, callback setup, or account permissions. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom REST API integration +- Status: success | partial | failed +- Details: endpoints, files changed, auth path, verification run +``` + +## Next Steps + +- Run one end-to-end request with real credentials if not already done. +- Add webhook handoff only if the use case actually needs it. +- If auth is incomplete, run `/setup-zoom-oauth` next. diff --git a/plugins/zoom-developers/commands/build-zoom-rivet-app.md b/plugins/zoom-developers/commands/build-zoom-rivet-app.md new file mode 100644 index 00000000..cbe0353c --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-rivet-app.md @@ -0,0 +1,53 @@ +--- +description: Implement a server-side Zoom integration with Rivet modules for auth, APIs, webhooks, and deployment-safe composition. +--- + +# Build Zoom Rivet App + +Use this command when the repo should build a server-side Zoom integration around the Rivet SDK instead of hand-rolled API and webhook plumbing. + +## Preflight + +1. Inspect the codebase for existing Rivet usage, server entry points, OAuth handling, webhook receivers, and deployment configuration. +2. Confirm Rivet is the intended abstraction and not just a generic backend integration that should stay with direct API and webhook code. +3. Identify the required modules: app configuration, OAuth, API clients, webhook handlers, and business workflow handlers. +4. Check whether the required environment variables and deployment model are already represented without printing secrets. + +## Plan + +Before making changes: + +- state the Rivet workflow being implemented +- list the files that will be changed +- state the auth path, webhook path, and module composition strategy +- state how the first working Rivet path will be verified + +## Commands + +1. Add or correct the Rivet-based module structure at the correct server layer. +2. Implement the smallest authenticated API path and webhook receiver before composing broader multi-module behavior. +3. Keep environment handling and deployment assumptions explicit, especially for Lambda-style or serverless receivers. +4. Reuse existing repo patterns for config, logging, and secret management where possible. +5. Avoid wrapping Rivet in unnecessary abstractions that make the auth and webhook paths harder to reason about. + +## Verification + +1. Re-read the Rivet module setup, auth handling, and webhook integration code after changes. +2. Run local build or tests where available. +3. Verify the module wiring, auth path, and first useful API or webhook flow. +4. State any remaining blocker such as framework drift, deployment mismatch, or missing app credentials. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom Rivet integration +- Status: success | partial | failed +- Details: files changed, module setup, auth path, verification run +``` + +## Next Steps + +- Exercise one real authenticated API path and one webhook flow. +- Expand to multi-module workflows only after the base path works. +- If Rivet is adding more complexity than value, fall back to direct REST plus webhook implementation. diff --git a/plugins/zoom-developers/commands/build-zoom-rtms-app.md b/plugins/zoom-developers/commands/build-zoom-rtms-app.md new file mode 100644 index 00000000..7e745e87 --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-rtms-app.md @@ -0,0 +1,54 @@ +--- +description: Implement a Zoom RTMS workflow for live media, transcript, or event-stream processing with the right stream lifecycle and backend handling. +--- + +# Build Zoom RTMS App + +Use this command when the repo needs Zoom RTMS for live meeting or contact-center media streams, transcript processing, or backend event-stream consumption. + +## Preflight + +1. Inspect the codebase for existing RTMS, WebSocket, bot, worker, or stream-processing code. +2. Confirm the use case actually belongs on RTMS rather than a visible Meeting SDK bot or a post-meeting retrieval workflow. +3. Identify the stream source: meeting media, transcript stream, contact-center voice stream, or another documented RTMS surface. +4. Confirm the owning backend runtime, downstream pipeline, and required credentials without printing secrets. + +## Plan + +Before making changes: + +- state the RTMS source surface and the downstream processing goal +- list the files that will be created or edited +- state the auth path, stream lifecycle responsibilities, and output artifacts +- state how the first working RTMS path will be verified + +## Commands + +1. Add or correct the RTMS connection lifecycle: connect, validate, subscribe, heartbeat, reconnect, and shutdown. +2. Keep the implementation in the backend or worker layer that owns stream processing, not in an unrelated UI surface. +3. Add the minimum parsing and buffering needed for the requested media or transcript type. +4. Wire the stream output into the existing processing pipeline, queue, or storage layer using repo-native patterns. +5. Keep RTMS-specific concerns separate from visible participant-bot logic unless the workflow explicitly needs both. +6. Avoid claiming live stream support works until the connection lifecycle and payload handling path are both checked. + +## Verification + +1. Re-read the stream connection code, auth handling, and downstream processing code that changed. +2. Run local build or tests where available. +3. Verify the connection lifecycle and payload handling path are coherent for the intended media type. +4. State any remaining blocker such as missing account prerequisites, stream authorization, network reachability, or downstream decoder work. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom RTMS workflow +- Status: success | partial | failed +- Details: source surface, files changed, auth path, verification run +``` + +## Next Steps + +- Test the RTMS path against a real stream source in a controlled environment. +- Add richer downstream AI, analytics, or storage steps only after the base stream loop works. +- If the workflow actually needs a visible participant or meeting controls, compare against `/build-zoom-bot` or Meeting SDK before expanding scope. diff --git a/plugins/zoom-developers/commands/build-zoom-scribe-app.md b/plugins/zoom-developers/commands/build-zoom-scribe-app.md new file mode 100644 index 00000000..afb6a51e --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-scribe-app.md @@ -0,0 +1,54 @@ +--- +description: Implement a Zoom Scribe transcription pipeline for uploaded or stored media with the right processing mode and callback flow. +--- + +# Build Zoom Scribe App + +Use this command when the repo needs a Zoom Scribe transcription workflow for uploaded or stored media rather than live in-meeting media. + +## Preflight + +1. Inspect the codebase for existing ingestion, storage, queueing, webhook, or transcript-processing code. +2. Confirm the media source, expected latency, language requirements, and whether fast mode or batch mode fits the workflow. +3. Identify the runtime that will own Build-platform credentials, request submission, and callback or polling behavior. +4. Check for required credentials, storage paths, and callback endpoints without printing secrets. + +## Plan + +Before making changes: + +- state the media source and chosen Scribe processing mode +- list the files that will be changed +- state the auth path, ingestion path, and transcript output destination +- state how the first transcription flow will be verified + +## Commands + +1. Add or correct the transcription request path in the backend or worker layer. +2. Keep Build-platform auth handling separate from transcript business logic. +3. Add the minimum ingest, submit, parse, and persist flow required for a working transcription pipeline. +4. If batch mode is used, wire the callback or job-status path explicitly. +5. Reuse existing queueing, storage, and observability patterns where possible. +6. Avoid presenting live meeting media workflows as Scribe if the use case really belongs on RTMS or a bot pipeline. + +## Verification + +1. Re-read the auth handling, submission path, and transcript parsing code after changes. +2. Run local build or tests where available. +3. Verify the chosen processing mode and transcript output path are coherent. +4. State any remaining blocker such as credential audience, media format, callback path, or size constraints. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom Scribe transcription workflow +- Status: success | partial | failed +- Details: processing mode, files changed, auth path, verification run +``` + +## Next Steps + +- Test the pipeline with one real media file in a controlled environment. +- Add retries, batching, or downstream AI processing only after the base path works. +- If the workflow needs live media instead of stored files, switch to `/build-zoom-rtms-app` or `/build-zoom-bot`. diff --git a/plugins/zoom-developers/commands/build-zoom-team-chat-app.md b/plugins/zoom-developers/commands/build-zoom-team-chat-app.md new file mode 100644 index 00000000..94ddd719 --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-team-chat-app.md @@ -0,0 +1,53 @@ +--- +description: Implement a Zoom Team Chat integration or chatbot flow with the right auth, eventing, and message handling. +--- + +# Build Zoom Team Chat App + +Use this command when the repo should send, receive, or automate Zoom Team Chat messages, cards, commands, or chatbot interactions. + +## Preflight + +1. Inspect the codebase for existing chat app routes, OAuth handling, webhook handlers, and message templates. +2. Identify whether the workflow is user-scoped messaging, chatbot behavior, slash-command handling, or notifications. +3. Confirm the runtime that will own incoming events and outgoing message calls. +4. Check for the required auth and event configuration without printing secrets. + +## Plan + +Before making changes: + +- state the Team Chat workflow being implemented +- list the files that will be changed +- state the auth path, inbound event path, and outbound message path +- state how the message flow will be verified + +## Commands + +1. Add or correct Team Chat auth, event handlers, and outbound message logic. +2. Keep the implementation aligned with the repo’s existing event and route patterns. +3. Add the minimum message formatting or card logic required for a useful first pass. +4. Keep secrets and user tokens out of output and logs. +5. Separate chat event handling from unrelated webhook logic where possible. + +## Verification + +1. Re-read the event handlers, auth code, and message formatting logic that changed. +2. Run local build or tests where available. +3. Verify the inbound and outbound message path shape. +4. State any remaining blocker such as missing app configuration, event subscription, or scope gap. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom Team Chat workflow +- Status: success | partial | failed +- Details: files changed, auth path, event path, verification run +``` + +## Next Steps + +- Send one real message flow through the app. +- Add richer cards or commands only after the base message path works. +- If webhook delivery fails, run `/debug-zoom-webhook`. diff --git a/plugins/zoom-developers/commands/build-zoom-ui-toolkit-app.md b/plugins/zoom-developers/commands/build-zoom-ui-toolkit-app.md new file mode 100644 index 00000000..68a3ad78 --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-ui-toolkit-app.md @@ -0,0 +1,53 @@ +--- +description: Implement a Zoom Video SDK UI Toolkit integration for a prebuilt web session UI with the right auth, mount strategy, and session lifecycle. +--- + +# Build Zoom UI Toolkit App + +Use this command when the repo needs a prebuilt web UI for a Zoom Video SDK session instead of a fully custom video interface. + +## Preflight + +1. Inspect the codebase for existing Video SDK usage, web frontend structure, server-side token generation, and session UI shells. +2. Confirm the requirement is a prebuilt Video SDK UI and not a real Zoom meeting flow. +3. Identify the host framework and where the UI Toolkit should mount. +4. Check whether the required Video SDK session auth path exists without printing secrets. + +## Plan + +Before making changes: + +- state the target web framework and session UX goal +- list the files that will be changed +- state the auth path, mount strategy, and session lifecycle responsibilities +- state how the UI Toolkit flow will be verified + +## Commands + +1. Add or correct the UI Toolkit integration in the appropriate web UI layer. +2. Wire the minimum server-side signature or auth path required for a working session join. +3. Keep custom UI work limited unless the user explicitly needs it beyond the toolkit surface. +4. Configure permissions, session join and leave behavior, and any required media settings. +5. Avoid mixing Meeting SDK assumptions into a Video SDK UI Toolkit integration. + +## Verification + +1. Re-read the UI mount code, Video SDK auth path, and session join flow after changes. +2. Run the relevant local build or tests where available. +3. Verify the toolkit mount path, auth flow, and base session lifecycle. +4. State any remaining blocker such as browser restrictions, token generation gaps, or unsupported environment requirements. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom UI Toolkit integration +- Status: success | partial | failed +- Details: files changed, auth path, mount strategy, verification run +``` + +## Next Steps + +- Test the session in the target browser environment. +- If deeper layout control is required, switch to `/build-zoom-video-sdk-app`. +- If auth is incomplete, fix the token path before expanding UI work. diff --git a/plugins/zoom-developers/commands/build-zoom-video-sdk-app.md b/plugins/zoom-developers/commands/build-zoom-video-sdk-app.md new file mode 100644 index 00000000..b7273a98 --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-video-sdk-app.md @@ -0,0 +1,53 @@ +--- +description: Implement a custom Zoom Video SDK session workflow with the right auth, session lifecycle, and verification path. +--- + +# Build Zoom Video SDK App + +Use this command when the repo needs a custom video experience rather than an actual Zoom meeting UI. + +## Preflight + +1. Inspect the codebase for existing video session code, frontend stack, backend support, and auth helpers. +2. Confirm the use case is a custom session workflow and not a standard Zoom meeting flow. +3. Identify the target platform and expected media features. +4. Check whether the required server-side auth or session token path already exists without printing secrets. + +## Plan + +Before making changes: + +- state the target platform and custom session goal +- list the files that will be changed +- state the auth path, session lifecycle responsibilities, and media features in scope +- state how the implementation will be verified locally + +## Commands + +1. Add or correct Video SDK initialization, session join flow, auth handling, and lifecycle management. +2. Keep the implementation aligned with the platform’s existing architecture. +3. Limit the first pass to the minimum working session path unless the user explicitly asks for broader UI or media controls. +4. Add or update the supporting server code needed to mint the correct auth material. +5. Avoid mixing Meeting SDK assumptions into Video SDK code. + +## Verification + +1. Re-read the client and server files that changed. +2. Run the relevant local build or tests. +3. Verify the session auth shape and the basic connect flow. +4. State any remaining blocker such as missing credentials, unsupported environment, or unresolved media prerequisites. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom Video SDK workflow +- Status: success | partial | failed +- Details: platform, files changed, auth path, verification run +``` + +## Next Steps + +- Test the session with real participants or a controlled local environment. +- Add advanced media features only after the base session path works. +- If the requirement is actually to join real Zoom meetings, switch to `/build-zoom-meeting-app`. diff --git a/plugins/zoom-developers/commands/build-zoom-virtual-agent.md b/plugins/zoom-developers/commands/build-zoom-virtual-agent.md new file mode 100644 index 00000000..b2286ed5 --- /dev/null +++ b/plugins/zoom-developers/commands/build-zoom-virtual-agent.md @@ -0,0 +1,53 @@ +--- +description: Implement a Zoom Virtual Agent integration for web or mobile wrapper environments with the right lifecycle and event handling. +--- + +# Build Zoom Virtual Agent + +Use this command when the repo needs to embed or wrap Zoom Virtual Agent on web, Android, or iOS. + +## Preflight + +1. Inspect the codebase for existing web views, wrapper bridges, event handlers, and auth or context-passing code. +2. Identify the target platform and Virtual Agent launch path. +3. Confirm the lifecycle constraints for the host app and any required native bridge behavior. +4. Check for the required configuration values without printing secrets. + +## Plan + +Before making changes: + +- state the target platform and Virtual Agent workflow +- list the files that will be changed +- state the launch path, event handling path, and host-app responsibilities +- state how the initial integration will be verified + +## Commands + +1. Add or correct the Virtual Agent embed or wrapper code at the appropriate platform layer. +2. Keep the implementation aligned with the host framework’s lifecycle and bridge patterns. +3. Add the minimum launch, event, and context update logic required for a working path. +4. Separate wrapper concerns from broader application state where possible. +5. Avoid leaking secrets or user tokens in logs or output. + +## Verification + +1. Re-read the embed or wrapper code and any supporting bridge files that changed. +2. Run local build or tests where available. +3. Verify the launch and message or event path shape. +4. State any remaining blocker such as missing platform config, unsupported host behavior, or app setup gaps. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom Virtual Agent workflow +- Status: success | partial | failed +- Details: platform, files changed, launch path, verification run +``` + +## Next Steps + +- Test the embedded agent in the intended runtime. +- Add deeper host-app integration only after the base launch path works. +- If wrapper lifecycle handling is unstable, narrow the integration before expanding scope. diff --git a/plugins/zoom-developers/commands/debug-zoom-auth.md b/plugins/zoom-developers/commands/debug-zoom-auth.md new file mode 100644 index 00000000..9672f3a7 --- /dev/null +++ b/plugins/zoom-developers/commands/debug-zoom-auth.md @@ -0,0 +1,53 @@ +--- +description: Isolate and fix Zoom authentication failures across OAuth, SDK signatures, and token refresh paths. +--- + +# Debug Zoom Auth + +Use this command when a Zoom login flow, token exchange, or signature flow is failing and you need a concrete failure diagnosis. + +## Preflight + +1. Capture the exact failing symptom: error text, failing endpoint, affected surface, and whether the failure happens at authorize, callback, token exchange, refresh, or SDK join time. +2. Inspect the repository for the auth implementation, env var usage, callback handling, and token lifecycle code. +3. Identify the auth model in use: user-level OAuth, server-to-server or service auth where applicable, or SDK signature and token flows. +4. Confirm the presence of required config values without printing secrets. +5. If the issue report lacks the concrete error, ask for it before making speculative auth changes. + +## Plan + +Before changing anything: + +- name the most likely failing layer +- list the evidence that will be checked +- list the files and commands involved +- state whether the workflow is read-only diagnosis or fix plus verification + +## Commands + +1. Inspect env and source usage for mismatched client IDs, redirect URIs, scope lists, token endpoints, or signature inputs. +2. For OAuth failures, verify exact redirect URI matching, token exchange parameters, and refresh-token rotation behavior. +3. For SDK auth failures, verify the expected server-side signature or token generation path for the specific SDK. +4. If the failure is caused by client capability mismatch, say that directly instead of pretending it is a token bug. +6. Apply only the minimum correction required to fix the identified layer. + +## Verification + +1. Re-run the failing path or reconstruct the exact auth request shape. +2. Confirm the corrected file, env var name, redirect URI, or server URL now matches the intended configuration. +3. If the failure persists, tighten the diagnosis to the next concrete layer rather than broadening scope. +4. State whether the issue is fixed, partially fixed, or blocked by missing credentials or external platform behavior. + +## Summary + +```text +## Result +- Action: diagnosed or fixed a Zoom auth failure +- Status: success | partial | failed +- Details: failing layer, evidence checked, fix applied, remaining blocker +``` + +## Next Steps + +- Re-run the failing flow with the corrected auth path. +- If the issue is webhook-related after auth succeeds, run `/debug-zoom-webhook`. diff --git a/plugins/zoom-developers/commands/debug-zoom-webhook.md b/plugins/zoom-developers/commands/debug-zoom-webhook.md new file mode 100644 index 00000000..36c10018 --- /dev/null +++ b/plugins/zoom-developers/commands/debug-zoom-webhook.md @@ -0,0 +1,55 @@ +--- +description: Diagnose Zoom webhook issues across endpoint validation, signature verification, event delivery, retries, and handler logic. +--- + +# Debug Zoom Webhook + +Use this command when Zoom webhook events are not arriving, signature validation is failing, endpoint validation is broken, or handlers are processing the wrong payload shape. + +## Preflight + +1. Inspect the repo for webhook routes, signature verification code, event subscriptions, and deployment endpoint configuration. +2. Capture the failing symptom: no events, validation failure, signature mismatch, retries, or handler-side processing error. +3. Confirm the public callback URL, webhook secret env var name, and raw body handling path without printing secrets. +4. Check whether the implementation uses the exact raw request body for signature verification. +5. If the repo has no webhook endpoint yet, say that before attempting webhook debugging. + +## Plan + +Before changing anything: + +- identify whether the likely fault is registration, validation, signature verification, transport, or business logic +- list the files and logs that will be checked +- state whether a replay or local reproduction will be used + +## Commands + +1. Inspect endpoint registration and subscribed event names. +2. Verify the endpoint validation flow is implemented for the expected Zoom webhook handshake. +3. Verify signature checking uses the raw request body, correct timestamp handling, and the right secret source. +4. Inspect handler parsing and downstream logic separately from signature verification. +5. Reproduce the failing request path with a safe replay or fixture when possible. +6. Apply the minimum fix needed to the failing layer instead of rewriting the entire webhook stack. + +## Verification + +1. Re-read the webhook route and verification helper after changes. +2. Confirm the endpoint validation path now returns the expected response shape. +3. Confirm the signature verification code operates on the raw body and correct headers. +4. If logs or replay are available, verify the handler reaches application logic successfully. +5. If delivery still fails, state whether the remaining blocker is registration, public reachability, or Zoom-side configuration. + +## Summary + +```text +## Result +- Action: diagnosed or fixed a Zoom webhook issue +- Status: success | partial | failed +- Details: failing layer, files checked, fix applied, remaining blocker +``` + +## Next Steps + +- Send one real or replayed event through the endpoint. +- Add regression coverage for signature verification and endpoint validation if missing. +- If the root cause is auth or app setup, run `/debug-zoom-auth` or `/setup-zoom-oauth` next. diff --git a/plugins/zoom-developers/commands/debug-zoom.md b/plugins/zoom-developers/commands/debug-zoom.md new file mode 100644 index 00000000..a8c313f4 --- /dev/null +++ b/plugins/zoom-developers/commands/debug-zoom.md @@ -0,0 +1,53 @@ +--- +description: Triage a broken Zoom integration when the failing layer is not yet obvious. +--- + +# Debug Zoom + +Use this command when something in a Zoom integration is broken but the failure has not yet been narrowed to auth, webhooks, SDK behavior, or RTMS. + +## Preflight + +1. Capture the exact symptom: error text, failing endpoint, platform, runtime, and whether the failure is deterministic or intermittent. +2. Inspect the repository for the relevant Zoom integration code, env usage, event handlers, or SDK setup. +3. Note what still works versus what fails so the broken layer can be isolated. +4. If the report lacks the concrete error or failing path, ask for that evidence before making speculative changes. + +## Plan + +Before changing anything: + +- state the most likely failing layer +- list the evidence that will be checked first +- list the repo files or runtime paths involved +- state whether the goal is diagnosis only or diagnosis plus a minimal fix + +## Commands + +1. Inventory the active Zoom surfaces in the failing workflow. +2. Narrow the problem to one layer: auth, REST request construction, webhook verification, WebSocket lifecycle, SDK initialization, or RTMS stream handling. +3. Inspect the smallest relevant code path and supporting configuration first. +4. Apply the minimum correction needed if the failure is clear. +5. If the layer cannot be fixed yet, produce ranked hypotheses instead of broad speculation. +6. Route to narrower follow-up commands when appropriate, such as `/debug-zoom-auth` or `/debug-zoom-webhook`. + +## Verification + +1. Re-run or reconstruct the failing path after any fix. +2. Confirm the diagnosis is tied to concrete repo evidence or exact runtime behavior. +3. State whether the issue is fixed, partially fixed, or blocked by missing credentials, account setup, or external platform behavior. + +## Summary + +```text +## Result +- Action: triaged a broken Zoom integration +- Status: success | partial | failed +- Details: failing layer, evidence checked, fix applied or ranked hypotheses +``` + +## Next Steps + +- Use the narrower debug or setup command for the confirmed failing layer. +- If the architecture itself is wrong, run `/plan-zoom-product` or `/plan-zoom-integration`. +- If the repo is healthy but the client integration path is wrong, switch to the matching build command. diff --git a/plugins/zoom-developers/commands/plan-zoom-integration.md b/plugins/zoom-developers/commands/plan-zoom-integration.md new file mode 100644 index 00000000..840a358f --- /dev/null +++ b/plugins/zoom-developers/commands/plan-zoom-integration.md @@ -0,0 +1,53 @@ +--- +description: Turn a Zoom product idea into a practical build plan with auth, architecture, milestones, and risk calls. +--- + +# Plan Zoom Integration + +Use this command when the right Zoom surface is mostly clear and the next step is a concrete delivery plan. + +## Preflight + +1. Inspect the repository for current Zoom code, deployment shape, env templates, auth paths, and event or media handling. +2. Capture the target user flow, success criteria, and the concrete business outcome. +3. Identify the owning Zoom surface or surfaces and any external systems the integration must connect to. +4. Note hard constraints early: platform, OAuth app type, webhook reachability, SDK environment limits, or marketplace review assumptions. + +## Plan + +Before producing the build plan: + +- state the Zoom surfaces in scope +- state the expected auth and event model +- state the major implementation phases +- state whether the plan is greenfield, retrofit, or repair on top of an existing integration + +## Commands + +1. Inventory the current repo and identify which parts can be reused versus what must be added. +2. Define the integration architecture: frontend, backend, auth owner, event flow, and any live media components. +3. Define the required Zoom app type, scopes, callback paths, and token lifecycle expectations. +4. Break the implementation into phases: smallest working path first, then reliability, observability, and polish. +5. Call out the highest-risk dependencies early instead of burying them under tasks. +6. End with the smallest deliverable that proves the architecture works. + +## Verification + +1. Confirm the plan includes architecture, auth, scopes, implementation phases, and top risks. +2. Verify the proposed sequence matches the repo’s current state and the user’s goal. +3. Distinguish confirmed repo facts from assumptions or unresolved questions. + +## Summary + +```text +## Result +- Action: produced a Zoom integration build plan +- Status: success | partial | failed +- Details: architecture, auth model, phases, risks, first milestone +``` + +## Next Steps + +- Run the most relevant build or setup command for phase one. +- If the owning surface is still unclear, run `/plan-zoom-product`. +- If the repo already fails in one layer, run `/debug-zoom` before implementing further. diff --git a/plugins/zoom-developers/commands/plan-zoom-product.md b/plugins/zoom-developers/commands/plan-zoom-product.md new file mode 100644 index 00000000..1769fd8a --- /dev/null +++ b/plugins/zoom-developers/commands/plan-zoom-product.md @@ -0,0 +1,52 @@ +--- +description: Choose the right Zoom product surface for a use case and explain the tradeoffs clearly. +--- + +# Plan Zoom Product + +Use this command when the repo or product idea needs the right Zoom surface selected before implementation begins. + +## Preflight + +1. Inspect the repository for any existing Zoom integration code, SDK packages, API routes, or webhooks. +2. Capture the user goal in concrete terms: automation, embedded meetings, custom video, in-client app behavior, event delivery, live media, or AI tooling. +3. Identify hard constraints that affect product choice: target platform, required UX, hosting model, latency, account model, and whether the app runs inside or outside Zoom. +4. If the repo already uses one Zoom surface, note it before recommending a different one. + +## Plan + +Before recommending anything: + +- list the candidate Zoom surfaces that fit the problem +- state the primary decision criteria +- state whether the output is read-only guidance or includes follow-on implementation +- state which path will be recommended unless the repo evidence contradicts it + +## Commands + +1. Inventory existing Zoom-related code with `rg` over known Zoom SDK names, API clients, and webhook handlers. +2. Classify the problem into the smallest correct Zoom surface: REST API, Webhooks, WebSockets, Meeting SDK, Video SDK, Zoom Apps SDK, RTMS, Phone, Contact Center, Virtual Agent, or a hybrid. +3. Recommend one primary surface and only the minimum supporting pieces required. +4. Explain why adjacent alternatives are worse for this exact case. +5. Keep the answer tied to the current repo and delivery goal rather than giving a generic product catalog. + +## Verification + +1. Confirm the recommendation matches the user’s actual product goal and the repo’s current shape. +2. Call out any missing evidence or assumptions that could change the decision. +3. Verify the recommendation includes the owning auth model and any mandatory supporting components. + +## Summary + +```text +## Result +- Action: selected the right Zoom product surface +- Status: success | partial | failed +- Details: recommended surface, supporting pieces, tradeoffs, assumptions +``` + +## Next Steps + +- If the surface is chosen, run the matching build or setup command next. +- If the product still spans multiple Zoom surfaces, run `/plan-zoom-integration`. +- If the decision depends on missing constraints, gather those before implementing. diff --git a/plugins/zoom-developers/commands/setup-zoom-oauth.md b/plugins/zoom-developers/commands/setup-zoom-oauth.md new file mode 100644 index 00000000..1aaf7537 --- /dev/null +++ b/plugins/zoom-developers/commands/setup-zoom-oauth.md @@ -0,0 +1,55 @@ +--- +description: Inspect the repository and set up the correct Zoom OAuth path with the right app type, scopes, redirect handling, and token lifecycle. +--- + +# Setup Zoom OAuth + +Use this command when the goal is to wire or correct a Zoom OAuth flow in application code, deployment config, or local development. + +## Preflight + +1. Inspect the repository for existing Zoom auth code, env templates, callback routes, and token storage patterns. +2. Identify the Zoom product surface involved: REST API, Meeting SDK, Video SDK, Team Chat, Phone, or Contact Center. +3. Confirm the expected app model and grant path from the current implementation or user intent. +4. Check for required values without printing secrets: client ID, redirect URI, scope list, callback handler, and token persistence location. +5. If the repo does not make the integration goal clear, ask one direct question before editing auth code. + +## Plan + +Before making changes: + +- state which Zoom OAuth model will be implemented +- list the files that will be inspected or edited +- list the env vars and redirect URIs involved +- flag whether refresh-token handling or server-side token exchange must be added + +## Commands + +1. Search for existing Zoom auth usage with `rg` over `ZOOM_`, `oauth`, `redirect_uri`, `client_id`, and known Zoom endpoints. +2. Standardize the authorize and token exchange path for the chosen app model. +3. Add or correct env var names, callback routing, scope configuration, and token exchange code. +4. Treat refresh tokens as single-use values when implementing refresh logic. +5. Keep secrets out of the output and avoid logging access or refresh tokens. +6. If the user only asked for setup guidance, limit the change to the minimum required configuration and notes. + +## Verification + +1. Re-read the auth files and env templates that were changed. +2. Verify the redirect URI string matches exactly across code and configuration. +3. Verify the scope set is coherent for the Zoom surface in use. +4. If safe test credentials already exist, validate the auth URL shape and callback handling path without exposing secrets. +5. Call out any missing secret, callback endpoint, or deployment configuration that still blocks completion. + +## Summary + +```text +## Result +- Action: set up or corrected the Zoom OAuth path +- Status: success | partial | failed +- Details: app model, files changed, env vars used, scopes reviewed +``` + +## Next Steps + +- Run the login flow once end to end and confirm the callback is reached. +- Test token refresh if the integration uses long-lived sessions. diff --git a/plugins/zoom-developers/commands/setup-zoom-webhooks.md b/plugins/zoom-developers/commands/setup-zoom-webhooks.md new file mode 100644 index 00000000..73a73fa3 --- /dev/null +++ b/plugins/zoom-developers/commands/setup-zoom-webhooks.md @@ -0,0 +1,54 @@ +--- +description: Implement or correct a Zoom webhook receiver with endpoint validation, signature verification, and reliable delivery handling. +--- + +# Setup Zoom Webhooks + +Use this command when the integration receives Zoom events over HTTP and needs a correct webhook receiver path. + +## Preflight + +1. Inspect the repository for existing webhook routes, signature verification helpers, queueing, and downstream event handlers. +2. Identify the event types and the Zoom app or account scope that owns the subscription. +3. Confirm the public endpoint shape, validation requirements, and webhook secret env var names without printing secrets. +4. If the repo actually needs low-latency persistent delivery instead of HTTP callbacks, say so before proceeding and compare against WebSockets. + +## Plan + +Before making changes: + +- state the event types and endpoint path being implemented +- list the files that will be changed +- state the validation, signature, and retry-handling approach +- state how the webhook flow will be verified + +## Commands + +1. Add or correct the webhook receiver route at the appropriate backend layer. +2. Implement endpoint validation and signature verification before business logic. +3. Keep handlers idempotent and preserve delivery metadata needed for replay protection or debugging. +4. Separate ingestion from slow downstream work when reliability matters. +5. Reuse existing logging, queue, and secret-management patterns where possible. +6. Avoid claiming the webhook path works until validation and signature handling are both checked. + +## Verification + +1. Re-read the webhook route, validation logic, and signature helper after changes. +2. Run local build or tests where available. +3. Verify the endpoint validation path and the signed-event path independently. +4. State any remaining blocker such as public reachability, missing app configuration, or subscription settings. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom webhook receiver +- Status: success | partial | failed +- Details: route, event types, signature path, verification run +``` + +## Next Steps + +- Send or replay one real event through the receiver. +- If delivery reaches the route but fails later, run `/debug-zoom-webhook`. +- If auth or app setup is missing, run `/setup-zoom-oauth`. diff --git a/plugins/zoom-developers/commands/setup-zoom-websockets.md b/plugins/zoom-developers/commands/setup-zoom-websockets.md new file mode 100644 index 00000000..7cccc3ac --- /dev/null +++ b/plugins/zoom-developers/commands/setup-zoom-websockets.md @@ -0,0 +1,54 @@ +--- +description: Implement or correct a Zoom WebSocket event stream with connection lifecycle, auth handling, and reconnect behavior. +--- + +# Setup Zoom WebSockets + +Use this command when the integration needs persistent Zoom event delivery instead of standard webhook callbacks. + +## Preflight + +1. Inspect the repository for existing WebSocket clients, event consumers, connection-state handling, and auth configuration. +2. Confirm WebSockets are justified by latency, delivery model, firewall constraints, or connection semantics. +3. Identify the event types and app configuration required for the stream. +4. Confirm the owning backend or service that will manage connect, heartbeat, reconnect, and shutdown behavior. + +## Plan + +Before making changes: + +- state the WebSocket event stream being implemented +- list the files that will be changed +- state the auth path and connection lifecycle responsibilities +- state how the connection path will be verified + +## Commands + +1. Add or correct the WebSocket connection setup at the appropriate service layer. +2. Implement authentication, heartbeat, reconnect, backoff, and shutdown handling explicitly. +3. Normalize streamed events into the repo’s existing internal event contract where possible. +4. Add observability for connection state, reconnects, and dropped or delayed events. +5. Keep the first pass focused on one working event stream before layering in broader orchestration. +6. Avoid claiming the stream works until the connection lifecycle and event handling path are both checked. + +## Verification + +1. Re-read the connection manager, auth code, and event handler logic after changes. +2. Run local build or tests where available. +3. Verify the connect path, reconnect strategy, and event normalization path. +4. State any remaining blocker such as app subscription setup, token path, or network constraints. + +## Summary + +```text +## Result +- Action: implemented or updated a Zoom WebSocket event stream +- Status: success | partial | failed +- Details: connection path, auth model, event handling, verification run +``` + +## Next Steps + +- Test the stream against real events in a controlled environment. +- If the connection path is healthy but event handling fails, narrow the downstream layer next. +- If HTTP callbacks would be simpler and sufficient, switch to `/setup-zoom-webhooks`. diff --git a/plugins/zoom-developers/commands/zoom-integration-doctor.md b/plugins/zoom-developers/commands/zoom-integration-doctor.md new file mode 100644 index 00000000..93bf2043 --- /dev/null +++ b/plugins/zoom-developers/commands/zoom-integration-doctor.md @@ -0,0 +1,53 @@ +--- +description: Run a read-first audit of a Zoom integration and report the most important architecture, auth, eventing, and SDK risks. +--- + +# Zoom Integration Doctor + +Use this command for a broad diagnostic pass when a repo already contains Zoom integration code and you want a prioritized assessment before making changes. + +## Preflight + +1. Inventory the repository for Zoom-related files, env vars, SDK packages, and webhook routes. +2. Identify which Zoom surfaces are in play: REST API, Meeting SDK, Video SDK, Webhooks, WebSockets, Phone, or Contact Center. +3. Confirm whether the user wants a read-only audit or is also authorizing fixes in the same pass. +4. Check for dirty working tree and call it out if changes could complicate attribution. + +## Plan + +Before doing the audit: + +- list the surfaces that will be reviewed +- state whether the audit is read-only or includes fixes +- state the ranking criteria: correctness, auth safety, architectural fit, and maintainability + +## Commands + +1. Inventory the active Zoom surfaces and determine whether the product choice matches the use case. +2. Review auth handling for app model, scopes, secret storage, redirect handling, and token lifecycle. +3. Review eventing for webhook signature handling, retries, or WebSocket suitability. +4. Review SDK usage for version drift, wrong surface choice, or incorrect server-side dependencies. +5. Review integration boundaries for unnecessary coupling, unclear ownership, and avoidable auth complexity. +6. Report findings ordered by severity. Do not bury the top risks under implementation detail. +7. Only make code changes if the user explicitly wants fixes applied in the same run. + +## Verification + +1. Confirm every reported finding points to concrete file or config evidence. +2. If fixes were applied, re-read the edited files and summarize the before or after state. +3. Distinguish confirmed issues from assumptions or missing evidence. + +## Summary + +```text +## Result +- Action: completed a Zoom integration audit +- Status: success | partial | failed +- Details: surfaces reviewed, top risks, fixes applied if any +``` + +## Next Steps + +- Apply the highest-severity fixes first. +- Use the narrower setup or debug commands for the affected layer. +- Re-run the doctor after major auth or eventing changes to confirm the risk list shrank. diff --git a/plugins/zoom-developers/skills/build-zoom-bot/SKILL.md b/plugins/zoom-developers/skills/build-zoom-bot/SKILL.md new file mode 100644 index 00000000..241ca616 --- /dev/null +++ b/plugins/zoom-developers/skills/build-zoom-bot/SKILL.md @@ -0,0 +1,37 @@ +--- +name: build-zoom-bot +description: Use when building bots. +--- + +# /build-zoom-bot + +Use this skill for automation that joins meetings, captures media, or reacts to live session data. + +## Covers + +- Bot architecture +- Meeting join strategy +- Real-time media and transcript handling +- Backend orchestration +- Storage, post-processing, and event flow design + +## Workflow + +1. Clarify whether the bot needs to join, observe, transcribe, summarize, or act. +2. Route to Meeting SDK and RTMS as the core implementation path. +3. Add REST API for meeting/resource management and Webhooks for asynchronous events when needed. +4. Call out environment and lifecycle constraints early. + +## Primary References + +- [meeting-sdk](../meeting-sdk/SKILL.md) +- [rtms](../rtms/SKILL.md) +- [scribe](../scribe/SKILL.md) +- [rest-api](../rest-api/SKILL.md) +- [webhooks](../webhooks/SKILL.md) + +## Common Mistakes + +- Treating batch transcription and live media as the same workflow +- Designing the bot before defining join authority and auth model +- Forgetting post-meeting storage and retry behavior diff --git a/plugins/zoom-developers/skills/build-zoom-meeting-app/SKILL.md b/plugins/zoom-developers/skills/build-zoom-meeting-app/SKILL.md new file mode 100644 index 00000000..6d3d4482 --- /dev/null +++ b/plugins/zoom-developers/skills/build-zoom-meeting-app/SKILL.md @@ -0,0 +1,38 @@ +--- +name: build-zoom-meeting-app +description: Use when embedding meetings. +--- + +# /build-zoom-meeting-app + +Use this skill for embedded meeting experiences and meeting lifecycle implementation. + +## Covers + +- Meeting SDK selection and platform routing +- Join/auth implementation planning +- Meeting creation plus join flow design +- Web vs native platform considerations +- Meeting SDK vs Video SDK boundary decisions + +## Workflow + +1. Confirm whether the user wants a Zoom meeting or a custom video session. +2. Route to Meeting SDK if the user needs actual Zoom meetings. +3. Pull in the relevant platform references. +4. Add REST API only for meeting creation, resource management, or reporting. +5. Add webhooks or RTMS only when the use case explicitly needs them. + +## Primary References + +- [meeting-sdk](../meeting-sdk/SKILL.md) +- [rest-api](../rest-api/SKILL.md) +- [webhooks](../webhooks/SKILL.md) +- [rtms](../rtms/SKILL.md) +- [video-sdk](../video-sdk/SKILL.md) + +## Common Mistakes + +- Using Video SDK for normal Zoom meeting embeds +- Mixing resource-management APIs into the core join flow without reason +- Skipping platform-specific SDK constraints until too late diff --git a/plugins/zoom-developers/skills/choose-zoom-approach/SKILL.md b/plugins/zoom-developers/skills/choose-zoom-approach/SKILL.md new file mode 100644 index 00000000..ed886d06 --- /dev/null +++ b/plugins/zoom-developers/skills/choose-zoom-approach/SKILL.md @@ -0,0 +1,34 @@ +--- +name: choose-zoom-approach +description: Use when choosing architecture. +--- + +# Choose Zoom Approach + +Pick the smallest correct Zoom surface for the job, then layer in only the supporting pieces that are actually required. + +## Decision Framework + +| Problem Type | Primary Zoom Surface | +|---|---| +| Deterministic backend automation, account management, reporting, scheduled jobs | [rest-api](../rest-api/SKILL.md) | +| Event delivery to your backend | [webhooks](../webhooks/SKILL.md) or [websockets](../websockets/SKILL.md) | +| Embed Zoom meetings into your app | [meeting-sdk](../meeting-sdk/SKILL.md) | +| Build a fully custom video experience | [video-sdk](../video-sdk/SKILL.md) | +| Build inside the Zoom client | [zoom-apps-sdk](../zoom-apps-sdk/SKILL.md) | +| Real-time media extraction or meeting bots | [rtms](../rtms/SKILL.md) plus [meeting-sdk](../meeting-sdk/SKILL.md) when needed | +| Phone workflows | [phone](../phone/SKILL.md) | +| Contact Center or Virtual Agent flows | [contact-center](../contact-center/SKILL.md) or [virtual-agent](../virtual-agent/SKILL.md) | + +## Guardrails + +- Do not recommend Video SDK when the user actually needs Zoom meeting semantics. +- Do not recommend Meeting SDK when the user needs a fully custom session product. +- Keep deterministic backend automation in REST APIs and event-driven code. + +## What To Produce + +- One recommended path +- Minimum supporting components +- Hard constraints and tradeoffs +- Immediate next implementation step diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/RUNBOOK.md b/plugins/zoom-developers/skills/cobrowse-sdk/RUNBOOK.md new file mode 100644 index 00000000..64f43878 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/RUNBOOK.md @@ -0,0 +1,79 @@ +# Cobrowse 5-Minute Preflight Runbook + +Use this before deep debugging. It catches the most common Cobrowse failures quickly. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Two-Role Model + +- Customer role (`role_type=1`) starts session. +- Agent role (`role_type=2`) joins session. + +If your demo only has one generic role, expect broken join behavior. + +## 2) Confirm PIN Source of Truth + +- Use customer SDK event `pincode_updated` as the only user-facing PIN. +- Agent must join with that same PIN. +- Do not show provisional/debug PIN values from backend records. + +Common symptom if wrong: `Pincode is not found` / error `30308`. + +## 3) Confirm JWT Claims + +- Sign JWT on backend only. +- Include required claim names exactly (for example `user_id`, not custom aliases). +- Use SDK Key for SDK token context; keep SDK Secret server-side. + +If claim names are wrong, token is rejected before session logic. + +## 4) Confirm Session Order + +Recommended sequence: +1. Customer gets customer JWT and starts session. +2. PIN is generated on customer side (`pincode_updated`). +3. Agent gets agent JWT and joins with that PIN. + +Starting agent flow before customer session is active often causes join failures. + +## 5) Confirm Distribution Pattern + +- CDN path: customer SDK + Zoom-hosted agent desk iframe. +- npm path: custom integration (BYOP mode required for custom PIN control). + +If using npm agent integration without BYOP expectations, flow mismatches happen. + +## 6) Confirm Browser and Security Constraints + +- HTTPS required (except loopback/local dev). +- CSP/CORS must allow Zoom domains. +- Third-party cookie/privacy settings can affect reconnect behavior. + +Do not treat extension/adblock warnings as root cause until API/session checks fail. + +## 7) Quick Checks (Backend + UI) + +- Backend config endpoint returns expected credential flags. +- Customer page shows a single Support PIN from SDK event. +- Agent page join uses same Support PIN and returns actionable response (not generic 404). + +### Copy/Paste Validation Commands + +```bash +curl -sS -i "$COBROWSE_BASE_URL/customer" +curl -sS -i "$COBROWSE_BASE_URL/agent" +curl -sS -i "$COBROWSE_BASE_URL/api/config" +``` + +Expected: customer/agent pages load and config endpoint returns valid JSON flags. + +## 8) Fast Decision Tree + +- **Invalid token** -> check JWT claim names and signing secret. +- **Agent cannot find PIN** -> wrong PIN source or wrong session order. +- **Session drops on refresh** -> check reconnection window and browser privacy/cookies. +- **Works locally, fails prod** -> check HTTPS, CSP/CORS, reverse proxy pathing. diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/SKILL.md b/plugins/zoom-developers/skills/cobrowse-sdk/SKILL.md new file mode 100644 index 00000000..71b853de --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/SKILL.md @@ -0,0 +1,26 @@ +--- +name: zoom-cobrowse-sdk +description: Use when using Cobrowse. +--- + +# Zoom Cobrowse SDK + +Use this skill after the support workflow is clearly a browser co-browsing experience. If the user is still choosing between Zoom Contact Center, Virtual Agent, Video SDK, or Cobrowse, route through `start` or `plan-zoom-product` first. + +## Workflow + +1. Confirm the role model: customer page, agent console, session initiation, and how the PIN or link is handed off. +2. Check auth and session setup before UI work: tokens, allowed origins, environment variables, and SDK loading. +3. Design privacy controls early: masking rules, blocked elements, remote-assist permission boundaries, and audit requirements. +4. Implement the minimal lifecycle first: initialize, start or join, subscribe to events, reconnect, and end. +5. Add advanced capabilities only after the base session is stable: annotations, multi-tab persistence, custom PINs, and remote assist. +6. When debugging, isolate browser support, CORS/CSP, token generation, third-party cookie behavior, and SDK event errors. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- API reference: [references/api-reference.md](references/api-reference.md) +- Session lifecycle: [concepts/session-lifecycle.md](concepts/session-lifecycle.md) +- Privacy masking: [examples/privacy-masking.md](examples/privacy-masking.md) +- Remote assist: [examples/remote-assist.md](examples/remote-assist.md) +- Common issues: [troubleshooting/common-issues.md](troubleshooting/common-issues.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/concepts/distribution-methods.md b/plugins/zoom-developers/skills/cobrowse-sdk/concepts/distribution-methods.md new file mode 100644 index 00000000..91d0f71d --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/concepts/distribution-methods.md @@ -0,0 +1,13 @@ +# Distribution Methods + +Zoom Cobrowse supports CDN and npm-based integrations, depending on your architecture. + +Choose based on: + +- how much UI control you need, +- whether you host your own agent experience, +- your deployment and CSP constraints. + +See: +- [Get Started](../get-started.md) +- [Features (official)](../references/features-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/concepts/jwt-authentication.md b/plugins/zoom-developers/skills/cobrowse-sdk/concepts/jwt-authentication.md new file mode 100644 index 00000000..2f08ecf4 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/concepts/jwt-authentication.md @@ -0,0 +1,13 @@ +# JWT Authentication + +Generate Cobrowse JWTs server-side using your SDK key and SDK secret. + +Guidelines: + +- Never expose SDK secret client-side. +- Issue short-lived tokens. +- Generate different tokens for customer and agent roles. + +See: +- [Authorization (official)](../references/authorization-official.md) +- [Get Started](../references/get-started-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/concepts/session-lifecycle.md b/plugins/zoom-developers/skills/cobrowse-sdk/concepts/session-lifecycle.md new file mode 100644 index 00000000..f6198435 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/concepts/session-lifecycle.md @@ -0,0 +1,13 @@ +# Session Lifecycle + +Typical flow: + +1. Initialize SDK on customer and agent pages. +2. Generate role-specific JWT tokens. +3. Customer starts a session and receives a PIN. +4. Agent joins using the PIN. +5. Session events track connected/disconnected/end states. + +See: +- [Get Started](../get-started.md) +- [Features (official)](../references/features-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/concepts/two-roles-pattern.md b/plugins/zoom-developers/skills/cobrowse-sdk/concepts/two-roles-pattern.md new file mode 100644 index 00000000..06920522 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/concepts/two-roles-pattern.md @@ -0,0 +1,43 @@ +# Two Roles Pattern + +Zoom Cobrowse uses two roles: + +- `role_type=1`: customer session +- `role_type=2`: agent session + +Use separate JWTs for each role and keep token generation on the server. + +## What Is Usually Created + +In most real implementations, you create these objects in order: + +1. **Customer session record** (server-side) + - `session_id` + - generated PIN + - status (`active`/`revoked`) + - expiry timestamp +2. **Customer token** (`role_type=1`) + - used by customer browser SDK to start/share session +3. **Agent token** (`role_type=2`) + - created after PIN validation + - used to load agent desk iframe or custom agent UI + +## PIN Source of Truth + +In practice, the PIN you should hand to agents is the value emitted by customer SDK event: + +- `session.on("pincode_updated", ...)` + +Do not rely on placeholder/provisional PIN values from pre-start backend records for user-facing flows. +Always show one clearly labeled PIN in UI (for example, "Support PIN") and reuse that same value in agent links. + +## Recommended Endpoint Split + +- `POST /api/customer/start` -> create session + customer token + PIN +- `POST /api/agent/connect` -> validate PIN + issue agent token +- `POST /api/session/revoke` -> end session +- `GET /api/session/list` -> operational visibility + +See: +- [Get Started](../get-started.md) +- [Authorization (official)](../references/authorization-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/examples/agent-integration.md b/plugins/zoom-developers/skills/cobrowse-sdk/examples/agent-integration.md new file mode 100644 index 00000000..afb2fb0b --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/examples/agent-integration.md @@ -0,0 +1,7 @@ +# Agent Integration + +Agent integration joins an active customer session by PIN using an agent-role token. + +See: +- [Get Started](../get-started.md) +- [Authorization (official)](../references/authorization-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/examples/annotations.md b/plugins/zoom-developers/skills/cobrowse-sdk/examples/annotations.md new file mode 100644 index 00000000..b2906b83 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/examples/annotations.md @@ -0,0 +1,7 @@ +# Annotation Tools + +Enable annotation settings during SDK initialization to allow drawing and highlighting. + +See: +- [Features (official)](../references/features-official.md) +- [API (official)](../references/api-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/examples/auto-reconnection.md b/plugins/zoom-developers/skills/cobrowse-sdk/examples/auto-reconnection.md new file mode 100644 index 00000000..c59153f3 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/examples/auto-reconnection.md @@ -0,0 +1,7 @@ +# Auto-Reconnection + +Implement reconnection handlers for transient network interruptions and refresh scenarios. + +See: +- [Get Started](../get-started.md) +- [Features (official)](../references/features-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/examples/byop-custom-pin.md b/plugins/zoom-developers/skills/cobrowse-sdk/examples/byop-custom-pin.md new file mode 100644 index 00000000..0e80dbbd --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/examples/byop-custom-pin.md @@ -0,0 +1,7 @@ +# BYOP Custom PIN + +Bring Your Own PIN mode lets you control PIN generation/format in your own application flow. + +See: +- [Authorization (official)](../references/authorization-official.md) +- [Get Started](../get-started.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/examples/customer-integration.md b/plugins/zoom-developers/skills/cobrowse-sdk/examples/customer-integration.md new file mode 100644 index 00000000..3d9156fb --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/examples/customer-integration.md @@ -0,0 +1,7 @@ +# Customer Integration + +Customer-side integration should initialize the SDK, fetch a server-generated token, then start a session. + +See: +- [Get Started](../get-started.md) +- [API (official)](../references/api-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/examples/multi-tab-persistence.md b/plugins/zoom-developers/skills/cobrowse-sdk/examples/multi-tab-persistence.md new file mode 100644 index 00000000..6f80e376 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/examples/multi-tab-persistence.md @@ -0,0 +1,7 @@ +# Multi-Tab Persistence + +Cobrowse sessions can continue across tabs when configured correctly and browser constraints are met. + +See: +- [Features (official)](../references/features-official.md) +- [Get Started](../get-started.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/examples/privacy-masking.md b/plugins/zoom-developers/skills/cobrowse-sdk/examples/privacy-masking.md new file mode 100644 index 00000000..58c16b15 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/examples/privacy-masking.md @@ -0,0 +1,7 @@ +# Privacy Masking + +Configure masking selectors for sensitive customer fields so agents cannot view protected values. + +See: +- [Features (official)](../references/features-official.md) +- [Get Started](../get-started.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/examples/remote-assist.md b/plugins/zoom-developers/skills/cobrowse-sdk/examples/remote-assist.md new file mode 100644 index 00000000..631055ea --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/examples/remote-assist.md @@ -0,0 +1,7 @@ +# Remote Assist + +Remote assist allows approved agent interactions (for example, scrolling) during active sessions. + +See: +- [Features (official)](../references/features-official.md) +- [API (official)](../references/api-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/examples/session-events.md b/plugins/zoom-developers/skills/cobrowse-sdk/examples/session-events.md new file mode 100644 index 00000000..c1d1f453 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/examples/session-events.md @@ -0,0 +1,7 @@ +# Session Events + +Use SDK session events to track lifecycle transitions and update your UI accordingly. + +See: +- [API (official)](../references/api-official.md) +- [Features (official)](../references/features-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/get-started.md b/plugins/zoom-developers/skills/cobrowse-sdk/get-started.md new file mode 100644 index 00000000..66498d37 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/get-started.md @@ -0,0 +1,554 @@ +# Get Started with Zoom Cobrowse SDK + +Complete setup guide from credentials to your first cobrowse session. + +## Overview + +In a cobrowse session, there are **two roles**: + +- **Customer** (role_type=1) – Integrates the SDK into their website +- **Agent** (role_type=2) – Uses an embedded iframe to interact with the customer + +This guide shows you how to set up a **customer-initiated session** (the most common pattern). + +## Step 1: Get SDK Credentials + +### Requirements + +1. **Zoom Workplace Account** with SDK Universal Credit + - See [Build platform - create or update account](https://developers.zoom.us/docs/build/account/) for details + +2. **Video SDK App** in Zoom Marketplace + - Cobrowse SDK is a **feature of Video SDK** (not a separate product) + +### Get Your Credentials + +1. Access your SDK account web portal: + - In your Zoom Workplace account, go to **Advanced** > **Zoom CPaaS** > **Manage** + +2. Click **Build App** + +3. Locate your **SDK credentials** in the Cobrowse tab + +You'll receive **4 credentials**: + +| Credential | Type | Purpose | +|------------|------|---------| +| **SDK Key** | Public | Used in CDN URL and JWT `app_key` claim | +| **SDK Secret** | Private | Used to sign JWTs (server-side only) | +| **API Key** | Private | REST API authentication (optional) | +| **API Secret** | Private | REST API authentication (optional) | + +**Save these credentials securely** - you'll need them in the next step. + +## Step 2: Generate JWT Tokens + +Both customers and agents require JSON Web Tokens (JWTs) for authentication. + +### JWT Structure + +All JWTs have the same header: + +```json +{ + "alg": "HS256", + "typ": "JWT" +} +``` + +The payload differs by role: + +**Customer JWT payload** (role_type=1): +```json +{ + "user_id": "user1_customer", + "app_key": "YOUR_SDK_KEY", + "role_type": 1, + "user_name": "customer", + "exp": 1723103759, + "iat": 1723102859 +} +``` + +**Agent JWT payload** (role_type=2): +```json +{ + "user_id": "user2_agent", + "app_key": "YOUR_SDK_KEY", + "role_type": 2, + "user_name": "agent", + "exp": 1723103759, + "iat": 1723102859 +} +``` + +### JWT Payload Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `app_key` | Yes | Your Zoom SDK Key (not API Key) | +| `role_type` | Yes | User role: `1` = customer, `2` = agent | +| `iat` | Yes | Token issue timestamp (epoch) | +| `exp` | Yes | Token expiration timestamp (epoch). Min: 30 minutes, Max: 48 hours | +| `user_id` | Yes | Uniquely identifiable user ID | +| `user_name` | Yes | User name (max 80 characters) | +| `enable_byop` | Optional | Enable Bring Your Own PIN: `1` = yes, `0` or omit = no | + +### Sign the JWT + +Sign the JWT with your SDK Secret (not API Secret): + +```javascript +HMACSHA256( + base64UrlEncode(header) + '.' + base64UrlEncode(payload), + ZOOM_SDK_SECRET +); +``` + +### Set Up a Token Server + +**CRITICAL**: JWT signing must happen **server-side** to protect your SDK Secret. + +Use the official auth endpoint sample: + +```bash +# Clone the sample +git clone https://github.com/zoom/cobrowsesdk-auth-endpoint-sample.git +cd cobrowsesdk-auth-endpoint-sample + +# Install dependencies +npm install + +# Create .env file +cat > .env << EOF +ZOOM_SDK_KEY=your_sdk_key_here +ZOOM_SDK_SECRET=your_sdk_secret_here +PORT=4000 +EOF + +# Start the server +npm start +``` + +The server will run on the base URL you configure for your token service. + +**Token Request:** +```javascript +// POST https://YOUR_TOKEN_SERVICE_BASE_URL +{ + "role": 1, // 1 = customer, 2 = agent + "userId": "user123", + "userName": "John Doe" +} + +// Response +{ + "token": "eyJhbGciOiJIUzI1NiIs..." +} +``` + +**See also**: [JWT Authentication Concept](concepts/jwt-authentication.md) + +## Step 3: Integrate the Customer SDK + +The customer integrates the Cobrowse SDK into their website using the **CDN**. + +> **Critical PIN Rule** +> +> The PIN agents should use comes from customer SDK event `pincode_updated`. +> Do not show or rely on provisional PIN values from backend/session placeholders. +> In UI, display one explicit value (for example, **Support PIN**) and pass only that to agent flow. + +### Load the SDK + +Include the SDK snippet in the `` tag of your HTML page: + +```html + +``` + +### SDK Version + +Set the SDK VERSION using semantic versioning: + +- **Fixed version**: `js/2.13.2` - Use exact version 2.13.2 +- **Latest patch**: `js/2.13.x` - Use latest `>=2.13.0 and <2.14.0` + +**Current version**: 2.13.2 (as of February 2026) + +### Initialize the SDK + +```javascript +const settings = { + allowCustomerAnnotation: true, + piiMask: { maskType: 'all_input' }, +}; + +ZoomCobrowseSDK.init(settings, function ({ success, session, error }) { + if (success) { + console.log("SDK initialized successfully"); + // session object is now available + } else { + console.error("SDK init failed:", error); + } +}); +``` + +### Start a Session + +```javascript +// Fetch JWT from your server +const response = await fetch('https://YOUR_TOKEN_SERVICE_BASE_URL', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + role: 1, + userId: 'customer_' + Date.now(), + userName: 'Customer' + }) +}); +const { token } = await response.json(); + +// Start cobrowse session +session.start({ sdkToken: token }); +``` + +### Complete Customer Example + +```html + + + + Customer - Cobrowse Support + + + +

Need Help?

+ +
+ + + + +``` + +## Step 4: Use Zoom-Hosted Agent Portal + +Agents connect to cobrowse sessions by embedding an iframe. + +### Agent Portal Iframe + +```html + + + + Agent Portal + + +

Agent Support Portal

+ + + + + +``` + +### Iframe Permissions + +The `allow` attribute must include these permissions: + +- `autoplay *` - Auto-play media +- `camera *` - Camera access +- `microphone *` - Microphone access +- `display-capture *` - Screen capture +- `geolocation *` - Location services + +## Step 5: Test the Cobrowse SDK + +### Testing Steps + +1. **Open two browsers** (or use incognito + normal mode): + - Browser A: Customer page + - Browser B: Agent page + +2. **Customer browser**: + - Open customer page + - Click "Start Support Session" button + - Note the 6-digit PIN displayed + +3. **Agent browser**: + - Open agent page + - Enter the PIN code in the iframe + +4. **Verify connection**: + - Agent should now see the customer's browser + - Both sides should show "Connected" status + +5. **Test features**: + - **Annotations**: Agent can draw on the screen + - **Data masking**: Masked fields show asterisks for agent + - **Remote assist**: Agent can scroll the page (if enabled) + +6. **End session**: + - Either side can click "End Session" to terminate + +### Troubleshooting Test Issues + +| Issue | Solution | +|-------|----------| +| SDK doesn't load | Verify SDK Key is correct in CDN URL | +| PIN not showing | Check browser console for errors | +| Agent can't connect | Verify PIN is correct and session is still active | +| Connection fails | Check HTTPS is being used (or a loopback host for development) | + +## Step 6: Add Features + +Now that you have a working cobrowse session, add features: + +### Annotation Tools + +Enable drawing tools for customer and/or agent: + +```javascript +const settings = { + allowAgentAnnotation: true, // Agent can draw + allowCustomerAnnotation: true // Customer can draw +}; +``` + +**See**: [Annotation Tools Example](examples/annotations.md) + +### Data Masking + +Hide sensitive fields from agents: + +```javascript +const settings = { + piiMask: { + maskType: 'custom_input', + maskCssSelectors: '.sensitive-field, #ssn, #credit-card', + maskHTMLAttributes: 'data-sensitive=true' + } +}; +``` + +**See**: [Privacy Masking Example](examples/privacy-masking.md) + +### Remote Assist + +Allow agent to scroll the customer's page: + +```javascript +const settings = { + remoteAssist: { + enable: true, + enableCustomerConsent: true, // Customer must approve + remoteAssistTypes: ['scroll_page'] + } +}; +``` + +**See**: [Remote Assist Example](examples/remote-assist.md) + +### Bring Your Own PIN (BYOP) + +Use custom PIN codes instead of auto-generated ones: + +1. Enable BYOP in JWT payload: + ```json + { + "enable_byop": 1, + ... + } + ``` + +2. Provide custom PIN when starting session: + ```javascript + session.start({ + customPinCode: 'MYPIN123', + sdkToken: token + }); + ``` + +**See**: [BYOP Custom PIN Example](examples/byop-custom-pin.md) + +## Next Steps + +- **Learn core concepts**: [Session Lifecycle](concepts/session-lifecycle.md) +- **Explore features**: [Complete documentation index](SKILL.md) +- **Handle errors**: [Error Codes Reference](troubleshooting/error-codes.md) +- **Production checklist**: [CORS and CSP Configuration](troubleshooting/cors-csp.md) + +## PIN Code Access - Bring Your Own PIN (BYOP) + +The Cobrowse SDK supports connecting agents and customers using a PIN code. In the simple example above, Zoom automatically generates a 6-digit PIN code displayed to the customer. + +**Auto-generated PIN flow:** +1. Customer clicks "Start Support Session" +2. Zoom generates 6-digit PIN +3. Customer shares PIN with agent +4. Agent enters PIN to connect + +**Custom PIN flow (BYOP):** +1. Your app generates custom PIN code (1-10 characters, letters/numbers) +2. Pass PIN when starting session: `session.start({ customPinCode: 'MYPIN', sdkToken })` +3. Agent enters your custom PIN to connect + +**BYOP enables**: +- Integration with existing support ticket systems +- Use of case/ticket IDs as PINs +- npm integration for custom agent UI + +**See**: [Bring Your Own PIN (BYOP)](examples/byop-custom-pin.md) for complete guide. + +## Resources + +- **Official Docs**: https://developers.zoom.us/docs/cobrowse-sdk/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/cobrowse/ +- **Quickstart Repo**: https://github.com/zoom/CobrowseSDK-Quickstart +- **Auth Endpoint Sample**: https://github.com/zoom/cobrowsesdk-auth-endpoint-sample +- **Dev Forum**: https://devforum.zoom.us/ + +## Common Questions + +**Q: Can I use HTTP instead of HTTPS?** +A: Only for loopback/local development. Production must use HTTPS. + +**Q: What's the difference between SDK Key and API Key?** +A: SDK Key is used in the CDN URL and JWT `app_key` claim. API Key is for optional REST API calls. + +**Q: Can multiple agents join the same session?** +A: Yes, up to 5 agents can join a single customer session. + +**Q: Does the customer need to install anything?** +A: No, it's pure JavaScript delivered via CDN. No plugins or extensions needed. + +**Q: What happens if the customer refreshes the page?** +A: The session will attempt to automatically reconnect within a 2-minute window. + +**Q: Can I customize the agent portal UI?** +A: Not with the iframe approach. For custom UI, use npm integration with BYOP mode. diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/references/api-official.md b/plugins/zoom-developers/skills/cobrowse-sdk/references/api-official.md new file mode 100644 index 00000000..09307df3 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/references/api-official.md @@ -0,0 +1,104 @@ +# Cobrowse SDK - API Reference + +SDK methods and events. + +## Initialization + +```javascript +const cobrowse = new ZoomCobrowse(config); +``` + +### Config Options + +| Option | Type | Description | +|--------|------|-------------| +| `sdkKey` | string | Your SDK Key | +| `token` | string | JWT token | +| `features.annotations` | boolean | Enable annotations | +| `masking.selectors` | array | CSS selectors to mask | +| `byop.enabled` | boolean | Use custom PINs | +| `byop.pin` | string | Custom PIN value | + +## Methods + +### startSession() + +Start a cobrowse session. + +```javascript +const session = await cobrowse.startSession(); +// Returns: { pin: string, sessionId: string } +``` + +### endSession() + +End the current session. + +```javascript +await cobrowse.endSession(); +``` + +### pause() + +Pause screen sharing. + +```javascript +cobrowse.pause(); +``` + +### resume() + +Resume screen sharing. + +```javascript +cobrowse.resume(); +``` + +## Events + +### sessionStarted + +```javascript +cobrowse.on('sessionStarted', (session) => { + // session.pin - PIN for agent to join + // session.sessionId - Unique session ID +}); +``` + +### agentJoined + +```javascript +cobrowse.on('agentJoined', (agent) => { + // agent.name - Agent display name + // agent.userId - Agent user ID +}); +``` + +### agentLeft + +```javascript +cobrowse.on('agentLeft', (agent) => { + // Agent disconnected +}); +``` + +### sessionEnded + +```javascript +cobrowse.on('sessionEnded', () => { + // Session terminated +}); +``` + +### error + +```javascript +cobrowse.on('error', (error) => { + // error.code - Error code + // error.message - Error description +}); +``` + +## Resources + +- **SDK Reference**: https://developers.zoom.us/docs/cobrowse-sdk/sdk-reference/ diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/references/api-reference.md b/plugins/zoom-developers/skills/cobrowse-sdk/references/api-reference.md new file mode 100644 index 00000000..2f96d3b3 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/references/api-reference.md @@ -0,0 +1,5 @@ +# API Reference + +This local reference points to the official Cobrowse API docs. + +- [API (official)](api-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/references/api.md b/plugins/zoom-developers/skills/cobrowse-sdk/references/api.md new file mode 100644 index 00000000..fcec5efe --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/references/api.md @@ -0,0 +1,5 @@ +# API (Reference) + +Canonical source: + +- [API (official)](api-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/references/authorization-official.md b/plugins/zoom-developers/skills/cobrowse-sdk/references/authorization-official.md new file mode 100644 index 00000000..722b8589 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/references/authorization-official.md @@ -0,0 +1,90 @@ +# Cobrowse SDK - Authorization + +JWT authentication for Cobrowse sessions. + +## Overview + +Both customers and agents require JWTs for authentication. Generate tokens server-side. + +## JWT Structure + +### Header + +```json +{ + "alg": "HS256", + "typ": "JWT" +} +``` + +### Payload + +| Claim | Type | Description | +|-------|------|-------------| +| `user_id` | string | Unique user identifier | +| `app_key` | string | Your SDK Key | +| `role_type` | number | 1 = customer, 2 = agent | +| `user_name` | string | Display name | +| `iat` | number | Issued at timestamp | +| `exp` | number | Expiration timestamp | + +### Strict Claim Names (Important) + +Cobrowse token validation is strict. Use these claim names exactly: + +- `user_id` (not `user_identity`) +- `app_key` +- `role_type` +- `user_name` +- `iat` +- `exp` + +Avoid adding unrecognized custom claims unless Zoom docs explicitly support them for your SDK version. +If you see `Invalid token` (code `124`), validate claim names first. + +## Role Types + +| Role | Value | Description | +|------|-------|-------------| +| Customer | 1 | User sharing their browser | +| Agent | 2 | Support staff viewing session | + +## Customer Token Example + +```javascript +const customerPayload = { + user_id: "customer_123", + app_key: "YOUR_SDK_KEY", + role_type: 1, + user_name: "John Customer", + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 3600 +}; + +const token = jwt.sign(customerPayload, SDK_SECRET, { algorithm: 'HS256' }); +``` + +## Agent Token Example + +```javascript +const agentPayload = { + user_id: "agent_456", + app_key: "YOUR_SDK_KEY", + role_type: 2, + user_name: "Support Agent", + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 3600 +}; + +const token = jwt.sign(agentPayload, SDK_SECRET, { algorithm: 'HS256' }); +``` + +## Security + +- Generate tokens server-side only +- Never expose SDK Secret in client code +- Use reasonable expiration times + +## Resources + +- **Auth docs**: https://developers.zoom.us/docs/cobrowse-sdk/authorize/ diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/references/authorization.md b/plugins/zoom-developers/skills/cobrowse-sdk/references/authorization.md new file mode 100644 index 00000000..8e0f6864 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/references/authorization.md @@ -0,0 +1,5 @@ +# Authorization (Reference) + +Canonical source: + +- [Authorization (official)](authorization-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/references/environment-variables.md b/plugins/zoom-developers/skills/cobrowse-sdk/references/environment-variables.md new file mode 100644 index 00000000..d9c3b5bb --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/references/environment-variables.md @@ -0,0 +1,20 @@ +# Zoom Cobrowse SDK Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_SDK_KEY` | Yes | SDK identity | Zoom Marketplace -> Cobrowse/Contact Center SDK app -> App Credentials | +| `ZOOM_SDK_SECRET` | Yes | SDK auth signing | Zoom Marketplace -> Cobrowse/Contact Center SDK app -> App Credentials | +| `COBROWSE_BASE_URL` | Optional | Regional/tenant SDK endpoint override | Cobrowse SDK documentation or tenant provisioning details | + +## Runtime-only values + +- `ZOOM_COBROWSE_SESSION_TOKEN` + +If your implementation mints short-lived tokens, store them in memory/cache only. + +## Notes + +- Keep `ZOOM_SDK_SECRET` server-side. +- Some samples use aliases (`SDK_KEY`, `SDK_SECRET`); normalize internally. diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/references/error-codes.md b/plugins/zoom-developers/skills/cobrowse-sdk/references/error-codes.md new file mode 100644 index 00000000..8574b439 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/references/error-codes.md @@ -0,0 +1,6 @@ +# Error Codes + +Use the official Cobrowse docs and API behavior notes for code-level troubleshooting. + +- [Get Started (official)](get-started-official.md) +- [API (official)](api-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/references/features-official.md b/plugins/zoom-developers/skills/cobrowse-sdk/references/features-official.md new file mode 100644 index 00000000..96574f51 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/references/features-official.md @@ -0,0 +1,111 @@ +# Cobrowse SDK - Features + +Annotations, masking, and advanced features. + +## Overview + +Cobrowse SDK includes features for privacy, collaboration, and customization. + +## Annotations + +Agents can draw and highlight on the shared screen. + +### Enable Annotations + +```javascript +const cobrowse = new ZoomCobrowse({ + sdkKey: SDK_KEY, + token: token, + features: { + annotations: true + } +}); +``` + +### Annotation Tools + +| Tool | Description | +|------|-------------| +| Pointer | Highlight cursor position | +| Draw | Freehand drawing | +| Highlight | Transparent highlight | +| Arrow | Point to elements | + +## Privacy Masking + +Hide sensitive information from agents. + +### Mask Elements + +```html + + + +
Sensitive content
+``` + +### Mask by CSS Selector + +```javascript +const cobrowse = new ZoomCobrowse({ + sdkKey: SDK_KEY, + token: token, + masking: { + selectors: [ + '.sensitive-data', + '#credit-card-field', + '[data-private]' + ] + } +}); +``` + +## Bring Your Own PIN (BYOP) + +Use your own PIN system instead of Zoom-generated PINs. + +```javascript +const cobrowse = new ZoomCobrowse({ + sdkKey: SDK_KEY, + token: token, + byop: { + enabled: true, + pin: 'YOUR_CUSTOM_PIN' + } +}); +``` + +## Session Control + +### End Session + +```javascript +cobrowse.endSession(); +``` + +### Pause/Resume + +```javascript +cobrowse.pause(); +cobrowse.resume(); +``` + +### Events + +```javascript +cobrowse.on('sessionStarted', (session) => { + console.log('Session started:', session.pin); +}); + +cobrowse.on('agentJoined', (agent) => { + console.log('Agent joined:', agent.name); +}); + +cobrowse.on('sessionEnded', () => { + console.log('Session ended'); +}); +``` + +## Resources + +- **Features docs**: https://developers.zoom.us/docs/cobrowse-sdk/add-features/ diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/references/features.md b/plugins/zoom-developers/skills/cobrowse-sdk/references/features.md new file mode 100644 index 00000000..7c9725fa --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/references/features.md @@ -0,0 +1,5 @@ +# Features (Reference) + +Canonical source: + +- [Features (official)](features-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/references/full-guide.md b/plugins/zoom-developers/skills/cobrowse-sdk/references/full-guide.md new file mode 100644 index 00000000..2ca6be4b --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/references/full-guide.md @@ -0,0 +1,881 @@ +# Zoom Cobrowse SDK - Web Development + +Background reference for collaborative browsing on the web with Zoom Cobrowse SDK. Use this after the support workflow is clear and you need implementation detail. + +**Official Documentation**: https://developers.zoom.us/docs/cobrowse-sdk/ +**API Reference**: https://marketplacefront.zoom.us/sdk/cobrowse/ +**Quickstart Repository**: https://github.com/zoom/CobrowseSDK-Quickstart +**Auth Endpoint Sample**: https://github.com/zoom/cobrowsesdk-auth-endpoint-sample + +## Quick Links + +**New to Cobrowse SDK? Follow this path:** + +1. **[Get Started Guide](../get-started.md)** - Complete setup from credentials to first session +2. **[Session Lifecycle](../concepts/session-lifecycle.md)** - Understanding customer and agent flows +3. **[JWT Authentication](../concepts/jwt-authentication.md)** - Token generation and security +4. **[Customer Integration](../examples/customer-integration.md)** - Integrate SDK into your website +5. **[Agent Integration](../examples/agent-integration.md)** - Set up agent portal (iframe or npm) + +**Core Concepts:** +- **[Two Roles Pattern](../concepts/two-roles-pattern.md)** - Customer vs Agent architecture +- **[Session Lifecycle](../concepts/session-lifecycle.md)** - PIN generation, connection, reconnection +- **[JWT Authentication](../concepts/jwt-authentication.md)** - SDK Key vs API Key, role_type, claims +- **[Distribution Methods](../concepts/distribution-methods.md)** - CDN vs npm (BYOP) + +**Features:** +- **[Annotation Tools](../examples/annotations.md)** - Drawing, highlighting, pointer tools +- **[Privacy Masking](../examples/privacy-masking.md)** - Hide sensitive fields from agents +- **[Remote Assist](../examples/remote-assist.md)** - Agent can scroll customer's page +- **[Multi-Tab Persistence](../examples/multi-tab-persistence.md)** - Session continues across tabs +- **[BYOP Mode](../examples/byop-custom-pin.md)** - Bring Your Own PIN with npm integration + +**Troubleshooting:** +- **[Common Issues](../troubleshooting/common-issues.md)** - Quick diagnostics and solutions +- **[Error Codes](../troubleshooting/error-codes.md)** - Complete error reference +- **[CORS and CSP](../troubleshooting/cors-csp.md)** - Cross-origin and security policy configuration +- **[Browser Compatibility](../troubleshooting/browser-compatibility.md)** - Supported browsers and limitations +- **[5-Minute Runbook](../RUNBOOK.md)** - Fast preflight checks before deep debugging + +**Reference:** +- **[API Reference](../references/api-reference.md)** - Complete SDK methods and events +- **[Settings Reference](../references/settings-reference.md)** - All initialization settings +- **Integrated Index** - see the section below in this file + +## SDK Overview + +The Zoom Cobrowse SDK is a JavaScript library that provides: + +- **Real-Time Co-Browsing**: Agent sees customer's browser activity live +- **PIN-Based Sessions**: Secure 6-digit PIN for customer-to-agent connection +- **Annotation Tools**: Drawing, highlighting, vanishing pen, rectangle, color picker +- **Privacy Masking**: CSS selector-based masking of sensitive form fields +- **Remote Assist**: Agent can scroll customer's page (with consent) +- **Multi-Tab Persistence**: Session continues when customer opens new tabs +- **Auto-Reconnection**: Session recovers from page refresh (2-minute window) +- **Session Events**: Real-time events for session state changes +- **HTTPS Required**: Secure connections (HTTP only works on loopback/local development hosts) +- **No Plugins**: Pure JavaScript, no browser extensions needed + +## Two Roles Architecture + +Cobrowse has **two distinct roles**, each with different integration patterns: + +| Role | role_type | Integration | JWT Required | Purpose | +|------|-----------|-------------|--------------|---------| +| **Customer** | 1 | Website integration (CDN or npm) | Yes | User who shares their browser session | +| **Agent** | 2 | Iframe (CDN) or npm (BYOP only) | Yes | Support staff who views/assists customer | + +**Key Insight**: Customer and agent use **different integration methods** but the same JWT authentication pattern. + +## Read This First (Critical) + +For customer/agent demos, treat the PIN from customer SDK event `pincode_updated` as the only user-facing PIN. + +- Show one clearly labeled value in UI (for example, **Support PIN**). +- Use that same PIN for agent join. +- Do not expose provisional/debug PINs from backend pre-start records to users. + +If these rules are ignored, agent desk often fails with `Pincode is not found` / code `30308`. + +### Typical Production Flow (Most Common) + +This is the flow most teams implement first, and what users usually expect in demos: + +1. **Customer starts session first** (`role_type=1`) + - Backend creates/records session + - Backend returns customer JWT + - Customer SDK starts and receives a PIN +2. **Agent joins second** (`role_type=2`) + - Agent enters customer PIN + - Backend validates PIN and session state + - Backend returns agent JWT + - Agent opens Zoom-hosted desk iframe (or custom npm agent UI in BYOP) + +If a demo only has one generic "session" user, it is incomplete for real cobrowse operations. + +## Prerequisites + +### Platform Requirements + +- **Supported Browsers**: + - Chrome 80+ ✓ + - Firefox 78+ ✓ + - Safari 14+ ✓ + - Edge 80+ ✓ + - Internet Explorer ✗ (not supported) + +- **Network Requirements**: + - HTTPS required (HTTP works on loopback/local development hosts only) + - Allow cross-origin requests to `*.zoom.us` + - CSP headers must allow Zoom domains (see [CORS and CSP guide](../troubleshooting/cors-csp.md)) + +- **Third-Party Cookies**: + - Must enable third-party cookies for refresh reconnection + - Privacy mode may limit certain features + +### Zoom Account Requirements + +1. **Zoom Workplace Account** with SDK Universal Credit +2. **Video SDK App** created in Zoom Marketplace +3. **Cobrowse SDK Credentials** from the app's Cobrowse tab + +**Note**: Cobrowse SDK is a **feature of Video SDK** (not a separate product). + +### Credentials Overview + +You'll receive **4 credentials** from Zoom Marketplace → Video SDK App → Cobrowse tab: + +| Credential | Type | Used For | Exposure Safe? | +|------------|------|----------|----------------| +| **SDK Key** | Public | CDN URL, JWT `app_key` claim | ✓ Yes (client-side) | +| **SDK Secret** | Private | Sign JWTs | ✗ No (server-side only) | +| **API Key** | Private | REST API calls (optional) | ✗ No (server-side only) | +| **API Secret** | Private | REST API calls (optional) | ✗ No (server-side only) | + +**Critical**: SDK Key is **public** (embedded in CDN URL), but SDK Secret must **never** be exposed client-side. + +## Quick Start + +### Step 1: Get SDK Credentials + +1. Go to [Zoom Marketplace](https://marketplace.zoom.us/) +2. Open your **Video SDK App** (or create one) +3. Navigate to the **Cobrowse** tab +4. Copy your credentials: + - SDK Key + - SDK Secret + - API Key (optional) + - API Secret (optional) + +### Step 2: Set Up Token Server + +Deploy a server-side endpoint to generate JWTs. Use the official sample: + +```bash +git clone https://github.com/zoom/cobrowsesdk-auth-endpoint-sample.git +cd cobrowsesdk-auth-endpoint-sample +npm install + +# Create .env file +cat > .env << EOF +ZOOM_SDK_KEY=your_sdk_key_here +ZOOM_SDK_SECRET=your_sdk_secret_here +PORT=4000 +EOF + +npm start +``` + +**Token endpoint:** +```javascript +// POST https://YOUR_TOKEN_SERVICE_BASE_URL +{ + "role": 1, // 1 = customer, 2 = agent + "userId": "user123", + "userName": "John Doe" +} + +// Response +{ + "token": "eyJhbGciOiJIUzI1NiIs..." +} +``` + +### Step 3: Customer Side Integration (CDN) + +```html + + + + Customer - Cobrowse Demo + + + +

Customer Support

+ + + + + + + + + +``` + +### Step 4: Agent Side Integration (Iframe) + +```html + + + + Agent Portal + + +

Agent Portal

+ + + + + +``` + +### Step 5: Test the Integration + +1. Open **two separate browsers** (or incognito + normal) +2. **Customer browser**: Open customer page, click "Start Cobrowse Session" +3. **Customer browser**: Note the 6-digit PIN displayed +4. **Agent browser**: Open agent page, enter the PIN code +5. **Both browsers**: Session connects, agent can see customer's page +6. **Test features**: Annotations, data masking, remote assist + +## Key Features + +### 1. Annotation Tools + +Both customer and agent can draw on the shared screen: + +```javascript +const settings = { + allowAgentAnnotation: true, // Agent can draw + allowCustomerAnnotation: true // Customer can draw +}; +``` + +**Available tools**: +- Pen (persistent) +- Vanishing pen (disappears after 4 seconds) +- Rectangle +- Color picker +- Eraser +- Undo/Redo + +### 2. Privacy Masking + +Hide sensitive fields from agents using CSS selectors: + +```javascript +const settings = { + piiMask: { + maskType: "custom_input", // Mask specific fields + maskCssSelectors: ".pii-mask, #ssn", // CSS selectors + maskHTMLAttributes: "data-sensitive=true" // HTML attributes + } +}; +``` + +**Supported masking**: +- Text nodes ✓ +- Form inputs ✓ +- Select elements ✓ +- Images ✗ (not supported) +- Links ✗ (not supported) + +### 3. Remote Assist + +Agent can scroll the customer's page: + +```javascript +const settings = { + remoteAssist: { + enable: true, + enableCustomerConsent: true, // Customer must approve + remoteAssistTypes: ['scroll_page'], // Only scroll supported + requireStopConfirmation: false // Confirmation when stopping + } +}; +``` + +### 4. Multi-Tab Session Persistence + +Session continues when customer opens new tabs: + +```javascript +const settings = { + multiTabSessionPersistence: { + enable: true, + stateCookieKey: '$$ZCB_SESSION$$' // Cookie key (base64 encoded) + } +}; +``` + +## Session Lifecycle + +### Customer Flow + +1. **Load SDK** → CDN script loads `ZoomCobrowseSDK` +2. **Initialize** → `ZoomCobrowseSDK.init(settings, callback)` +3. **Fetch JWT** → Request token from your server (role_type=1) +4. **Start Session** → `session.start({ sdkToken })` +5. **PIN Generated** → `pincode_updated` event fires +6. **Share PIN** → Customer gives 6-digit PIN to agent +7. **Agent Joins** → `agent_joined` event fires +8. **Session Active** → Real-time synchronization begins +9. **End Session** → `session.end()` or agent leaves + +### Agent Flow + +1. **Fetch JWT** → Request token from your server (role_type=2) +2. **Load Iframe** → Point to Zoom agent portal with token +3. **Enter PIN** → Agent inputs customer's 6-digit PIN +4. **Connect** → `session_joined` event fires +5. **View Session** → Agent sees customer's browser +6. **Use Tools** → Annotations, remote assist, zoom +7. **Leave Session** → Click "Leave Cobrowse" button + +### Session Recovery (Auto-Reconnect) + +When customer refreshes the page: + +```javascript +ZoomCobrowseSDK.init(settings, function({ success, session, error }) { + if (success) { + const sessionInfo = session.getSessionInfo(); + + // Check if session is recoverable + if (sessionInfo.sessionStatus === 'session_recoverable') { + session.join(); // Auto-rejoin previous session + } else { + // Start new session + session.start({ sdkToken }); + } + } +}); +``` + +**Recovery window**: 2 minutes. After 2 minutes, session ends. + +## Critical Gotchas and Best Practices + +### ⚠️ CRITICAL: SDK Secret Must Stay Server-Side + +**Problem**: Developers often accidentally embed SDK Secret in frontend code. + +**Solution**: +- ✓ **SDK Key** → Safe to expose (embedded in CDN URL) +- ✗ **SDK Secret** → Never expose (use for JWT signing server-side) + +```javascript +// ❌ WRONG - Secret exposed in frontend +const jwt = signJWT(payload, 'YOUR_SDK_SECRET'); // Security risk! + +// ✅ CORRECT - Secret stays on server +const response = await fetch('/api/token', { + method: 'POST', + body: JSON.stringify({ role: 1, userId, userName }) +}); +const { token } = await response.json(); +``` + +### SDK Key vs API Key (Different Purposes!) + +| Credential | Used For | JWT Claim | +|------------|----------|-----------| +| **SDK Key** | CDN URL, JWT `app_key` | `app_key: "SDK_KEY"` | +| **API Key** | REST API calls (optional) | Not used in JWT | + +**Common mistake**: Using API Key instead of SDK Key in JWT `app_key` claim. + +### Session Limits + +| Limit | Value | What Happens | +|-------|-------|--------------| +| Customers per session | 1 | Error 1012: `SESSION_CUSTOMER_COUNT_LIMIT` | +| Agents per session | 5 | Error 1013: `SESSION_AGENT_COUNT_LIMIT` | +| Active sessions per browser | 1 | Error 1004: `SESSION_COUNT_LIMIT` | +| PIN code length | 10 chars max | Error 1008: `SESSION_PIN_INVALID_FORMAT` | + +### Session Timeout Behavior + +| Event | Timeout | What Happens | +|-------|---------|--------------| +| Agent waiting for customer | 3 minutes | Session ends automatically | +| Page refresh reconnection | 2 minutes | Session ends if not reconnected | +| Reconnection attempts | 2 times max | Session ends after 2 failed attempts | + +### HTTPS Requirement + +**Problem**: SDK doesn't load on HTTP sites. + +**Solution**: +- Production: Use HTTPS ✓ +- Development: Use a loopback host for local HTTP testing ✓ +- Development: Use a local HTTPS endpoint with a trusted/self-signed cert if required ✓ + +### Third-Party Cookies Required + +**Problem**: Refresh reconnection doesn't work. + +**Solution**: Enable third-party cookies in browser settings. + +**Affected scenarios**: +- Browser privacy mode +- Safari with "Prevent cross-site tracking" enabled +- Chrome with "Block third-party cookies" enabled + +### Distribution Method Confusion + +| Method | Use Case | Agent Integration | BYOP Required | +|--------|----------|-------------------|---------------| +| **CDN** | Most use cases | Zoom-hosted iframe | No (auto PIN) | +| **npm** | Custom agent UI, full control | Custom npm integration | Yes (required) | + +**Key Insight**: If you want **npm** integration, you **must** use BYOP (Bring Your Own PIN) mode. + +### Cross-Origin Iframe Handling + +**Problem**: Cobrowse doesn't work in cross-origin iframes. + +**Solution**: Inject SDK snippet into cross-origin iframes: + +```html + +``` + +**Same-origin iframes**: No extra setup needed. + +## Known Limitations + +### Synchronization Limits + +**Not synchronized**: +- HTML5 Canvas elements +- WebGL content +- Audio and Video elements +- Shadow DOM +- PDF rendered with Canvas +- Web Components + +**Partially synchronized**: +- Drop-down boxes (only selected result) +- Date pickers (only selected result) +- Color pickers (only selected result) + +### Rendering Limits + +- High-resolution images may be compressed +- Different screen sizes may cause CSS media query differences +- Cross-origin images may not render (CORS restrictions) +- Cross-origin fonts may not render (CORS restrictions) + +### Masking Limits + +**Supported**: +- Text nodes ✓ +- Form inputs ✓ +- Select elements ✓ + +**Not supported**: +- `` elements ✗ +- Links ✗ + +## Complete Documentation Library + +This skill includes comprehensive guides organized by category: + +### Core Concepts +- **[Two Roles Pattern](../concepts/two-roles-pattern.md)** - Customer vs Agent architecture +- **[Session Lifecycle](../concepts/session-lifecycle.md)** - Complete flow from start to end +- **[JWT Authentication](../concepts/jwt-authentication.md)** - Token structure and signing +- **[Distribution Methods](../concepts/distribution-methods.md)** - CDN vs npm (BYOP) + +### Examples +- **[Customer Integration](../examples/customer-integration.md)** - Complete customer-side setup +- **[Agent Integration](../examples/agent-integration.md)** - Iframe and npm agent setups +- **[Annotations](../examples/annotations.md)** - Drawing tools configuration +- **[Privacy Masking](../examples/privacy-masking.md)** - Field masking patterns +- **[Remote Assist](../examples/remote-assist.md)** - Agent page control +- **[Multi-Tab Persistence](../examples/multi-tab-persistence.md)** - Cross-tab sessions +- **[BYOP Custom PIN](../examples/byop-custom-pin.md)** - Custom PIN codes + +### References +- **[API Reference](../references/api-reference.md)** - Complete SDK methods and events +- **[Settings Reference](../references/settings-reference.md)** - All initialization settings +- **[Error Codes](../references/error-codes.md)** - Complete error reference +- **[Session Events](../references/session-events.md)** - All event types + +### Troubleshooting +- **[Common Issues](../troubleshooting/common-issues.md)** - Quick diagnostics +- **[Error Codes](../troubleshooting/error-codes.md)** - Error code reference +- **[CORS and CSP](../troubleshooting/cors-csp.md)** - Cross-origin configuration +- **[Browser Compatibility](../troubleshooting/browser-compatibility.md)** - Browser support + +## Resources + +- **Official Docs**: https://developers.zoom.us/docs/cobrowse-sdk/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/cobrowse/ +- **Quickstart Repo**: https://github.com/zoom/CobrowseSDK-Quickstart +- **Auth Endpoint Sample**: https://github.com/zoom/cobrowsesdk-auth-endpoint-sample +- **Dev Forum**: https://devforum.zoom.us/ +- **Developer Blog**: https://developers.zoom.us/blog/?category=zoom-cobrowse-sdk + +--- + +**Need help?** Start with Integrated Index section below for complete navigation. + +--- + +## Integrated Index + +_This section was migrated from `SKILL.md`._ + +**Complete navigation guide for all Cobrowse SDK documentation.** + +## Getting Started (Start Here!) + +If you're new to Zoom Cobrowse SDK, follow this learning path: + +1. **[SKILL.md](../SKILL.md)** - Main overview and quick start +2. **[5-Minute Runbook](../RUNBOOK.md)** - Preflight checks for common failures +3. **[Get Started Guide](../get-started.md)** - Step-by-step setup from credentials to first session +4. **[Session Lifecycle](../concepts/session-lifecycle.md)** - Understand the complete customer and agent flow +5. **[Customer Integration](../examples/customer-integration.md)** - Integrate SDK into your website +6. **[Agent Integration](../examples/agent-integration.md)** - Set up agent portal + +## Core Concepts + +Foundational concepts you need to understand: + +- **[Two Roles Pattern](../concepts/two-roles-pattern.md)** - Customer (role_type=1) vs Agent (role_type=2) architecture +- **[Session Lifecycle](../concepts/session-lifecycle.md)** - Complete flow: init → start → PIN → connect → end +- **[JWT Authentication](../concepts/jwt-authentication.md)** - Token structure, signing, SDK Key vs API Key +- **[Distribution Methods](../concepts/distribution-methods.md)** - CDN vs npm (BYOP mode) + +## Examples and Patterns + +Complete working examples for common scenarios: + +### Session Management +- **[Customer Integration](../examples/customer-integration.md)** - Complete customer-side implementation (CDN and npm) +- **[Agent Integration](../examples/agent-integration.md)** - Iframe and npm agent setup patterns +- **[Session Events](../examples/session-events.md)** - Handle all session lifecycle events +- **[Auto-Reconnection](../examples/auto-reconnection.md)** - Page refresh and session recovery + +### Features +- **[Annotation Tools](../examples/annotations.md)** - Enable drawing, highlighting, vanishing pen +- **[Privacy Masking](../examples/privacy-masking.md)** - Mask sensitive fields with CSS selectors +- **[Remote Assist](../examples/remote-assist.md)** - Agent can scroll customer's page +- **[Multi-Tab Persistence](../examples/multi-tab-persistence.md)** - Session continues across browser tabs +- **[BYOP Custom PIN](../examples/byop-custom-pin.md)** - Bring Your Own PIN with npm integration + +## References + +Complete API and configuration references: + +### SDK Reference +- **[API Reference](../references/api-reference.md)** - All SDK methods and interfaces + - ZoomCobrowseSDK.init() + - session.start() + - session.join() + - session.end() + - session.on() + - session.getSessionInfo() + +- **[Settings Reference](../references/settings-reference.md)** - All initialization settings + - allowAgentAnnotation + - allowCustomerAnnotation + - piiMask + - remoteAssist + - multiTabSessionPersistence + +- **[Session Events Reference](../references/session-events.md)** - All event types + - pincode_updated + - session_started + - session_ended + - agent_joined + - agent_left + - session_error + - session_reconnecting + - remote_assist_started + - remote_assist_stopped + +### Error Reference +- **[Error Codes](../references/error-codes.md)** - Complete error code reference + - 1001-1017: Session errors + - 2001: Token errors + - 9999: Service errors + +### Official Documentation +- **[Get Started](../references/get-started.md)** - Official get started documentation (crawled) +- **[Features](../references/features.md)** - Official features documentation (crawled) +- **[Authorization](../references/authorization.md)** - Official JWT authorization docs (crawled) +- **[API Documentation](../references/api.md)** - Crawled API reference docs + +## Troubleshooting + +Quick diagnostics and common issue resolution: + +- **[Common Issues](../troubleshooting/common-issues.md)** - Quick fixes for frequent problems + - SDK not loading + - Token generation fails + - Agent can't connect + - Fields not masked + - Session doesn't reconnect after refresh + +- **[Error Codes](../troubleshooting/error-codes.md)** - Error code lookup and solutions + - Session start/join failures (1001, 1011, 1016) + - Session limit errors (1002, 1004, 1012, 1013, 1015) + - PIN code errors (1006, 1008, 1009, 1010) + - Token errors (2001) + +- **[CORS and CSP](../troubleshooting/cors-csp.md)** - Cross-origin and Content Security Policy setup + - Access-Control-Allow-Origin headers + - Content-Security-Policy headers + - Cross-origin iframe handling + - Same-origin iframe handling + +- **[Browser Compatibility](../troubleshooting/browser-compatibility.md)** - Browser requirements and limitations + - Supported browsers (Chrome 80+, Firefox 78+, Safari 14+, Edge 80+) + - Internet Explorer not supported + - Privacy mode limitations + - Third-party cookie requirements + +## By Use Case + +Find documentation by what you're trying to do: + +### I want to... + +**Set up cobrowse for the first time:** +- [Get Started Guide](../get-started.md) +- [JWT Authentication](../concepts/jwt-authentication.md) +- [Customer Integration](../examples/customer-integration.md) +- [Agent Integration](../examples/agent-integration.md) + +**Add annotation tools:** +- [Annotation Tools Example](../examples/annotations.md) +- [Settings Reference - allowAgentAnnotation](../references/settings-reference.md) +- [Settings Reference - allowCustomerAnnotation](../references/settings-reference.md) + +**Hide sensitive data from agents:** +- [Privacy Masking Example](../examples/privacy-masking.md) +- [Settings Reference - piiMask](../references/settings-reference.md) + +**Let agents control customer's page:** +- [Remote Assist Example](../examples/remote-assist.md) +- [Settings Reference - remoteAssist](../references/settings-reference.md) + +**Use custom PIN codes:** +- [BYOP Custom PIN Example](../examples/byop-custom-pin.md) +- [JWT Authentication - enable_byop](../concepts/jwt-authentication.md) + +**Handle page refreshes:** +- [Auto-Reconnection Example](../examples/auto-reconnection.md) +- [Session Lifecycle - Recovery](../concepts/session-lifecycle.md) + +**Integrate with npm (not CDN):** +- [BYOP Custom PIN Example](../examples/byop-custom-pin.md) +- [Distribution Methods](../concepts/distribution-methods.md) + +**Debug session connection issues:** +- [Common Issues](../troubleshooting/common-issues.md) +- [Error Codes](../troubleshooting/error-codes.md) +- [Session Events - session_error](../examples/session-events.md) + +**Configure CORS and CSP headers:** +- [CORS and CSP Guide](../troubleshooting/cors-csp.md) +- [Browser Compatibility](../troubleshooting/browser-compatibility.md) + +## By Error Code + +Quick lookup for error code solutions: + +### Session Errors +- **1001** (SESSION_START_FAILED) → [Error Codes](../troubleshooting/error-codes.md) +- **1002** (SESSION_CONNECTING_IN_PROGRESS) → [Error Codes](../troubleshooting/error-codes.md) +- **1004** (SESSION_COUNT_LIMIT) → [Error Codes](../troubleshooting/error-codes.md) +- **1011** (SESSION_JOIN_FAILED) → [Error Codes](../troubleshooting/error-codes.md) +- **1012** (SESSION_CUSTOMER_COUNT_LIMIT) → [Error Codes](../troubleshooting/error-codes.md) +- **1013** (SESSION_AGENT_COUNT_LIMIT) → [Error Codes](../troubleshooting/error-codes.md) +- **1015** (SESSION_DUPLICATE_USER) → [Error Codes](../troubleshooting/error-codes.md) +- **1016** (NETWORK_ERROR) → [Error Codes](../troubleshooting/error-codes.md) +- **1017** (SESSION_CANCELING_IN_PROGRESS) → [Error Codes](../troubleshooting/error-codes.md) + +### PIN Errors +- **1006** (SESSION_JOIN_PIN_NOT_FOUND) → [Error Codes](../troubleshooting/error-codes.md) +- **1008** (SESSION_PIN_INVALID_FORMAT) → [Error Codes](../troubleshooting/error-codes.md) +- **1009** (SESSION_START_PIN_REQUIRED) → [Error Codes](../troubleshooting/error-codes.md) +- **1010** (SESSION_START_PIN_CONFLICT) → [Error Codes](../troubleshooting/error-codes.md) + +### Auth Errors +- **2001** (TOKEN_INVALID) → [Error Codes](../troubleshooting/error-codes.md) + +### Service Errors +- **9999** (UNDEFINED) → [Error Codes](../troubleshooting/error-codes.md) + +## Official Resources + +External documentation and samples: + +- **Official Docs**: https://developers.zoom.us/docs/cobrowse-sdk/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/cobrowse/ +- **Quickstart Repo**: https://github.com/zoom/CobrowseSDK-Quickstart +- **Auth Endpoint Sample**: https://github.com/zoom/cobrowsesdk-auth-endpoint-sample +- **Dev Forum**: https://devforum.zoom.us/ +- **Developer Blog**: https://developers.zoom.us/blog/?category=zoom-cobrowse-sdk + +## Documentation Structure + +``` +cobrowse-sdk/ +├── SKILL.md # Main skill entry point +├── SKILL.md # This file - complete navigation +├── get-started.md # Step-by-step setup guide +│ +├── concepts/ # Core concepts +│ ├── two-roles-pattern.md +│ ├── session-lifecycle.md +│ ├── jwt-authentication.md +│ └── distribution-methods.md +│ +├── examples/ # Working examples +│ ├── customer-integration.md +│ ├── agent-integration.md +│ ├── annotations.md +│ ├── privacy-masking.md +│ ├── remote-assist.md +│ ├── multi-tab-persistence.md +│ ├── byop-custom-pin.md +│ ├── session-events.md +│ └── auto-reconnection.md +│ +├── references/ # API and config references +│ ├── api-reference.md # SDK methods +│ ├── settings-reference.md # Init settings +│ ├── session-events.md # Event types +│ ├── error-codes.md # Error reference +│ ├── get-started.md # Official docs (crawled) +│ ├── features.md # Official docs (crawled) +│ ├── authorization.md # Official docs (crawled) +│ └── api.md # API docs (crawled) +│ +└── troubleshooting/ # Problem resolution + ├── common-issues.md + ├── error-codes.md + ├── cors-csp.md + └── browser-compatibility.md +``` + +## Search Tips + +**Find by keyword:** +- "annotation" → [Annotation Tools](../examples/annotations.md) +- "mask" or "privacy" → [Privacy Masking](../examples/privacy-masking.md) +- "PIN" or "custom PIN" → [BYOP Custom PIN](../examples/byop-custom-pin.md) +- "JWT" or "token" → [JWT Authentication](../concepts/jwt-authentication.md) +- "error" → [Error Codes](../troubleshooting/error-codes.md) +- "CORS" or "CSP" → [CORS and CSP](../troubleshooting/cors-csp.md) +- "iframe" → [Agent Integration](../examples/agent-integration.md) +- "npm" → [Distribution Methods](../concepts/distribution-methods.md), [BYOP](../examples/byop-custom-pin.md) +- "refresh" or "reconnect" → [Auto-Reconnection](../examples/auto-reconnection.md) +- "agent" → [Agent Integration](../examples/agent-integration.md), [Two Roles Pattern](../concepts/two-roles-pattern.md) +- "customer" → [Customer Integration](../examples/customer-integration.md), [Two Roles Pattern](../concepts/two-roles-pattern.md) + +--- + +**Not finding what you need?** Check the [Official Documentation](https://developers.zoom.us/docs/cobrowse-sdk/) or ask on the [Dev Forum](https://devforum.zoom.us/). + +## Environment Variables + +- See [references/environment-variables.md](../references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/references/get-started-official.md b/plugins/zoom-developers/skills/cobrowse-sdk/references/get-started-official.md new file mode 100644 index 00000000..34716243 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/references/get-started-official.md @@ -0,0 +1,91 @@ +# Cobrowse SDK - Get Started + +Set up collaborative browsing on your website. + +## Overview + +This guide walks through integrating the Cobrowse SDK for customer-initiated sessions. + +## Prerequisites + +1. SDK Universal Credit on your Zoom account +2. SDK Key and Secret +3. Token server for JWT generation + +## Step 1: Get SDK Credentials + +1. In Zoom Workplace, go to **Advanced** → **Zoom CPaaS** → **Manage** +2. Click **Build App** +3. Locate **SDK Key** and **SDK Secret** + +## Step 2: Set Up Token Server + +Generate JWTs server-side to protect your SDK Secret. + +```javascript +const jwt = require('jsonwebtoken'); + +function generateCobrowseToken(userId, userName, roleType) { + const iat = Math.floor(Date.now() / 1000); + const exp = iat + 3600; // 1 hour + + const payload = { + user_id: userId, + app_key: SDK_KEY, + role_type: roleType, // 1 = customer, 2 = agent + user_name: userName, + iat: iat, + exp: exp + }; + + return jwt.sign(payload, SDK_SECRET, { algorithm: 'HS256' }); +} +``` + +## Step 3: Integrate Customer SDK + +Add to your website: + +```html + + + + + +``` + +## Step 4: Set Up Agent View + +Agents join via iframe: + +```html + +``` + +## Next Steps + +- Configure [privacy masking](features.md#masking) +- Set up [annotations](features.md#annotations) +- Implement [Bring Your Own PIN](features.md#byop) + +## Resources + +- **Cobrowse docs**: https://developers.zoom.us/docs/cobrowse-sdk/get-started/ diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/references/get-started.md b/plugins/zoom-developers/skills/cobrowse-sdk/references/get-started.md new file mode 100644 index 00000000..db6d6831 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/references/get-started.md @@ -0,0 +1,5 @@ +# Get Started (Reference) + +Canonical source: + +- [Get Started (official)](get-started-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/references/session-events.md b/plugins/zoom-developers/skills/cobrowse-sdk/references/session-events.md new file mode 100644 index 00000000..a99f29c2 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/references/session-events.md @@ -0,0 +1,5 @@ +# Session Events Reference + +Event names and payload behavior are covered in official Cobrowse API documentation. + +- [API (official)](api-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/references/settings-reference.md b/plugins/zoom-developers/skills/cobrowse-sdk/references/settings-reference.md new file mode 100644 index 00000000..d3529aa6 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/references/settings-reference.md @@ -0,0 +1,6 @@ +# Settings Reference + +Initialization and runtime settings are documented in the official Cobrowse references. + +- [Features (official)](features-official.md) +- [API (official)](api-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/troubleshooting/browser-compatibility.md b/plugins/zoom-developers/skills/cobrowse-sdk/troubleshooting/browser-compatibility.md new file mode 100644 index 00000000..aa595c6a --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/troubleshooting/browser-compatibility.md @@ -0,0 +1,7 @@ +# Browser Compatibility + +Validate your supported browser matrix and test privacy/cookie constraints that may affect sessions. + +See: +- [Features (official)](../references/features-official.md) +- [Get Started](../get-started.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/cobrowse-sdk/troubleshooting/common-issues.md new file mode 100644 index 00000000..34554d70 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/troubleshooting/common-issues.md @@ -0,0 +1,58 @@ +# Common Issues + +Quick diagnostics for Zoom CoBrowse SDK issues. + +- Ensure SDK script/package is loaded. +- Verify role-specific JWT generation on server. +- Validate token expiry and clock skew. +- Confirm session PIN flow between customer and agent. + +## Docs Links / 404s + +**Symptom**: Official doc links you found are stale or return 404. + +**Fix**: +- Prefer the curated references under `references/` (these are meant to stay stable even if external URLs drift). +- If you need working code, start from official sample repos referenced by the skill, then adapt to your stack. + +## Confusing "Who Creates the Session?" + +**Symptom**: You built an "agent creates session" endpoint, but the customer flow seems to actually start the share / generate the PIN. + +**Fix**: +- Treat **customer start/share** as the action that creates the shareable context (PIN/session), then the **agent joins** using that PIN/session info. +- Keep your server responsibilities narrow: token minting, optional auditing, and routing; avoid inventing "session creation" semantics that the SDK already owns. + +## Two PIN Values (Most Common Integration Mistake) + +**Symptom**: UI shows one PIN from backend/session record and another PIN from SDK event, agent gets `Pin not found` or `Cobrowse code not found`. + +**Fix**: +- Treat `session.on("pincode_updated")` as the **authoritative support PIN** for agent entry. +- Display exactly one primary PIN in UI (label it clearly as "Support PIN"). +- Do not surface provisional/debug PINs to users. +- When opening agent page with `?pin=...`, prefer freshly generated links and avoid stale bookmarks. + +## Agent Desk Error `30308` (Pincode is not found) + +**Symptom**: Zoom-hosted agent desk shows: +- `Cobrowse code not found` +- error code `30308` + +**Fix**: +- Ensure customer session is active and not expired before agent joins. +- Use the latest PIN emitted by `pincode_updated`. +- If your app restarts or uses in-memory state, persist session/PIN mapping or avoid strict local PIN gating for desk launch. +- Have agent re-enter a fresh PIN from a newly started customer session. + +## Plain HTML / Express Integration Friction + +**Symptom**: Quickstarts assume Vite/modern build pipeline; your plain HTML/Express adaptation breaks. + +**Fix**: +- Load the SDK exactly as the official snippet expects (script order matters). +- Avoid bundler-only patterns in plain HTML (ESM imports, `import.meta`, etc.) unless you add a bundler. + +See: +- [Get Started](../get-started.md) +- [Get Started (official)](../references/get-started-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/troubleshooting/cors-csp.md b/plugins/zoom-developers/skills/cobrowse-sdk/troubleshooting/cors-csp.md new file mode 100644 index 00000000..ea375778 --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/troubleshooting/cors-csp.md @@ -0,0 +1,11 @@ +# CORS and CSP + +For browser integrations: + +- allow required Zoom domains in CSP, +- avoid blocking SDK/script origins, +- validate iframe embedding and cross-origin constraints. + +See: +- [Get Started](../get-started.md) +- [Get Started (official)](../references/get-started-official.md) diff --git a/plugins/zoom-developers/skills/cobrowse-sdk/troubleshooting/error-codes.md b/plugins/zoom-developers/skills/cobrowse-sdk/troubleshooting/error-codes.md new file mode 100644 index 00000000..7f7cb56a --- /dev/null +++ b/plugins/zoom-developers/skills/cobrowse-sdk/troubleshooting/error-codes.md @@ -0,0 +1,7 @@ +# Error Codes Troubleshooting + +Use official API guidance and startup diagnostics to map error behavior. + +See: +- [Error Codes Reference](../references/error-codes.md) +- [API (official)](../references/api-official.md) diff --git a/plugins/zoom-developers/skills/contact-center/RUNBOOK.md b/plugins/zoom-developers/skills/contact-center/RUNBOOK.md new file mode 100644 index 00000000..8041edf7 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/RUNBOOK.md @@ -0,0 +1,73 @@ +# Contact Center 5-Minute Preflight Runbook + +Use this before deep debugging. It catches the most common Zoom Contact Center integration failures quickly. + +## Skill Doc Standard Note + +- Skill entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is a navigation convention for larger skill docs. + +## 1) Confirm Integration Path + +- Contact Center app inside Zoom client: use Zoom Apps SDK APIs/events (`getEngagementContext`, `onEngagementStatusChange`, etc.). +- Website embed: use Contact Center web SDK/campaign script path. +- Native mobile app: use Android/iOS Contact Center SDK binaries and service lifecycle. + +Wrong path is the top source of confusion. + +## 2) Confirm Required Credentials + +- `entryId` for chat/video/ZVA channels. +- `apiKey` for scheduled callback and campaign/web-tag scenarios. +- If building a Contact Center app in Zoom client, validate app credentials and OAuth setup in Marketplace. + +## 3) Confirm Lifecycle Order + +Common native/mobile order: +1. Initialize SDK context early. +2. Get service instance. +3. Initialize service with `ZoomCCItem`. +4. Register listener/delegate. +5. `login()` where required (typically chat/ZVA). +6. `fetchUI()` to present the channel view. + +Web app path: +1. `zoomSdk.config(...)` +2. `getEngagementContext()` and `getEngagementStatus()` +3. subscribe to `onEngagementContextChange` and `onEngagementStatusChange` +4. persist state keyed by `engagementId` + +## 4) Confirm Context Switching Behavior + +- A single app instance can receive multiple engagement contexts. +- Persist draft/workflow state by `engagementId`. +- Do not assume only one active engagement for chat/SMS/email workflows. + +## 5) Confirm Cleanup Semantics + +- End action (`endChat`, `endVideo`, `endScheduledCallback`) is not the same as service release. +- Apply platform-specific cleanup (`logout`/`logoff`, release/uninitialize APIs). +- On iOS, forward app lifecycle callbacks (`appDidBecomeActive`, `appWillTerminate`, etc.) to `ZoomCCInterface`. + +## 6) Version + Drift Checks + +- Zoom enforces minimum SDK versions quarterly (first weekend of February, May, August, November). +- Re-check docs and changelog before release; naming and signatures can drift. +- Watch deprecations: + - iOS `onService:error:detail:` is deprecated in favor of `onService:error:detail:description:`. + +## 7) Quick Probes + +- App context/status APIs return valid values. +- Engagement events fire when agent switches engagements. +- Chat/video/scheduled callback can be started and ended once each without stale state. +- No CSP or domain allow-list blocks for web integrations. + +## 8) Fast Decision Tree + +- No engagement data in Contact Center app -> missing SDK `config` capabilities or wrong runtime context. +- Channel UI does not open -> invalid `entryId`/`apiKey`, missing init, or wrong service/channel mapping. +- Events not firing on switch/end -> listeners not attached early enough or removed incorrectly. +- Rejoin fails on mobile -> deep-link/scheme configuration mismatch. + diff --git a/plugins/zoom-developers/skills/contact-center/SKILL.md b/plugins/zoom-developers/skills/contact-center/SKILL.md new file mode 100644 index 00000000..ff6938f0 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/SKILL.md @@ -0,0 +1,25 @@ +--- +name: build-zoom-contact-center-app +description: Use when using Contact Center. +--- + +# Build Zoom Contact Center App + +Use this skill when the integration targets Zoom Contact Center rather than general meetings, chat, or phone. Route to the platform-specific skill once the client surface is clear. + +## Workflow + +1. Identify the channel and client: web, Android, iOS, campaign embed, video engagement, chat engagement, Virtual Agent handoff, or scheduled callback. +2. Confirm the lifecycle: initialization, engagement start, context retrieval, state changes, transfer or handoff, and cleanup. +3. Choose the platform reference before coding; Contact Center SDKs differ by event names, lifecycle hooks, and wrapper requirements. +4. Treat engagement context as shared state and validate how it flows into CRM, ticketing, analytics, or AI workflows. +5. Debug version drift by checking SDK version, documented event names, app settings, campaign configuration, and browser or mobile lifecycle behavior. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- Web SDK: [web/SKILL.md](web/SKILL.md) +- Android SDK: [android/SKILL.md](android/SKILL.md) +- iOS SDK: [ios/SKILL.md](ios/SKILL.md) +- High-level scenarios: [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +- Common drift and breaks: [troubleshooting/common-drift-and-breaks.md](troubleshooting/common-drift-and-breaks.md) diff --git a/plugins/zoom-developers/skills/contact-center/android/RUNBOOK.md b/plugins/zoom-developers/skills/contact-center/android/RUNBOOK.md new file mode 100644 index 00000000..8838dc11 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/android/RUNBOOK.md @@ -0,0 +1,64 @@ +# Contact Center 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 channel target and integration mode for Android. +- Contact Center app path and web embed path have different lifecycle rules. +- For mobile SDKs, verify native service lifecycle and listener registration order. + +## 2) Confirm Required Credentials + +- `entryId` for chat/video/ZVA entry points. +- `apiKey` for scheduled callback and campaign/tag use cases. +- If in-client app behavior is needed, verify Zoom App credentials and required scopes. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK context early. +2. Get channel service and register listeners/delegates before actions. +3. Authenticate/login where required. +4. Start/fetch channel UI and handle engagement status transitions. + +## 4) Confirm Event/State Handling + +- Track state by `engagementId`; do not assume single engagement forever. +- Handle context-switch events without losing draft/chat workflow state. +- Keep service/channel state isolated per active engagement. + +## 5) Confirm Cleanup + Upgrade Posture + +- End channel session and release service resources cleanly. +- Forward app lifecycle callbacks for iOS integrations. +- Re-check release notes for renamed/deprecated methods before upgrades. + +## 6) Quick Probes + +- Engagement context/status APIs return valid values. +- Start/end flow works once end-to-end for target channel. +- Listener callbacks fire on switch/end events without stale state. + +## 7) Fast Decision Tree + +- UI does not open -> invalid `entryId`/`apiKey` or missing init/listener sequence. +- Events missing -> listener registered too late or detached unexpectedly. +- Rejoin/resume fails -> lifecycle callbacks or deep-link/scheme config mismatch. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/contact-center/android/ +- https://marketplacefront.zoom.us/sdk/contact/android/index.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/contact-center/android/` +- `raw-docs/marketplacefront.zoom.us/sdk/contact/android/` diff --git a/plugins/zoom-developers/skills/contact-center/android/SKILL.md b/plugins/zoom-developers/skills/contact-center/android/SKILL.md new file mode 100644 index 00000000..1f05cda0 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/android/SKILL.md @@ -0,0 +1,44 @@ +--- +name: zoom-contact-center-android +description: "Zoom Contact Center SDK for Android. Use for native Android chat/video/ZVA/scheduled callback integrations, campaign mode, service lifecycle, and rejoin handling." +--- + +# Zoom Contact Center SDK - Android + +Official docs: +- https://developers.zoom.us/docs/contact-center/android/ +- https://marketplacefront.zoom.us/sdk/contact/android/index.html + +## Quick Links + +1. [concepts/sdk-lifecycle.md](concepts/sdk-lifecycle.md) +2. [examples/service-patterns.md](examples/service-patterns.md) +3. [references/android-reference-map.md](references/android-reference-map.md) +4. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## SDK Surface Summary + +- SDK manager: `ZoomCCInterface` +- Channel services: +- `getZoomCCChatService()` +- `getZoomCCVideoService()` +- `getZoomCCZVAService()` +- `getZoomCCScheduledCallbackService()` +- Campaign support via web campaign service and campaign metadata. + +## Hard Guardrails + +- Initialize SDK in `Application.onCreate`. +- Use `ZoomCCItem` to define channel + identifiers. +- Use `entryId` for chat/video/ZVA. +- Use `apiKey` for scheduled callback and campaign mode. +- Release services on teardown. + +## Common Chains + +- Contact Center app and engagement context: [../../zoom-apps-sdk/SKILL.md](../../zoom-apps-sdk/SKILL.md) +- Contact Center API automation: [../../rest-api/SKILL.md](../../rest-api/SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/contact-center/android/concepts/sdk-lifecycle.md b/plugins/zoom-developers/skills/contact-center/android/concepts/sdk-lifecycle.md new file mode 100644 index 00000000..c8c04b03 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/android/concepts/sdk-lifecycle.md @@ -0,0 +1,42 @@ +# Android SDK Lifecycle + +## Startup + +1. Initialize once in `Application.onCreate`. +2. Optionally set/update context user name before channel launch. + +## Channel Initialization + +1. Get service from `ZoomCCInterface`. +2. Build `ZoomCCItem` with: +- `sdkType` +- `entryId` or `apiKey` +- `serverType` +- campaign fields when needed +3. `service.init(item)`. +4. Add listener(s). + +## Launch + +1. Chat/ZVA: +- call `login()` then `fetchUI()`. +2. Video: +- configure preview/auto-join options as needed. +- call `fetchUI()`; login is typically internal for video flow. +3. Scheduled callback: +- init with `apiKey`. +- `fetchUI()`. + +## End and Cleanup + +1. End engagement (`endChat` / `endVideo`) when needed. +2. `logoff()` when you need to stop callbacks. +3. `releaseZoomCCService(key)` in teardown paths (`onDestroy`). + +## Campaign Mode + +1. Request campaigns with campaign API key. +2. Select channel from campaign metadata. +3. Reinitialize service using campaign-mode item. +4. Release or end conflicting channel services before switch. + diff --git a/plugins/zoom-developers/skills/contact-center/android/examples/service-patterns.md b/plugins/zoom-developers/skills/contact-center/android/examples/service-patterns.md new file mode 100644 index 00000000..4d6d3f9d --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/android/examples/service-patterns.md @@ -0,0 +1,68 @@ +# Android Service Patterns + +## Chat Pattern + +```kotlin +val service = ZoomCCInterface.getZoomCCChatService() +service.init( + ZoomCCItem( + entryId = chatEntryId, + sdkType = ZoomCCIInterfaceType.CHAT, + serverType = CCServerType.CCServerWWW + ) +) +service.addListener(object : ZoomCCChatListener { + override fun unreadMsgCountChanged(count: Int) {} + override fun onClientEvent(event: ClientEvent) {} + override fun onEngagementEnd(engagementId: String) {} + override fun onEngagementStart(engagementId: String) {} + override fun onLoginStatus(status: IMStatus?) {} + override fun onError(error: Int, detail: Long, description: String) {} +}) +service.login() +service.fetchUI() +``` + +## Video Pattern + +```kotlin +val service = ZoomCCInterface.getZoomCCVideoService() +service.init( + ZoomCCItem( + entryId = videoEntryId, + sdkType = ZoomCCIInterfaceType.VIDEO, + serverType = CCServerType.CCServerWWW + ) +) +service.setVideoPreviewOption(VideoPreviewOption.ZmCCVideoPreviewOptionDefault) +service.setAutoJoinWhenVideoCreated(false) +service.setUseBackwardFacingCameraByDefault(false) +service.addListener(object : ZoomCCVideoListener {}) +service.fetchUI() +``` + +## Scheduled Callback Pattern + +```kotlin +val service = ZoomCCInterface.getZoomCCScheduledCallbackService() +service.init( + ZoomCCItem( + apiKey = callbackApiKey, + sdkType = ZoomCCIInterfaceType.SCHEDULED_CALLBACK, + serverType = CCServerType.CCServerWWW + ) +) +service.fetchUI() +``` + +## Cleanup Pattern + +```kotlin +override fun onDestroy() { + ZoomCCInterface.releaseZoomCCService(chatEntryId) + ZoomCCInterface.releaseZoomCCService(videoEntryId) + ZoomCCInterface.releaseZoomCCService(callbackApiKey) + super.onDestroy() +} +``` + diff --git a/plugins/zoom-developers/skills/contact-center/android/references/android-reference-map.md b/plugins/zoom-developers/skills/contact-center/android/references/android-reference-map.md new file mode 100644 index 00000000..444092dd --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/android/references/android-reference-map.md @@ -0,0 +1,51 @@ +# Android Reference Map + +Primary reference: +- https://marketplacefront.zoom.us/sdk/contact/android/index.html + +## Core Types + +- `ZoomCCInterface` +- `ZoomCCItem` +- `ZoomCCContext` +- `ZoomCCService` +- `ZoomCCChatService` +- `ZoomCCVideoService` +- `ZoomCCScheduledCallbackService` + +## Listener Types + +- `ZoomCCServiceListener` +- `ZoomCCChatListener` +- `ZoomCCVideoListener` + +## Enums + +- `ZoomCCIInterfaceType` +- `ClientEvent` +- `IMStatus` +- `CCServerType` +- `VideoPreviewOption` + +## Common Methods + +- SDK init/context: +- `ZoomCCInterface.init(...)` +- `ZoomCCInterface.setContext(...)` +- service factory: +- `getZoomCCChatService()` +- `getZoomCCVideoService()` +- `getZoomCCZVAService()` +- `getZoomCCScheduledCallbackService()` +- service lifecycle: +- `init(item)`, `login()`, `logoff()`, `fetchUI()` +- engagement control: +- `endChat()`, `endVideo()` +- release: +- `releaseZoomCCService(key)` + +## Deprecation Notes + +- Review `deprecated.html` in each SDK version package. +- Keep runtime guards for enum/value additions and optional callbacks. + diff --git a/plugins/zoom-developers/skills/contact-center/android/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/contact-center/android/troubleshooting/common-issues.md new file mode 100644 index 00000000..7de43da8 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/android/troubleshooting/common-issues.md @@ -0,0 +1,44 @@ +# Android Common Issues + +## SDK Works Inconsistently Across Screens + +Cause: +- SDK initialized too late. + +Fix: +- Initialize in `Application.onCreate`. + +## `NoClassDefFoundError` / viewBinding Errors + +Cause: +- Missing expected dependencies or view binding configuration. + +Fix: +- Match SDK package module requirements. +- Ensure build config aligns with current SDK release notes. + +## Video/Chat UI Does Not Open + +Cause: +- Wrong identifier type in `ZoomCCItem`. + +Fix: +- `entryId` for chat/video/ZVA. +- `apiKey` for scheduled callback/campaign. + +## Events Not Firing + +Cause: +- Listener attached after service launch or removed early. + +Fix: +- Add listeners before `fetchUI`. + +## Rejoin Link Opens Browser But Not App + +Cause: +- Deep-link host/scheme mismatch. + +Fix: +- Align Android manifest intent filters with generated rejoin URL format. + diff --git a/plugins/zoom-developers/skills/contact-center/concepts/architecture-and-lifecycle.md b/plugins/zoom-developers/skills/contact-center/concepts/architecture-and-lifecycle.md new file mode 100644 index 00000000..74f71c46 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/concepts/architecture-and-lifecycle.md @@ -0,0 +1,63 @@ +# Contact Center Architecture and Lifecycle + +This document defines a stable architecture pattern that works across Contact Center app, web, and mobile integrations. + +## Architecture Layers + +1. Integration Surface +- Zoom Contact Center App (Zoom client embedded webview). +- Web SDK/Campaign SDK on external sites. +- Android/iOS native SDK. + +2. Engagement State Layer +- Current `engagementId`. +- Engagement status (`start`, `hold`, `resume`, `end`). +- Engagement-scoped draft data. + +3. Channel Service Layer +- Chat. +- Video. +- ZVA. +- Scheduled Callback. + +4. Persistence Layer +- Transient per-engagement state cache (frontend local storage or backend session store). +- Optional backend persistence for long-running workflows and compliance logging. + +## Canonical Lifecycle + +1. Initialize context. +2. Determine active engagement context. +3. Build/init channel service/client. +4. Register callbacks before launching UI. +5. Start channel view. +6. Process status/context events. +7. End and cleanup. + +## Context-Switching Contract + +- Treat `engagementId` as the primary state key. +- Never assume a single engagement in memory for messaging channels. +- Restore state on each engagement context change. +- Clear or archive engagement state only when end-state logic is complete. + +## Event-Driven Contract + +- Do not poll as a primary strategy. +- Subscribe early and keep handlers idempotent. +- Handle out-of-order or repeated events safely. + +## Campaign Mode Pattern + +1. Fetch campaigns with campaign API key. +2. Pick channel from `translatedCampaignChannels`. +3. Create channel item with `useCampaignMode=true`. +4. Launch service UI. +5. Release conflicting channel services when switching channels. + +## Security and Identity + +- Use explicit user/session identity refresh paths (`authorize`, `getAppContext`) for Contact Center app scenarios. +- For PWA flows, do not depend on `x-zoom-app-context` header. +- Keep OAuth and app context decryption on backend where possible. + diff --git a/plugins/zoom-developers/skills/contact-center/ios/RUNBOOK.md b/plugins/zoom-developers/skills/contact-center/ios/RUNBOOK.md new file mode 100644 index 00000000..20fadf2e --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/ios/RUNBOOK.md @@ -0,0 +1,64 @@ +# Contact Center 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 channel target and integration mode for iOS. +- Contact Center app path and web embed path have different lifecycle rules. +- For mobile SDKs, verify native service lifecycle and listener registration order. + +## 2) Confirm Required Credentials + +- `entryId` for chat/video/ZVA entry points. +- `apiKey` for scheduled callback and campaign/tag use cases. +- If in-client app behavior is needed, verify Zoom App credentials and required scopes. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK context early. +2. Get channel service and register listeners/delegates before actions. +3. Authenticate/login where required. +4. Start/fetch channel UI and handle engagement status transitions. + +## 4) Confirm Event/State Handling + +- Track state by `engagementId`; do not assume single engagement forever. +- Handle context-switch events without losing draft/chat workflow state. +- Keep service/channel state isolated per active engagement. + +## 5) Confirm Cleanup + Upgrade Posture + +- End channel session and release service resources cleanly. +- Forward app lifecycle callbacks for iOS integrations. +- Re-check release notes for renamed/deprecated methods before upgrades. + +## 6) Quick Probes + +- Engagement context/status APIs return valid values. +- Start/end flow works once end-to-end for target channel. +- Listener callbacks fire on switch/end events without stale state. + +## 7) Fast Decision Tree + +- UI does not open -> invalid `entryId`/`apiKey` or missing init/listener sequence. +- Events missing -> listener registered too late or detached unexpectedly. +- Rejoin/resume fails -> lifecycle callbacks or deep-link/scheme config mismatch. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/contact-center/ios/ +- https://marketplacefront.zoom.us/sdk/contact/ios/index.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/contact-center/ios/` +- `raw-docs/marketplacefront.zoom.us/sdk/contact/ios/` diff --git a/plugins/zoom-developers/skills/contact-center/ios/SKILL.md b/plugins/zoom-developers/skills/contact-center/ios/SKILL.md new file mode 100644 index 00000000..00637c2b --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/ios/SKILL.md @@ -0,0 +1,44 @@ +--- +name: zoom-contact-center-ios +description: "Zoom Contact Center SDK for iOS. Use for native iOS chat/video/ZVA/scheduled callback integrations, app lifecycle bridging, rejoin flow, and callback handling." +--- + +# Zoom Contact Center SDK - iOS + +Official docs: +- https://developers.zoom.us/docs/contact-center/ios/ +- https://marketplacefront.zoom.us/sdk/contact/ios/index.html + +## Quick Links + +1. [concepts/sdk-lifecycle.md](concepts/sdk-lifecycle.md) +2. [examples/service-patterns.md](examples/service-patterns.md) +3. [references/ios-reference-map.md](references/ios-reference-map.md) +4. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## SDK Surface Summary + +- Manager: `ZoomCCInterface.sharedInstance()` +- Context: `ZoomCCContext` +- Items: `ZoomCCItem` +- Services: +- `chatService` +- `zvaService` +- `videoService` +- `scheduledCallbackService` + +## Hard Guardrails + +- Set `ZoomCCContext` before channel operations. +- Forward app lifecycle calls (`appDidBecomeActive`, `appDidEnterBackgroud`, `appWillResignActive`, `appWillTerminate`). +- Use item-based initialization for channels. +- Keep rejoin URL handling connected to the video service path. + +## Common Chains + +- Contact Center apps in Zoom client: [../../zoom-apps-sdk/SKILL.md](../../zoom-apps-sdk/SKILL.md) +- OAuth and identity: [../../oauth/SKILL.md](../../oauth/SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/contact-center/ios/concepts/sdk-lifecycle.md b/plugins/zoom-developers/skills/contact-center/ios/concepts/sdk-lifecycle.md new file mode 100644 index 00000000..ca867fa0 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/ios/concepts/sdk-lifecycle.md @@ -0,0 +1,46 @@ +# iOS SDK Lifecycle + +## Context Initialization + +1. Create `ZoomCCContext`. +2. Configure user name, cache folder, and optional share settings. +3. Set context on `ZoomCCInterface.sharedInstance()`. + +## Service Initialization Pattern + +1. Build `ZoomCCItem`. +2. Select channel type: +- `.chat` +- `.video` +- `.ZVA` +- `.scheduledCallback` +3. Populate `entryId` or `apiKey` depending on channel. +4. Get service instance. +5. Set delegate. +6. Call `initialize(with:)`. +7. Call `login()` where required. +8. `fetchUI` and push returned view controller. + +## Lifecycle Bridging + +Forward these app delegate callbacks: +- `applicationDidBecomeActive` -> `appDidBecomeActive` +- `applicationWillResignActive` -> `appWillResignActive` +- `applicationDidEnterBackground` -> `appDidEnterBackgroud` +- `applicationWillTerminate` -> `appWillTerminate` + +## Rejoin Flow + +1. Configure app URL scheme and admin rejoin URL. +2. Forward `open url` callback to rejoin handler. +3. Call video service rejoin API with prepared `ZoomCCItem`. +4. Push returned view controller in completion block. + +## Cleanup + +- End service-specific engagement methods: +- `endChat` +- `endVideo` +- `endScheduledCallback` +- Use service `logout` / uninitialize patterns when needed by flow design. + diff --git a/plugins/zoom-developers/skills/contact-center/ios/examples/service-patterns.md b/plugins/zoom-developers/skills/contact-center/ios/examples/service-patterns.md new file mode 100644 index 00000000..af2cb950 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/ios/examples/service-patterns.md @@ -0,0 +1,72 @@ +# iOS Service Patterns + +## Context Setup + +```swift +let context = ZoomCCContext() +context.cacheFolder = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! +context.userName = userName +context.domainType = .US01 +ZoomCCInterface.sharedInstance().context = context +``` + +## Chat Pattern + +```swift +let item = ZoomCCItem() +item.sdkType = .chat +item.entryId = chatEntryId + +let chat = ZoomCCInterface.sharedInstance().chatService() +chat.chatDelegate = self +if chat.status == .initial { + chat.initialize(with: item) + chat.login() +} +chat.fetchUI { vc in + if let vc { self.navigationController?.pushViewController(vc, animated: true) } +} +``` + +## Video Pattern + +```swift +let item = ZoomCCItem() +item.sdkType = .video +item.entryId = videoEntryId + +let video = ZoomCCInterface.sharedInstance().videoService() +video.videoDelegate = self +if video.status == .initial { + video.initialize(with: item) +} +video.fetchUI { vc in + if let vc { self.navigationController?.pushViewController(vc, animated: true) } +} +``` + +## Scheduled Callback Pattern + +```swift +let item = ZoomCCItem() +item.sdkType = .scheduledCallback +item.apiKey = callbackApiKey + +let scheduled = ZoomCCInterface.sharedInstance().scheduledCallbackService() +scheduled.scheduledCallbackDelegate = self +if scheduled.status == .initial { + scheduled.initialize(with: item) +} +scheduled.fetchUI { vc in + if let vc { self.navigationController?.pushViewController(vc, animated: true) } +} +``` + +## Rejoin Pattern + +```swift +func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + return rootVC.handleRejoinVideoOpenURL(url) +} +``` + diff --git a/plugins/zoom-developers/skills/contact-center/ios/references/ios-reference-map.md b/plugins/zoom-developers/skills/contact-center/ios/references/ios-reference-map.md new file mode 100644 index 00000000..211abd99 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/ios/references/ios-reference-map.md @@ -0,0 +1,48 @@ +# iOS Reference Map + +Primary references: +- https://marketplacefront.zoom.us/sdk/contact/ios/index.html +- SDK headers packaged in iOS SDK zip (`ZoomCCInterface.h`) + +## Core Types + +- `ZoomCCInterface` +- `ZoomCCContext` +- `ZoomCCItem` +- `ZoomCCCampaignInfo` + +## Service Protocols + +- `ZoomCCService` +- `ZoomCCChatService` +- `ZoomCCVideoService` +- `ZoomCCScheduledCallbackService` + +## Delegate Protocols + +- `ZoomCCServiceDelegate` +- `ZoomCCChatServiceDelegate` +- `ZoomCCAppLifecyleDelegate` + +## Key Methods + +- Interface: +- `sharedInstance` +- `chatService` +- `zvaService` +- `videoService` +- `scheduledCallbackService` +- `getCampaigns` +- Service lifecycle: +- `initializeWithItem` +- `login` +- `logout` +- `fetchUI` +- Video: +- `handleRejoinVideoOpenURL:item:videoDelegate:complete:` + +## Deprecation Note + +- `onService:error:detail:` is deprecated. +- Use `onService:error:detail:description:`. + diff --git a/plugins/zoom-developers/skills/contact-center/ios/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/contact-center/ios/troubleshooting/common-issues.md new file mode 100644 index 00000000..d43d9721 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/ios/troubleshooting/common-issues.md @@ -0,0 +1,42 @@ +# iOS Common Issues + +## Service Starts But View Never Appears + +Cause: +- Missing `fetchUI` handling or wrong navigation presentation path. + +Fix: +- Ensure returned view controller is pushed/presented on main thread. + +## App Background/Foreground Breaks Session + +Cause: +- App lifecycle callbacks not forwarded to SDK. + +Fix: +- Wire app delegate lifecycle methods to `ZoomCCInterface`. + +## Rejoin URL Arrives But Rejoin Fails + +Cause: +- URL scheme mismatch or context not initialized. + +Fix: +- Verify URL types config, rejoin URL settings, and context setup before calling rejoin API. + +## Duplicate or Stale Channel Sessions + +Cause: +- Previous service instance left active during channel switches. + +Fix: +- End current engagement and rebuild service item when changing channel/campaign context. + +## Error Callback Signature Drift + +Cause: +- Implemented only deprecated callback signature. + +Fix: +- Implement `onService:error:detail:description:` and keep compatibility wrappers as needed. + diff --git a/plugins/zoom-developers/skills/contact-center/references/environment-variables.md b/plugins/zoom-developers/skills/contact-center/references/environment-variables.md new file mode 100644 index 00000000..1fc11af4 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/references/environment-variables.md @@ -0,0 +1,25 @@ +# Zoom Contact Center Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_CLIENT_ID` | Yes (API/OAuth integrations) | OAuth app identity for Contact Center APIs | Zoom Marketplace -> OAuth app -> App Credentials | +| `ZOOM_CLIENT_SECRET` | Yes (API/OAuth integrations) | OAuth token exchange | Zoom Marketplace -> OAuth app -> App Credentials | +| `ZOOM_REDIRECT_URI` | User OAuth flow | OAuth callback URL | Zoom Marketplace -> OAuth redirect/allow list | +| `ZCC_CHAT_ENTRY_ID` | Web/chat entry flows | Contact Center chat entry point routing | Contact Center Admin -> Flows -> Entry Points | +| `ZCC_VIDEO_ENTRY_ID` | Video engagement flows | Contact Center video entry point routing | Contact Center Admin -> Flows -> Entry Points | +| `ZCC_ZVA_ENTRY_ID` | Optional (Virtual Agent) | Virtual agent entry routing | Contact Center Admin -> Flows -> Entry Points | +| `ZCC_CAMPAIGN_API_KEY` | Campaign/web embed mode | Campaign authorization for web embed | Contact Center Admin -> Campaign Management -> Web and In-App -> Embed Web Tag | +| `ZCC_WEB_API_KEY` | Web SDK/embed mode | Client-side Contact Center embed initialization | Contact Center Admin -> Campaign Management -> Web and In-App -> Embed Web Tag | +| `ZCC_SCHEDULED_CALLBACK_API_KEY` | Scheduled callback flows | Callback scheduling authorization | Contact Center campaign/flow callback configuration | + +## Runtime-only values + +- `ZOOM_ACCESS_TOKEN` +- Contact/session IDs issued by Contact Center runtime APIs + +## Notes + +- Contact Center implementations often mix OAuth credentials with flow/campaign keys. +- Keep OAuth secrets and campaign keys out of client-side source control. diff --git a/plugins/zoom-developers/skills/contact-center/references/forum-top-questions.md b/plugins/zoom-developers/skills/contact-center/references/forum-top-questions.md new file mode 100644 index 00000000..5285fcc1 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/references/forum-top-questions.md @@ -0,0 +1,82 @@ +--- +title: "Forum-Derived Top Questions (Contact Center)" +--- + +# Forum-Derived Top Questions (Contact Center) + +Use this as a checklist of the most common recent Developer Forum asks for Zoom Contact Center integrations. + +## Fast Routing Questions (Ask First) + +- Surface: Contact Center app in Zoom client, web SDK/campaign embed, Smart Embed, or REST API workflow. +- Runtime: web vs Android vs iOS and exact SDK version. +- Auth context: app type, scopes, token owner, and Contact Center admin role. +- Resource target: queue/flow/engagement IDs and expected channel (`voice`, `video`, `chat`, callback). +- Failure proof: exact endpoint/event, full response code/message, and one representative payload. + +## Smart Embed Login/Origin Problems + +Common asks: +- Login popup completes but embed never receives session. +- Hosted environment fails while local HTML test works. + +Answer pattern: +- Verify allowed domain configuration exactly matches production origin. +- Validate `origin` usage and `postMessage` contract assumptions. +- Check iframe/sandbox/CSP restrictions for hosted environments. +- Reproduce with a minimal page (embed only) to isolate app-layer interference. + +## Token Works for Phone But Contact Center API Returns 401 + +Common asks: +- Same bearer token can call Phone endpoints but Contact Center endpoints return invalid token. + +Answer pattern: +- Confirm Contact Center scopes are on the active token (not only app config). +- Confirm requester has Contact Center admin permissions in target account. +- Confirm account context did not drift (owner/admin reassignment can break behavior). +- Regenerate token after any scope/role changes. + +## Event Gaps and State-Change Confusion + +Common asks: +- `contact_center.user_status_changed` or engagement events appear missing. +- Documented event name does not fire as expected in a given lifecycle. + +Answer pattern: +- Attach listeners before channel/session start. +- Verify event coverage for the specific channel and engagement phase. +- Confirm network/security layers are not blocking webhook deliveries. +- Add reconciliation logic instead of assuming every state transition emits one event. + +## Recordings and Transcripts Edge Cases + +Common asks: +- Recording rows exist but media/transcript is unavailable. +- Transcript download fails or payload differs from expectations. + +Answer pattern: +- Check recording duration/status before download attempts. +- Handle not-ready and no-recording states explicitly. +- Retry with bounded backoff for newly completed engagements. +- Keep fallback handling for empty/partial recording metadata. + +## Analytics Pagination Repeats First Page + +Common asks: +- `next_page_token` loops the same records in historical analytics endpoints. + +Answer pattern: +- Keep all filter params stable while paging. +- Use token exactly as returned; do not mutate sort/filter inputs mid-stream. +- Add duplicate-page detection and stop conditions in client code. + +## Data Availability Boundaries + +Common asks: +- Access to in-progress chat messages or other live interaction internals. + +Answer pattern: +- Distinguish near-real-time events from post-engagement reporting APIs. +- Set expectations early when an in-progress data surface is unavailable. +- Design workflows around available lifecycle events and finalized engagement data. diff --git a/plugins/zoom-developers/skills/contact-center/references/full-guide.md b/plugins/zoom-developers/skills/contact-center/references/full-guide.md new file mode 100644 index 00000000..b91a407e --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/references/full-guide.md @@ -0,0 +1,105 @@ +# /build-zoom-contact-center-app + +Background reference for Zoom Contact Center integrations across app, web, and native mobile surfaces. + +Implementation guidance for Zoom Contact Center across: +- Contact Center apps in the Zoom client (Zoom Apps SDK path) +- Web channel embeds (chat/video/campaign) +- Native mobile SDKs (Android/iOS) + +Official docs: +- https://developers.zoom.us/docs/contact-center/ +- https://developers.zoom.us/docs/contact-center/web/sdk-reference/ +- https://marketplacefront.zoom.us/sdk/contact/android/index.html +- https://marketplacefront.zoom.us/sdk/contact/ios/index.html + +## Routing Guardrail + +- If the user is building an app inside the Zoom Contact Center desktop client, stay on the Zoom Apps SDK path and use this skill plus `zoom-apps-sdk`. +- If the user is embedding chat/video widgets on a website, route to [web/SKILL.md](../web/SKILL.md). +- If the user is integrating native Android or iOS SDK binaries, route to [android/SKILL.md](../android/SKILL.md) or [ios/SKILL.md](../ios/SKILL.md). +- If the user needs Contact Center call-control or queue APIs, chain with [../rest-api/SKILL.md](../../rest-api/SKILL.md). + +## Quick Links + +Start here: +1. [concepts/architecture-and-lifecycle.md](../concepts/architecture-and-lifecycle.md) +2. [scenarios/high-level-scenarios.md](../scenarios/high-level-scenarios.md) +3. [references/forum-top-questions.md](../references/forum-top-questions.md) +4. [references/versioning-and-compatibility.md](../references/versioning-and-compatibility.md) +5. [references/samples-validation.md](../references/samples-validation.md) +6. [references/environment-variables.md](../references/environment-variables.md) +7. [troubleshooting/common-drift-and-breaks.md](../troubleshooting/common-drift-and-breaks.md) +8. [RUNBOOK.md](../RUNBOOK.md) + +Platform skills: +- [android/SKILL.md](../android/SKILL.md) +- [ios/SKILL.md](../ios/SKILL.md) +- [web/SKILL.md](../web/SKILL.md) + +## Documentation Structure + +``` +contact-center/ +├── SKILL.md +├── RUNBOOK.md +├── concepts/ +│ └── architecture-and-lifecycle.md +├── scenarios/ +│ └── high-level-scenarios.md +├── references/ +│ ├── versioning-and-compatibility.md +│ ├── samples-validation.md +│ └── environment-variables.md +├── troubleshooting/ +│ └── common-drift-and-breaks.md +├── android/ +│ ├── SKILL.md +│ ├── concepts/sdk-lifecycle.md +│ ├── examples/service-patterns.md +│ ├── references/android-reference-map.md +│ └── troubleshooting/common-issues.md +├── ios/ +│ ├── SKILL.md +│ ├── concepts/sdk-lifecycle.md +│ ├── examples/service-patterns.md +│ ├── references/ios-reference-map.md +│ └── troubleshooting/common-issues.md +└── web/ + ├── SKILL.md + ├── concepts/lifecycle-and-events.md + ├── examples/app-context-and-state.md + ├── references/web-reference-map.md + └── troubleshooting/common-issues.md +``` + +## Common Lifecycle Pattern + +1. Initialize platform context early. +2. Build a channel item (`entryId` for chat/video/ZVA, `apiKey` for scheduled callback and campaign flows). +3. Get service/client instance. +4. Register listeners/delegates before user interaction. +5. Start flow (`fetchUI`, `startVideo`, or web SDK open/show path). +6. Handle engagement state changes (`start`, `hold`, `resume`, `end`) and context switching. +7. End flow and release resources (`endChat`/`endVideo`, `logout/logoff`, uninitialize/release). + +## High-Level Scenarios + +- Agent side-panel app that stores notes per `engagementId` and survives context switching. +- Browser chat/video campaigns launched from web tags. +- Native mobile customer app for chat/video/scheduled callback. +- Campaign-driven channel selection (chat, ZVA, video, scheduled callback). +- Rejoin flow for dropped video engagements on mobile. +- Smart Embed CRM softphone with postMessage event contracts. + +See [scenarios/high-level-scenarios.md](../scenarios/high-level-scenarios.md) for details. + +## Chaining + +- Auth and in-client app identity: [../zoom-apps-sdk/SKILL.md](../../zoom-apps-sdk/SKILL.md) and [../oauth/SKILL.md](../../oauth/SKILL.md) +- Contact Center REST workflows: [../rest-api/SKILL.md](../../rest-api/SKILL.md) +- Cobrowse on web voice/chat channels: [../cobrowse-sdk/SKILL.md](../../cobrowse-sdk/SKILL.md) + +## Environment Variables + +- See [references/environment-variables.md](../references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/plugins/zoom-developers/skills/contact-center/references/samples-validation.md b/plugins/zoom-developers/skills/contact-center/references/samples-validation.md new file mode 100644 index 00000000..1b67da5d --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/references/samples-validation.md @@ -0,0 +1,48 @@ +# Samples Validation Summary + +This summary captures lifecycle and architecture checks against these references: + +- Web: +- https://github.com/zoom/ZCC-Zoom-App-Advanced-Sample +- https://github.com/zoom/zcc-javascript-quickstart +- https://github.com/zoom/zcc-nextjs-sample +- iOS package: `ios-zccsdk-5.2.0.zip` +- Android package: `android-zccsdk-5.2.0.zip` + +## Confirmed Lifecycle Patterns + +1. Contact Center App (Zoom Apps SDK): +- Configure capabilities. +- Query engagement context/status. +- Subscribe to engagement change events. +- Persist state by `engagementId`. + +2. Android Native: +- Initialize in `Application.onCreate`. +- Service `init` with `ZoomCCItem`. +- Use `fetchUI` to present channel. +- `logoff` and `releaseZoomCCService` on cleanup. + +3. iOS Native: +- Set `ZoomCCInterface.sharedInstance().context`. +- Initialize service with item. +- Use `fetchUI` to present. +- Forward app lifecycle callbacks to SDK. +- Use rejoin handler path for video reconnect. + +## Contradictions and Drift Signals + +- Some docs show simplified `service.init("EntryId")` signatures while current references emphasize item-based initialization. +- iOS deprecated error callback still appears in older sample/docs. +- Some public sample manifests contain values that conflict with expected Contact Center embedding configuration and should be reviewed per environment. +- Scraped reference pages include parser artifacts (`TODO`/error pages) and should not be treated as canonical API surfaces. + +## Operational Guidance + +- Treat samples as architecture guidance, not immutable source of truth. +- Resolve conflicts in this order: +1. Current official docs. +2. Current platform API reference. +3. Latest shipped SDK headers/binaries. +4. Samples. + diff --git a/plugins/zoom-developers/skills/contact-center/references/versioning-and-compatibility.md b/plugins/zoom-developers/skills/contact-center/references/versioning-and-compatibility.md new file mode 100644 index 00000000..29c33100 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/references/versioning-and-compatibility.md @@ -0,0 +1,41 @@ +# Versioning and Compatibility Notes + +## Minimum Version Enforcement + +- Zoom enforces SDK minimum versions quarterly. +- Enforcement windows are announced with advance notice. +- Older SDKs can stop functioning in production even if code has not changed. + +## Practical Policy + +1. Track SDK version in runtime telemetry. +2. Maintain a scheduled upgrade cadence. +3. Validate critical flows every release: +- launch/init +- engagement events +- channel open/close +- rejoin (mobile) + +## Known Drift Patterns + +- API shape drift between docs and generated references. +- Legacy snippets showing old method signatures. +- Event naming/style differences between product surfaces. +- Deprecated callbacks preserved for backward compatibility but replaced in newer signatures. + +## iOS Notable Deprecation + +- `onService:error:detail:` is deprecated. +- Prefer `onService:error:detail:description:`. + +## Smart Embed Version Note + +- Smart Embed v3 is the forward path in docs. +- Maintain version-gated integration code if your account still has older embed behavior. + +## Defensive Design + +- Feature-detect methods/events before calling them. +- Keep adapters between your domain model and SDK payloads. +- Avoid hard-coding assumptions about optional fields. + diff --git a/plugins/zoom-developers/skills/contact-center/scenarios/high-level-scenarios.md b/plugins/zoom-developers/skills/contact-center/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..cb54fb8c --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/scenarios/high-level-scenarios.md @@ -0,0 +1,59 @@ +# High-Level Scenarios + +## 1. Agent Notes App in Contact Center + +Goal: +- Agent writes notes that follow engagement context switching. + +Flow: +1. `config` Zoom Apps SDK with engagement capabilities. +2. Load `getEngagementContext` + `getEngagementStatus`. +3. Store notes by `engagementId`. +4. On `onEngagementContextChange`, swap UI state to selected engagement. +5. On `onEngagementStatusChange` `end`, finalize or clear engagement draft. + +## 2. Web Chat Campaign Launch + +Goal: +- Product team controls targeting in admin without code redeploy. + +Flow: +1. Add campaign web tag script. +2. Wait for `zoomCampaignSdk:ready`. +3. Programmatically `open/show/hide/close` as needed. +4. Listen for engagement events for analytics and CRM writes. + +## 3. Mobile Chat and Video with Native SDK + +Goal: +- Customer mobile app can launch chat/video and recover from interruptions. + +Flow: +1. Initialize SDK context in app startup. +2. Build `ZoomCCItem` for channel. +3. Initialize service, attach delegates/listeners, and launch UI. +4. Handle disconnect/rejoin links for video. +5. End flow and release service resources. + +## 4. Campaign-Mode Channel Router + +Goal: +- Runtime selection of chat/video/ZVA/scheduled callback per campaign. + +Flow: +1. Fetch campaigns by API key. +2. Inspect campaign channels. +3. Build channel-specific item with campaign mode. +4. Release previous conflicting service before opening new channel. + +## 5. Smart Embed CRM Integration + +Goal: +- Embed Contact Center softphone in CRM with screen-pop and contact lookup. + +Flow: +1. Load Smart Embed iframe. +2. Handle postMessage events (`zcc-init-config-request`, search, resize, engagement events). +3. Return contact search results and route screen-pop in CRM. +4. Keep feature flags aligned with Smart Embed version path. + diff --git a/plugins/zoom-developers/skills/contact-center/troubleshooting/common-drift-and-breaks.md b/plugins/zoom-developers/skills/contact-center/troubleshooting/common-drift-and-breaks.md new file mode 100644 index 00000000..7cb17c00 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/troubleshooting/common-drift-and-breaks.md @@ -0,0 +1,62 @@ +# Common Drift and Breaks + +## Symptom: Engagement Context Missing + +Likely causes: +- App is not running in Contact Center context. +- Missing SDK capabilities in `config`. +- Identity/context token path is incomplete. + +Checks: +1. Confirm running context. +2. Confirm capabilities include engagement APIs/events. +3. Confirm app manifest and feature toggles. + +## Symptom: Campaign SDK Methods Throw + +Likely causes: +- Calling methods before `zoomCampaignSdk:ready`. +- Invalid API key or missing campaign configuration. +- Script blocked by CSP/ad-blockers/tag-manager path. + +Checks: +1. Add ready gate before method calls. +2. Validate key/env and script URL. +3. Validate CSP/domain allow lists. + +## Symptom: Native Service Not Responding + +Likely causes: +- SDK init executed too late. +- Wrong channel item (`entryId` vs `apiKey` mismatch). +- Listeners/delegates attached after service start. + +Checks: +1. Move init earlier in app lifecycle. +2. Validate item/channel pairing. +3. Register listeners before `fetchUI`. + +## Symptom: Rejoin Flow Fails + +Likely causes: +- Deep link scheme/host mismatch. +- Rejoin URL or web relay page not configured. +- App lifecycle hooks/context not initialized. + +Checks: +1. Verify platform URL/deep link configuration. +2. Verify admin rejoin settings. +3. Verify rejoin handler wiring. + +## Symptom: Behavior Changed After Release + +Likely causes: +- Minimum version enforcement date reached. +- Deprecated callback removed or changed. +- New SDK defaults in channel behavior. + +Checks: +1. Confirm SDK version in production. +2. Review changelog/deprecation notes. +3. Add adapter guards for optional fields/methods. + diff --git a/plugins/zoom-developers/skills/contact-center/web/RUNBOOK.md b/plugins/zoom-developers/skills/contact-center/web/RUNBOOK.md new file mode 100644 index 00000000..fb1103e5 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/web/RUNBOOK.md @@ -0,0 +1,63 @@ +# Contact Center Web 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 channel target and integration mode for Web. +- Contact Center app path and web embed path have different lifecycle rules. +- For mobile SDKs, verify native service lifecycle and listener registration order. + +## 2) Confirm Required Credentials + +- `entryId` for chat/video/ZVA entry points. +- `apiKey` for scheduled callback and campaign/tag use cases. +- If in-client app behavior is needed, verify Zoom App credentials and required scopes. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK context early. +2. Get channel service and register listeners/delegates before actions. +3. Authenticate/login where required. +4. Start/fetch channel UI and handle engagement status transitions. + +## 4) Confirm Event/State Handling + +- Track state by `engagementId`; do not assume single engagement forever. +- Handle context-switch events without losing draft/chat workflow state. +- Keep service/channel state isolated per active engagement. + +## 5) Confirm Cleanup + Upgrade Posture + +- End channel session and release service resources cleanly. +- Forward app lifecycle callbacks for iOS integrations. +- Re-check release notes for renamed/deprecated methods before upgrades. + +## 6) Quick Probes + +- Engagement context/status APIs return valid values. +- Start/end flow works once end-to-end for target channel. +- Listener callbacks fire on switch/end events without stale state. + +## 7) Fast Decision Tree + +- UI does not open -> invalid `entryId`/`apiKey` or missing init/listener sequence. +- Events missing -> listener registered too late or detached unexpectedly. +- Rejoin/resume fails -> lifecycle callbacks or deep-link/scheme config mismatch. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/contact-center/web/ +- https://developers.zoom.us/docs/contact-center/web/sdk-reference/ + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/contact-center/web/` diff --git a/plugins/zoom-developers/skills/contact-center/web/SKILL.md b/plugins/zoom-developers/skills/contact-center/web/SKILL.md new file mode 100644 index 00000000..f8ce3c3e --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/web/SKILL.md @@ -0,0 +1,46 @@ +--- +name: zoom-contact-center-web +description: "Zoom Contact Center SDK for Web. Use for web chat/video/campaign embeds, engagement event handling, app-context integrations, and Smart Embed postMessage workflows." +--- + +# Zoom Contact Center SDK - Web + +Official docs: +- https://developers.zoom.us/docs/contact-center/web/ +- https://developers.zoom.us/docs/contact-center/web/sdk-reference/ + +## Quick Links + +1. [concepts/lifecycle-and-events.md](concepts/lifecycle-and-events.md) +2. [examples/app-context-and-state.md](examples/app-context-and-state.md) +3. [references/web-reference-map.md](references/web-reference-map.md) +4. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Integration Modes + +1. Contact Center App in Zoom client: +- Zoom Apps SDK engagement APIs/events. + +2. External website embed: +- Campaign SDK/web scripts (`zoomCampaignSdk` pattern). +- Video client initialization pattern. + +3. Smart Embed: +- iframe + `postMessage` event contract. + +## Hard Guardrails + +- For campaign SDK, gate calls behind `zoomCampaignSdk:ready`. +- Persist state by `engagementId`. +- Expect context switching and background app behavior. +- Validate CSP and allow-list settings before debugging logic. + +## Chaining + +- For in-client app APIs and auth flows: [../../zoom-apps-sdk/SKILL.md](../../zoom-apps-sdk/SKILL.md) +- For identity and OAuth: [../../oauth/SKILL.md](../../oauth/SKILL.md) +- For cobrowse workflow: [../../cobrowse-sdk/SKILL.md](../../cobrowse-sdk/SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/contact-center/web/concepts/lifecycle-and-events.md b/plugins/zoom-developers/skills/contact-center/web/concepts/lifecycle-and-events.md new file mode 100644 index 00000000..c0672f0a --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/web/concepts/lifecycle-and-events.md @@ -0,0 +1,45 @@ +# Web Lifecycle and Event Model + +## Contact Center App Runtime (Zoom Client) + +1. Configure SDK capabilities. +2. Read running context. +3. Read engagement context/status. +4. Subscribe to: +- `onEngagementContextChange` +- `onEngagementStatusChange` +- optional variable change events +5. Maintain engagement-scoped state. + +## Web Campaign SDK Runtime + +1. Load script with API key. +2. Wait for `zoomCampaignSdk:ready`. +3. Call methods: +- `open` +- `close` +- `show` +- `hide` +- `endChat` +4. Subscribe/unsubscribe to SDK events. + +## Video Client Runtime + +1. Create client. +2. Initialize with entry identifier and optional metadata. +3. Start video. +4. Handle `video-start` and `video-end` events. + +## Smart Embed Runtime + +1. Load Smart Embed iframe. +2. Listen for `message` events from iframe. +3. Respond to init/search/control requests. +4. Map engagement and contact data to CRM/app entities. + +## State Strategy + +- Key all session data by `engagementId`. +- Keep event handlers re-entrant and idempotent. +- Treat `end` status as cleanup boundary. + diff --git a/plugins/zoom-developers/skills/contact-center/web/examples/app-context-and-state.md b/plugins/zoom-developers/skills/contact-center/web/examples/app-context-and-state.md new file mode 100644 index 00000000..aeb2a609 --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/web/examples/app-context-and-state.md @@ -0,0 +1,60 @@ +# Web Example: Engagement-Aware State + +```javascript +await zoomSdk.config({ + version: "0.16.0", + capabilities: [ + "getRunningContext", + "getEngagementContext", + "getEngagementStatus", + "onEngagementContextChange", + "onEngagementStatusChange", + ], +}); + +const stateByEngagement = new Map(); +let currentEngagementId = ""; + +function ensureState(id) { + if (!stateByEngagement.has(id)) { + stateByEngagement.set(id, { notes: "", formDraft: {} }); + } + return stateByEngagement.get(id); +} + +async function hydrate() { + const [ctx, status] = await Promise.all([ + zoomSdk.callZoomApi("getEngagementContext"), + zoomSdk.callZoomApi("getEngagementStatus"), + ]); + currentEngagementId = ctx?.engagementContext?.engagementId || ""; + if (currentEngagementId) ensureState(currentEngagementId); + render(currentEngagementId, status?.engagementStatus?.state); +} + +zoomSdk.addEventListener("onEngagementContextChange", (evt) => { + currentEngagementId = evt?.engagementContext?.engagementId || ""; + if (currentEngagementId) ensureState(currentEngagementId); + render(currentEngagementId); +}); + +zoomSdk.addEventListener("onEngagementStatusChange", (evt) => { + const state = evt?.engagementStatus?.state; + if (state === "end" && currentEngagementId) { + stateByEngagement.delete(currentEngagementId); + } + render(currentEngagementId, state); +}); + +hydrate(); +``` + +## Campaign SDK Ready Gate + +```javascript +window.addEventListener("zoomCampaignSdk:ready", () => { + if (!window.zoomCampaignSdk) return; + window.zoomCampaignSdk.show(); +}); +``` + diff --git a/plugins/zoom-developers/skills/contact-center/web/references/web-reference-map.md b/plugins/zoom-developers/skills/contact-center/web/references/web-reference-map.md new file mode 100644 index 00000000..c31a797e --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/web/references/web-reference-map.md @@ -0,0 +1,54 @@ +# Web Reference Map + +Primary docs: +- https://developers.zoom.us/docs/contact-center/web/get-started/ +- https://developers.zoom.us/docs/contact-center/web/chat/ +- https://developers.zoom.us/docs/contact-center/web/video/ +- https://developers.zoom.us/docs/contact-center/web/campaigns/ +- https://developers.zoom.us/docs/contact-center/web/sdk-reference/ +- https://developers.zoom.us/docs/contact-center/smart-embed/ + +## Engagement APIs/Events (Contact Center App) + +- `getEngagementContext` +- `getEngagementStatus` +- `onEngagementContextChange` +- `onEngagementStatusChange` +- `onEngagementVariableValueChange` + +## Campaign SDK Events + +- `open` +- `close` +- `show` +- `hide` +- `engagement_started` +- `engagement_ended` + +## Campaign SDK Methods + +- `open()` +- `close()` +- `show()` +- `hide()` +- `endChat()` +- `waitForInit()` +- `waitForReady()` +- `updateUserContext()` + +## Video Client Events + +- `video-start` +- `video-end` +- `notification-join-call` +- `video-click-end` +- `video-force-end` +- `task-created` + +## Smart Embed Event Surface + +- init/config events (`zcc-init-config-request`, `zcc-init-config-response`) +- engagement and channel events +- contact search request/response patterns +- resize and interaction events + diff --git a/plugins/zoom-developers/skills/contact-center/web/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/contact-center/web/troubleshooting/common-issues.md new file mode 100644 index 00000000..e8eacd4c --- /dev/null +++ b/plugins/zoom-developers/skills/contact-center/web/troubleshooting/common-issues.md @@ -0,0 +1,42 @@ +# Web Common Issues + +## `zoomCampaignSdk` Is Undefined + +Cause: +- Calls happen before readiness event. + +Fix: +- Wait for `zoomCampaignSdk:ready` before calling SDK methods. + +## Widget Does Not Load + +Cause: +- CSP or domain allow-list blocks script/network access. + +Fix: +- Update CSP headers and Marketplace domain allow list entries. + +## App Context Header Missing in PWA + +Cause: +- PWA path does not provide `x-zoom-app-context` header consistently. + +Fix: +- Use `getAppContext()` and backend token decryption flow. + +## Engagement Data Gets Overwritten + +Cause: +- State keyed globally instead of by `engagementId`. + +Fix: +- Persist and restore state per engagement key. + +## Smart Embed Events Not Received + +Cause: +- postMessage listener origin/type filtering missing or incorrect. + +Fix: +- Implement strict message handling and respond to required init/search events. + diff --git a/plugins/zoom-developers/skills/debug-zoom-integration/SKILL.md b/plugins/zoom-developers/skills/debug-zoom-integration/SKILL.md new file mode 100644 index 00000000..24a8ae24 --- /dev/null +++ b/plugins/zoom-developers/skills/debug-zoom-integration/SKILL.md @@ -0,0 +1,40 @@ +--- +name: debug-zoom-integration +description: Use when isolating failures. +--- + +# Debug Zoom Integration + +Use this skill when the user already built something and it is failing. + +## Triage Order + +1. Auth and app configuration +2. Request construction or event verification +3. SDK initialization or platform mismatch +4. Media/session behavior +5. Client platform and capability assumptions + +## Evidence To Request + +- Exact error text +- Platform and SDK/runtime +- Relevant request or payload sample +- What worked versus what failed +- Whether the issue is reproducible or intermittent + +## Reference Routing + +- [oauth](../oauth/SKILL.md) +- [rest-api](../rest-api/SKILL.md) +- [webhooks](../webhooks/SKILL.md) +- [meeting-sdk](../meeting-sdk/SKILL.md) +- [video-sdk](../video-sdk/SKILL.md) +- [rtms](../rtms/SKILL.md) + +## Output + +- Most likely failing layer +- Ranked hypotheses +- Short fix plan +- Verification steps diff --git a/plugins/zoom-developers/skills/debug-zoom/SKILL.md b/plugins/zoom-developers/skills/debug-zoom/SKILL.md new file mode 100644 index 00000000..d0f51d75 --- /dev/null +++ b/plugins/zoom-developers/skills/debug-zoom/SKILL.md @@ -0,0 +1,37 @@ +--- +name: debug-zoom +description: Use when debugging issues. +--- + +# /debug-zoom + +> For local plugin installation and app mapping details, see [README.md](../../README.md). + +Debug Zoom auth, API, webhook, or SDK issues without wandering through the entire docs set. + +## Usage + +```text +/debug-zoom $ARGUMENTS +``` + +## Workflow + +1. Identify the failing layer: auth, API request, webhook, SDK init, or media/session behavior. +2. Ask for the minimum missing evidence: exact error, platform, request/response, event payload, or code path. +3. Produce 2-4 plausible causes ranked by likelihood. +4. Route to the most relevant deep references in `skills/`. +5. Give a short verification plan so the user can confirm the fix. + +## Output + +- Most likely failure layer +- Ranked hypotheses +- Targeted fix steps +- Verification checklist +- Relevant skill links + +## Related Skills + +- [debug-zoom-integration](../debug-zoom-integration/SKILL.md) +- [setup-zoom-oauth](../setup-zoom-oauth/SKILL.md) diff --git a/plugins/zoom-developers/skills/general/RUNBOOK.md b/plugins/zoom-developers/skills/general/RUNBOOK.md new file mode 100644 index 00000000..c1014193 --- /dev/null +++ b/plugins/zoom-developers/skills/general/RUNBOOK.md @@ -0,0 +1,65 @@ +# General Skill 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 + +- Use `general` as the routing hub for cross-product intent selection. +- Confirm each use-case links to the correct product skill chain. +- Use this runbook before troubleshooting multi-product integrations. + +## 2) Confirm Required Credentials + +- Validate OAuth model selection (User OAuth vs Server-to-Server OAuth) before implementation. +- Ensure required scopes are documented in each use-case. +- Keep credential storage server-side; only expose short-lived tokens to clients. + +## 3) Confirm Lifecycle Order + +1. Pick product path (`REST`, `Meeting SDK`, `Video SDK`, `Apps SDK`, `Phone`, `Contact Center`, etc.). +2. Map auth flow and required scopes. +3. Define event model (`webhooks` or `websockets`) and correlation IDs. +4. Validate deployment model and operational monitoring requirements. + +## 4) Confirm Event/State Handling + +- Keep use-case assumptions explicit when combining multiple products. +- Store cross-system identifiers (meeting/session/call/engagement IDs) for traceability. +- Document fallback behavior when API names/fields drift between versions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Remove stale route links whenever skills are renamed or moved. +- Keep `.env` key references centralized in environment variable reference docs. +- Refresh compatibility notes after each major SDK/API update cycle. + +## 6) Quick Probes + +- Routing matrix still points to existing `SKILL.md` files. +- Use-cases include at least one concrete implementation chain. +- OAuth/scopes guidance matches current Marketplace app model. + +## 7) Fast Decision Tree + +- Unsure between Meeting SDK and Video SDK -> route by UX model (Zoom meeting UI vs fully custom session). +- Need lowest-latency events -> use websockets; otherwise webhooks are acceptable. +- Scope/auth failures in execution -> pause and re-authorize with correct app type and scopes. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/ +- https://marketplace.zoom.us/ +- https://devforum.zoom.us/ + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/` +- `raw-docs/marketplacefront.zoom.us/sdk/` diff --git a/plugins/zoom-developers/skills/general/SKILL.md b/plugins/zoom-developers/skills/general/SKILL.md new file mode 100644 index 00000000..937dd901 --- /dev/null +++ b/plugins/zoom-developers/skills/general/SKILL.md @@ -0,0 +1,25 @@ +--- +name: zoom-general +description: Use when comparing products. +--- + +# Zoom General + +Use this skill for cross-product platform context after the user’s goal is understood. For first-step routing, prefer `start`, `plan-zoom-product`, or `plan-zoom-integration`. + +## Workflow + +1. Classify the job by outcome: embed meetings, custom video, automate resources, consume events, process media, use meeting intelligence, or publish a Marketplace app. +2. Choose the Zoom surface that owns the behavior: REST API, webhooks, WebSockets, Meeting SDK, Video SDK, Zoom Apps SDK, Phone, Contact Center, Virtual Agent, Scribe, Rivet, or Cobrowse. +3. Confirm auth and scope model before implementation; Zoom surfaces differ on user-level OAuth, account-level OAuth, and SDK signatures. +4. Route to the narrow skill once the surface is chosen rather than keeping broad guidance in context. +5. Use the preserved guide only when a task crosses product boundaries or needs detailed comparison tables. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- App types: [references/app-types.md](references/app-types.md) +- Authentication: [references/authentication.md](references/authentication.md) +- Scopes: [references/scopes.md](references/scopes.md) +- Marketplace: [references/marketplace.md](references/marketplace.md) +- Query routing playbook: [references/query-routing-playbook.md](references/query-routing-playbook.md) diff --git a/plugins/zoom-developers/skills/general/references/app-types.md b/plugins/zoom-developers/skills/general/references/app-types.md new file mode 100644 index 00000000..a0852091 --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/app-types.md @@ -0,0 +1,106 @@ +# App Types + +Choose the right Zoom app type for your integration. + +## Overview + +Zoom Marketplace has 3 app types: + +| App Type | Use Case | +|----------|----------| +| **General App** | Flexible - configure surfaces, embeds, OAuth, webhooks | +| **Server-to-Server OAuth** | Backend automation, no user authorization | +| **Webhook Only** | Receive events only, no API access | + +## General App + +The modular app type. Pick what you need: + +### OAuth Type (choose one) + +| Type | Scopes | Authorization | +|------|--------|---------------| +| **Admin** | Admin scopes (`*:admin`) | Entire account OR specific users | +| **User** | User scopes | Only themselves (self-service) | + +### Surfaces (product contexts) + +Your app can interact with these Zoom products: + +- Meetings +- Webinars +- Rooms +- Phone +- Team Chat +- Contact Center +- Whiteboard +- Virtual Agent +- Events +- Mail +- Workflows + +### Embeds (SDKs) + +Embed Zoom functionality in your app: + +| Embed | Description | +|-------|-------------| +| **Meeting SDK** | Embed Zoom meetings | +| **Contact Center SDK** | Embed Contact Center | +| **Phone SDK** | Embed Phone functionality | + +### Access + +Configure in the Access tab: + +- **Secret Token** - Verify webhook notifications +- **Event Subscription** - Webhooks +- **WebSockets** - Real-time event connections + +### Scopes + +Define which API methods the app can call. Scopes are: +- Restricted to specific resources +- Reviewed by Zoom during app submission + +### Features in General App + +General App can also include: +- **Zoom Apps** - Apps that run inside Zoom client + +## Server-to-Server OAuth + +Backend automation without user authorization. + +- No user interaction required +- Access your account's data +- Can include webhooks and zoom-websockets +- Best for: automation, reporting, integrations + +## Webhook Only + +Event notifications only. + +- Receive events, no API calls +- No OAuth tokens needed +- Best for: event logging, triggering external workflows + +Use this when you ONLY need events. Otherwise, add webhooks to General App or S2S. + +## Decision Guide + +| Need | App Type | +|------|----------| +| Call APIs for your account (backend) | Server-to-Server OAuth | +| Call APIs on behalf of users | General App (Admin or User OAuth) | +| Embed Zoom meetings | General App + Meeting SDK embed | +| Embed Contact Center | General App + Contact Center SDK embed | +| Embed Phone | General App + Phone SDK embed | +| Build in-client app | General App + Zoom Apps | +| Receive events only | Webhook Only | +| Receive events + call APIs | General App or S2S (with webhooks) | + +## Resources + +- **App types docs**: https://developers.zoom.us/docs/integrations/ +- **Marketplace**: https://marketplace.zoom.us/ diff --git a/plugins/zoom-developers/skills/general/references/authentication.md b/plugins/zoom-developers/skills/general/references/authentication.md new file mode 100644 index 00000000..275d9d46 --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/authentication.md @@ -0,0 +1,82 @@ +# Authentication + +Authentication methods for Zoom APIs and SDKs. + +## Overview + +Zoom supports multiple authentication methods depending on your use case: + +| Method | Use Case | +|--------|----------| +| **OAuth 2.0** | User-authorized access (on behalf of user) | +| **Server-to-Server OAuth** | Server-side automation (no user interaction) | +| **SDK JWT** | Meeting SDK and Video SDK authentication | + +## OAuth 2.0 + +For apps that act on behalf of users. + +### Flow + +``` +1. User clicks "Connect with Zoom" +2. Redirect to Zoom authorization URL +3. User grants permission +4. Zoom redirects back with auth code +5. Exchange code for access token +6. Use token to call APIs +``` + +### Authorization URL + +``` +https://zoom.us/oauth/authorize?response_type=code&client_id={clientId}&redirect_uri={redirectUri} +``` + +### Token Exchange + +```bash +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic {base64(clientId:clientSecret)}" \ + -d "grant_type=authorization_code&code={authCode}&redirect_uri={redirectUri}" +``` + +## Server-to-Server OAuth + +For server-side automation without user interaction. + +### Get Access Token + +```bash +curl -X POST "https://zoom.us/oauth/token?grant_type=account_credentials&account_id={accountId}" \ + -H "Authorization: Basic {base64(clientId:clientSecret)}" +``` + +### Response + +```json +{ + "access_token": "eyJ...", + "token_type": "bearer", + "expires_in": 3600 +} +``` + +## SDK JWT Signatures + +For Meeting SDK and Video SDK authentication. See: +- [Meeting SDK Authorization](../../meeting-sdk/references/authorization.md) +- [Video SDK Authorization](../../video-sdk/references/authorization.md) + +### Best Practices + +| Practice | Recommendation | +|----------|----------------| +| **Expiry (`exp`)** | Set ~10 seconds after generation | +| **Issued At (`iat`)** | Set 2 hours in the past (if `exp - iat >= 2 hours` required) | +| **Generate server-side** | Never expose secrets in client code | + +## Resources + +- **OAuth docs**: https://developers.zoom.us/docs/integrations/oauth/ +- **S2S OAuth docs**: https://developers.zoom.us/docs/internal-apps/s2s-oauth/ diff --git a/plugins/zoom-developers/skills/general/references/authorization-patterns.md b/plugins/zoom-developers/skills/general/references/authorization-patterns.md new file mode 100644 index 00000000..13acf4b9 --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/authorization-patterns.md @@ -0,0 +1,562 @@ +# Authorization Patterns + +Permission validation middleware and role-based access control for Zoom API integrations. + +> **Note**: These are **implementation patterns for YOUR application** when building Zoom integrations. These are not Zoom's internal authorization mechanisms - they are examples of how to structure authorization logic in your own backend. + +## Overview + +When chaining multiple Zoom API calls, each step may require different scopes and permissions. This document provides patterns for validating authorization at each step before proceeding. + +## Authorization Flow + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ AUTHORIZATION VALIDATION FLOW │ +└─────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ 1. Check Token Validity │ +│ └── Is token expired? → Refresh or re-authenticate │ +│ └── Is token revoked? → Re-authenticate │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 2. Validate Required Scopes │ +│ └── Does token have scopes for this operation? │ +│ └── If missing → Return 403 with required scopes │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 3. Check Resource Permissions │ +│ └── Does user have access to this resource? │ +│ └── Is user admin/owner/member? │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 4. Execute Operation │ +│ └── Call Zoom API │ +│ └── Handle API-level authorization errors │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Scope Validation Middleware + +### Express.js Middleware + +```javascript +const axios = require('axios'); + +/** + * Middleware to validate OAuth token has required scopes + * @param {string[]} requiredScopes - Scopes required for this route + */ +function requireScopes(requiredScopes) { + return async (req, res, next) => { + const token = req.headers.authorization?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ + error: 'unauthorized', + message: 'No access token provided' + }); + } + + try { + // Get token info to check scopes + const tokenInfo = await getTokenInfo(token); + + // Check if token has all required scopes + const tokenScopes = tokenInfo.scope.split(' '); + const missingScopes = requiredScopes.filter( + scope => !tokenScopes.includes(scope) + ); + + if (missingScopes.length > 0) { + return res.status(403).json({ + error: 'insufficient_scope', + message: 'Token missing required scopes', + required_scopes: requiredScopes, + missing_scopes: missingScopes, + your_scopes: tokenScopes + }); + } + + // Attach token info to request for downstream use + req.zoomToken = tokenInfo; + req.zoomScopes = tokenScopes; + next(); + + } catch (error) { + if (error.response?.status === 401) { + return res.status(401).json({ + error: 'invalid_token', + message: 'Token is invalid or expired' + }); + } + next(error); + } + }; +} + +/** + * Get token information including scopes + * + * IMPORTANT: Scopes are returned during OAuth token exchange, not from API calls. + * You should store the scopes when you receive the access token. + */ +async function getTokenInfo(accessToken) { + // For Server-to-Server OAuth: Decode JWT to get scopes + const parts = accessToken.split('.'); + if (parts.length === 3) { + const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString()); + return { + scope: payload.scope || '', + exp: payload.exp, + aud: payload.aud + }; + } + + // For User OAuth tokens: Scopes are NOT available from API responses. + // You must store scopes when you receive them during token exchange. + // + // During OAuth token exchange, the response includes: + // { + // "access_token": "...", + // "token_type": "bearer", + // "scope": "user:read meeting:write ...", <-- Store this! + // "expires_in": 3600 + // } + // + // Store the scope in your database alongside the token. + + throw new Error( + 'User OAuth token scopes must be stored during token exchange. ' + + 'Cannot retrieve scopes from an opaque access token.' + ); +} + +/** + * Example: Store scopes during OAuth token exchange + */ +async function handleOAuthCallback(code) { + const response = await axios.post('https://zoom.us/oauth/token', null, { + params: { + grant_type: 'authorization_code', + code: code, + redirect_uri: REDIRECT_URI + }, + auth: { + username: CLIENT_ID, + password: CLIENT_SECRET + } + }); + + const { access_token, refresh_token, scope, expires_in } = response.data; + + // IMPORTANT: Store the scope along with the token + await saveTokenToDatabase({ + accessToken: access_token, + refreshToken: refresh_token, + scope: scope, // <-- Store this for later permission checks + expiresAt: Date.now() + (expires_in * 1000) + }); + + return { access_token, scope }; +} + +// Usage +const express = require('express'); +const app = express(); + +// Route requiring meeting:read scope +app.get('/api/meetings/:id', + requireScopes(['meeting:read']), + async (req, res) => { + // Token already validated, proceed with API call + const meeting = await getMeeting(req.params.id, req.headers.authorization); + res.json(meeting); + } +); + +// Route requiring multiple scopes +app.post('/api/users/:id/meetings', + requireScopes(['user:read', 'meeting:write']), + async (req, res) => { + const meeting = await createMeeting(req.params.id, req.body, req.headers.authorization); + res.json(meeting); + } +); +``` + +### Scope Requirements by Operation + +| Operation | User Scope | Admin Scope (S2S) | +|-----------|------------|-------------------| +| Get own user info | `user:read` | `user:read:admin` | +| List all users | N/A | `user:read:admin` | +| Create user | N/A | `user:write:admin` | +| Get own meetings | `meeting:read` | `meeting:read:admin` | +| Get any user's meetings | N/A | `meeting:read:admin` | +| Create meeting for self | `meeting:write` | `meeting:write:admin` | +| Create meeting for others | N/A | `meeting:write:admin` | +| List own recordings | `recording:read` | `recording:read:admin` | +| List any user's recordings | N/A | `recording:read:admin` | +| Delete own recording | `recording:write` | `recording:write:admin` | +| Delete any recording | N/A | `recording:write:admin` | +| Access own phone | `phone:read` | `phone:read:admin` | +| Access any user's phone | N/A | `phone:read:admin` | +| Manage phone settings | `phone:write` | `phone:write:admin` | + +> **Note**: "N/A" means this operation requires admin-level scopes and cannot be done with user-level OAuth. + +## Role-Based Access Control + +### Define Roles + +```javascript +/** + * Role definitions with allowed scopes + */ +const ROLES = { + admin: { + scopes: [ + 'user:read:admin', 'user:write:admin', + 'meeting:read:admin', 'meeting:write:admin', + 'recording:read:admin', 'recording:write:admin', + 'account:read:admin', 'account:write:admin' + ], + description: 'Full administrative access' + }, + + manager: { + scopes: [ + 'user:read:admin', + 'meeting:read:admin', 'meeting:write:admin', + 'recording:read:admin' + ], + description: 'Manage meetings and view users' + }, + + user: { + scopes: [ + 'user:read', + 'meeting:read', 'meeting:write', + 'recording:read' + ], + description: 'Manage own meetings and recordings' + }, + + viewer: { + scopes: [ + 'meeting:read', + 'recording:read' + ], + description: 'View-only access' + } +}; + +/** + * Check if user role has required scope + */ +function roleHasScope(role, requiredScope) { + const roleConfig = ROLES[role]; + if (!roleConfig) return false; + + return roleConfig.scopes.some(scope => { + // Exact match + if (scope === requiredScope) return true; + + // Admin scope covers non-admin version + // e.g., meeting:read:admin covers meeting:read + if (scope.endsWith(':admin')) { + const baseScope = scope.replace(':admin', ''); + if (baseScope === requiredScope) return true; + } + + return false; + }); +} + +/** + * Middleware to require a specific role + */ +function requireRole(allowedRoles) { + return (req, res, next) => { + const userRole = req.user?.role; // From your auth system + + if (!userRole || !allowedRoles.includes(userRole)) { + return res.status(403).json({ + error: 'forbidden', + message: 'Insufficient role permissions', + required_roles: allowedRoles, + your_role: userRole || 'none' + }); + } + + next(); + }; +} + +// Usage +app.delete('/api/users/:id', + requireRole(['admin']), + requireScopes(['user:write:admin']), + async (req, res) => { + // Only admins can delete users + await deleteUser(req.params.id); + res.json({ success: true }); + } +); +``` + +## Permission Checking Between Chained Operations + +### Chain Validation Pattern + +```javascript +/** + * Validate permissions for a multi-step operation + * before executing any steps + */ +async function validateChainPermissions(operations, tokenScopes) { + const allRequiredScopes = new Set(); + + for (const op of operations) { + for (const scope of op.requiredScopes) { + allRequiredScopes.add(scope); + } + } + + const missingScopes = [...allRequiredScopes].filter( + scope => !tokenScopes.includes(scope) + ); + + if (missingScopes.length > 0) { + return { + valid: false, + missingScopes, + message: `Cannot complete operation chain. Missing scopes: ${missingScopes.join(', ')}` + }; + } + + return { valid: true }; +} + +/** + * Execute a chain of operations with permission validation + */ +async function executeAuthorizedChain(operations, accessToken) { + // Get token scopes + const tokenInfo = await getTokenInfo(accessToken); + const tokenScopes = tokenInfo.scope.split(' '); + + // Validate all permissions upfront + const validation = await validateChainPermissions(operations, tokenScopes); + if (!validation.valid) { + throw new Error(validation.message); + } + + // Execute operations in sequence + const results = []; + for (const op of operations) { + console.log(`Executing: ${op.name}`); + + try { + const result = await op.execute(accessToken, results); + results.push({ name: op.name, success: true, data: result }); + } catch (error) { + // Check if it's an authorization error + if (error.response?.status === 403) { + throw new Error(`Authorization failed at step "${op.name}": ${error.response.data.message}`); + } + throw error; + } + } + + return results; +} + +// Example: User + Meeting creation chain +const userMeetingChain = [ + { + name: 'createUser', + requiredScopes: ['user:write:admin'], + execute: async (token, previousResults) => { + return await createUser({ + email: 'new@example.com', + firstName: 'New', + lastName: 'User' + }, token); + } + }, + { + name: 'createMeeting', + requiredScopes: ['meeting:write:admin'], + execute: async (token, previousResults) => { + const user = previousResults.find(r => r.name === 'createUser').data; + return await createMeeting(user.id, { + topic: 'Onboarding Meeting' + }, token); + } + } +]; + +// Usage +try { + const results = await executeAuthorizedChain(userMeetingChain, accessToken); + console.log('Chain completed:', results); +} catch (error) { + console.error('Chain failed:', error.message); +} +``` + +## Graceful Degradation + +### Handle Partial Permissions + +```javascript +/** + * Execute with graceful degradation when permissions are partial + */ +async function executeWithDegradation(operations, accessToken) { + const tokenInfo = await getTokenInfo(accessToken); + const tokenScopes = tokenInfo.scope.split(' '); + + const results = []; + + for (const op of operations) { + // Check if we have permission for this operation + const hasPermission = op.requiredScopes.every( + scope => tokenScopes.includes(scope) + ); + + if (!hasPermission) { + if (op.required) { + // Required operation - fail the chain + throw new Error(`Missing required scopes for ${op.name}: ${op.requiredScopes.join(', ')}`); + } else { + // Optional operation - skip with warning + console.warn(`Skipping ${op.name}: insufficient permissions`); + results.push({ + name: op.name, + skipped: true, + reason: 'insufficient_permissions', + required_scopes: op.requiredScopes + }); + continue; + } + } + + // Execute operation + const result = await op.execute(accessToken, results); + results.push({ name: op.name, success: true, data: result }); + } + + return results; +} + +// Example with optional operations +const meetingWithOptionalRecording = [ + { + name: 'getMeeting', + required: true, + requiredScopes: ['meeting:read'], + execute: async (token) => getMeetingDetails(meetingId, token) + }, + { + name: 'getRecordings', + required: false, // Optional - won't fail chain + requiredScopes: ['recording:read'], + execute: async (token, prev) => { + const meeting = prev.find(r => r.name === 'getMeeting').data; + return getRecordings(meeting.uuid, token); + } + } +]; +``` + +## Authorization Decision Flowchart + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ AUTHORIZATION DECISION FLOW │ +└──────────────────────────────────────────────────────────────────────────┘ + + ┌─────────────────┐ + │ Receive Request │ + └────────┬────────┘ + │ + ▼ + ┌────────────────────────┐ + │ Is token present? │ + └───────────┬────────────┘ + │ + ┌───────────┴───────────┐ + │ NO │ YES + ▼ ▼ + ┌───────────────┐ ┌────────────────────┐ + │ Return 401 │ │ Is token valid? │ + │ Unauthorized │ └─────────┬──────────┘ + └───────────────┘ │ + ┌───────────┴───────────┐ + │ NO │ YES + ▼ ▼ + ┌───────────────┐ ┌────────────────────┐ + │ Return 401 │ │ Has required │ + │ Invalid Token │ │ scopes? │ + └───────────────┘ └─────────┬──────────┘ + │ + ┌───────────┴───────────┐ + │ NO │ YES + ▼ ▼ + ┌───────────────┐ ┌────────────────────┐ + │ Return 403 │ │ Has resource │ + │ Insufficient │ │ access? │ + │ Scope │ └─────────┬──────────┘ + └───────────────┘ │ + ┌───────────┴───────────┐ + │ NO │ YES + ▼ ▼ + ┌───────────────┐ ┌────────────────┐ + │ Return 403 │ │ Execute │ + │ Forbidden │ │ Operation │ + └───────────────┘ └────────────────┘ +``` + +## Common Authorization Errors + +| Status | Error | Cause | Solution | +|--------|-------|-------|----------| +| 401 | `invalid_token` | Token expired or revoked | Refresh token or re-authenticate | +| 401 | `unauthorized` | No token provided | Include Authorization header | +| 403 | `insufficient_scope` | Token missing required scope | Request additional scopes | +| 403 | `forbidden` | User lacks resource access | Check user permissions | +| 403 | `access_denied` | Admin-only operation | Use admin account | + +## Best Practices + +1. **Validate upfront** - Check all permissions before starting a chain +2. **Fail fast** - Return clear error messages with required scopes +3. **Graceful degradation** - Skip optional steps rather than fail entirely +4. **Audit logging** - Log all authorization decisions +5. **Principle of least privilege** - Request only needed scopes +6. **Token caching** - Cache token info to avoid repeated validation calls + +## Real-World Examples + +See these use-cases for authorization patterns in action: + +- **[User + Meeting Creation](../use-cases/user-and-meeting-creation.md)** - Multi-step provisioning with scope validation +- **[Meeting Details with Events](../use-cases/meeting-details-with-events.md)** - REST API + webhooks with permission checking +- **[Meeting Automation](../use-cases/meeting-automation.md)** - Meeting management with admin scope requirements + +## Resources + +- **OAuth Scopes Reference**: https://developers.zoom.us/docs/integrations/oauth-scopes/ +- **API Error Codes**: https://developers.zoom.us/docs/api/rest/error-handling/ +- **Authentication Guide**: [authentication.md](authentication.md) +- **Scopes Reference**: [scopes.md](scopes.md) diff --git a/plugins/zoom-developers/skills/general/references/automatic-skill-chaining-rest-webhooks.md b/plugins/zoom-developers/skills/general/references/automatic-skill-chaining-rest-webhooks.md new file mode 100644 index 00000000..e2f9fc6a --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/automatic-skill-chaining-rest-webhooks.md @@ -0,0 +1,176 @@ +# Automatic Skill Chaining: REST API + Webhooks + +This guide provides executable patterns for handling a multi-faceted workflow that needs both: +- synchronous REST API operations (`zoom-rest-api`) +- asynchronous event processing (`zoom-webhooks`) + +## Chain Selection Logic + +```ts +export type SkillChain = { + selectedSkills: string[]; + executionOrder: string[]; +}; + +export function chooseRestWebhookChain(query: string): SkillChain { + const q = query.toLowerCase(); + const needsRest = /create meeting|update meeting|list users|rest api|\/v2\//.test(q); + const needsWebhook = /webhook|event|meeting\.started|participant|real-time update/.test(q); + + const selectedSkills = ['zoom-general']; + if (needsRest || needsWebhook) selectedSkills.push('zoom-oauth'); + if (needsRest) selectedSkills.push('zoom-rest-api'); + if (needsWebhook) selectedSkills.push('zoom-webhooks'); + + return { + selectedSkills, + executionOrder: selectedSkills, + }; +} +``` + +## Reference Architecture + +```text +Client/API Caller + -> Orchestrator API + -> OAuth token manager + -> REST API worker (create/update meetings) + -> Persistence (meeting state + idempotency keys) + <- immediate REST result + +Zoom Event Pipeline + Zoom -> Webhook ingress (signature verify + URL validation) + -> Queue + -> Event processors + -> State projection / downstream notifications +``` + +## Minimal Runnable Example (Node.js) + +```js +import express from 'express'; +import crypto from 'crypto'; + +const app = express(); +app.use(express.json({ + verify: (req, _res, buf) => { + req.rawBody = buf.toString('utf8'); + }, +})); + +const tokenCache = { accessToken: '', expiresAt: 0 }; +const meetingStore = new Map(); + +async function getAccessToken() { + const now = Date.now(); + if (tokenCache.accessToken && now < tokenCache.expiresAt - 60_000) { + return tokenCache.accessToken; + } + + const params = new URLSearchParams({ + grant_type: 'account_credentials', + account_id: process.env.ZOOM_ACCOUNT_ID, + }); + + const basic = Buffer.from(`${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`).toString('base64'); + const res = await fetch(`https://zoom.us/oauth/token?${params}`, { + method: 'POST', + headers: { Authorization: `Basic ${basic}` }, + }); + + if (!res.ok) throw new Error(`token_exchange_failed:${res.status}`); + const data = await res.json(); + + tokenCache.accessToken = data.access_token; + tokenCache.expiresAt = now + data.expires_in * 1000; + return tokenCache.accessToken; +} + +app.post('/api/meetings', async (req, res) => { + try { + const token = await getAccessToken(); + const hostUserId = process.env.ZOOM_HOST_USER_ID; + if (!hostUserId) { + return res.status(500).json({ error: 'missing_host_user_id', detail: 'Set ZOOM_HOST_USER_ID for S2S meeting creation' }); + } + + const body = { + topic: req.body.topic || 'Auto Meeting', + type: 2, + start_time: req.body.start_time, + duration: req.body.duration || 30, + timezone: req.body.timezone || 'UTC', + }; + + const z = await fetch(`https://api.zoom.us/v2/users/${encodeURIComponent(hostUserId)}/meetings`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + const data = await z.json(); + if (!z.ok) return res.status(z.status).json(data); + + meetingStore.set(String(data.id), { status: 'scheduled', topic: data.topic, participants: 0 }); + return res.status(201).json(data); + } catch (err) { + return res.status(500).json({ error: 'create_meeting_failed', detail: String(err) }); + } +}); + +function verifySignature(req) { + const ts = req.headers['x-zm-request-timestamp']; + const sig = req.headers['x-zm-signature']; + const msg = `v0:${ts}:${req.rawBody || ''}`; + const expected = `v0=${crypto.createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET).update(msg).digest('hex')}`; + return sig === expected; +} + +app.post('/webhooks/zoom', (req, res) => { + if (req.body.event === 'endpoint.url_validation') { + const plainToken = req.body.payload?.plainToken; + const encryptedToken = crypto.createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET).update(plainToken).digest('hex'); + return res.json({ plainToken, encryptedToken }); + } + + if (!verifySignature(req)) return res.status(401).send('invalid_signature'); + + const evt = req.body.event; + const id = String(req.body.payload?.object?.id || ''); + if (id && !meetingStore.has(id)) meetingStore.set(id, { status: 'unknown', participants: 0 }); + + const state = meetingStore.get(id); + if (state) { + if (evt === 'meeting.started') state.status = 'in_progress'; + if (evt === 'meeting.ended') state.status = 'ended'; + if (evt === 'meeting.participant_joined') state.participants += 1; + if (evt === 'meeting.participant_left') state.participants = Math.max(0, state.participants - 1); + } + + return res.status(200).send('ok'); +}); + +app.listen(process.env.PORT || 3001, () => { + console.log('orchestrator listening'); +}); +``` + +## Failure Handling Minimums + +- REST call failures: retry with jitter for `429/5xx`; do not retry `4xx` business errors blindly. +- Webhook ingestion: always return `200` after durable enqueue or local persistence. +- Idempotency: dedupe by `event_id` or (`event`,`event_ts`,`meeting_uuid`) composite key. +- Reconciliation: periodic REST poll to repair missed webhook events. + +## Environment Variables + +- `ZOOM_ACCOUNT_ID` +- `ZOOM_CLIENT_ID` +- `ZOOM_CLIENT_SECRET` +- `ZOOM_HOST_USER_ID` (required for S2S meeting creation; do not rely on `me`) +- `ZOOM_WEBHOOK_SECRET` +- `PORT` diff --git a/plugins/zoom-developers/skills/general/references/community-repos.md b/plugins/zoom-developers/skills/general/references/community-repos.md new file mode 100644 index 00000000..b3f2be82 --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/community-repos.md @@ -0,0 +1,157 @@ +# Official Zoom Sample Repositories + +Curated list of official repositories from Zoom for development. Organized by product/SDK. + +--- + +## Meeting SDK + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [meetingsdk-web-sample](https://github.com/zoom/meetingsdk-web-sample) | 643 | Web SDK sample - Component View and Client View | +| [meetingsdk-web](https://github.com/zoom/meetingsdk-web) | 324 | NPM package for embedding meetings | +| [meetingsdk-react-sample](https://github.com/zoom/meetingsdk-react-sample) | 177 | React integration sample | +| [meetingsdk-auth-endpoint-sample](https://github.com/zoom/meetingsdk-auth-endpoint-sample) | 124 | Generate Meeting SDK JWT signatures | +| [meetingsdk-angular-sample](https://github.com/zoom/meetingsdk-angular-sample) | 60 | Angular integration sample | +| [meetingsdk-vuejs-sample](https://github.com/zoom/meetingsdk-vuejs-sample) | 42 | Vue.js integration sample | +| [meetingsdk-javascript-sample](https://github.com/zoom/meetingsdk-javascript-sample) | 41 | Vanilla JavaScript sample | +| [meetingsdk-headless-linux-sample](https://github.com/zoom/meetingsdk-headless-linux-sample) | 3 | Headless Linux bot with Docker | + +--- + +## Video SDK + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [videosdk-web-sample](https://github.com/zoom/videosdk-web-sample) | 137 | Web Video SDK sample | +| [videosdk-web](https://github.com/zoom/videosdk-web) | 56 | NPM package for custom video | +| [videosdk-auth-endpoint-sample](https://github.com/zoom/videosdk-auth-endpoint-sample) | 23 | Generate Video SDK JWT signatures | +| [videosdk-zoom-ui-toolkit-web](https://github.com/zoom/videosdk-zoom-ui-toolkit-web) | 17 | Prebuilt video chat UI | +| [videosdk-zoom-ui-toolkit-react-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-react-sample) | 17 | UI Toolkit in React | +| [videosdk-nextjs-quickstart](https://github.com/zoom/videosdk-nextjs-quickstart) | 16 | Next.js integration | +| [videosdk-zoom-ui-toolkit-javascript-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-javascript-sample) | 11 | UI Toolkit in vanilla JS | +| [VideoSDK-Web-Telehealth](https://github.com/zoom/VideoSDK-Web-Telehealth) | 11 | Telehealth starter kit | +| [videosdk-workshop](https://github.com/zoom/videosdk-workshop) | 9 | Workshop project | +| [videosdk-s3-cloud-recordings](https://github.com/zoom/videosdk-s3-cloud-recordings) | 8 | Auto-upload recordings to S3 | +| [videosdk-web-helloworld](https://github.com/zoom/videosdk-web-helloworld) | 4 | Minimal hello world | +| [videosdk-zoom-ui-toolkit-angular-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-angular-sample) | 4 | UI Toolkit in Angular | +| [videosdk-zoom-ui-toolkit-vuejs-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-vuejs-sample) | 3 | UI Toolkit in Vue.js | +| [videosdk-vue-nuxt-quickstart](https://github.com/zoom/videosdk-vue-nuxt-quickstart) | 1 | Vue/Nuxt quickstart | +| [videosdk-electron-sample](https://github.com/zoom/videosdk-electron-sample) | 1 | Electron sample | +| [videosdk-linux-raw-recording-sample](https://github.com/zoom/videosdk-linux-raw-recording-sample) | - | Linux headless raw data capture | + +--- + +## REST API + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [oauth-sample-app](https://github.com/zoom/oauth-sample-app) | 91 | Node.js OAuth sample | +| [server-to-server-oauth-starter-api](https://github.com/zoom/server-to-server-oauth-starter-api) | 54 | S2S OAuth starter API | +| [api](https://github.com/zoom/api) | 44 | API v2 documentation | +| [user-level-oauth-starter](https://github.com/zoom/user-level-oauth-starter) | 27 | User-level OAuth starter | +| [server-to-server-oauth-token](https://github.com/zoom/server-to-server-oauth-token) | 15 | S2S token generation utility | +| [rivet-javascript](https://github.com/zoom/rivet-javascript) | 13 | Rivet API library (auth + webhooks + API) | +| [websocket-js-sample](https://github.com/zoom/websocket-js-sample) | 5 | WebSocket connection demo | +| [websocket-redis-example](https://github.com/zoom/websocket-redis-example) | 4 | WebSocket with Redis | +| [server-to-server-python-sample](https://github.com/zoom/server-to-server-python-sample) | 4 | Python S2S OAuth sample | +| [task-manager-sample](https://github.com/zoom/task-manager-sample) | 3 | Unified build flow showcase | +| [rivet-javascript-sample](https://github.com/zoom/rivet-javascript-sample) | 3 | Rivet standup bot sample | +| [sample-registration-app](https://github.com/zoom/sample-registration-app) | 3 | Webinar registration with rate limits | + +--- + +## Webhooks + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [webhook-sample](https://github.com/zoom/webhook-sample) | 34 | Receive Zoom webhooks (Node.js) | +| [zoom-webhook-verification-headers](https://github.com/zoom/zoom-webhook-verification-headers) | - | Custom header auth + webhook validation | +| [webhook-to-postgres](https://github.com/zoom/webhook-to-postgres) | 5 | Store webhooks in PostgreSQL | +| [Go-Webhooks](https://github.com/zoom/Go-Webhooks) | - | Go/Fiber webhook listener | + +--- + +## Zoom Apps SDK + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [zoomapps-sample-js](https://github.com/zoom/zoomapps-sample-js) | 66 | Hello World Zoom App (vanilla JS) | +| [zoomapps-advancedsample-react](https://github.com/zoom/zoomapps-advancedsample-react) | 55 | Advanced React sample | +| [appssdk](https://github.com/zoom/appssdk) | 49 | Zoom Apps SDK NPM package | +| [zoomapps-texteditor-vuejs](https://github.com/zoom/zoomapps-texteditor-vuejs) | 16 | Collaborate Mode text editor | +| [zoomapps-customlayout-js](https://github.com/zoom/zoomapps-customlayout-js) | 16 | Immersive Mode / Layers API | +| [zoomapps-workshop-sample](https://github.com/zoom/zoomapps-workshop-sample) | 6 | Getting started workshop | +| [zoomapps-serverless-vuejs](https://github.com/zoom/zoomapps-serverless-vuejs) | 6 | Serverless on Firebase | +| [zoomapps-cameramode-vuejs](https://github.com/zoom/zoomapps-cameramode-vuejs) | 6 | Camera Mode + Immersive Mode | +| [arlo-meeting-assistant](https://github.com/zoom/arlo-meeting-assistant) | 2 | RTMS-powered meeting assistant | +| [meetingbot-recall-sample](https://github.com/zoom/meetingbot-recall-sample) | 2 | Meeting bot with Recall.ai + LLM analysis | + +--- + +## RTMS (Real-Time Media Streams) + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [zoom-rtms](https://github.com/zoom/rtms) | 29 | Cross-platform RTMS wrapper (Node.js, Python, Go) | +| [rtms-samples](https://github.com/zoom/rtms-samples) | 22 | Official RTMS sample apps | +| [rtms-developer-preview-js](https://github.com/zoom/rtms-developer-preview-js) | 3 | Developer preview hello world | +| [rtms-sdk-cpp](https://github.com/zoom/rtms-sdk-cpp) | 2 | C++ RTMS SDK (librtmsdk) | +| [rtms-meeting-assistant-starter-kit](https://github.com/zoom/rtms-meeting-assistant-starter-kit) | 1 | Meeting assistant starter kit | +| [rtms-quickstart-js](https://github.com/zoom/rtms-quickstart-js) | 1 | Node.js quickstart | +| [zoom_rtms_langchain_sample](https://github.com/zoom/zoom_rtms_langchain_sample) | 1 | LangChain + transcripts for action items | + +--- + +## Team Chat & Chatbots + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [unsplash-chatbot](https://github.com/zoom/unsplash-chatbot) | 19 | Send Unsplash photos in Team Chat | +| [node.js-chatbot](https://github.com/zoom/node.js-chatbot) | 18 | Node.js chatbot library | +| [vote-chatbot](https://github.com/zoom/vote-chatbot) | 10 | Voting bot for Team Chat | +| [catbot](https://github.com/zoom/catbot) | 9 | Cat photo bot | +| [node.js-chatbot-cli](https://github.com/zoom/node.js-chatbot-cli) | 8 | Chatbot CLI tool | +| [Zoom-Chat-Neural-Search-Assistant-Sample](https://github.com/zoom/Zoom-Chat-Neural-Search-Assistant-Sample) | 2 | Cerebras + Exa search bot | +| [zoom-team-chat-shortcut-sample](https://github.com/zoom/zoom-team-chat-shortcut-sample) | 1 | Recording management shortcut | +| [zoom-teams-chat-snowflake-sample](https://github.com/zoom/zoom-teams-chat-snowflake-sample) | 1 | Snowflake + Cortex integration | +| [zoom-erp-chatbot-sample](https://github.com/zoom/zoom-erp-chatbot-sample) | 1 | Oracle ERP integration | +| [chatbot-nodejs-quickstart](https://github.com/zoom/chatbot-nodejs-quickstart) | - | Node.js chatbot quickstart | +| [chatbot-python-sample](https://github.com/zoom/chatbot-python-sample) | - | Python chatbot with threading | + +--- + +## Cobrowse SDK + +### Official Samples (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [CobrowseSDK-Quickstart](https://github.com/zoom/CobrowseSDK-Quickstart) | 1 | Cobrowse SDK quickstart | +| [cobrowsesdk-auth-endpoint-sample](https://github.com/zoom/cobrowsesdk-auth-endpoint-sample) | 2 | JWT generation for Cobrowse | + +--- + +## Tooling & Utilities + +### Official Tools (by Zoom) + +| Repository | Stars | Description | +|------------|-------|-------------| +| [probesdk-web](https://github.com/zoom/probesdk-web) | 3 | Test device/network/server connection | + +--- diff --git a/plugins/zoom-developers/skills/general/references/distributed-meeting-fallback-architecture.md b/plugins/zoom-developers/skills/general/references/distributed-meeting-fallback-architecture.md new file mode 100644 index 00000000..4002ff0e --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/distributed-meeting-fallback-architecture.md @@ -0,0 +1,459 @@ +# Distributed Meeting Creation and Event Processing with Fallbacks + +Use this architecture for high-volume meeting creation with resilient event processing. + +## Core Architectural Considerations + +1. **Separation of planes** +- Command plane: REST meeting creation/update APIs. +- Event plane: webhook ingestion and async projection. + +2. **Idempotency and dedupe** +- Require caller-provided idempotency key per create request. +- Dedupe webhook events by stable event key. + +3. **Token isolation** +- Central token broker with distributed lock (Redis/Postgres advisory lock). + +4. **Backpressure and queueing** +- Queue all webhook events and meeting commands. +- Use DLQ for poison messages. + +5. **Fallback mechanisms** +- Retry with exponential backoff + jitter for retriable failures (`429/5xx/network`). +- Circuit breaker around Zoom API dependency. +- Reconciliation poller when webhook delivery is delayed/missed. + +## Reference Topology + +```text +API Gateway + -> Meeting Command Service + -> Idempotency Store (Redis/Postgres) + -> Token Broker + -> Zoom REST API + -> Outbox/Event Bus + +Webhook Ingress + -> Signature Verify + URL Validation + -> Queue (Kafka/SQS/Rabbit) + -> Projection Workers + -> Meeting State Store + +Recovery Services + -> Retry Worker + -> Reconciliation Poller (REST pull) + -> Dead Letter Reprocessor +``` + +## Command Plane Example (Meeting Creation Service) + +```ts +type CreateMeetingInput = { + idempotencyKey: string; + hostUserId: string; // explicit user for S2S + topic: string; + startTime: string; + duration: number; +}; + +type QueuePublisher = { publish: (topic: string, payload: object) => Promise }; +type IdempotencyStore = { + get: (key: string) => Promise; + put: (key: string, value: object, ttlSec: number) => Promise; +}; + +export async function createMeetingCommand( + input: CreateMeetingInput, + deps: { + tokenBroker: { getToken: () => Promise }; + idempotency: IdempotencyStore; + queue: QueuePublisher; + breaker: CircuitBreaker; + }, +) { + const cached = await deps.idempotency.get(input.idempotencyKey); + if (cached) return cached; + + if (!deps.breaker.canCall()) { + // degraded mode: queue command for delayed processing + await deps.queue.publish('meeting.create.delayed', input); + return { accepted: true, mode: 'degraded_queued' }; + } + + const op = async () => { + const token = await deps.tokenBroker.getToken(); + const res = await fetch( + `https://api.zoom.us/v2/users/${encodeURIComponent(input.hostUserId)}/meetings`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + topic: input.topic, + type: 2, + start_time: input.startTime, + duration: input.duration, + }), + }, + ); + if (!res.ok) { + const err = new Error(`zoom_create_failed:${res.status}`); + (err as any).status = res.status; + throw err; + } + return res.json(); + }; + + try { + const created = await retry( + op, + { retries: 4, baseMs: 300, maxMs: 5000 }, + (e) => [429, 500, 502, 503, 504].includes((e as any).status), + ); + + deps.breaker.recordSuccess(); + await deps.idempotency.put(input.idempotencyKey, created, 3600); + await deps.queue.publish('meeting.created', { meetingId: created.id, hostUserId: input.hostUserId }); + return created; + } catch (e) { + deps.breaker.recordFailure(); + throw e; + } +} +``` + +## Event Plane Example (Webhook Ingress + Queue + Projection) + +```ts +import crypto from 'crypto'; + +export function verifyWebhook(rawBody: string, ts: string, sig: string, secret: string): boolean { + // reject stale requests to reduce replay risk + const nowSec = Math.floor(Date.now() / 1000); + const tsSec = Number(ts || 0); + if (!Number.isFinite(tsSec) || Math.abs(nowSec - tsSec) > 300) return false; + + const msg = `v0:${ts}:${rawBody}`; + const expected = `v0=${crypto.createHmac('sha256', secret).update(msg).digest('hex')}`; + return sig === expected; +} + +export async function ingestWebhook(req: any, res: any, queue: QueuePublisher, secret: string) { + if (req.body.event === 'endpoint.url_validation') { + const plainToken = req.body.payload?.plainToken; + const encryptedToken = crypto.createHmac('sha256', secret).update(plainToken).digest('hex'); + return res.json({ plainToken, encryptedToken }); + } + + const ts = String(req.headers['x-zm-request-timestamp'] || ''); + const sig = String(req.headers['x-zm-signature'] || ''); + const raw = String(req.rawBody || ''); + if (!verifyWebhook(raw, ts, sig, secret)) return res.status(401).send('invalid_signature'); + + try { + // durable write first, then ack + await queue.publish('zoom.webhook.raw', req.body); + return res.status(200).send('ok'); + } catch { + // non-200 triggers Zoom retry for at-least-once delivery + return res.status(503).send('queue_unavailable'); + } +} + +export async function projectEvent(evt: any, stateStore: any, dedupe: IdempotencyStore) { + const dedupeKey = `${evt.event}:${evt.event_ts}:${evt.payload?.object?.uuid || evt.payload?.object?.id || 'unknown'}`; + const seen = await dedupe.get(dedupeKey); + if (seen) return; + + const id = String(evt.payload?.object?.id || ''); + const current = (await stateStore.get(id)) || { status: 'unknown', participants: 0, lastEventTs: 0 }; + if (evt.event_ts < current.lastEventTs) { + await dedupe.put(dedupeKey, { stale: true }, 86400); + return; + } // stale event guard + + if (evt.event === 'meeting.started') current.status = 'in_progress'; + if (evt.event === 'meeting.ended') current.status = 'ended'; + if (evt.event === 'meeting.participant_joined') current.participants += 1; + if (evt.event === 'meeting.participant_left') current.participants = Math.max(0, current.participants - 1); + current.lastEventTs = evt.event_ts; + + await stateStore.put(id, current); + await dedupe.put(dedupeKey, { ok: true }, 86400); +} +``` + +### Express raw-body setup (required for signature verification) + +```ts +app.use(express.json({ + verify: (req: any, _res, buf) => { + req.rawBody = buf.toString('utf8'); + }, +})); +``` + +## Retry + Circuit Breaker Example (TypeScript) + +```ts +type RetryOptions = { + retries: number; + baseMs: number; + maxMs: number; +}; + +function sleep(ms: number) { + return new Promise((r) => setTimeout(r, ms)); +} + +function backoff(attempt: number, baseMs: number, maxMs: number) { + const exp = Math.min(maxMs, baseMs * 2 ** attempt); + const jitter = Math.floor(Math.random() * Math.min(250, exp / 4)); + return exp + jitter; +} + +export async function retry(fn: () => Promise, opts: RetryOptions, isRetriable: (e: any) => boolean): Promise { + let lastErr: any; + for (let i = 0; i <= opts.retries; i += 1) { + try { + return await fn(); + } catch (e) { + lastErr = e; + if (i === opts.retries || !isRetriable(e)) break; + await sleep(backoff(i, opts.baseMs, opts.maxMs)); + } + } + throw lastErr; +} + +export class CircuitBreaker { + private failures = 0; + private openUntil = 0; + + constructor(private threshold = 5, private coolDownMs = 15_000) {} + + canCall() { + return Date.now() > this.openUntil; + } + + recordSuccess() { + this.failures = 0; + } + + recordFailure() { + this.failures += 1; + if (this.failures >= this.threshold) { + this.openUntil = Date.now() + this.coolDownMs; + } + } +} +``` + +## Reconciliation Poller Example (Fallback for Missed Events) + +```ts +export async function reconcileMeetingState( + meetingId: string, + hostUserId: string, + deps: { + tokenBroker: { getToken: () => Promise }; + stateStore: { get: (id: string) => Promise; put: (id: string, v: any) => Promise }; + }, +) { + const token = await deps.tokenBroker.getToken(); + const res = await fetch(`https://api.zoom.us/v2/meetings/${encodeURIComponent(meetingId)}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) return; + + const apiState = await res.json(); + const projected = (await deps.stateStore.get(meetingId)) || {}; + const merged = { + ...projected, + status: apiState.status || projected.status, + topic: apiState.topic || projected.topic, + hostId: hostUserId, + reconciledAt: Date.now(), + }; + await deps.stateStore.put(meetingId, merged); +} +``` + +## Distributed Coordination and Load-Balancing Considerations + +- Partition command/event streams by `meetingId` or `hostUserId` so all updates for one meeting land on the same consumer shard. +- Use a distributed lock for shared singleton jobs (token refresh rotation, reconciliation scheduler leader). +- Keep webhook ingress stateless so horizontal autoscaling is safe behind L4/L7 load balancers. +- Apply queue consumer concurrency limits to protect downstream Zoom API quotas. + +### Redis-Style Lock Skeleton + +```ts +export async function withLock(lock: { acquire: (k: string, ttlMs: number) => Promise; release: (k: string) => Promise }, key: string, fn: () => Promise) { + const got = await lock.acquire(key, 10_000); + if (!got) return; + try { + await fn(); + } finally { + await lock.release(key); + } +} +``` + +## Token Broker Example (Cached Refresh + Distributed Lock) + +```ts +type CachedToken = { accessToken: string; expiresAtMs: number }; + +export class TokenBroker { + constructor( + private cache: { get: (k: string) => Promise; put: (k: string, v: CachedToken, ttlSec: number) => Promise }, + private lock: { acquire: (k: string, ttlMs: number) => Promise; release: (k: string) => Promise }, + private fetchToken: () => Promise<{ access_token: string; expires_in: number }>, + ) {} + + async getToken(): Promise { + const cached = await this.cache.get('zoom:s2s-token'); + const now = Date.now(); + if (cached && cached.expiresAtMs - now > 60_000) { + return cached.accessToken; + } + + const gotLock = await this.lock.acquire('zoom:s2s-token:refresh', 10_000); + if (!gotLock) { + await sleep(200); + const retryCached = await this.cache.get('zoom:s2s-token'); + if (retryCached && retryCached.expiresAtMs - Date.now() > 30_000) { + return retryCached.accessToken; + } + throw new Error('token_refresh_lock_contention'); + } + + try { + const fresh = await this.fetchToken(); + const value = { + accessToken: fresh.access_token, + expiresAtMs: Date.now() + fresh.expires_in * 1000, + }; + await this.cache.put('zoom:s2s-token', value, Math.max(60, fresh.expires_in - 90)); + return value.accessToken; + } finally { + await this.lock.release('zoom:s2s-token:refresh'); + } + } +} +``` + +## High-Volume Create Worker (Concurrency + Rate Protection) + +```ts +type CreateJob = CreateMeetingInput & { attempts: number }; + +class TokenBucket { + private tokens: number; + private lastRefill = Date.now(); + + constructor(private readonly capacity: number, private readonly refillPerSec: number) { + this.tokens = capacity; + } + + async take() { + while (true) { + const now = Date.now(); + const elapsedSec = (now - this.lastRefill) / 1000; + this.tokens = Math.min(this.capacity, this.tokens + elapsedSec * this.refillPerSec); + this.lastRefill = now; + if (this.tokens >= 1) { + this.tokens -= 1; + return; + } + await sleep(100); + } + } +} + +export async function runCreateWorker( + queue: { receiveBatch: (n: number) => Promise; ack: (job: CreateJob) => Promise; retryLater: (job: CreateJob, delayMs: number) => Promise }, + deps: { + createMeeting: (job: CreateJob) => Promise; + breaker: CircuitBreaker; + limiter: TokenBucket; + }, + concurrency = 8, +) { + while (true) { + const jobs = await queue.receiveBatch(concurrency); + await Promise.all(jobs.map(async (job) => { + if (!deps.breaker.canCall()) { + await queue.retryLater(job, 30_000); + return; + } + + try { + await deps.limiter.take(); + await deps.createMeeting(job); + deps.breaker.recordSuccess(); + await queue.ack(job); + } catch (e: any) { + deps.breaker.recordFailure(); + const delay = backoff(job.attempts, 500, 60_000); + await queue.retryLater({ ...job, attempts: job.attempts + 1 }, delay); + } + })); + } +} +``` + +## Reconciliation Scheduler (Lag Detection + Leader Election) + +```ts +export async function reconcileLaggingMeetings( + deps: { + lock: { acquire: (k: string, ttlMs: number) => Promise; release: (k: string) => Promise }; + stateStore: { listLagging: (ageMs: number, limit: number) => Promise> }; + reconcile: (meetingId: string, hostUserId: string) => Promise; + }, +) { + await withLock(deps.lock, 'zoom:reconcile:leader', async () => { + const lagging = await deps.stateStore.listLagging(5 * 60_000, 250); + for (const item of lagging) { + await deps.reconcile(item.meetingId, item.hostUserId); + } + }); +} +``` + +## DLQ Replay Worker + +```ts +export async function replayDlq( + dlq: { receiveBatch: (n: number) => Promise; ack: (msg: any) => Promise; moveBack: (topic: string, msg: any) => Promise }, + topic = 'meeting.create.delayed', +) { + const failed = await dlq.receiveBatch(100); + for (const msg of failed) { + await dlq.moveBack(topic, { ...msg, replayedAt: Date.now() }); + await dlq.ack(msg); + } +} +``` + +## Distributed State Rules + +- Meeting state is event-sourced or projection-based, not only request-response based. +- Persist `last_seen_event_ts` and status transitions to handle out-of-order events. +- Add monotonic transition guards (e.g., do not move `ended -> in_progress`). + +## Fallback Matrix + +| Failure | Primary response | Fallback | +|---|---|---| +| Token refresh failure | Retry token exchange | Fail fast + alert + pause new create requests | +| REST `429` / `5xx` | Retry w/ backoff | Queue command for delayed retry | +| Webhook verification failure | Reject `401` | Alert security pipeline | +| Webhook processor down | Buffer in queue | DLQ + replay job | +| Missing webhook event | Detect via reconciliation lag | REST poll and repair projection | +| Dependency outage | Open circuit breaker | Serve degraded status + queued commands | diff --git a/plugins/zoom-developers/skills/general/references/environment-variables.md b/plugins/zoom-developers/skills/general/references/environment-variables.md new file mode 100644 index 00000000..b48dc794 --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/environment-variables.md @@ -0,0 +1,38 @@ +# Cross-Product Environment Variables (Hub) + +Use this file as a normalization map. Product-specific details are maintained in each product skill reference. + +## Common `.env` keys + +| Variable | Typical products | Where to find | +| --- | --- | --- | +| `ZOOM_CLIENT_ID` | OAuth, REST API, Team Chat, WebSockets, RTMS (OAuth mode), Contact Center APIs | Zoom Marketplace -> your app -> App Credentials | +| `ZOOM_CLIENT_SECRET` | OAuth, REST API, Team Chat, WebSockets, RTMS (OAuth mode), Contact Center APIs | Zoom Marketplace -> your app -> App Credentials | +| `ZOOM_ACCOUNT_ID` | Server-to-Server OAuth flows | Zoom Marketplace -> Server-to-Server OAuth app credentials | +| `ZOOM_REDIRECT_URI` | User-level OAuth apps | Zoom Marketplace -> OAuth redirect/allow list | +| `ZOOM_WEBHOOK_SECRET` / `WEBHOOK_SECRET_TOKEN` | Webhooks and event validation | Zoom Marketplace -> Event Subscriptions -> Secret Token | +| `ZOOM_SDK_KEY` / `ZOOM_SDK_SECRET` | Meeting SDK or SDK-based products | Zoom Marketplace -> SDK app credentials | +| `ZOOM_VIDEO_SDK_KEY` / `ZOOM_VIDEO_SDK_SECRET` | Video SDK and UI Toolkit | Zoom Marketplace -> Video SDK app credentials | +| `PROBE_JS_URL` / `PROBE_WASM_URL` | Probe SDK | Your app/CDN hosted Probe SDK assets (or bundler output paths) | +| `PROBE_DOMAIN` / `PROBE_CONNECT_TIMEOUT_MS` | Probe SDK | Product policy + Probe SDK diagnostics configuration | + +## Product references + +- [../../zoom-apps-sdk/references/environment-variables.md](../../zoom-apps-sdk/references/environment-variables.md) +- [../../cobrowse-sdk/references/environment-variables.md](../../cobrowse-sdk/references/environment-variables.md) +- [../../meeting-sdk/references/environment-variables.md](../../meeting-sdk/references/environment-variables.md) +- [../../oauth/references/environment-variables.md](../../oauth/references/environment-variables.md) +- [../../rest-api/references/environment-variables.md](../../rest-api/references/environment-variables.md) +- [../../rtms/references/environment-variables.md](../../rtms/references/environment-variables.md) +- [../../team-chat/references/environment-variables.md](../../team-chat/references/environment-variables.md) +- [../../ui-toolkit/references/environment-variables.md](../../ui-toolkit/references/environment-variables.md) +- [../../video-sdk/references/environment-variables.md](../../video-sdk/references/environment-variables.md) +- [../../webhooks/references/environment-variables.md](../../webhooks/references/environment-variables.md) +- [../../websockets/references/environment-variables.md](../../websockets/references/environment-variables.md) +- [../../contact-center/references/environment-variables.md](../../contact-center/references/environment-variables.md) +- [../../phone/references/environment-variables.md](../../phone/references/environment-variables.md) +- [../../probe-sdk/references/environment-variables.md](../../probe-sdk/references/environment-variables.md) + +## Probe SDK note + +- Probe SDK core diagnostics do not require Zoom OAuth/Marketplace credentials. diff --git a/plugins/zoom-developers/skills/general/references/full-guide.md b/plugins/zoom-developers/skills/general/references/full-guide.md new file mode 100644 index 00000000..8b724dfa --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/full-guide.md @@ -0,0 +1,74 @@ +# Zoom General Cross-Product Guide + +Background reference for cross-product Zoom developer questions. Prefer the workflow skills first, then use this file for shared platform guidance and routing detail. + +## Choose Your Path + +| I want to... | Use this skill | +|---|---| +| Build deterministic automation, account management, reporting, or scheduled jobs | [rest-api](../../rest-api/SKILL.md) | +| Receive event notifications over HTTP | [webhooks](../../webhooks/SKILL.md) | +| Receive event notifications over WebSocket | [websockets](../../websockets/SKILL.md) | +| Embed Zoom meetings into an app | [meeting-sdk](../../meeting-sdk/SKILL.md) | +| Build custom video experiences | [video-sdk](../../video-sdk/SKILL.md) | +| Build an app that runs inside the Zoom client | [zoom-apps-sdk](../../zoom-apps-sdk/SKILL.md) | +| Access live meeting media or transcripts | [rtms](../../rtms/SKILL.md) | +| Transcribe uploaded or stored media | [scribe](../../scribe/SKILL.md) | +| Enable collaborative browsing for support | [cobrowse-sdk](../../cobrowse-sdk/SKILL.md) | +| Build Contact Center apps and channel integrations | [contact-center](../../contact-center/SKILL.md) | +| Build Virtual Agent web or mobile chatbot experiences | [virtual-agent](../../virtual-agent/SKILL.md) | +| Build Zoom Phone integrations | [phone](../../phone/SKILL.md) | +| Build Team Chat apps and integrations | [team-chat](../../team-chat/SKILL.md) | +| Build server-side integrations with Rivet | [rivet-sdk](../../rivet-sdk/SKILL.md) | +| Run browser, device, or network preflight diagnostics | [probe-sdk](../../probe-sdk/SKILL.md) | +| Add prebuilt UI components for Video SDK | [ui-toolkit](../../ui-toolkit/SKILL.md) | +| Implement OAuth authentication | [oauth](../../oauth/SKILL.md) | + +## Routing Rules + +- If the user asks for meeting embed or join behavior, route to Meeting SDK before REST. +- If the user asks for a fully custom session product, route to Video SDK. +- If the user asks for account, user, meeting, recording, reporting, or scheduled automation, route to REST API. +- If the user asks for event ingestion, choose Webhooks for HTTP delivery and WebSockets for lower-latency persistent connections. +- If the user asks for live media, raw streams, or live transcripts, route to RTMS and pair it with Meeting SDK or REST only when needed. +- If the user asks for an app inside the Zoom client, route to Zoom Apps SDK. +- If auth, scopes, redirect handling, or token refresh are involved, chain OAuth guidance. + +## Webhooks vs WebSockets + +| Aspect | Webhooks | WebSockets | +|---|---|---| +| Connection | HTTP POST to your endpoint | Persistent WebSocket | +| Latency | Higher | Lower | +| Public endpoint | Required | Not required in the same way | +| Setup | Simpler | More operationally complex | +| Best for | Most event ingestion | Low-latency or private-network event streams | + +## Common Use Cases + +| Use Case | Description | Skills Needed | +|---|---|---| +| [Meeting + Webhooks + OAuth Refresh](../references/meeting-webhooks-oauth-refresh-orchestration.md) | Create a meeting, process real-time updates, and refresh OAuth tokens safely | [rest-api](../../rest-api/SKILL.md) + [oauth](../../oauth/SKILL.md) + [webhooks](../../webhooks/SKILL.md) | +| [Scribe Transcription Pipeline](../use-cases/scribe-transcription-pipeline.md) | Transcribe uploaded files or S3 archives | [scribe](../../scribe/SKILL.md) | +| [Custom Meeting UI (Web)](../use-cases/custom-meeting-ui-web.md) | Build a custom video UI for a real Zoom meeting | [meeting-sdk](../../meeting-sdk/SKILL.md) | +| [Meeting Automation](../use-cases/meeting-automation.md) | Schedule, update, and delete meetings programmatically | [rest-api](../../rest-api/SKILL.md) | +| [Meeting Bots](../use-cases/meeting-bots.md) | Build bots that join meetings for AI, transcription, or recording | [meeting-sdk/linux](../../meeting-sdk/linux/SKILL.md) + [rest-api](../../rest-api/SKILL.md) | +| [Recording & Transcription](../use-cases/recording-transcription.md) | Download recordings and get transcripts | [webhooks](../../webhooks/SKILL.md) + [rest-api](../../rest-api/SKILL.md) | +| [Real-Time Media Streams](../use-cases/real-time-media-streams.md) | Access live audio, video, and transcript streams | [rtms](../../rtms/SKILL.md) | +| [In-Meeting Apps](../use-cases/in-meeting-apps.md) | Build apps that run inside Zoom meetings | [zoom-apps-sdk](../../zoom-apps-sdk/SKILL.md) | +| [Zoom Phone Smart Embed CRM Integration](../use-cases/zoom-phone-smart-embed-crm.md) | Build CRM dialer and call logging flows | [phone](../../phone/SKILL.md) + [oauth](../../oauth/SKILL.md) | +| [Rivet Event-Driven API Orchestrator](../use-cases/rivet-event-driven-api-orchestrator.md) | Combine webhooks and API actions through Rivet | [rivet-sdk](../../rivet-sdk/SKILL.md) + [oauth](../../oauth/SKILL.md) + [rest-api](../../rest-api/SKILL.md) | + +## Detailed References + +- [Authentication](authentication.md) +- [App types](app-types.md) +- [Scopes](scopes.md) +- [Marketplace](marketplace.md) +- [Query routing playbook](query-routing-playbook.md) +- [Automatic REST and webhook chaining](automatic-skill-chaining-rest-webhooks.md) +- [Meeting webhook and OAuth refresh orchestration](meeting-webhooks-oauth-refresh-orchestration.md) +- [Distributed meeting fallback architecture](distributed-meeting-fallback-architecture.md) +- [Community repositories](community-repos.md) +- [SDK upgrade guide](sdk-upgrade-guide.md) +- [SDK logs troubleshooting](sdk-logs-troubleshooting.md) diff --git a/plugins/zoom-developers/skills/general/references/interview-answer-routing.md b/plugins/zoom-developers/skills/general/references/interview-answer-routing.md new file mode 100644 index 00000000..715d2ca5 --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/interview-answer-routing.md @@ -0,0 +1,20 @@ +# Interview Answer: Routing with zoom-general + +Use `zoom-general` as the triage layer, then route implementation to specialized skills. + +## Short answer + +1. Classify the query in `zoom-general` by product intent, platform, and integration pattern. +2. Route to the minimum specialized skills: +- Auth/scopes -> `zoom-oauth` +- API operations -> `zoom-rest-api` +- Embedded meetings -> `zoom-meeting-sdk` +- Custom video experiences -> `zoom-video-sdk` +- Event delivery -> `zoom-webhooks` or `zoom-websockets` +- Live media/transcripts -> `zoom-rtms` +3. Execute in sequence: `zoom-general` -> auth -> core product -> events/media. +4. If ambiguous, ask one disambiguation question before locking the chain. + +Canonical guidance and handoff structure: +- [Query Routing Playbook](query-routing-playbook.md) + diff --git a/plugins/zoom-developers/skills/general/references/known-limitations.md b/plugins/zoom-developers/skills/general/references/known-limitations.md new file mode 100644 index 00000000..3e79a594 --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/known-limitations.md @@ -0,0 +1,101 @@ +# Known Limitations & Quirks + +Common gotchas and limitations developers encounter. + +## Recording Limitations + +### Minimum Recording Duration + +**Recordings shorter than 3-5 seconds will NOT be saved.** + +This applies to: +- Cloud recordings +- Local recordings via SDK + +If you need to capture very short sessions, ensure the recording runs for at least 5 seconds. + +## API Limitations + +### Rate Limits + +See [Rate Limits](../../rest-api/references/rate-limits.md) for detailed information. + +Key points: +- Create/update meeting endpoints are **Heavy** (stricter limits) +- Response headers show remaining quota +- Implement exponential backoff for 429 errors + +### Error Code 0 + +**The enum value 0 often represents SUCCESS, not failure.** + +Always check the SDK enum values: +```cpp +// Example: Meeting SDK +SDKERR_SUCCESS = 0 // This is success! +SDKERR_UNKNOWN = 1 // This is an error +``` + +Don't assume 0 = error in your error handling. + +## Video SDK Web Limitations + +### Video Rendering Performance + +**Use ONE rendering control for all videos, not one per participant.** + +Multiple rendering controls severely degrade performance. See [Video SDK Web](../../video-sdk/web/references/web.md#video-rendering-best-practices). + +### SharedArrayBuffer + +Some features require SharedArrayBuffer headers: +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +As of v1.11.2, this is elective for basic functionality. + +## SDK Signature Limitations + +### Minimum Token Validity + +Zoom may require `exp - iat >= 2 hours`. + +**Workaround:** Set `iat` in the past: +```javascript +const iat = Math.floor(Date.now() / 1000) - 7200; // 2 hours ago +const exp = Math.floor(Date.now() / 1000) + 10; // 10 seconds from now +``` + +This gives you a short-lived token while satisfying the validity requirement. + +## SDK Download + +### Marketplace Sign-in Required + +Meeting SDK and Video SDK (except Web npm packages) must be downloaded from [Marketplace](https://marketplace.zoom.us/) after signing in. + +They are not available on public package managers for native platforms. + +## Platform-Specific + +### iOS + +- Requires camera/microphone entitlements +- Background audio requires special configuration + +### Android + +- Requires runtime permissions for camera/mic +- ProGuard rules may be needed + +### Linux + +- Headless operation requires X virtual framebuffer (Xvfb) for some features +- Limited UI customization compared to other platforms + +## Resources + +- **Developer forum**: https://devforum.zoom.us/ (search for known issues) +- **Support**: https://devsupport.zoom.us/ diff --git a/plugins/zoom-developers/skills/general/references/marketplace.md b/plugins/zoom-developers/skills/general/references/marketplace.md new file mode 100644 index 00000000..7d634683 --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/marketplace.md @@ -0,0 +1,67 @@ +# Zoom App Marketplace + +Navigate the Zoom Marketplace developer portal. + +## Overview + +The [Zoom App Marketplace](https://marketplace.zoom.us/) is where you create, configure, and publish Zoom apps. + +## Getting Started + +1. Go to [marketplace.zoom.us](https://marketplace.zoom.us/) +2. Sign in with your Zoom account +3. Click **Develop** → **Build App** +4. Choose app type +5. Configure app settings + +## Portal Sections + +### Develop + +- **Build App** - Create new apps +- **Manage** - Edit existing apps +- **Logs** - View API and webhook logs + +### App Configuration + +| Section | Purpose | +|---------|---------| +| **App Credentials** | SDK Key/Secret, Client ID/Secret | +| **Scopes** | Configure OAuth permissions | +| **Feature** | Enable Meeting SDK, Video SDK, Webhooks | +| **Activation** | Make app installable | + +## SDK Downloads + +**Important:** Meeting SDK and Video SDK must be downloaded from Marketplace after signing in. They are not available on public package managers (except Web SDKs via npm). + +1. Go to your app's **Download** section +2. Select platform (iOS, Android, Windows, macOS, Linux) +3. Download SDK package + +## Credentials + +### OAuth Apps + +- **Client ID** - Public identifier +- **Client Secret** - Keep secret, server-side only + +### SDK Apps + +- **SDK Key** - Used in JWT payload +- **SDK Secret** - Used to sign JWT, keep secret + +## Publishing + +To publish to Marketplace: + +1. Complete app configuration +2. Submit for review +3. Address feedback +4. Get approved +5. Go live + +## Resources + +- **Marketplace**: https://marketplace.zoom.us/ +- **Developer docs**: https://developers.zoom.us/ diff --git a/plugins/zoom-developers/skills/general/references/meeting-webhooks-oauth-refresh-orchestration.md b/plugins/zoom-developers/skills/general/references/meeting-webhooks-oauth-refresh-orchestration.md new file mode 100644 index 00000000..588cf71a --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/meeting-webhooks-oauth-refresh-orchestration.md @@ -0,0 +1,173 @@ +# Meeting + Webhooks + OAuth Refresh Orchestration + +This guide implements one solution that handles all three simultaneously: +1. create meeting, +2. process webhook updates, +3. refresh OAuth tokens safely. + +## Direct Answer + +Use this skill chain: + +1. `zoom-general` to classify the request +2. `zoom-oauth` for token brokerage and refresh control +3. `zoom-rest-api` to create the meeting +4. `zoom-webhooks` to receive real-time updates + +Minimal flow: + +```text +client request + -> TokenBroker.getToken() + -> POST /v2/users/{userId}/meetings + -> persist meeting + idempotency key + -> Zoom sends webhooks to your ingress + -> verify signature + -> enqueue event + -> projection worker updates meeting state +``` + +Webhook subscription note: +- the receiver implementation lives in your app code +- the actual Zoom event subscription is configured at the Marketplace app level +- do not model webhook subscription enablement as a per-request runtime API step unless Zoom exposes a product-specific admin API for that exact surface + +## Skill Chain + +1. `zoom-general` +2. `zoom-oauth` +3. `zoom-rest-api` +4. `zoom-webhooks` + +## Component Design + +- `TokenBroker`: central access token cache + refresh lock. +- `MeetingService`: REST calls using broker. +- `WebhookIngress`: signature validation + URL validation + event enqueue. +- `ProjectionWorker`: applies events to meeting state. + +## Token Broker with Refresh Lock (TypeScript) + +```ts +type TokenState = { accessToken: string; expiresAt: number; refreshing?: Promise }; + +export class TokenBroker { + private state: TokenState = { accessToken: '', expiresAt: 0 }; + + constructor( + private accountId: string, + private clientId: string, + private clientSecret: string, + ) {} + + async getToken(): Promise { + const now = Date.now(); + if (this.state.accessToken && now < this.state.expiresAt - 60_000) { + return this.state.accessToken; + } + + if (!this.state.refreshing) { + this.state.refreshing = this.refresh(); + this.state.refreshing.finally(() => { this.state.refreshing = undefined; }); + } + + return this.state.refreshing; + } + + invalidate() { + this.state.accessToken = ''; + this.state.expiresAt = 0; + } + + async forceRefresh(): Promise { + this.invalidate(); + return this.getToken(); + } + + private async refresh(): Promise { + const q = new URLSearchParams({ grant_type: 'account_credentials', account_id: this.accountId }); + const basic = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64'); + + const res = await fetch(`https://zoom.us/oauth/token?${q.toString()}`, { + method: 'POST', + headers: { Authorization: `Basic ${basic}` }, + }); + + if (!res.ok) throw new Error(`token_refresh_failed:${res.status}`); + const data = await res.json() as { access_token: string; expires_in: number }; + + this.state.accessToken = data.access_token; + this.state.expiresAt = Date.now() + data.expires_in * 1000; + return this.state.accessToken; + } +} +``` + +## Meeting Service with 401 Retry-once + +```ts +export async function createMeeting(tokenBroker: TokenBroker, userId: string, payload: object) { + async function call(): Promise { + const token = await tokenBroker.getToken(); + return fetch(`https://api.zoom.us/v2/users/${encodeURIComponent(userId)}/meetings`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + } + + let res = await call(); + if (res.status === 401) { + await tokenBroker.forceRefresh(); + res = await call(); // retry once with fresh token + } + + if (!res.ok) throw new Error(`create_meeting_failed:${res.status}`); + return res.json(); +} +``` + +## Webhook Ingress Skeleton + +```ts +import crypto from 'crypto'; +import type { Request, Response } from 'express'; + +export function verifyZoomSignature(req: Request, secret: string): boolean { + const ts = String(req.headers['x-zm-request-timestamp'] || ''); + const sig = String(req.headers['x-zm-signature'] || ''); + const rawBody = (req as any).rawBody || JSON.stringify(req.body); + const msg = `v0:${ts}:${rawBody}`; + const expected = `v0=${crypto.createHmac('sha256', secret).update(msg).digest('hex')}`; + return sig === expected; +} + +export async function handleWebhook(req: Request, res: Response, secret: string, enqueue: (e: any) => Promise) { + if (req.body?.event === 'endpoint.url_validation') { + const plainToken = req.body.payload?.plainToken; + const encryptedToken = crypto.createHmac('sha256', secret).update(plainToken).digest('hex'); + return res.json({ plainToken, encryptedToken }); + } + + if (!verifyZoomSignature(req, secret)) { + return res.status(401).send('invalid_signature'); + } + + await enqueue(req.body); // durable queue write + return res.status(200).send('ok'); +} +``` + +## Event Processing Rules + +- Apply idempotency key to avoid duplicate state updates. +- Accept out-of-order events; keep `last_event_ts` and reject stale writes when necessary. +- Add reconciliation worker that polls REST meeting status if webhook lag or failures are detected. + +## Runtime Setup Notes + +- For Server-to-Server OAuth meeting creation, pass an explicit host `userId`/email instead of relying on `me`. +- In Express, capture raw request body in `express.json({ verify })` and use it for signature verification. diff --git a/plugins/zoom-developers/skills/general/references/query-routing-playbook.md b/plugins/zoom-developers/skills/general/references/query-routing-playbook.md new file mode 100644 index 00000000..2c40e836 --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/query-routing-playbook.md @@ -0,0 +1,87 @@ +# Query Routing Playbook (zoom-general) + +Use `zoom-general` as the routing/orchestration layer. +Do not implement product-specific logic in `zoom-general` if a specialized skill exists. + +## Goal + +Convert a complex developer query into: +- `selected_skills` +- `execution_order` +- `assumptions` +- `next_actions` + +## Routing rules + +| Query signal | Route to skill | Why | +|---|---|---| +| OAuth, scopes, S2S, token strategy | `zoom-oauth` | Authentication and authorization design | +| Meetings/users/recordings/reports API operations | `zoom-rest-api` | Server-side Zoom resource management | +| Embed full Zoom meetings/webinars | `zoom-meeting-sdk` | Meeting runtime integration | +| Build custom video session experience | `zoom-video-sdk` | Custom media UX runtime | +| Receive event callbacks via HTTP | `zoom-webhooks` | Event lifecycle notifications | +| Need lower-latency event stream | `zoom-websockets` | Persistent real-time event transport | +| Live audio/video/transcript stream ingestion | `zoom-rtms` | Real-time media and transcript pipeline | +| App runs inside Zoom client | `zoom-apps-sdk` | In-client app model and APIs | + +## Sequencing + +1. Start with `zoom-general` (triage and architecture). +2. Add `zoom-oauth` if any protected resource access is required. +3. Select one primary runtime/API skill (`zoom-meeting-sdk`, `zoom-video-sdk`, or `zoom-rest-api`). +4. Add event/media skills (`zoom-webhooks`, `zoom-websockets`, `zoom-rtms`) based on requirements. +5. Keep the chain minimal; do not add extra skills without explicit need. + +## Handoff contract + +```json +{ + "selected_skills": [ + "zoom-general", + "zoom-oauth", + "zoom-meeting-sdk", + "zoom-webhooks" + ], + "execution_order": [ + "zoom-general", + "zoom-oauth", + "zoom-meeting-sdk", + "zoom-webhooks" + ], + "assumptions": [ + "embedded meeting experience required", + "server-side event endpoint available" + ], + "next_actions": [ + "confirm OAuth scopes", + "implement auth/token flow", + "implement runtime integration", + "implement event consumer and verification" + ] +} +``` + +## Ambiguity handling + +If confidence is low, ask one focused question before final routing: +- “Do you need embedded Zoom meetings, or a fully custom video session UI?” +- “Is webhook latency acceptable, or do you require persistent low-latency events?” + +## Example route + +Query: “Build a Linux bot that joins meetings, auto-creates meetings, streams transcript, and tracks lifecycle events.” + +Recommended chain: +- `zoom-general` +- `zoom-oauth` +- `zoom-rest-api` +- `zoom-meeting-sdk` +- `zoom-rtms` +- `zoom-webhooks` + +Why: +- `zoom-rest-api` for meeting provisioning +- `zoom-meeting-sdk` for runtime join/control +- `zoom-rtms` for live transcript/media stream +- `zoom-webhooks` for lifecycle notifications + diff --git a/plugins/zoom-developers/skills/general/references/routing-implementation.md b/plugins/zoom-developers/skills/general/references/routing-implementation.md new file mode 100644 index 00000000..399b8373 --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/routing-implementation.md @@ -0,0 +1,96 @@ +# Routing Implementation + +This reference provides a compact implementation model for routing a complex developer query from `zoom-general` to specialized skills. + +```ts +export type SkillId = + | 'zoom-general' + | 'zoom-rest-api' + | 'zoom-webhooks' + | 'zoom-websockets' + | 'zoom-meeting-sdk' + | 'zoom-meeting-sdk-web' + | 'zoom-meeting-sdk-web-component-view' + | 'zoom-video-sdk' + | 'zoom-video-sdk-web' + | 'zoom-apps-sdk' + | 'zoom-rtms' + | 'zoom-team-chat' + | 'contact-center' + | 'virtual-agent' + | 'phone' + | 'rivet-sdk' + | 'probe-sdk' + | 'zoom-ui-toolkit' + | 'zoom-cobrowse-sdk' + | 'zoom-oauth'; + +interface Signals { + meetingEmbed: boolean; + meetingCustomUi: boolean; + customVideo: boolean; + restApi: boolean; + webhooks: boolean; + websockets: boolean; + zoomApps: boolean; + oauth: boolean; + rtms: boolean; + teamChat: boolean; + contactCenter: boolean; + virtualAgent: boolean; + phone: boolean; + rivet: boolean; + preflight: boolean; + uiToolkit: boolean; + cobrowse: boolean; +} + +const hasAny = (q: string, words: string[]) => words.some((word) => q.includes(word)); + +export function detectSignals(rawQuery: string): Signals { + const q = rawQuery.toLowerCase(); + return { + meetingEmbed: hasAny(q, ['meeting sdk', 'embed meeting', 'join meeting ui', 'client view', 'component view']), + meetingCustomUi: hasAny(q, ['custom meeting ui', 'zoommtgembedded', 'zoomapproot', 'embeddable meeting ui']), + customVideo: hasAny(q, ['video sdk', 'custom video', 'attachvideo', 'peer-video-state-change']), + restApi: hasAny(q, ['rest api', 'api create meeting', 'api list meetings', '/v2/', 'list users', 'meeting endpoint']), + webhooks: hasAny(q, ['webhook', 'x-zm-signature', 'event subscription', 'crc']), + websockets: hasAny(q, ['websocket', 'real-time events', 'persistent connection']), + zoomApps: hasAny(q, ['zoom apps sdk', 'in-client app', 'layers api', 'collaborate mode']), + oauth: hasAny(q, ['oauth', 'pkce', 'authorization code', 'token refresh']), + rtms: hasAny(q, ['rtms', 'real-time media streams', 'live transcript stream', 'audio stream']), + teamChat: hasAny(q, ['team chat', 'chatbot', 'chat card', 'chat message']), + contactCenter: hasAny(q, ['contact center', 'engagement context', 'zcc']), + virtualAgent: hasAny(q, ['virtual agent', 'zva', 'knowledge base sync']), + phone: hasAny(q, ['zoom phone', 'phone smart embed', 'phone api', 'click to dial']), + rivet: hasAny(q, ['rivet', 'zoom rivet']), + preflight: hasAny(q, ['probe sdk', 'preflight', 'diagnostics', 'network readiness']), + uiToolkit: hasAny(q, ['ui toolkit', 'prebuilt video ui']), + cobrowse: hasAny(q, ['cobrowse', 'co-browse', 'shared browsing']), + }; +} + +export function pickPrimarySkill(s: Signals): SkillId { + if (s.meetingCustomUi) return 'zoom-meeting-sdk-web-component-view'; + if (s.meetingEmbed && !s.customVideo) return 'zoom-meeting-sdk-web'; + if (s.meetingEmbed) return 'zoom-meeting-sdk'; + if (s.customVideo && !s.meetingEmbed) return 'zoom-video-sdk-web'; + if (s.customVideo) return 'zoom-video-sdk'; + if (s.virtualAgent) return 'virtual-agent'; + if (s.contactCenter) return 'contact-center'; + if (s.zoomApps) return 'zoom-apps-sdk'; + if (s.rtms) return 'zoom-rtms'; + if (s.teamChat) return 'zoom-team-chat'; + if (s.phone) return 'phone'; + if (s.cobrowse) return 'zoom-cobrowse-sdk'; + if (s.uiToolkit) return 'zoom-ui-toolkit'; + if (s.preflight) return 'probe-sdk'; + if (s.websockets) return 'zoom-websockets'; + if (s.webhooks) return 'zoom-webhooks'; + if (s.restApi) return 'zoom-rest-api'; + if (s.oauth) return 'zoom-oauth'; + return 'zoom-general'; +} +``` + +Use this only as a routing aid. The selected skill remains responsible for implementation details and verification. diff --git a/plugins/zoom-developers/skills/general/references/scopes.md b/plugins/zoom-developers/skills/general/references/scopes.md new file mode 100644 index 00000000..f5ebdc56 --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/scopes.md @@ -0,0 +1,94 @@ +# OAuth Scopes + +OAuth scopes define what your app can access. + +## Overview + +Scopes are permissions requested during OAuth authorization. Request only the scopes you need. + +## IMPORTANT: Scope Types + +**Different OAuth types have different scopes available:** + +| OAuth Type | Scope Suffix | Access Level | Example | +|------------|--------------|--------------|---------| +| **User OAuth** | (none) | Current user's data only | `meeting:read` | +| **Admin OAuth** | `:admin` | All users in account | `meeting:read:admin` | +| **Server-to-Server (S2S)** | `:admin` | All users in account (no user consent) | `meeting:read:admin` | + +### Key Differences + +- **User scopes** (`meeting:read`): Access only the authenticated user's data +- **Admin scopes** (`meeting:read:admin`): Access data for ALL users in the account +- **S2S OAuth**: Uses admin-level scopes but doesn't require user login - intended for backend integrations + +### Choosing the Right Scope Type + +| Use Case | OAuth Type | Scope Example | +|----------|------------|---------------| +| User manages their own meetings | User OAuth | `meeting:write` | +| Admin dashboard for all users | Admin OAuth | `meeting:read:admin` | +| Backend automation (no user login) | Server-to-Server | `meeting:write:admin` | +| Bot that creates meetings for users | Server-to-Server | `meeting:write:admin` | + +## Common Scopes + +### Meetings + +| User Scope | Admin Scope | Description | +|------------|-------------|-------------| +| `meeting:read` | `meeting:read:admin` | View meeting details | +| `meeting:write` | `meeting:write:admin` | Create, update, delete meetings | +| `meeting:master` | `meeting:master:admin` | Full meeting access | + +### Users + +| User Scope | Admin Scope | Description | +|------------|-------------|-------------| +| `user:read` | `user:read:admin` | View user profile | +| `user:write` | `user:write:admin` | Update user settings | +| `user:master` | `user:master:admin` | Full user access | + +### Recordings + +| User Scope | Admin Scope | Description | +|------------|-------------|-------------| +| `recording:read` | `recording:read:admin` | View/download recordings | +| `recording:write` | `recording:write:admin` | Delete recordings | +| `recording:master` | `recording:master:admin` | Full recording access | + +### Webinars + +| User Scope | Admin Scope | Description | +|------------|-------------|-------------| +| `webinar:read` | `webinar:read:admin` | View webinar details | +| `webinar:write` | `webinar:write:admin` | Create, update webinars | +| `webinar:master` | `webinar:master:admin` | Full webinar access | + +### Reports + +| User Scope | Admin Scope | Description | +|------------|-------------|-------------| +| `report:read` | `report:read:admin` | View reports and analytics | +| `report:master` | `report:master:admin` | Full report access | + +## Scope Patterns + +| Pattern | Meaning | +|---------|---------| +| `resource:read` | Read-only access (current user) | +| `resource:write` | Read and write access (current user) | +| `resource:master` | Full access including delete (current user) | +| `resource:read:admin` | Read-only access (all account users) | +| `resource:write:admin` | Read and write access (all account users) | +| `resource:master:admin` | Full access including delete (all account users) | + +## Best Practices + +1. **Request minimum scopes** - Only what you need +2. **Explain to users** - Why you need each scope +3. **Handle denied scopes** - Graceful fallback + +## Resources + +- **Scopes reference**: https://developers.zoom.us/docs/integrations/oauth-scopes/ diff --git a/plugins/zoom-developers/skills/general/references/sdk-logs-troubleshooting.md b/plugins/zoom-developers/skills/general/references/sdk-logs-troubleshooting.md new file mode 100644 index 00000000..a21b249e --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/sdk-logs-troubleshooting.md @@ -0,0 +1,194 @@ +# SDK Logs & Troubleshooting + +Collecting SDK logs for debugging and support. + +## Official Log Retrieval Guides + +**IMPORTANT**: Always refer to the official Zoom log retrieval guides for the most up-to-date instructions: + +- **Video SDK Log Retrieval**: https://developers.zoom.us/blog/vsdk-log-retrieval-instructions/ +- **Meeting SDK Log Retrieval**: https://developers.zoom.us/blog/msdk-log-retrieval-instructions/ + +If these URLs are unavailable, search for "zoom sdk log retrieval" to find the current documentation. + +## Overview + +SDK logs help diagnose issues during development and for Zoom support escalations. + +## Enabling Logs + +### Web SDK + +```javascript +// Enable verbose logging +ZoomMtg.setLogLevel('verbose'); + +// Or for Video SDK +client.init('en-US', 'CDN', { debug: true }); +``` + +**Web Tracking ID**: For Web SDK troubleshooting, get the **Web Tracking ID** which helps Zoom support trace your session. + +**Meeting SDK Web**: +1. Open browser DevTools → **Network** tab +2. Look for a request starting with `info?meetingNumber...` +3. Click on the request and check the **Response Headers** +4. Find the `x-zm-trackingid` header value +5. Copy this ID for support tickets + +**Video SDK Web**: +1. Open browser DevTools → **Network** tab +2. Look for a request starting with `lsdk?topic...` +3. Click on the request and check the **Response Headers** +4. Find the `x-zm-trackingid` header value +5. Copy this ID for support tickets + +``` +Example header: +x-zm-trackingid: v=2.0;clid=us04;rid=WEB_abc123xyz... +``` + +The Web Tracking ID is essential for Zoom support to investigate Web SDK issues. + +**To get help with logs and tracking IDs:** +- **Open a support ticket**: https://devsupport.zoom.us/ +- **Post on Developer Forum**: https://devforum.zoom.us/ + +Include the tracking ID and relevant logs when requesting assistance. + +### iOS SDK + +```swift +// Set log file path +let initParams = MobileRTCSDKInitParams() +initParams.enableLog = true +initParams.logFilePrefix = "zoom_sdk" +``` + +### Android SDK + +```kotlin +val initParams = ZoomSDKInitParams().apply { + enableLog = true + logSize = 5 // MB +} +``` + +### Desktop SDKs (Windows/macOS/Linux) + +```cpp +initParam.enableLogByDefault = true; +initParam.logFilePrefix = L"zoom_sdk"; +``` + +## Log Locations + +| Platform | Default Location | +|----------|------------------| +| iOS | App's Documents directory | +| Android | App's files directory | +| Windows | `%APPDATA%\ZoomSDK\` | +| macOS | `~/Library/Logs/ZoomSDK/` | +| Linux | Working directory | + +## Common Issues and Solutions + +| Issue | Possible Cause | Solution | +|-------|----------------|----------| +| Join failed | Invalid signature | Check JWT generation (exp should be ~10s after iat, but iat can be up to 2 hours in past) | +| Join failed | Meeting not found | Verify meeting number and that meeting hasn't ended | +| No audio | Permission denied | Request microphone permission before joining | +| No video | Permission denied | Request camera permission before joining | +| Video scales down | Container too small | Ensure container is at least 1280x720 for 720p | +| SharedArrayBuffer error | Missing headers | Add COOP/COEP headers to server | +| Error code 0 | Actually success | Check SDK docs - 0 often means success, not error | +| SDK crash | ProGuard enabled | Disable ProGuard/R8 for Zoom SDK classes | +| DLL not found | Missing files | Copy ALL DLLs from SDK bin folder | + +### Debugging Join Failures + +```javascript +// Web SDK - enable verbose logging +ZoomMtg.setLogLevel('verbose'); + +// Check signature +console.log('Signature:', signature); +console.log('Meeting:', meetingNumber); + +// Verify callback +client.join({ + // ...params + success: (res) => console.log('Join success:', res), + error: (err) => console.error('Join error:', err) +}); +``` + +### Debugging Audio/Video Issues + +```javascript +// Check device availability +const devices = await navigator.mediaDevices.enumerateDevices(); +console.log('Audio inputs:', devices.filter(d => d.kind === 'audioinput')); +console.log('Video inputs:', devices.filter(d => d.kind === 'videoinput')); + +// Check permissions +const micPermission = await navigator.permissions.query({ name: 'microphone' }); +const camPermission = await navigator.permissions.query({ name: 'camera' }); +console.log('Mic:', micPermission.state); +console.log('Cam:', camPermission.state); +``` + +### Native SDK Crash Debugging + +**iOS**: +```swift +// Enable crash reporting +MobileRTC.shared().setEnableCrashReport(true) + +// Get logs +let logPath = MobileRTC.shared().getLogPath() +print("Logs at: \(logPath)") +``` + +**Android**: +```kotlin +// Check logcat for crashes +adb logcat -s ZoomSDK:V + +// Disable ProGuard for Zoom +// In proguard-rules.pro: +-keep class us.zoom.** { *; } +-keep interface us.zoom.** { *; } +``` + +### Error Code Reference + +| Code | Meaning | Platform | +|------|---------|----------| +| 0 | Success (NOT error!) | All | +| 1 | Generic error | All | +| 2 | Invalid argument | All | +| 3 | Invalid token | All | +| 4 | Timeout | All | +| 8 | SDK not authorized | Windows | +| 100000400 | Meeting join failed | Windows | +| MobileRTCMeetError_Success | Success | iOS | +| MEETING_STATUS_INMEETING | In meeting | Android | + +## Getting Support + +When contacting Zoom support: + +1. Include SDK version +2. Attach log files +3. Describe steps to reproduce +4. Include error codes (check if 0 means success!) + +## Resources + +- **Video SDK Log Retrieval Guide**: https://developers.zoom.us/blog/vsdk-log-retrieval-instructions/ +- **Meeting SDK Log Retrieval Guide**: https://developers.zoom.us/blog/msdk-log-retrieval-instructions/ +- **Developer support**: https://devsupport.zoom.us/ +- **Developer forum**: https://devforum.zoom.us/ + +> **Note for LLMs/Agents**: If the log retrieval URLs return 404 or have moved, perform a web search for "zoom sdk log retrieval instructions" to find the current documentation. diff --git a/plugins/zoom-developers/skills/general/references/sdk-upgrade-guide.md b/plugins/zoom-developers/skills/general/references/sdk-upgrade-guide.md new file mode 100644 index 00000000..6e97fdb5 --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/sdk-upgrade-guide.md @@ -0,0 +1,164 @@ +# SDK Upgrade Guide + +Guide for upgrading Meeting SDK and Video SDK versions. + +For customer upgrades from older versions to latest, use: +- [sdk-upgrade-workflow.md](sdk-upgrade-workflow.md) - changelog + RSS, version-by-version migration workflow. + +## IMPORTANT: Check the Changelog First + +**Before any upgrade, always check the official Zoom changelog:** + +**Primary URL**: https://developers.zoom.us/changelog/ + +If the above URL is unavailable or has moved, search for **"zoom changelog"** or **"zoom developer changelog"** to find the current location. + +The changelog contains: +- Latest SDK versions and release dates +- Breaking changes and deprecations +- New features and improvements +- Bug fixes and security patches + +## Overview + +Zoom releases SDK updates regularly. This guide covers version policy and upgrade procedures. + +## Version Policy + +- **Major versions** - May contain breaking changes +- **Minor versions** - New features, backward compatible +- **Patch versions** - Bug fixes + +## Before Upgrading + +1. Read changelog for target version +2. Note breaking changes and deprecations +3. Test in development environment +4. Plan migration for deprecated APIs + +## Upgrade Steps + +### Web SDK (npm) + +```bash +# Check current version +npm list @zoom/meetingsdk + +# Update to latest +npm update @zoom/meetingsdk + +# Or specific version +npm install @zoom/meetingsdk@2.18.0 +``` + +### Native SDKs + +1. Download new SDK from [Marketplace](https://marketplace.zoom.us/) (sign-in required) +2. Replace SDK files in your project +3. Update linker/framework settings if needed +4. Rebuild project + +## Common Migration Tasks + +### API Signature Changes + +When methods change signatures between versions: + +```javascript +// Old (v2.x) +client.join({ + sdkKey: key, + sdkSecret: secret, // REMOVED in v3.x + meetingNumber: number +}); + +// New (v3.x) - signature generated server-side +client.join({ + sdkKey: key, + signature: serverGeneratedSignature, // NEW + meetingNumber: number +}); +``` + +**Action**: Update to server-side signature generation for security. + +### Deprecated Method Replacements + +| Old Method | New Method | Version | +|------------|------------|---------| +| `ZoomMtg.init()` | `client.init()` | Web SDK 3.x | +| `startVideo()` | `startVideo()` + `renderVideo()` | Video SDK 1.8+ | +| `getMeetingUUID()` | Use webhook payload | Meeting SDK 2.x | + +### New Initialization Requirements + +**Meeting SDK Web 3.x**: +```javascript +// Now requires explicit preload +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +const client = ZoomMtgEmbedded.createClient(); + +// Must init before join +await client.init({ + zoomAppRoot: document.getElementById('root'), + language: 'en-US' +}); +``` + +**Video SDK 1.8+**: +```javascript +// Video rendering is now two-step +await stream.startVideo(); +await stream.renderVideo( + document.querySelector('#video-canvas'), + myUserId, + 1280, 720, 0, 0, 3 // width, height, x, y, quality +); +``` + +### Breaking Changes Checklist + +When upgrading major versions, check: + +- [ ] Initialization flow changed? +- [ ] Authentication method changed? +- [ ] Event names/signatures changed? +- [ ] Required permissions changed? +- [ ] Minimum platform version changed? +- [ ] New required headers (COOP/COEP)? + +### Testing Upgrade + +```bash +# Create upgrade branch +git checkout -b sdk-upgrade-v3 + +# Update package +npm install @zoom/meetingsdk@latest + +# Run tests +npm test + +# Test manually +# - Join meeting +# - Audio/video functionality +# - Screen sharing +# - Recording (if used) +# - Custom UI features +``` + +## Version Support Policy + +- **Latest version**: Full support +- **Previous major**: Security fixes only +- **Older versions**: No support, upgrade recommended + +## Resources + +- **Main Changelog**: https://developers.zoom.us/changelog/ (check here first!) +- **Meeting SDK changelog**: https://developers.zoom.us/changelog/meeting-sdk/ +- **Video SDK changelog**: https://developers.zoom.us/changelog/video-sdk/ +- **Migration guides**: https://developers.zoom.us/docs/meeting-sdk/web/migrate/ + +> **Note for LLMs/Agents**: If the changelog URLs return 404 or have moved, perform a web search for "zoom developer changelog" or "zoom sdk changelog" to find the current location. Zoom occasionally restructures their documentation. diff --git a/plugins/zoom-developers/skills/general/references/sdk-upgrade-workflow.md b/plugins/zoom-developers/skills/general/references/sdk-upgrade-workflow.md new file mode 100644 index 00000000..f4e05e57 --- /dev/null +++ b/plugins/zoom-developers/skills/general/references/sdk-upgrade-workflow.md @@ -0,0 +1,144 @@ +# SDK Upgrade Workflow (Changelog + RSS) + +Reusable process for upgrading Zoom SDK integrations from an older customer version to latest with low regression risk. + +## Use This When + +- Customer is multiple versions behind. +- Breaking changes may exist between current and latest. +- You need a defensible, version-by-version upgrade plan. + +## Inputs Required + +1. Product and platform +- Example: `Meeting SDK Android`, `Video SDK iOS`, `Contact Center Web`. + +2. Current version in production +- Example: `6.3.1`, `2.1.0`. + +3. Target version +- Usually latest stable from changelog. + +4. Critical features in use +- Example: custom UI, raw data, recording, chat, live transcription, token flow. + +## Canonical Source + +- Changelog entry point: https://developers.zoom.us/changelog/ + +## Workflow + +### 1) Scope the upgrade lane + +- Confirm exact product + platform lane before collecting releases. +- Do not mix lanes (for example, Meeting SDK Web and Meeting SDK iOS must be treated separately). + +### 2) Locate the platform-specific RSS feed + +From `https://developers.zoom.us/changelog/`: +- Filter by product/platform. +- Find the RSS link for that filtered lane. +- Use only that feed for release collection. + +If feed discovery is unclear: +- Open the filtered changelog page and locate the RSS icon/link. +- Confirm feed entries match the same product/platform lane. + +### 3) Build the release ledger + +Collect all releases from: +- `current_version` (exclusive) up to `target_version` (inclusive), then latest if target is `latest`. + +For each release entry capture: +- Version +- Release date +- Release URL +- Breaking/deprecated notes +- Required migration actions + +Sort upgrade steps in ascending version order. + +### 4) Plan upgrade hops + +Default strategy: +- Patch/minor jumps can often be grouped. +- Major changes should be isolated into dedicated hops. + +Recommended hop pattern: +1. `current -> next safe checkpoint` +2. `checkpoint -> next major boundary` +3. Repeat until latest + +### 5) Extract required actions per hop + +For each hop, classify actions under: +- Auth/token contract changes +- API renames/signature changes +- Initialization/lifecycle changes +- Event payload/callback changes +- Build/dependency/runtime requirements +- Feature removals/deprecations + +### 6) Apply compatibility guards + +- Wrap renamed/deprecated calls behind adapters. +- Keep temporary compatibility mappings for payload changes. +- Add feature flags for behavior toggles when needed. + +### 7) Validate each hop before continuing + +Minimum validation set: +- SDK init/auth +- Join/start/session entry flow +- Core media flows (audio/video/share) if applicable +- Critical product-specific features used by customer +- Cleanup/leave/disconnect behavior + +Do not skip to next hop if the current hop is unstable. + +### 8) Produce final upgrade package + +Deliver: +- Step-by-step upgrade matrix +- Per-hop code/config change list +- Deprecated-to-replacement map +- Risks and rollback notes +- Final target-state checklist + +## Output Template + +```markdown +## Upgrade Plan: + +- Current: +- Target: +- Source feed: + +### Hop 1: x.y+1.z> +- Release notes: + - +- Breaking/deprecations: + - +- Required changes: + - +- Validation: + - + +### Hop 2: <...> +... + +## Deprecated -> Replacement Map +- -> + +## Risks +- + +## Rollback +- +``` + +## Operating Rules + +- Never assume only latest release notes are sufficient. +- Always process intermediate releases between customer version and target. +- Prefer smallest-risk path over fastest path for production upgrades. diff --git a/plugins/zoom-developers/skills/general/use-cases/ai-companion-integration.md b/plugins/zoom-developers/skills/general/use-cases/ai-companion-integration.md new file mode 100644 index 00000000..08ae0eb9 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/ai-companion-integration.md @@ -0,0 +1,390 @@ +# AI Companion Integration + +Integrate with Zoom AI Companion for meeting summaries, transcripts, and AI-powered features. + +## Overview + +Zoom AI Companion provides AI-powered features including: +- Meeting summaries (auto-generated) +- Meeting transcripts +- Real-time transcription +- Smart recording highlights +- Conversation archives + +## What's Available via API + +| Feature | API Access | Method | +|---------|------------|--------| +| Meeting Summaries | ✅ Yes | REST API | +| Meeting Transcripts | ✅ Yes | REST API (Cloud Recording) | +| Real-Time Transcripts | ✅ Yes | RTMS SDK | +| AI Companion Panel | ⚠️ Limited | Archive only | +| Conversation Archives | ✅ Yes | REST API | +| AI Controls in Meeting | ✅ Yes | Meeting SDK | + +## Skills Needed + +| Use Case | Skills | +|----------|--------| +| Get meeting summaries after meeting | **zoom-rest-api** | +| Get meeting transcripts in deterministic backend pipeline | **zoom-rest-api** + **zoom-webhooks** | +| Real-time transcript streaming | **rtms** | +| Control AI features in embedded meetings | **zoom-meeting-sdk** | + +## Routing Modes + +- Use **zoom-rest-api** when you need deterministic backend jobs, strict retry behavior, and explicit endpoint control. +- Use **rtms** when the workflow needs live transcript streaming. + +--- + +## Get Meeting Summary (REST API) + +### Endpoint + +``` +GET /v2/meetings/{meetingUUID}/meeting_summary +``` + +### Prerequisites + +1. Meeting Summary feature enabled in account settings +2. Server-to-Server OAuth app with admin scopes +3. Meeting must have ended with summary generated + +### Example + +```javascript +// Get meeting summary +async function getMeetingSummary(meetingUUID) { + // Double-encode UUID if it contains / or // + const encodedUUID = meetingUUID.startsWith('/') + ? encodeURIComponent(encodeURIComponent(meetingUUID)) + : encodeURIComponent(meetingUUID); + + const response = await fetch( + `https://api.zoom.us/v2/meetings/${encodedUUID}/meeting_summary`, + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + return response.json(); +} + +// Response example +{ + "meeting_uuid": "abc123...", + "meeting_id": 12345678901, + "meeting_topic": "Weekly Team Sync", + "meeting_start_time": "2024-01-15T10:00:00Z", + "meeting_end_time": "2024-01-15T10:45:00Z", + "summary_start_time": "2024-01-15T10:00:00Z", + "summary_end_time": "2024-01-15T10:45:00Z", + "summary_content": { + "summary": "The team discussed Q1 roadmap priorities...", + "next_steps": [ + "John to finalize design specs by Friday", + "Sarah to schedule customer interviews" + ], + "keywords": ["roadmap", "Q1", "design", "customers"] + } +} +``` + +### Important Notes + +- **Admin role required**: Standard users may not have access +- **Make app admin-managed**: Resolves permission issues +- **UUID encoding**: Double-encode UUIDs starting with `/` + +--- + +## Get Meeting Transcript (REST API) + +Transcripts are accessed via the Cloud Recording API. + +### Endpoint + +``` +GET /v2/meetings/{meetingId}/recordings +``` + +### Example + +```javascript +async function getMeetingTranscript(meetingId) { + const response = await fetch( + `https://api.zoom.us/v2/meetings/${meetingId}/recordings`, + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + const data = await response.json(); + + // Find transcript files + const transcriptFiles = data.recording_files.filter( + file => file.file_type === 'TRANSCRIPT' || + file.file_type === 'CC' || + file.file_type === 'SUMMARY' + ); + + // Download transcript + for (const file of transcriptFiles) { + const transcriptResponse = await fetch(file.download_url, { + headers: { 'Authorization': `Bearer ${accessToken}` } + }); + + if (file.file_extension === 'VTT') { + const vttContent = await transcriptResponse.text(); + console.log('VTT Transcript:', vttContent); + } else if (file.file_extension === 'JSON') { + const jsonContent = await transcriptResponse.json(); + console.log('JSON Transcript:', jsonContent); + } + } + + return transcriptFiles; +} +``` + +### Transcript File Types + +| File Type | Extension | Description | +|-----------|-----------|-------------| +| `TRANSCRIPT` | VTT, JSON | Full meeting transcript | +| `CC` | VTT | Closed captions | +| `SUMMARY` | JSON | AI-generated summary | + +--- + +## Webhooks for AI Content + +Listen for when AI content is ready: + +```javascript +// Webhook handler +app.post('/webhook', (req, res) => { + const { event, payload } = req.body; + + switch (event) { + case 'recording.transcript_completed': + // Transcript is ready + console.log('Transcript ready for meeting:', payload.object.uuid); + fetchAndStoreTranscript(payload.object.uuid); + break; + + case 'recording.completed': + // Recording processing complete (may include summary) + console.log('Recording ready:', payload.object.uuid); + break; + + case 'meeting.ended': + // Meeting ended - summary will be generated soon + console.log('Meeting ended:', payload.object.uuid); + break; + } + + res.sendStatus(200); +}); +``` + +--- + +## Real-Time Transcripts (RTMS) + +For live transcript streaming during meetings, use RTMS SDK. + +### Prerequisites + +- RTMS access approval from Zoom +- Server infrastructure for WebSocket connections + +### Example + +```javascript +import { RTMSClient } from "@zoom/rtms"; + +const client = new RTMSClient({ + clientId: process.env.ZOOM_CLIENT_ID, + clientSecret: process.env.ZOOM_CLIENT_SECRET, + secretToken: process.env.ZOOM_SECRET_TOKEN +}); + +// Connect to meeting +await client.joinMeeting({ + meetingUuid: meetingUUID, + streamId: streamId, + serverUrl: "wss://rtms.zoom.us" +}); + +// Listen for transcript events +client.on('transcript', (data) => { + console.log(`[${data.speakerName}]: ${data.text}`); + + // Process real-time transcript + // - Send to AI for sentiment analysis + // - Display live captions + // - Log for compliance +}); +``` + +See **rtms** skill for full RTMS documentation. + +--- + +## Meeting SDK - AI Companion Controls + +Control AI Companion features in embedded meetings. + +### Web SDK + +```javascript +// Check if AI Companion is available +const aiCompanionStatus = ZoomMtg.getAICompanionStatus(); + +// AI Companion features are controlled by meeting settings +// The SDK respects account/meeting-level AI Companion settings +``` + +### Native SDKs (Android/iOS/Desktop) + +Use `InMeetingAICompanionController`: + +```java +// Android example +InMeetingAICompanionController aiController = + ZoomSDK.getInstance().getInMeetingService().getInMeetingAICompanionController(); + +// Check AI Companion status +boolean isEnabled = aiController.isAICompanionEnabled(); + +// Get available features +AICompanionFeature[] features = aiController.getAvailableFeatures(); +// Features: QUERY, SMART_SUMMARY, SMART_RECORDING +``` + +```swift +// iOS example +let aiController = MobileRTC.shared().getMeetingService()?.getInMeetingAICompanionController() + +if let isEnabled = aiController?.isAICompanionEnabled() { + print("AI Companion enabled: \(isEnabled)") +} +``` + +### Feature Constants + +| Feature | Description | +|---------|-------------| +| `QUERY` | Ask AI Companion questions | +| `SMART_SUMMARY` | Meeting summary generation | +| `SMART_RECORDING` | Smart recording highlights | + +--- + +## Conversation Archives API + +Archive AI Companion panel conversations (new September 2025). + +```javascript +// Get conversation archives +async function getConversationArchives(userId) { + const response = await fetch( + `https://api.zoom.us/v2/users/${userId}/conversation_archive`, + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + return response.json(); +} +``` + +--- + +## Common Integration Patterns + +### Pattern 1: Post-Meeting Summary Pipeline + +```javascript +// 1. Listen for meeting end +webhooks.on('meeting.ended', async (meeting) => { + // 2. Wait for transcript to be ready (or use webhook) + await delay(60000); // Processing time varies + + // 3. Fetch summary + const summary = await getMeetingSummary(meeting.uuid); + + // 4. Store or distribute + await saveSummaryToDatabase(summary); + await sendSummaryToParticipants(meeting.participants, summary); +}); +``` + +### Pattern 2: Real-Time AI Processing + +```javascript +// Using RTMS for live processing +rtmsClient.on('transcript', async (data) => { + // Send to your AI service for analysis + const sentiment = await analyzesentiment(data.text); + const actionItems = await extractActionItems(data.text); + + // Update live dashboard + updateDashboard({ sentiment, actionItems }); +}); +``` + +### Pattern 3: Compliance Archival + +```javascript +// Archive all AI-generated content +async function archiveMeetingAIContent(meetingId) { + const [summary, transcript, archives] = await Promise.all([ + getMeetingSummary(meetingId), + getMeetingTranscript(meetingId), + getConversationArchives(meetingId) + ]); + + await complianceStore.save({ + meetingId, + summary, + transcript, + aiConversations: archives, + archivedAt: new Date() + }); +} +``` + +--- + +## Required Scopes + +| Scope | Description | +|-------|-------------| +| `meeting:read:admin` | Read meeting data including summaries | +| `recording:read:admin` | Access recordings and transcripts | +| `user:read:admin` | Read user data for archives | + +--- + +## Limitations + +| Limitation | Notes | +|------------|-------| +| AI Companion Panel | Most panel features NOT available via API | +| Admin access | Some endpoints require admin role | +| Processing time | Summaries/transcripts not instant after meeting | +| RTMS approval | Real-time access requires Zoom approval | +| Bot restrictions | Meeting SDK does NOT support bots (use RTMS) | + +--- + +## Resources + +- **AI Companion APIs**: https://developers.zoom.us/docs/api/ai-companion/ +- **Meeting Summary API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetingSummary +- **Cloud Recording API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Cloud-Recording +- **RTMS Documentation**: https://developers.zoom.us/docs/rtms/ diff --git a/plugins/zoom-developers/skills/general/use-cases/ai-integration.md b/plugins/zoom-developers/skills/general/use-cases/ai-integration.md new file mode 100644 index 00000000..48d07d73 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/ai-integration.md @@ -0,0 +1,224 @@ +# AI Integration + +Build real-time AI features for Zoom meetings - sentiment analysis, summarization, and more. + +## Overview + +Integrate AI/ML capabilities with Zoom meetings using real-time media streams for live transcription, sentiment analysis, meeting summarization, and intelligent automation. + +## Skills Needed + +- **rtms** - Primary (real-time media access) +- **zoom-meeting-sdk** (Linux) - For meeting bots + +## AI Use Cases + +| Use Case | Input | Output | +|----------|-------|--------| +| Transcription | Audio stream | Real-time text | +| Sentiment | Audio/transcript | Mood indicators | +| Summarization | Transcript | Meeting summary | +| Action items | Transcript | Task list | +| Translation | Audio/transcript | Multi-language | + +## Architecture + +``` +AI Integration Architecture: +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Zoom │────▶│ RTMS / │────▶│ AI/ML │ +│ Meeting │ │ Bot SDK │ │ Pipeline │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ + Audio/Video/Transcript +``` + +## Prerequisites + +- RTMS access or Meeting SDK (Linux) +- AI/ML service (OpenAI, Azure, custom) +- Real-time processing infrastructure + +## Common Tasks + +### Setting Up RTMS for AI + +```javascript +// 1. Configure RTMS app in Marketplace +// Enable: Audio stream, Video stream, Transcript stream + +// 2. Handle webhook to get connection details +app.post('/webhook', (req, res) => { + if (req.body.event === 'meeting.rtms_started') { + const { server_urls, stream_id, signature } = req.body.payload; + + // Start AI processing pipeline + aiPipeline.connect({ + url: server_urls[0], + streamId: stream_id, + signature: signature + }); + } + res.status(200).send(); +}); +``` + +### Real-Time Transcription Pipeline + +```javascript +// Option 1: Use Zoom's built-in transcript (via RTMS) +rtmsClient.on('transcript', (data) => { + const { text, speaker_id, is_final } = data; + if (is_final) { + transcriptStore.append(speaker_id, text); + } +}); + +// Option 2: Send audio to external STT (Whisper, Deepgram) +const deepgram = new Deepgram(DEEPGRAM_KEY); +const transcriber = deepgram.transcription.live({ + punctuate: true, + interim_results: true, + language: 'en-US' +}); + +rtmsClient.on('audio', (audioChunk) => { + transcriber.send(audioChunk); +}); + +transcriber.on('transcriptReceived', (data) => { + const transcript = data.channel.alternatives[0].transcript; + processTranscript(transcript); +}); +``` + +### Sentiment Analysis Integration + +```javascript +// Real-time sentiment on transcript segments +async function analyzeSentiment(text) { + const response = await openai.chat.completions.create({ + model: 'gpt-4', + messages: [{ + role: 'system', + content: 'Analyze sentiment. Return JSON: {sentiment: "positive|negative|neutral", confidence: 0-1, emotions: []}' + }, { + role: 'user', + content: text + }], + response_format: { type: 'json_object' } + }); + + return JSON.parse(response.choices[0].message.content); +} + +// Track sentiment over time +class SentimentTracker { + constructor() { + this.history = []; + } + + async process(transcript) { + const sentiment = await analyzeSentiment(transcript); + this.history.push({ + timestamp: Date.now(), + text: transcript, + ...sentiment + }); + + // Alert on negative sentiment + if (sentiment.sentiment === 'negative' && sentiment.confidence > 0.8) { + this.emit('alert', { type: 'negative_sentiment', data: sentiment }); + } + } + + getOverallSentiment() { + // Aggregate sentiment over meeting duration + } +} +``` + +### Meeting Summarization + +```javascript +// Generate summary after meeting ends +async function generateMeetingSummary(fullTranscript) { + const response = await openai.chat.completions.create({ + model: 'gpt-4', + messages: [{ + role: 'system', + content: `Summarize this meeting transcript. Include: + 1. Key discussion points + 2. Decisions made + 3. Action items with owners + 4. Follow-up needed` + }, { + role: 'user', + content: fullTranscript + }] + }); + + return response.choices[0].message.content; +} + +// Extract action items +async function extractActionItems(transcript) { + const response = await openai.chat.completions.create({ + model: 'gpt-4', + messages: [{ + role: 'system', + content: 'Extract action items as JSON array: [{task, owner, deadline}]' + }, { + role: 'user', + content: transcript + }], + response_format: { type: 'json_object' } + }); + + return JSON.parse(response.choices[0].message.content); +} +``` + +### Latency Considerations + +| Processing Type | Target Latency | Recommendation | +|-----------------|----------------|----------------| +| Live captions | < 500ms | Use streaming STT (Deepgram, AssemblyAI) | +| Sentiment | < 2s | Batch every 10-15 seconds | +| Summarization | Post-meeting | Process after meeting ends | +| Action items | < 5s | Process paragraph by paragraph | + +### Example AI Pipeline Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ RTMS WebSocket │ +└─────────────┬───────────────┬───────────────┬──────────┘ + │ │ │ + Audio Stream Video Stream Transcript + │ │ │ + ▼ ▼ ▼ + ┌───────────────┐ ┌──────────┐ ┌───────────────┐ + │ Speech-to-Text│ │Face/OCR │ │ NLP Pipeline │ + │ (Deepgram) │ │Detection │ │ (OpenAI GPT) │ + └───────┬───────┘ └────┬─────┘ └───────┬───────┘ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────────────────────────────────────┐ + │ Results Aggregator │ + │ - Transcripts - Sentiment - Action Items │ + └─────────────────────┬───────────────────────────┘ + │ + ▼ + ┌───────────────┐ + │ Storage / │ + │ Dashboard │ + └───────────────┘ +``` + +## Resources + +- **RTMS docs**: https://developers.zoom.us/docs/rtms/ +- **Meeting SDK Linux**: https://developers.zoom.us/docs/meeting-sdk/linux/ +- **Deepgram**: https://deepgram.com/ +- **OpenAI API**: https://platform.openai.com/docs diff --git a/plugins/zoom-developers/skills/general/use-cases/backend-automation-s2s-oauth.md b/plugins/zoom-developers/skills/general/use-cases/backend-automation-s2s-oauth.md new file mode 100644 index 00000000..ac174c46 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/backend-automation-s2s-oauth.md @@ -0,0 +1,221 @@ +# Backend Automation with Server-to-Server OAuth + +Automate Zoom account operations using Server-to-Server OAuth for machine-to-machine authentication. + +## Scenario + +You're building a backend service that needs to: +- Automatically create and manage meetings for your organization +- Generate meeting reports +- Provision and deprovision users +- No user interaction required +- Account-wide API access + +## Required Skills + +1. **oauth** - S2S OAuth token management +2. **zoom-rest-api** - Account management endpoints + +## Architecture + +``` +Cron Job / Backend Service + ↓ + Token Cache (Redis) + ↓ + Zoom APIs (account-wide access) +``` + +## Implementation + +### 1. S2S OAuth Setup (oauth) + +**Configure app in Zoom Marketplace:** +- App Type: Server-to-Server OAuth +- Add required scopes: `meeting:write:admin`, `user:write:admin`, `report:read:admin` +- Get credentials: Account ID, Client ID, Client Secret + +**See:** `oauth/concepts/oauth-flows.md#server-to-server-s2s-oauth` + +### 2. Token Management with Redis (oauth) + +```javascript +const redis = require('redis'); +const client = redis.createClient(); + +async function getZoomToken() { + // Check cache first + let token = await client.get('zoom_s2s_token'); + + if (!token) { + // Request new token + const response = await axios.post( + 'https://zoom.us/oauth/token', + 'grant_type=account_credentials&account_id=' + ACCOUNT_ID, + { + headers: { + 'Authorization': 'Basic ' + Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64') + } + } + ); + + token = response.data.access_token; + + // Cache with TTL (10 second buffer before actual expiration) + await client.setex('zoom_s2s_token', response.data.expires_in - 10, token); + } + + return token; +} +``` + +**See:** `oauth/examples/s2s-oauth-redis.md` + +### 3. Automated User Provisioning (zoom-rest-api) + +```javascript +// Daily cron job to sync users +cron.schedule('0 0 * * *', async () => { + const token = await getZoomToken(); + + const newUsers = await getNewUsersFromHR(); + + for (const user of newUsers) { + await axios.post( + 'https://api.zoom.us/v2/users', + { + action: 'create', + user_info: { + email: user.email, + type: 1, + first_name: user.firstName, + last_name: user.lastName + } + }, + { + headers: { Authorization: `Bearer ${token}` } + } + ); + } +}); +``` + +**Chain to:** `zoom-rest-api` for endpoint details + +### 4. Meeting Reports (zoom-rest-api) + +```javascript +// Generate weekly meeting reports +async function generateWeeklyReport() { + const token = await getZoomToken(); + + const response = await axios.get( + 'https://api.zoom.us/v2/report/users', + { + params: { + from: startOfWeek(), + to: endOfWeek() + }, + headers: { Authorization: `Bearer ${token}` } + } + ); + + return response.data.users; +} +``` + +**Chain to:** `zoom-rest-api` reporting endpoints + +## Production Deployment + +### Docker Setup (oauth) + +```yaml +# docker-compose.yml +version: '3.8' +services: + redis: + image: redis:7-alpine + + automation-service: + build: . + environment: + - ZOOM_ACCOUNT_ID=${ZOOM_ACCOUNT_ID} + - ZOOM_CLIENT_ID=${ZOOM_CLIENT_ID} + - ZOOM_CLIENT_SECRET=${ZOOM_CLIENT_SECRET} + - REDIS_URL=redis://redis:6379 + depends_on: + - redis +``` + +**See:** `oauth/examples/s2s-oauth-redis.md#docker-deployment` + +## Error Handling + +### Token Errors (oauth) + +```javascript +try { + const token = await getZoomToken(); +} catch (error) { + if (error.response?.data?.error === 'invalid_client') { + // Invalid credentials + logger.error('Invalid Zoom credentials'); + alertOps('Zoom integration broken - check credentials'); + } +} +``` + +**See:** `oauth/troubleshooting/common-errors.md` + +### Rate Limiting (zoom-rest-api) + +```javascript +// Implement retry logic for rate limits +const retryRequest = async (fn, retries = 3) => { + for (let i = 0; i < retries; i++) { + try { + return await fn(); + } catch (error) { + if (error.response?.status === 429) { + // Rate limited - wait and retry + await sleep(Math.pow(2, i) * 1000); + continue; + } + throw error; + } + } +}; +``` + +**Chain to:** `zoom-rest-api` for rate limit details + +## Testing + +```javascript +// Test S2S token acquisition +describe('S2S OAuth', () => { + it('should get valid access token', async () => { + const token = await getZoomToken(); + expect(token).toMatch(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/); + }); + + it('should cache token in Redis', async () => { + await getZoomToken(); + const cached = await client.get('zoom_s2s_token'); + expect(cached).toBeTruthy(); + }); +}); +``` + +## Related Use Cases + +- `meeting-automation.md` - Advanced meeting workflows +- `usage-reporting-analytics.md` - Account usage analytics +- `user-and-meeting-creation.md` - Bulk operations + +## Skills Used + +- **oauth** (primary) - S2S OAuth, token caching +- **zoom-rest-api** - Account management, reporting +- **webhooks** - Real-time event notifications diff --git a/plugins/zoom-developers/skills/general/use-cases/collaborative-apps.md b/plugins/zoom-developers/skills/general/use-cases/collaborative-apps.md new file mode 100644 index 00000000..a4d374d2 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/collaborative-apps.md @@ -0,0 +1,89 @@ +# Collaborative Apps + +Build real-time shared experiences across meeting participants. + +## Overview + +Collaborative Zoom Apps let multiple participants interact with the same shared state simultaneously - like Google Docs for Zoom meetings. Examples: shared whiteboards, polls, text editors, dashboards. + +## Skills Needed + +- **zoom-apps-sdk** (Collaborate Mode, App Communication) - Primary +- **oauth** - Authentication + +## Synchronization Patterns + +| Pattern | Technology | Best For | Complexity | +|---------|-----------|----------|------------| +| **SDK messaging** | connect() + postMessage() | Simple state (counters, toggles) | Low | +| **Server relay** | Socket.io / WebSocket | Polls, games, dashboards | Medium | +| **CRDT sync** | Y.js + WebRTC | Text editors, whiteboards | High | + +### Pattern 1: SDK Messaging (Simplest) + +No server needed for state sync: + +```javascript +await zoomSdk.connect(); + +// Send state change +await zoomSdk.postMessage({ + payload: JSON.stringify({ type: 'vote', option: 'A' }) +}); + +// Receive state changes +zoomSdk.addEventListener('onMessage', (event) => { + const data = JSON.parse(event.payload); + applyChange(data); +}); +``` + +### Pattern 2: Server Relay + +Your backend is the source of truth: + +```javascript +const socket = io('https://your-server.com'); +const { meetingUUID } = await zoomSdk.getMeetingUUID(); +socket.emit('join', { room: meetingUUID }); + +socket.on('state-update', (state) => renderApp(state)); +socket.emit('action', { type: 'vote', option: 'A' }); +``` + +### Pattern 3: CRDT (Conflict-Free) + +Best for concurrent editing (text, drawings): + +```javascript +import * as Y from 'yjs'; +import { WebrtcProvider } from 'y-webrtc'; + +const { meetingUUID } = await zoomSdk.getMeetingUUID(); +const ydoc = new Y.Doc(); +const provider = new WebrtcProvider(meetingUUID, ydoc); +// Changes sync automatically via CRDT +``` + +## Meeting UUID as Room ID + +Use `getMeetingUUID()` as the room identifier for state synchronization: + +```javascript +const { meetingUUID } = await zoomSdk.getMeetingUUID(); +// Same UUID for all participants in the same meeting/room +// Different UUID in breakout rooms +``` + +## Detailed Guides + +- **[Collaborate Mode Example](../../zoom-apps-sdk/examples/collaborate-mode.md)** - Complete implementation +- **[App Communication](../../zoom-apps-sdk/examples/app-communication.md)** - Instance messaging +- **[Breakout Rooms](../../zoom-apps-sdk/examples/breakout-rooms.md)** - Cross-room state sync +- **Sample app**: https://github.com/zoom/zoomapps-texteditor-vuejs + +## Skill Chain + +``` +zoom-apps-sdk (Collaborate + Communication) --> oauth (authorization) +``` diff --git a/plugins/zoom-developers/skills/general/use-cases/contact-center-app-lifecycle-and-context-switching.md b/plugins/zoom-developers/skills/general/use-cases/contact-center-app-lifecycle-and-context-switching.md new file mode 100644 index 00000000..95bc7d47 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/contact-center-app-lifecycle-and-context-switching.md @@ -0,0 +1,36 @@ +# Contact Center App Lifecycle and Context Switching + +Build a Contact Center app that survives engagement switching without losing in-progress work. + +## Skills Needed + +- `contact-center` +- `zoom-apps-sdk` +- `zoom-oauth` (if backend identity mapping is required) + +## Problem + +Agents can switch between active engagements. Your app instance may remain alive while visible context changes. If state is global rather than engagement-scoped, data corruption and agent frustration follow. + +## Recommended Pattern + +1. Configure SDK with engagement capabilities. +2. Query initial context and status. +3. Subscribe to engagement context and status change events. +4. Store drafts and workflow state by `engagementId`. +5. On context switch, load the target engagement state. +6. On end state, finalize or clear that engagement data. + +## Failure Modes To Avoid + +- Single shared draft object for all engagements. +- Late event subscription after user interaction starts. +- Hard cleanup on tab switch instead of engagement end. +- Assuming visibility equals process lifetime. + +## Implementation References + +- `../../contact-center/web/examples/app-context-and-state.md` +- `../../contact-center/concepts/architecture-and-lifecycle.md` +- `../../contact-center/RUNBOOK.md` + diff --git a/plugins/zoom-developers/skills/general/use-cases/contact-center-integration.md b/plugins/zoom-developers/skills/general/use-cases/contact-center-integration.md new file mode 100644 index 00000000..7d1b112b --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/contact-center-integration.md @@ -0,0 +1,39 @@ +# Contact Center Integration + +Build support and engagement workflows with Zoom Contact Center across app, web, and mobile surfaces. + +## Skills Needed + +- `contact-center` (primary) +- `zoom-apps-sdk` (for Contact Center apps in Zoom client) +- `zoom-rest-api` (for Contact Center API automation) +- `zoom-oauth` (for authorization patterns) + +## Choose the Surface + +1. Contact Center app in Zoom client: +- Use engagement APIs/events and state by `engagementId`. +2. Website embed: +- Use campaign/web SDK scripts with readiness gating. +3. Native mobile: +- Use Android/iOS SDK service lifecycle patterns. + +## Core Architecture + +1. Initialize context and identity. +2. Start channel service (chat/video/ZVA/scheduled callback). +3. Handle engagement events and context switches. +4. Persist engagement-scoped workflow state. +5. End and cleanup channel services. + +## High-Value Use Cases + +- Agent notes app keyed by engagement. +- CRM integration with Smart Embed events. +- Campaign-driven routing to chat/video channels. +- Native app rejoin flow for dropped video sessions. + +## Where to Go Next + +- `../../contact-center/SKILL.md` +- `../../contact-center/RUNBOOK.md` diff --git a/plugins/zoom-developers/skills/general/use-cases/custom-meeting-ui-web.md b/plugins/zoom-developers/skills/general/use-cases/custom-meeting-ui-web.md new file mode 100644 index 00000000..b9e683cd --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/custom-meeting-ui-web.md @@ -0,0 +1,96 @@ +# Custom Meeting UI (Web) + +Build a custom video user interface around a real Zoom meeting in a web application. + +## Correct Skill Path + +- Primary skill: [../../meeting-sdk/web/component-view/SKILL.md](../../meeting-sdk/web/component-view/SKILL.md) +- Supporting auth guidance: [../../oauth/SKILL.md](../../oauth/SKILL.md) + +Do not route this use case to Video SDK unless the user is building a non-meeting custom +session product. + +## Why Component View + +Use Meeting SDK Component View when: +- the app must join a real Zoom meeting +- the meeting should render inside your page layout +- you need custom placement and styling around the Zoom meeting UI +- you still want Zoom meeting semantics such as real meeting join/start behavior + +Do not use Video SDK when: +- the requirement is specifically a Zoom meeting +- the user expects Meeting SDK auth, meeting numbers, passwords, ZAK/OBF rules, or webinar behavior + +## Minimal Architecture + +```text +Browser UI + -> fetch signature from backend + -> ZoomMtgEmbedded.createClient() + -> client.init({ zoomAppRoot }) + -> client.join({ signature, sdkKey, meetingNumber, userName, password }) +``` + +## Minimal Flow + +1. Create a backend signature endpoint. +2. In the browser, create one `ZoomMtgEmbedded` client instance. +3. Initialize it with a real DOM container. +4. Join using backend-generated signature plus meeting credentials. +5. Handle join/init errors explicitly in UI state. + +## Minimal Example + +```javascript +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +const client = ZoomMtgEmbedded.createClient(); + +export async function joinEmbeddedMeeting({ + meetingNumber, + userName, + password, +}) { + const res = await fetch('/api/signature', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ meetingNumber, role: 0 }), + }); + + if (!res.ok) { + throw new Error(`signature_fetch_failed:${res.status}`); + } + + const { signature, sdkKey } = await res.json(); + + await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', + patchJsMedia: true, + leaveOnPageUnload: true, + }); + + await client.join({ + signature, + sdkKey, + meetingNumber, + userName, + password, + }); +} +``` + +## Common Failure Points + +- Wrong route: using Video SDK instead of Meeting SDK Component View +- Missing backend signature generation +- Wrong password field name in the wrong view (`password` here, not `passWord`) +- Missing OBF/ZAK requirements for meetings outside the app account +- Missing SharedArrayBuffer headers when higher-end web meeting features are expected + +## References + +- [../../meeting-sdk/web/SKILL.md](../../meeting-sdk/web/SKILL.md) +- [../../meeting-sdk/web/component-view/SKILL.md](../../meeting-sdk/web/component-view/SKILL.md) +- [../../meeting-sdk/web/troubleshooting/error-codes.md](../../meeting-sdk/web/troubleshooting/error-codes.md) diff --git a/plugins/zoom-developers/skills/general/use-cases/custom-video.md b/plugins/zoom-developers/skills/general/use-cases/custom-video.md new file mode 100644 index 00000000..ae0c0910 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/custom-video.md @@ -0,0 +1,232 @@ +# Custom Video + +Build fully branded video experiences with complete UI control. + +## Overview + +Use the Zoom Video SDK to create custom video applications with your own UI, branding, and user experience - powered by Zoom's infrastructure. + +## Skills Needed + +- **zoom-video-sdk** - Primary + +## Meeting SDK vs Video SDK + +| Aspect | Meeting SDK | Video SDK | +|--------|-------------|-----------| +| UI | Zoom's UI | Your custom UI | +| Branding | Zoom branding | Your branding | +| Experience | Zoom meetings | Video sessions | +| Features | Full Zoom features | Core video features | + +## Platform Options + +| Platform | Use Case | Guide | +|----------|----------|-------| +| Web | Browser-based custom video | [video-sdk/SKILL.md](../../video-sdk/SKILL.md) | +| Linux | Headless bots, raw media capture/injection | [video-sdk/linux/linux.md](../../video-sdk/linux/linux.md) | +| iOS | Custom video on iPhone/iPad | - | +| Android | Custom video on Android | - | +| Desktop | Custom desktop video apps | - | + +## Quick Start (Web) + +```javascript +import ZoomVideo from '@zoom/videosdk'; + +const client = ZoomVideo.createClient(); +await client.init('en-US', 'CDN'); +await client.join(topic, signature, userName, password); + +const stream = client.getMediaStream(); +await stream.startVideo(); +await stream.startAudio(); +``` + +## Common Tasks + +### Building Custom Video Layouts + +```javascript +// Initialize Video SDK +const client = ZoomVideo.createClient(); +await client.init('en-US', 'CDN'); +await client.join(topic, signature, userName, password); + +const stream = client.getMediaStream(); + +// Start my video +await stream.startVideo(); +await stream.renderVideo( + document.querySelector('#my-video'), + client.getSessionInfo().userId, + 1280, 720, // width, height + 0, 0, // x, y offset + 3 // quality (1-4) +); + +// Listen for other participants +client.on('user-added', (payload) => { + payload.forEach(async (user) => { + if (user.bVideoOn) { + await renderParticipantVideo(user.userId); + } + }); +}); + +async function renderParticipantVideo(userId) { + const container = createVideoContainer(userId); + await stream.renderVideo(container, userId, 640, 360, 0, 0, 2); +} +``` + +### Gallery View Implementation + +```javascript +class GalleryView { + constructor(containerEl, maxPerPage = 25) { + this.container = containerEl; + this.maxPerPage = maxPerPage; + this.currentPage = 0; + this.participants = []; + } + + updateLayout() { + const count = Math.min(this.participants.length, this.maxPerPage); + const cols = Math.ceil(Math.sqrt(count)); + const rows = Math.ceil(count / cols); + + const cellWidth = this.container.clientWidth / cols; + const cellHeight = this.container.clientHeight / rows; + + // Render each participant in grid + this.participants.slice(0, this.maxPerPage).forEach((userId, index) => { + const x = (index % cols) * cellWidth; + const y = Math.floor(index / cols) * cellHeight; + + stream.renderVideo( + this.getCanvas(userId), + userId, + cellWidth, cellHeight, + x, y, 2 + ); + }); + } + + addParticipant(userId) { + this.participants.push(userId); + this.updateLayout(); + } + + removeParticipant(userId) { + this.participants = this.participants.filter(id => id !== userId); + this.updateLayout(); + } +} +``` + +### Speaker View Implementation + +```javascript +class SpeakerView { + constructor(mainEl, stripEl) { + this.mainVideo = mainEl; + this.stripContainer = stripEl; + this.activeSpeaker = null; + this.participants = []; + } + + setActiveSpeaker(userId) { + if (this.activeSpeaker === userId) return; + + this.activeSpeaker = userId; + + // Render active speaker large + stream.renderVideo( + this.mainVideo, + userId, + 1280, 720, 0, 0, 4 // High quality + ); + + // Update thumbnail strip + this.updateStrip(); + } + + updateStrip() { + const others = this.participants.filter(id => id !== this.activeSpeaker); + const thumbWidth = 160; + const thumbHeight = 90; + + others.forEach((userId, index) => { + stream.renderVideo( + this.getStripCanvas(userId), + userId, + thumbWidth, thumbHeight, + index * thumbWidth, 0, 1 // Lower quality + ); + }); + } +} + +// Auto-switch to active speaker +client.on('active-speaker', (payload) => { + if (payload.userId) { + speakerView.setActiveSpeaker(payload.userId); + } +}); +``` + +### Custom Controls + +```javascript +// Audio controls +async function toggleMute() { + const stream = client.getMediaStream(); + const isMuted = stream.isAudioMuted(); + + if (isMuted) { + await stream.unmuteAudio(); + } else { + await stream.muteAudio(); + } + + updateMuteButton(!isMuted); +} + +// Video controls +async function toggleVideo() { + const stream = client.getMediaStream(); + const isVideoOn = stream.isCapturingVideo(); + + if (isVideoOn) { + await stream.stopVideo(); + } else { + await stream.startVideo(); + await stream.renderVideo(myCanvas, myUserId, 1280, 720, 0, 0, 3); + } + + updateVideoButton(!isVideoOn); +} + +// Device selection +async function switchCamera(deviceId) { + const stream = client.getMediaStream(); + await stream.switchCamera(deviceId); +} + +async function switchMicrophone(deviceId) { + const stream = client.getMediaStream(); + await stream.switchMicrophone(deviceId); +} + +// Get available devices +const cameras = await ZoomVideo.getCameras(); +const mics = await ZoomVideo.getMicrophones(); +const speakers = await ZoomVideo.getSpeakers(); +``` + +## Resources + +- **Video SDK docs**: https://developers.zoom.us/docs/video-sdk/ +- **Web sample**: https://github.com/zoom/videosdk-web-sample +- **UI Toolkit**: https://developers.zoom.us/docs/video-sdk/web/ui-toolkit/ diff --git a/plugins/zoom-developers/skills/general/use-cases/customer-support-cobrowsing.md b/plugins/zoom-developers/skills/general/use-cases/customer-support-cobrowsing.md new file mode 100644 index 00000000..00b1ece0 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/customer-support-cobrowsing.md @@ -0,0 +1,453 @@ +# Customer Support Co-Browsing + +Enable real-time collaborative browsing between support agents and customers for efficient issue resolution and form completion assistance. + +## Use Case Overview + +**Problem**: Customers struggle to describe issues or complete complex forms, leading to long support calls and high frustration. + +**Solution**: Implement Zoom Cobrowse SDK to allow support agents to view and assist customers' browsers in real-time, with privacy controls for sensitive data. + +**Related Skills**: +- [Zoom Cobrowse SDK](../../cobrowse-sdk/SKILL.md) +- [Contact Center Integration](contact-center-integration.md) + +## Architecture + +``` +┌──────────────────────┐ ┌──────────────────────┐ +│ Customer Browser │ │ Support Agent │ +│ • View form/page │◄───────►│ • View customer │ +│ • Share PIN │ Sync │ • Provide guidance │ +│ • Get assistance │ │ • Draw annotations │ +└──────────────────────┘ └──────────────────────┘ + │ │ + └────────────┬───────────────────┘ + ▼ + ┌──────────────────────┐ + │ Your Auth Server │ + │ • Generate JWTs │ + │ • Log sessions │ + │ • Track agents │ + └──────────────────────┘ +``` + +## Implementation Steps + +### 1. Set Up Authentication Server + +```javascript +// server.js - JWT token generation +const express = require('express'); +const { KJUR } = require('jsrsasign'); + +app.post('/cobrowse-token', (req, res) => { + const { role, userId, userName, caseId } = req.body; + + const iat = Math.floor(Date.now() / 1000); + const exp = iat + 60 * 60 * 2; // 2 hours + + const payload = { + app_key: process.env.ZOOM_SDK_KEY, + role_type: role, // 1 = customer, 2 = agent + user_id: userId, + user_name: userName, + iat, + exp + }; + + const token = KJUR.jws.JWS.sign('HS256', + JSON.stringify({ alg: 'HS256', typ: 'JWT' }), + JSON.stringify(payload), + process.env.ZOOM_SDK_SECRET + ); + + // Log session for tracking + logCobrowseSession(caseId, userId, role); + + res.json({ token }); +}); +``` + +### 2. Integrate Customer Support Page + +```html + + + + + + + +
+ + +
+ + +
+ + + + +
+ + + + +``` + +### 3. Agent Portal Integration + +```html + + + + + Support Agent - Co-Browse + + +
+
+

Case #

+

Customer:

+
+ + +
+ + + + +``` + +## Key Features + +### Privacy Masking + +Automatically hide sensitive customer data: + +```javascript +const settings = { + piiMask: { + maskType: "custom_input", + maskCssSelectors: ".pii-mask, .sensitive, [data-private]", + maskHTMLAttributes: "data-private=true" + } +}; +``` + +**What gets masked:** +- Social Security Numbers +- Credit Card Numbers +- Bank Account Numbers +- Passwords +- Any field with `.pii-mask` class + +### Annotation Tools + +Agents can guide customers visually: +- **Pen tool**: Highlight form fields +- **Rectangle**: Circle important sections +- **Pointer**: Direct attention to specific elements + +### Session Management + +Track and log all cobrowse sessions: + +```javascript +function logCobrowseSession(caseId, userId, role) { + const log = { + caseId, + userId, + role: role === 1 ? 'customer' : 'agent', + timestamp: new Date(), + sessionType: 'cobrowse' + }; + + database.sessions.insert(log); +} +``` + +## Use Case Scenarios + +### Scenario 1: Complex Form Assistance + +**Context**: Customer struggles with multi-step insurance application + +**Flow**: +1. Customer clicks "Get Help" on form page +2. PIN generated and displayed +3. Customer calls support, shares PIN +4. Agent enters PIN, sees customer's form +5. Agent uses annotation to highlight next field +6. Customer fills form with real-time guidance +7. Sensitive fields (SSN, medical info) masked from agent + +**Outcome**: Form completed in 5 minutes vs 20 minutes phone call + +### Scenario 2: Technical Troubleshooting + +**Context**: Customer can't find account settings + +**Flow**: +1. Customer initiates cobrowse from help chat +2. Agent joins session +3. Agent uses pointer to show navigation path +4. Customer follows visual guidance +5. Issue resolved in real-time + +**Outcome**: No screen sharing software needed, instant resolution + +### Scenario 3: Onboarding New Users + +**Context**: First-time user needs guided tour + +**Flow**: +1. Onboarding specialist starts cobrowse +2. Customer joins via PIN +3. Specialist guides through features +4. Annotations highlight key functions +5. Customer follows along in their browser + +**Outcome**: Interactive onboarding, higher completion rate + +## Security Considerations + +### 1. Privacy Compliance + +```javascript +// GDPR/CCPA compliant data handling +const privacySettings = { + piiMask: { + maskType: "custom_input", + maskCssSelectors: ".pii-mask" + }, + sessionRecording: false, // Don't record sessions + dataRetention: "24h" // Auto-delete session logs +}; +``` + +### 2. Agent Authentication + +```javascript +// Verify agent credentials before token generation +async function validateAgent(agentId) { + const agent = await database.agents.findOne({ id: agentId }); + + if (!agent || !agent.cobrowseEnabled) { + throw new Error("Agent not authorized for cobrowse"); + } + + return agent; +} +``` + +### 3. Session Limits + +- Max 5 agents per customer session +- 2-hour token expiration +- Automatic session end on inactivity (10 minutes) +- HTTPS required for all connections + +## Metrics and Analytics + +Track cobrowse effectiveness: + +```javascript +// Track session metrics +const metrics = { + sessionDuration: calculateDuration(startTime, endTime), + issueResolved: true, + customerSatisfaction: 5, + formFieldsCompleted: 12, + annotationsUsed: 8 +}; + +analytics.track('cobrowse_session_completed', metrics); +``` + +**Key Metrics**: +- Average session duration +- Issue resolution rate +- Customer satisfaction (CSAT) +- Time saved vs phone support +- Conversion rate (form completion) + +## Integration with Existing Systems + +### Salesforce Integration + +```javascript +// Log cobrowse session to Salesforce case +async function logToSalesforce(caseId, sessionData) { + await salesforce.cases.update(caseId, { + Cobrowse_Session_Date__c: new Date(), + Cobrowse_PIN__c: sessionData.pin, + Agent_Id__c: sessionData.agentId, + Session_Duration__c: sessionData.duration + }); +} +``` + +### Zendesk Integration + +```javascript +// Add cobrowse note to Zendesk ticket +await zendesk.tickets.addComment(ticketId, { + body: `Cobrowse session completed. PIN: ${pin}. Duration: ${duration}`, + public: false +}); +``` + +## Best Practices + +1. **Clear Privacy Disclosure** + - Inform customer what agent can see + - Display "Agent is viewing your screen" banner + +2. **Selective Masking** + - Mask by default, unmask if needed + - Use `.pii-mask` class liberally + +3. **Session Logging** + - Track all sessions for compliance + - Log agent actions for quality assurance + +4. **Agent Training** + - Train agents on privacy controls + - Establish annotation best practices + +5. **Customer Consent** + - Get explicit consent before starting + - Allow customer to end session anytime + +## Cost Considerations + +**Zoom SDK Pricing**: +- Pay per cobrowse minute +- SDK Universal Credits required +- See [Zoom pricing](https://zoom.us/pricing/sdk) + +**Estimated Costs**: +- 100 sessions/day × 10 min avg = 1,000 minutes/day +- ~$30-50/day depending on plan + +## Related Resources + +- [Zoom Cobrowse SDK Documentation](../../cobrowse-sdk/SKILL.md) +- [Get Started Guide](../../cobrowse-sdk/get-started.md) +- [Session Events Reference](../../cobrowse-sdk/references/session-events.md) +- [Privacy Masking Example](../../cobrowse-sdk/examples/privacy-masking.md) +- [Contact Center Integration](contact-center-integration.md) + +## Common Issues + +**Issue**: Customer can't see PIN +**Solution**: Check `pincode_updated` event handler is properly attached + +**Issue**: Agent sees sensitive data +**Solution**: Verify `.pii-mask` class applied to sensitive fields + +**Issue**: Session disconnects on page refresh +**Solution**: Implement auto-reconnection pattern (see [Auto-Reconnection](../../cobrowse-sdk/examples/auto-reconnection.md)) + +## Next Steps + +1. Review [Get Started Guide](../../cobrowse-sdk/get-started.md) +2. Set up auth server using [JWT Authentication](../../cobrowse-sdk/concepts/jwt-authentication.md) +3. Integrate customer page with [Customer Integration](../../cobrowse-sdk/examples/customer-integration.md) +4. Deploy agent portal with [Agent Integration](../../cobrowse-sdk/examples/agent-integration.md) +5. Test privacy masking with [Privacy Masking Example](../../cobrowse-sdk/examples/privacy-masking.md) diff --git a/plugins/zoom-developers/skills/general/use-cases/electron-meeting-embed.md b/plugins/zoom-developers/skills/general/use-cases/electron-meeting-embed.md new file mode 100644 index 00000000..91c2041f --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/electron-meeting-embed.md @@ -0,0 +1,28 @@ +# Electron Meeting Embed + +Use this flow when you need Zoom meetings embedded inside a desktop Electron application. + +## When to Use + +- You ship a desktop app with Electron. +- You need native-like meeting controls in app workflows. +- You need meeting modules beyond basic join/leave (recording, participants, share, raw data). + +## Skill Chain + +1. [meeting-sdk/electron](../../meeting-sdk/electron/SKILL.md) +2. [zoom-oauth](../../oauth/SKILL.md) + +## Typical Flow + +1. Backend signs short-lived Meeting SDK JWT. +2. Electron app initializes SDK and authenticates. +3. App joins/starts meeting and binds controllers. +4. Optional advanced modules (raw data, webinar, whiteboard) are enabled as needed. +5. App leaves and performs explicit SDK cleanup. + +## References + +- [Electron Meeting SDK Skill](../../meeting-sdk/electron/SKILL.md) +- [Lifecycle Workflow](../../meeting-sdk/electron/concepts/lifecycle-workflow.md) +- [Deprecated and Contradictions](../../meeting-sdk/electron/troubleshooting/deprecated-and-contradictions.md) diff --git a/plugins/zoom-developers/skills/general/use-cases/embed-meetings.md b/plugins/zoom-developers/skills/general/use-cases/embed-meetings.md new file mode 100644 index 00000000..9cd6b943 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/embed-meetings.md @@ -0,0 +1,230 @@ +# Embed Meetings + +Embed the full Zoom meeting experience into your web or mobile application. + +## Overview + +Use the Zoom Meeting SDK to embed complete Zoom meetings into your application with Zoom's UI and features. + +## Skills Needed + +- **zoom-meeting-sdk** - Primary + +## Platform Options + +| Platform | View Options | +|----------|--------------| +| Web | Component View, Client View | +| iOS | Native SDK | +| Android | Native SDK | +| Desktop | Native SDK | + +## Web Views + +| View | Description | +|------|-------------| +| Component View | Extractable, customizable UI elements | +| Client View | Full-page Zoom meeting experience | + +## Quick Start (Web Component View) + +```javascript +const client = ZoomMtgEmbedded.createClient(); + +client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', +}); + +client.join({ + sdkKey: 'YOUR_SDK_KEY', + signature: 'YOUR_SIGNATURE', + meetingNumber: 'MEETING_NUMBER', + userName: 'User Name', +}); +``` + +## Common Tasks + +### Component View Setup + +Component View embeds the meeting in a specific DOM element, allowing you to build UI around it. + +```javascript +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +// Create client +const client = ZoomMtgEmbedded.createClient(); + +// Initialize +await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', + customize: { + video: { + isResizable: true, + viewSizes: { + default: { width: 1000, height: 600 }, + ribbon: { width: 300, height: 700 } + } + }, + meetingInfo: ['topic', 'host', 'mn', 'pwd', 'telPwd', 'invite', 'participant', 'dc', 'enctype'], + toolbar: { + buttons: [ + { text: 'Custom', className: 'CustomButton', onClick: () => console.log('Custom clicked') } + ] + } + } +}); + +// Join meeting +await client.join({ + sdkKey: SDK_KEY, + signature: signature, + meetingNumber: '123456789', + password: 'password', + userName: 'John Doe', + userEmail: 'john@example.com' +}); +``` + +### Client View Setup + +Client View opens a full-page meeting experience (traditional Zoom UI). + +```javascript +import { ZoomMtg } from '@zoom/meetingsdk'; + +// Load dependencies +ZoomMtg.preLoadWasm(); +ZoomMtg.prepareWebSDK(); + +// Initialize +ZoomMtg.init({ + leaveUrl: 'https://your-app.com/meeting-ended', + success: () => { + ZoomMtg.join({ + sdkKey: SDK_KEY, + signature: signature, + meetingNumber: '123456789', + passWord: 'password', + userName: 'John Doe', + userEmail: 'john@example.com', + success: (res) => { + console.log('Joined meeting', res); + }, + error: (err) => { + console.error('Join error', err); + } + }); + } +}); +``` + +### Customizing Meeting UI + +```javascript +// Hide specific UI elements +await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + customize: { + // Hide meeting info + meetingInfo: [], + + // Customize toolbar + toolbar: { + buttons: [ + // Add custom buttons + { + text: 'Info', + className: 'info-btn', + onClick: () => showInfo() + } + ] + }, + + // Video layout + video: { + viewSizes: { + default: { width: 1280, height: 720 } + }, + popper: { + disableDraggable: false + } + }, + + // Active speaker view + activeStateEnabledMode: { + enabled: true + } + } +}); + +// Change view programmatically +client.changeView('gallery'); // 'speaker' | 'gallery' | 'ribbon' +``` + +### Handling Meeting Events + +```javascript +// Meeting status events +client.on('connection-change', (payload) => { + const { state } = payload; + switch (state) { + case 'Connected': + console.log('Connected to meeting'); + break; + case 'Reconnecting': + console.log('Reconnecting...'); + break; + case 'Closed': + console.log('Meeting ended'); + handleMeetingEnd(); + break; + } +}); + +// User events +client.on('user-added', (payload) => { + console.log('User joined:', payload); +}); + +client.on('user-removed', (payload) => { + console.log('User left:', payload); +}); + +// Audio/video events +client.on('active-speaker', (payload) => { + console.log('Active speaker:', payload.userId); +}); + +// Error handling +client.on('error', (payload) => { + console.error('SDK Error:', payload); +}); +``` + +### Leave/End Meeting + +```javascript +// Leave meeting (participant) +client.leaveMeeting(); + +// End meeting (host only) +client.endMeeting(); + +// Handle leave URL (Client View) +// User is redirected to leaveUrl specified in init() +``` + +## Native SDK (iOS/Android) + +For native mobile apps, see: +- [Meeting SDK iOS](../../meeting-sdk/references/ios.md) +- [Meeting SDK Android](../../meeting-sdk/references/android.md) + +## Resources + +- **Meeting SDK docs**: https://developers.zoom.us/docs/meeting-sdk/ +- **Web sample**: https://github.com/zoom/meetingsdk-web-sample +- **Component View docs**: https://developers.zoom.us/docs/meeting-sdk/web/component-view/ diff --git a/plugins/zoom-developers/skills/general/use-cases/enterprise-app-deployment.md b/plugins/zoom-developers/skills/general/use-cases/enterprise-app-deployment.md new file mode 100644 index 00000000..4da03b7c --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/enterprise-app-deployment.md @@ -0,0 +1,34 @@ +# Enterprise App Deployment (Account-Wide Install / Approval) + +High-frequency questions in the forum clusters: + +- "How do I deploy an app to an entire company?" +- "How do I allow other users to use my app?" +- "Why can only the admin use it?" + +## Skills Needed + +| Order | Skill | Purpose | +|------:|------|---------| +| 1 | **zoom-general** | Understand Marketplace deployment/approval concepts | +| 2 | **zoom-rest-api** (optional) | Automate admin tasks (where supported) | + +## What To Clarify Upfront + +- Is this an internal app (single account) or public Marketplace app? +- Does the customer want: + - admin pre-approval + user install? + - account-level installation? + - restricting installs to specific users/groups? + +## Common Fix Patterns + +- Ensure the app is configured for the right audience and install flow. +- Ensure required scopes are approved and users re-consented if scopes changed. +- If users are blocked, check account admin Marketplace policies. + +## Links + +- `marketplace-publishing.md` +- `../references/marketplace.md` + diff --git a/plugins/zoom-developers/skills/general/use-cases/flutter-video-sessions.md b/plugins/zoom-developers/skills/general/use-cases/flutter-video-sessions.md new file mode 100644 index 00000000..00f11fc0 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/flutter-video-sessions.md @@ -0,0 +1,27 @@ +# Flutter Video Sessions + +Use this flow when you are building custom real-time video sessions in a Flutter mobile app. + +## When to Use + +- You need full control over UI/UX (not Zoom Meeting UI). +- You are building iOS/Android mobile session experiences in Flutter. +- You need helper-driven features such as chat, share, recording, or transcription. + +## Skill Chain + +1. [video-sdk/flutter](../../video-sdk/flutter/SKILL.md) +2. [zoom-oauth](../../oauth/SKILL.md) + +## Typical Flow + +1. Backend signs short-lived Video SDK JWT. +2. Flutter app initializes SDK and binds event listeners. +3. App joins session and activates media/helpers. +4. App leaves and cleans up explicitly. + +## References + +- [Flutter Video SDK Skill](../../video-sdk/flutter/SKILL.md) +- [Lifecycle Workflow](../../video-sdk/flutter/concepts/lifecycle-workflow.md) +- [Session Join Pattern](../../video-sdk/flutter/examples/session-join-pattern.md) diff --git a/plugins/zoom-developers/skills/general/use-cases/form-completion-assistant.md b/plugins/zoom-developers/skills/general/use-cases/form-completion-assistant.md new file mode 100644 index 00000000..89d74350 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/form-completion-assistant.md @@ -0,0 +1,527 @@ +# Form Completion Assistant with Co-Browsing + +Guide customers through complex forms in real-time using visual assistance and privacy-protected co-browsing. + +## Use Case Overview + +**Problem**: High form abandonment rates due to complexity, confusion, or lack of confidence in data entry. + +**Solution**: Implement Zoom Cobrowse SDK to provide real-time visual guidance while protecting sensitive customer data through privacy masking. + +**Related Skills**: +- [Zoom Cobrowse SDK](../../cobrowse-sdk/SKILL.md) +- [Customer Support Co-Browsing](customer-support-cobrowsing.md) + +## Target Scenarios + +### Financial Services +- Loan applications +- Account opening forms +- Investment questionnaires +- Insurance claims + +### Healthcare +- Patient intake forms +- Insurance enrollment +- Medical history questionnaires +- Telehealth registration + +### E-Commerce +- Complex checkout flows +- B2B order forms +- Subscription sign-ups +- Custom product configurators + +## Implementation + +### Quick Start Pattern + +```html + + + + Loan Application - Form Assistant + + + +
+

Loan Application

+ +
+ +
+ +
+

Personal Information

+ + + + +
+ + +
+

Identification

+ + 🔒 This field is hidden from support agents + + + 🔒 This field is hidden from support agents +
+ + +
+

Financial Information

+ + + + + 🔒 This field is hidden from support agents + + + 🔒 This field is hidden from support agents +
+ + +
+

Loan Request

+ + +
+
+ + + + + + +``` + +## Key Features + +### Progressive Privacy Masking + +Mask fields based on sensitivity level: + +```javascript +// Tier 1: Fully masked (never visible to agent) +// - SSN, passwords, credit cards +const tier1Fields = ".pii-mask, [data-privacy='full']"; + +// Tier 2: Masked by default, can be unmasked with consent +// - Bank account numbers, tax IDs +const tier2Fields = "[data-privacy='optional']"; + +const settings = { + piiMask: { + maskType: "custom_input", + maskCssSelectors: tier1Fields, + } +}; +``` + +### Smart Annotation Guidance + +Agent can highlight and guide: + +```html + + +``` + +### Real-Time Validation Assistance + +Agent sees validation errors in real-time: + +```javascript +// Show validation status to agent +form.addEventListener("blur", (e) => { + if (e.target.validity.valid) { + e.target.classList.add("valid"); + } else { + e.target.classList.add("invalid"); + // Agent can see this and provide guidance + } +}, true); +``` + +## Workflow Example + +### Multi-Step Form Assistance + +``` +CUSTOMER AGENT + │ │ + │ 1. Opens loan application │ + │ (Step 1 of 4) │ + │ │ + │ 2. Confused at Step 2 │ + │ Clicks "Need Help?" │ + ├────────► Request Help │ + │ (PIN: 123456) │ + │ │ + │ 3. Calls support │ + │ "I need help with │ + │ loan application" │ + │ │ + │ │ 4. Agent opens case + │ │ Enters PIN: 123456 + │ │ + │ ◄─────── Agent Joined ─────────┤ + │ │ + │ 5. Agent sees form (Step 2) │ + │ Masked fields: SSN, DOB │ + │ Visible: Everything else │ + │ │ + │ │ 6. Agent uses pen tool + │ ◄────── Highlight field ───────┤ Highlights "SSN" + │ │ Says: "Enter your SSN" + │ │ + │ 7. Enters SSN │ + │ (Agent sees: ***-**-****) │ + │ │ + │ │ 8. Agent highlights next + │ ◄────── Highlight DOB ─────────┤ "Now your date of birth" + │ │ + │ 9. Completes Step 2 │ + │ Moves to Step 3 │ + │ │ + │ 10. Finishes form │ + │ ─────► Form Submitted ─────────► + │ │ + │ 11. Thanks agent │ + │ ◄────── Session Ends ──────────┤ +``` + +## Privacy Protection Patterns + +### Pattern 1: Full Masking (Recommended) + +```javascript +// All sensitive fields completely hidden +const settings = { + piiMask: { + maskType: "custom_input", + maskCssSelectors: ".pii-mask, .sensitive, [data-private]" + } +}; +``` + +**Use for**: SSN, passwords, credit cards, medical records + +### Pattern 2: Partial Masking + +```javascript +// Show last 4 digits (not natively supported, requires custom implementation) +function partialMask(value) { + return '*'.repeat(value.length - 4) + value.slice(-4); +} +``` + +**Use for**: Phone numbers (show area code), account numbers + +### Pattern 3: Contextual Masking + +```javascript +// Mask based on form section +function updateMasking(stepNumber) { + const maskingRules = { + 1: ".none", // No masking on basic info + 2: ".ssn, .dob", // Mask ID fields + 3: ".account, .routing", // Mask financial fields + 4: ".none" // No masking on loan details + }; + + // Update SDK settings dynamically + // Note: Requires re-initialization in current SDK version +} +``` + +## Analytics and Tracking + +### Measure Assistance Effectiveness + +```javascript +// Track form completion with/without assistance +const metrics = { + formType: "loan_application", + assistanceRequested: true, + assistanceDuration: 420, // seconds + stepWhereHelpRequested: 2, + completionRate: 1, // 1 = completed, 0 = abandoned + timeToComplete: 1200, // seconds + fieldsModified: 18, + validationErrors: 2 +}; + +analytics.track("form_completion", metrics); +``` + +### Key Metrics to Track + +1. **Form Abandonment Rate** + - Before assistance: 35% + - With assistance: 8% + - **73% improvement** + +2. **Time to Complete** + - Without assistance: 15 min avg + - With assistance: 8 min avg + - **47% faster** + +3. **Error Rate** + - Without assistance: 3.2 errors/form + - With assistance: 0.8 errors/form + - **75% fewer errors** + +4. **Customer Satisfaction** + - CSAT score: 4.7/5 + - NPS: +68 + +## Integration Examples + +### Salesforce Integration + +```javascript +// Log form assistance session to Salesforce +async function logFormAssistance(leadId, sessionData) { + await salesforce.leads.update(leadId, { + Form_Assistance_Date__c: new Date(), + Assistance_Duration__c: sessionData.duration, + Form_Completed__c: sessionData.completed, + Agent_Id__c: sessionData.agentId + }); +} +``` + +### HubSpot Integration + +```javascript +// Create activity for form assistance +await hubspot.contacts.createActivity(contactId, { + type: "cobrowse_assistance", + timestamp: Date.now(), + properties: { + form_type: "loan_application", + duration: sessionDuration, + completed: formCompleted + } +}); +``` + +## Best Practices + +### 1. Clear Help Triggers + +Place help buttons strategically: +- Top of form (always visible) +- Beginning of each complex section +- Near confusing field labels + +### 2. Privacy First + +- Mask by default, unmask only if absolutely necessary +- Show clear indicators when fields are masked +- Get consent before starting session + +### 3. Progress Preservation + +```javascript +// Save form state before assistance +function saveFormState() { + const formData = new FormData(document.getElementById("loan-application")); + localStorage.setItem("form_draft", JSON.stringify(Object.fromEntries(formData))); +} + +// Restore on page reload +function restoreFormState() { + const saved = localStorage.getItem("form_draft"); + if (saved) { + const data = JSON.parse(saved); + Object.entries(data).forEach(([name, value]) => { + const field = document.querySelector(`[name="${name}"]`); + if (field) field.value = value; + }); + } +} +``` + +### 4. Agent Training + +Train agents on: +- Which fields are masked vs visible +- How to use annotation tools effectively +- When to suggest breaks for complex forms +- Privacy compliance requirements + +## Common Challenges & Solutions + +**Challenge**: Customer refreshes page during assistance +**Solution**: Implement auto-reconnection (see [Auto-Reconnection Guide](../../cobrowse-sdk/examples/auto-reconnection.md)) + +**Challenge**: Agent can't see validation errors +**Solution**: Add visual indicators that aren't masked: +```javascript +field.parentElement.classList.add("has-error"); +``` + +**Challenge**: Multi-page forms lose session +**Solution**: Use session persistence across page navigation: +```javascript +const settings = { + multiTabSessionPersistence: { + enable: true + } +}; +``` + +## Cost-Benefit Analysis + +### Costs +- **Zoom SDK**: ~$0.05 per assistance minute +- **Development**: 2-3 weeks integration +- **Agent Training**: 4 hours per agent + +### Benefits +- **Reduced Abandonment**: 35% → 8% = 27% more conversions +- **Faster Completion**: 15 min → 8 min = 47% time saved +- **Fewer Errors**: 75% reduction in submission errors +- **Higher CSAT**: 4.7/5 satisfaction score + +**ROI Example** (1000 forms/month): +- Before: 650 completed (35% abandoned) +- After: 920 completed (8% abandoned) +- Additional conversions: 270/month +- Avg value/conversion: $50 +- Additional revenue: $13,500/month +- SDK cost: ~$500/month +- **Net benefit: $13,000/month** + +## Related Resources + +- [Zoom Cobrowse SDK](../../cobrowse-sdk/SKILL.md) +- [Customer Support Co-Browsing](customer-support-cobrowsing.md) +- [Privacy Masking Example](../../cobrowse-sdk/examples/privacy-masking.md) +- [Session Events Reference](../../cobrowse-sdk/references/session-events.md) +- [Get Started Guide](../../cobrowse-sdk/get-started.md) + +## Next Steps + +1. **Identify high-abandon forms** in your application +2. **Review privacy requirements** for your industry +3. **Set up auth server** using [JWT Authentication](../../cobrowse-sdk/concepts/jwt-authentication.md) +4. **Integrate customer SDK** using [Customer Integration](../../cobrowse-sdk/examples/customer-integration.md) +5. **Train support agents** on co-browsing and privacy controls +6. **Measure results** and iterate based on analytics diff --git a/plugins/zoom-developers/skills/general/use-cases/hd-video-resolution.md b/plugins/zoom-developers/skills/general/use-cases/hd-video-resolution.md new file mode 100644 index 00000000..a39dc788 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/hd-video-resolution.md @@ -0,0 +1,336 @@ +# HD Video Resolution + +Achieve 720p and 1080p video quality in Zoom Web SDKs, including viewport size requirements that affect resolution. + +## Overview + +HD video quality in Zoom SDKs depends on multiple factors: container/viewport size, network bandwidth, SharedArrayBuffer support, and concurrent stream limits. **Video automatically scales down if the container is smaller than required dimensions.** + +## Skills Needed + +- **zoom-meeting-sdk** (Web) +- **zoom-video-sdk** (Web) + +## Viewport Size Requirements + +**Critical:** Video resolution is automatically adjusted based on the rendered container size. + +### Resolution Thresholds + +| Target Resolution | Minimum Container Size | Bandwidth Required | +|-------------------|------------------------|-------------------| +| **360p** | 480 × 270 | 600 kbps | +| **720p** | 1280 × 720 (or 720 × 411 gallery) | 1.2-1.5 Mbps | +| **1080p** | 1920 × 1080 | 2.5-3.0 Mbps | + +**If your video container is smaller than 1280×720, you will NOT get 720p video - it will automatically scale down.** + +### Meeting SDK Component View Constraints + +| View Type | Minimum | Maximum | Aspect Ratio | +|-----------|---------|---------|--------------| +| **Speaker** | 240 × 135 | 1440 × 810 | 16:9 | +| **Gallery** | 720 × 411 | 1440 × 720 | 16:9 | +| **Ribbon** | 240 × 135 | 316 × 720 | Variable | + +### Recommended Sizes for HD + +```javascript +// For 720p in speaker view +const speakerContainer = { + width: 1280, + height: 720 +}; + +// For 720p in gallery view (minimum) +const galleryContainer = { + width: 720, + height: 411 +}; + +// For 1080p (speaker view only) +const fullHDContainer = { + width: 1920, + height: 1080 +}; +``` + +## Concurrent Stream Limits + +**Video SDK enforces strict concurrent HD limits:** + +| Resolution | Concurrent Limit | Notes | +|------------|------------------|-------| +| **720p** | **Max 2 streams** | Attempting 3rd results in `Errors_Wrong_Usage` | +| **1080p** | **Only 1 stream** | Only one 1080p video can be rendered at a time | + +### Recommended Quality by View + +| View Type | Active Speaker | Other Participants | +|-----------|----------------|-------------------| +| **Speaker View** | 720p or 1080p | 180p | +| **Gallery (3×3)** | 360p all | 360p | +| **Gallery (5×5)** | 180p all | 180p | +| **Small thumbnails** | 180p | 180p | + +## SharedArrayBuffer (SAB) Requirement + +### Features Requiring SAB + +| Feature | SAB Required | +|---------|--------------| +| **Sending 720p video** | ✅ Yes | +| **Virtual Background** | ✅ Yes | +| **Gallery view (multiple videos)** | ✅ Yes | +| **Background noise suppression** | ✅ Yes | + +### Enabling SharedArrayBuffer + +Requires Cross-Origin Isolation headers on your server: + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +**Express.js example:** +```javascript +app.use((req, res, next) => { + res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); + res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); + next(); +}); +``` + +**Nginx example:** +```nginx +add_header Cross-Origin-Opener-Policy same-origin; +add_header Cross-Origin-Embedder-Policy require-corp; +``` + +### Browser Support for SAB + +| Browser | Minimum Version | +|---------|-----------------| +| Chrome | 68+ | +| Edge | 79+ | +| Firefox | 79+ | +| Safari | 15.2+ (macOS), iOS 15.2+ | +| Opera | 73+ | + +**Note:** Safari requires newer versions and may have limitations. + +### Impact of Missing SAB + +Without SharedArrayBuffer: +- Max sending resolution: **360p** +- No virtual background +- Limited to single video rendering +- No background noise suppression + +## Video SDK Configuration + +### Enable HD Video Capture + +```javascript +// Start video with HD enabled +await stream.startVideo({ + hd: true, // Enable 720p + fullHd: true, // Enable 1080p (if supported) + captureWidth: 1280, + captureHeight: 720 +}); +``` + +### Subscribe to Specific Quality + +```javascript +// VideoQuality enum values: +// Video_90P = 0 +// Video_180P = 1 +// Video_360P = 2 +// Video_720P = 3 +// Video_1080P = 4 + +// Attach video at specific quality +await stream.attachVideo(userId, VideoQuality.Video_720P); + +// Or with renderVideo +await stream.renderVideo(canvas, userId, 1280, 720, 0, 0, VideoQuality.Video_720P); +``` + +### Check Device Capabilities + +```javascript +// Check if device supports HD +const capabilities = await stream.getVideoCapabilities(); +console.log('Max resolution:', capabilities.maxResolution); +console.log('HD supported:', capabilities.hdSupported); +``` + +## Meeting SDK Configuration + +### Component View HD + +```javascript +ZoomMtg.init({ + leaveUrl: 'https://your-site.com', + disablePreview: false, + videoResolution: '720p', // or '1080p' + success: () => { + console.log('Init success'); + } +}); +``` + +### Responsive Container Setup + +```html +
+ +
+``` + +```javascript +// Ensure container meets minimum size for HD +const container = document.getElementById('zoom-container'); +const rect = container.getBoundingClientRect(); + +if (rect.width < 1280 || rect.height < 720) { + console.warn('Container too small for 720p - video will be downscaled'); +} +``` + +## WebRTC vs WebAssembly Mode + +| Mode | Characteristics | +|------|-----------------| +| **WebRTC** (Primary, SDK v2+) | Enhanced performance, adaptive bitrate, better congestion control | +| **WebAssembly** (Fallback) | Custom Zoom codec, more reliable 720p, supports virtual backgrounds | + +SDK v2 automatically selects mode based on network and device conditions. + +## Account Requirements (Zoom Meetings) + +For **Group HD** in Zoom Meetings (not SDK): + +| Requirement | 720p | 1080p | +|-------------|------|-------| +| Max video participants | 2 | 2 | +| Full-screen mode | Required | Required | +| Active speaker mode | Required | Required | +| CPU | Minimum specs | i7 Quad Core+ | +| Bandwidth | 1.5 Mbps | 3.0 Mbps | +| Mobile support | ❌ No | ❌ No | + +**Key:** If a third participant turns video on, quality reverts to standard definition. + +## Best Practices + +### 1. Size Your Container Correctly + +```javascript +function ensureHDContainer(container, targetResolution = 720) { + const minWidth = targetResolution === 1080 ? 1920 : 1280; + const minHeight = targetResolution === 1080 ? 1080 : 720; + + container.style.minWidth = `${minWidth}px`; + container.style.minHeight = `${minHeight}px`; + container.style.aspectRatio = '16/9'; +} +``` + +### 2. Handle Window Resize + +```javascript +window.addEventListener('resize', () => { + const container = document.getElementById('zoom-container'); + const rect = container.getBoundingClientRect(); + + // Adjust quality based on available space + if (rect.width >= 1920 && rect.height >= 1080) { + stream.attachVideo(userId, VideoQuality.Video_1080P); + } else if (rect.width >= 1280 && rect.height >= 720) { + stream.attachVideo(userId, VideoQuality.Video_720P); + } else { + stream.attachVideo(userId, VideoQuality.Video_360P); + } +}); +``` + +### 3. Check SAB Support + +```javascript +function checkSABSupport() { + if (typeof SharedArrayBuffer === 'undefined') { + console.warn('SharedArrayBuffer not available - HD features limited'); + return false; + } + + // Check if cross-origin isolated + if (!crossOriginIsolated) { + console.warn('Not cross-origin isolated - SAB may not work'); + return false; + } + + return true; +} +``` + +### 4. Limit Concurrent HD Streams + +```javascript +const MAX_720P_STREAMS = 2; +let current720pCount = 0; + +async function subscribeToVideo(userId, preferredQuality) { + let quality = preferredQuality; + + if (quality === VideoQuality.Video_720P) { + if (current720pCount >= MAX_720P_STREAMS) { + console.warn('Max 720p streams reached, using 360p'); + quality = VideoQuality.Video_360P; + } else { + current720pCount++; + } + } + + await stream.attachVideo(userId, quality); +} +``` + +### 5. Maintain 16:9 Aspect Ratio + +```css +.video-container { + position: relative; + width: 100%; + padding-bottom: 56.25%; /* 16:9 aspect ratio */ +} + +.video-container canvas, +.video-container video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +``` + +## Troubleshooting + +| Issue | Cause | Solution | +|-------|-------|----------| +| Video stuck at 360p | Container too small | Resize container to ≥1280×720 | +| Video stuck at 360p | Missing SAB headers | Add COOP/COEP headers | +| 720p works, 1080p doesn't | Only one 1080p allowed | Check concurrent streams | +| HD works in dev, not prod | Different CORS headers | Verify production headers | +| Safari HD not working | SAB not supported | Check Safari version ≥15.2 | + +## Resources + +- **Video SDK HD docs**: https://developers.zoom.us/docs/video-sdk/web/video-hd/ +- **Meeting SDK resizing**: https://developers.zoom.us/docs/meeting-sdk/web/component-view/resizing/ +- **SharedArrayBuffer docs**: https://developers.zoom.us/docs/meeting-sdk/web/sharedarraybuffer/ +- **Cross-Origin Isolation**: https://web.dev/articles/coop-coep/ diff --git a/plugins/zoom-developers/skills/general/use-cases/high-volume-meeting-platform.md b/plugins/zoom-developers/skills/general/use-cases/high-volume-meeting-platform.md new file mode 100644 index 00000000..bbe92e9e --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/high-volume-meeting-platform.md @@ -0,0 +1,56 @@ +# High-Volume Meeting Platform + +Design a distributed system that creates large numbers of meetings and keeps meeting state accurate under retries, webhook delay, and partial outages. + +## Skills Needed + +- Primary: [../../rest-api/SKILL.md](../../rest-api/SKILL.md) +- Events: [../../webhooks/SKILL.md](../../webhooks/SKILL.md) +- Auth/token broker: [../../oauth/SKILL.md](../../oauth/SKILL.md) +- Deep implementation reference: [../references/distributed-meeting-fallback-architecture.md](../references/distributed-meeting-fallback-architecture.md) + +## Architecture Summary + +```text +API Gateway + -> Command Service + -> Idempotency Store + -> Token Broker + -> Zoom REST API + -> Outbox / Queue + +Webhook Ingress + -> Signature Verification + -> Durable Queue + -> Projection Workers + -> Meeting State Store + +Recovery + -> Retry Workers + -> Reconciliation Poller + -> DLQ Replay +``` + +## Core Rules + +1. Keep command and event planes separate. +2. Require idempotency keys on all meeting-creation requests. +3. Queue everything that touches Zoom when you need backpressure control. +4. Verify webhook signatures before durable write. +5. Reconcile state from REST when event delivery is delayed or incomplete. + +## Concrete Fallbacks + +- `429` / `5xx` from Zoom REST API -> retry with jitter, then queue for delayed retry. +- Token refresh contention -> single token broker refresh under distributed lock. +- Webhook processor outage -> accept only after queue write, use DLQ replay. +- Missing lifecycle events -> scheduled reconciliation poller repairs the projection. +- Downstream Zoom outage -> circuit breaker opens and create commands stay queued. + +## When to Use This + +Use this pattern when: +- multiple workers or services create meetings concurrently +- you need durable event processing +- missed webhook events are unacceptable +- you need a degraded-but-safe mode during Zoom or network instability diff --git a/plugins/zoom-developers/skills/general/use-cases/immersive-experiences.md b/plugins/zoom-developers/skills/general/use-cases/immersive-experiences.md new file mode 100644 index 00000000..d64578db --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/immersive-experiences.md @@ -0,0 +1,83 @@ +# Immersive Experiences + +Custom video layouts replacing the standard Zoom gallery view using the Layers API. + +## Overview + +The Layers API lets you take over the meeting's video display to create custom layouts - podcast formats, talk shows, classrooms, game shows, and branded meeting experiences. + +## Skills Needed + +- **zoom-apps-sdk** (Layers API) - Primary +- **oauth** - Authentication + +## Use Case Patterns + +| Pattern | Description | Key APIs | +|---------|-------------|----------| +| Podcast layout | 2-3 hosts with custom background | drawParticipant, drawImage | +| Talk show | Large host + row of guests | drawParticipant, drawImage | +| Classroom | Teacher prominent + student thumbnails | drawParticipant, drawImage | +| Game show | Custom positions with animated overlays | drawParticipant, drawImage, drawWebView | +| Branded meeting | Company background + participant positions | drawParticipant, drawImage | +| Interactive dashboard | Participants + live data panels | drawParticipant, drawWebView | + +## Architecture + +``` +Host Participants +──── ──────────── +Controls layout (UI panel) --> Receive layout via Socket.io/backend +runRenderingContext() --> runRenderingContext() +drawParticipant() x N --> drawParticipant() x N (same positions) +drawImage() (background) --> drawImage() (same background) +``` + +All participants must be in immersive mode and rendering the same layout. The host typically controls layout changes and broadcasts them via your backend (Socket.io, WebSocket). + +## Quick Start + +```javascript +import zoomSdk from '@zoom/appssdk'; + +await zoomSdk.config({ + capabilities: [ + 'runRenderingContext', 'closeRenderingContext', + 'drawParticipant', 'clearParticipant', + 'drawImage', 'clearImage', + 'getMeetingParticipants', 'onParticipantChange' + ], + version: '0.16' +}); + +// Start immersive mode +await zoomSdk.runRenderingContext({ view: 'immersive' }); + +// Get participants and layout them +const { participants } = await zoomSdk.getMeetingParticipants(); +// ... position participants with drawParticipant() +``` + +## Performance Considerations + +- Pre-render backgrounds to a single canvas image +- Minimize drawImage calls during animations +- Use requestAnimationFrame for smooth transitions +- Test on lower-end hardware (not everyone has a fast machine) +- Keep zIndex values in 0-10 range + +## Detailed Guides + +- **[Layers Immersive Example](../../zoom-apps-sdk/examples/layers-immersive.md)** - Complete podcast layout code +- **[Layers API Reference](../../zoom-apps-sdk/references/layers-api.md)** - All drawing methods +- **[Camera Mode Example](../../zoom-apps-sdk/examples/layers-camera.md)** - Virtual camera overlays +- **Sample app**: https://github.com/zoom/zoomapps-customlayout-js + +## Skill Chain + +``` +zoom-apps-sdk (Layers API) --> oauth (authorization) + | + v +zoom-rest-api (optional: meeting management) +``` diff --git a/plugins/zoom-developers/skills/general/use-cases/in-meeting-apps.md b/plugins/zoom-developers/skills/general/use-cases/in-meeting-apps.md new file mode 100644 index 00000000..a9932e42 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/in-meeting-apps.md @@ -0,0 +1,306 @@ +# In-Meeting Apps + +Build apps that run inside the Zoom client during meetings. + +## Overview + +Create Zoom Apps that appear within the Zoom meeting interface - polls, games, collaboration tools, and more that participants can interact with during meetings. + +## Skills Needed + +- **zoom-apps-sdk** - Primary +- **oauth** - Authentication +- **zoom-rest-api** - Server-side API calls (optional) + +## App Types + +| Type | Description | Key APIs | +|------|-------------|----------| +| Sidebar app | Panel alongside meeting | getMeetingContext, shareApp | +| Immersive app | Full-screen Layers API | runRenderingContext, drawParticipant | +| Camera mode | Virtual camera overlay | runRenderingContext({ view: 'camera' }) | +| Collaborate | Shared state app | startCollaborate, connect, postMessage | +| Background app | Runs without visible UI | Events, REST API calls | + +## Architecture + +``` +Frontend (Zoom embedded browser) Backend (Express/Node.js) +───────────────────────────────── ──────────────────────── +@zoom/appssdk OAuth token exchange +zoomSdk.config() REST API calls +zoomSdk.getMeetingContext() Token storage (Redis) +fetch('/api/data') ─────────────> Business logic +``` + +## Quick Start + +```javascript +import zoomSdk from '@zoom/appssdk'; + +await zoomSdk.config({ + capabilities: ['shareApp', 'getMeetingContext', 'getUserContext'], + version: '0.16' +}); + +const context = await zoomSdk.getMeetingContext(); +console.log('Meeting ID:', context.meetingID); + +await zoomSdk.shareApp(); +``` + +## Common Tasks + +### Building a Poll App + +```javascript +import zoomSdk from '@zoom/appssdk'; + +// Initialize +await zoomSdk.config({ + capabilities: [ + 'shareApp', + 'getMeetingContext', + 'getMeetingParticipants', + 'sendAppInvitation' + ] +}); + +// Poll state +let currentPoll = { + question: '', + options: [], + votes: {} +}; + +// Create poll +function createPoll(question, options) { + currentPoll = { + question, + options, + votes: {} + }; + broadcastPollState(); +} + +// Submit vote +async function submitVote(optionIndex) { + const context = await zoomSdk.getMeetingContext(); + currentPoll.votes[context.participantId] = optionIndex; + broadcastPollState(); +} + +// Share results +function getResults() { + const counts = currentPoll.options.map((_, i) => + Object.values(currentPoll.votes).filter(v => v === i).length + ); + return currentPoll.options.map((opt, i) => ({ + option: opt, + count: counts[i], + percentage: (counts[i] / Object.keys(currentPoll.votes).length * 100).toFixed(1) + })); +} + +// Invite others to participate +async function inviteParticipants() { + await zoomSdk.sendAppInvitation({ + action: 'open', + message: 'Join the poll!' + }); +} +``` + +### Collaborative Whiteboard + +```javascript +// Whiteboard with real-time sync +const canvas = document.getElementById('whiteboard'); +const ctx = canvas.getContext('2d'); + +// Drawing state +let isDrawing = false; +let lastX = 0; +let lastY = 0; + +canvas.addEventListener('mousedown', (e) => { + isDrawing = true; + [lastX, lastY] = [e.offsetX, e.offsetY]; +}); + +canvas.addEventListener('mousemove', (e) => { + if (!isDrawing) return; + + const stroke = { + from: { x: lastX, y: lastY }, + to: { x: e.offsetX, y: e.offsetY }, + color: currentColor, + width: currentWidth + }; + + drawStroke(stroke); + broadcastStroke(stroke); // Sync with others + + [lastX, lastY] = [e.offsetX, e.offsetY]; +}); + +function drawStroke(stroke) { + ctx.beginPath(); + ctx.moveTo(stroke.from.x, stroke.from.y); + ctx.lineTo(stroke.to.x, stroke.to.y); + ctx.strokeStyle = stroke.color; + ctx.lineWidth = stroke.width; + ctx.lineCap = 'round'; + ctx.stroke(); +} + +// Receive strokes from others +onRemoteStroke((stroke) => { + drawStroke(stroke); +}); +``` + +### Meeting Timer/Agenda + +```javascript +import zoomSdk from '@zoom/appssdk'; + +// Timer app +class MeetingTimer { + constructor() { + this.agenda = []; + this.currentItem = 0; + this.startTime = null; + } + + async init() { + await zoomSdk.config({ + capabilities: ['getMeetingContext', 'shareApp'] + }); + } + + setAgenda(items) { + // items: [{ title: 'Intro', duration: 5 }, ...] + this.agenda = items.map(item => ({ + ...item, + elapsed: 0, + status: 'pending' + })); + this.broadcastState(); + } + + start() { + this.startTime = Date.now(); + this.agenda[this.currentItem].status = 'active'; + this.tick(); + } + + tick() { + const item = this.agenda[this.currentItem]; + const elapsed = Math.floor((Date.now() - this.startTime) / 1000 / 60); + item.elapsed = elapsed; + + if (elapsed >= item.duration) { + this.alertTimeUp(); + } + + this.broadcastState(); + setTimeout(() => this.tick(), 1000); + } + + nextItem() { + this.agenda[this.currentItem].status = 'completed'; + this.currentItem++; + if (this.currentItem < this.agenda.length) { + this.startTime = Date.now(); + this.agenda[this.currentItem].status = 'active'; + } + } + + alertTimeUp() { + // Visual/audio alert + document.getElementById('timer').classList.add('warning'); + } +} +``` + +### Layers API Visuals + +```javascript +import zoomSdk from '@zoom/appssdk'; + +// Layers API for immersive experiences +await zoomSdk.config({ + capabilities: ['runRenderingContext', 'clearRenderingContext'] +}); + +// Start Layers mode +await zoomSdk.runRenderingContext({ + view: 'immersive' +}); + +// Draw on the video layer +const canvas = document.getElementById('layers-canvas'); +const ctx = canvas.getContext('2d'); + +// Example: Add participant name labels +function drawNameLabel(participant, x, y) { + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(x, y - 25, 150, 25); + ctx.fillStyle = 'white'; + ctx.font = '14px Arial'; + ctx.fillText(participant.name, x + 5, y - 8); +} + +// Example: Add virtual background effects +function drawVirtualEffect() { + // Draw confetti, borders, icons, etc. + // These overlay on top of video +} + +// Stop Layers mode +async function exitLayers() { + await zoomSdk.clearRenderingContext(); +} +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `ZOOM_APP_CLIENT_ID` | Marketplace App Credentials | +| `ZOOM_APP_CLIENT_SECRET` | Marketplace App Credentials | +| `ZOOM_APP_REDIRECT_URI` | Your server URL + /auth | +| `SESSION_SECRET` | Random string for cookie signing | + +## Detailed Guides + +- **[zoom-apps-sdk SKILL.md](../../zoom-apps-sdk/SKILL.md)** - Comprehensive SDK guide +- **[Quick Start](../../zoom-apps-sdk/examples/quick-start.md)** - Hello World app +- **[In-Client OAuth](../../zoom-apps-sdk/examples/in-client-oauth.md)** - Authorization flow +- **[Layers API](../../zoom-apps-sdk/references/layers-api.md)** - Immersive experiences +- **[Immersive Experiences](immersive-experiences.md)** - Custom video layouts +- **[Collaborative Apps](collaborative-apps.md)** - Real-time shared state + +## Skill Chain + +``` +zoom-apps-sdk --> oauth --> zoom-rest-api (optional) +``` + +## App Publishing Checklist + +- [ ] OWASP security headers on all responses +- [ ] HTTPS enforced, valid SSL certificate +- [ ] PKCE OAuth implemented +- [ ] Error handling for all SDK calls +- [ ] Browser preview fallback UI +- [ ] Domain allowlist configured +- [ ] Tested on multiple screen sizes +- [ ] Submit to Zoom Marketplace + +## Resources + +- **Zoom Apps docs**: https://developers.zoom.us/docs/zoom-apps/ +- **Layers API**: https://developers.zoom.us/docs/zoom-apps/guides/layers-api/ +- **Sample apps**: https://github.com/zoom/zoomapps-sample-js diff --git a/plugins/zoom-developers/skills/general/use-cases/marketplace-publishing.md b/plugins/zoom-developers/skills/general/use-cases/marketplace-publishing.md new file mode 100644 index 00000000..6de0231e --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/marketplace-publishing.md @@ -0,0 +1,384 @@ +# Marketplace Publishing & ISV Guide + +Build and publish apps on the Zoom App Marketplace for multiple customers. + +## Overview + +This guide covers building multi-tenant applications for the Zoom Marketplace, handling multiple customer accounts, and the app review process. + +## Skills Needed + +- **general** - App configuration +- **zoom-rest-api** - Multi-tenant API calls +- **webhooks** - Per-customer event handling + +--- + +## App Types for Marketplace + +| App Type | Visibility | Use Case | +|----------|-----------|----------| +| **Account-level (Private)** | Your org only | Internal tools | +| **User-managed (Public)** | Individual users | User-facing apps | +| **Admin-managed (Public)** | Org admins install | Enterprise tools | + +For Marketplace publishing, you'll create **Public** apps. + +--- + +## Multi-Tenant Architecture + +### Database Schema + +Store per-customer OAuth tokens and settings: + +```sql +CREATE TABLE zoom_installations ( + id SERIAL PRIMARY KEY, + account_id VARCHAR(255) UNIQUE NOT NULL, + access_token TEXT NOT NULL, + refresh_token TEXT NOT NULL, + token_expires_at TIMESTAMP NOT NULL, + installed_at TIMESTAMP DEFAULT NOW(), + settings JSONB DEFAULT '{}' +); + +CREATE INDEX idx_zoom_account ON zoom_installations(account_id); +``` + +### OAuth Token Storage + +```javascript +// Store tokens after OAuth callback +async function handleOAuthCallback(code, state) { + // Exchange code for tokens + const tokens = await exchangeCodeForTokens(code); + + // Get account info + const accountInfo = await getAccountInfo(tokens.access_token); + + // Store or update installation + await db.query(` + INSERT INTO zoom_installations + (account_id, access_token, refresh_token, token_expires_at) + VALUES ($1, $2, $3, $4) + ON CONFLICT (account_id) + DO UPDATE SET + access_token = $2, + refresh_token = $3, + token_expires_at = $4 + `, [ + accountInfo.account_id, + tokens.access_token, + tokens.refresh_token, + new Date(Date.now() + tokens.expires_in * 1000) + ]); + + return accountInfo.account_id; +} +``` + +### Token Refresh + +```javascript +async function getValidToken(accountId) { + const installation = await db.query( + 'SELECT * FROM zoom_installations WHERE account_id = $1', + [accountId] + ); + + if (!installation) { + throw new Error('Account not installed'); + } + + // Check if token needs refresh + if (new Date(installation.token_expires_at) < new Date()) { + const newTokens = await refreshTokens(installation.refresh_token); + + await db.query(` + UPDATE zoom_installations + SET access_token = $1, refresh_token = $2, token_expires_at = $3 + WHERE account_id = $4 + `, [ + newTokens.access_token, + newTokens.refresh_token, + new Date(Date.now() + newTokens.expires_in * 1000), + accountId + ]); + + return newTokens.access_token; + } + + return installation.access_token; +} +``` + +--- + +## Webhook Handling for Multi-Tenant + +### Routing by Account + +```javascript +app.post('/webhook', async (req, res) => { + // Verify signature first + if (!verifyWebhookSignature(req)) { + return res.status(401).send('Invalid signature'); + } + + const { event, payload } = req.body; + const accountId = payload.account_id; + + // Check if this account has installed our app + const installation = await getInstallation(accountId); + if (!installation) { + console.log(`Webhook for unknown account: ${accountId}`); + return res.status(200).send(); // Still return 200 + } + + // Process event for this customer + await processEventForCustomer(accountId, event, payload); + + res.status(200).send(); +}); + +async function processEventForCustomer(accountId, event, payload) { + switch (event) { + case 'meeting.started': + await handleMeetingStarted(accountId, payload); + break; + case 'recording.completed': + await handleRecordingCompleted(accountId, payload); + break; + } +} +``` + +### Deauthorization Handling + +When a customer uninstalls your app: + +```javascript +app.post('/webhook', async (req, res) => { + const { event, payload } = req.body; + + if (event === 'app_deauthorized') { + const accountId = payload.account_id; + + // Clean up customer data + await db.query('DELETE FROM zoom_installations WHERE account_id = $1', [accountId]); + + // Optional: Delete customer data per compliance requirements + await cleanupCustomerData(accountId); + + console.log(`App deauthorized for account: ${accountId}`); + } + + res.status(200).send(); +}); +``` + +--- + +## API Calls for Specific Customers + +```javascript +class ZoomAPIClient { + constructor(accountId) { + this.accountId = accountId; + } + + async request(method, endpoint, data = null) { + const token = await getValidToken(this.accountId); + + const response = await axios({ + method, + url: `https://api.zoom.us/v2${endpoint}`, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + data + }); + + return response.data; + } + + async createMeeting(userId, meetingData) { + return this.request('POST', `/users/${userId}/meetings`, meetingData); + } + + async getUsers() { + return this.request('GET', '/users'); + } +} + +// Usage +const client = new ZoomAPIClient('customer_account_id'); +const meeting = await client.createMeeting('me', { topic: 'Team Sync' }); +``` + +--- + +## Scopes for Marketplace Apps + +Request minimal scopes needed: + +```javascript +// Good - specific scopes +const scopes = [ + 'meeting:read', + 'meeting:write', + 'user:read' +]; + +// Bad - overly broad +const scopes = [ + 'account:read:admin', + 'account:write:admin' +]; +``` + +### Scope Descriptions + +Provide clear descriptions for each scope in Marketplace: + +| Scope | User-Facing Description | +|-------|------------------------| +| `meeting:read` | View your meetings | +| `meeting:write` | Create and update meetings on your behalf | +| `recording:read` | Access your meeting recordings | + +--- + +## App Review Process + +### Pre-Submission Checklist + +- [ ] All required scopes have descriptions +- [ ] Privacy policy URL is valid and accessible +- [ ] Terms of service URL is valid +- [ ] Support email/URL is configured +- [ ] App description is clear and accurate +- [ ] Screenshots show actual app functionality +- [ ] Deauthorization webhook handles cleanup +- [ ] OAuth flow completes successfully +- [ ] Error handling is user-friendly + +### Common Rejection Reasons + +1. **Excessive scopes** - Only request what you need +2. **Missing deauthorization handling** - Must handle `app_deauthorized` +3. **Broken OAuth flow** - Test thoroughly +4. **Poor error messages** - Be user-friendly +5. **Privacy policy issues** - Must cover Zoom data usage +6. **Non-functional features** - All advertised features must work + +### Testing Before Submission + +```javascript +// Test OAuth flow +async function testOAuthFlow() { + // 1. Generate auth URL + const authUrl = generateAuthUrl(); + console.log('Auth URL:', authUrl); + + // 2. Complete OAuth manually in browser + // 3. Verify token storage + + // 4. Test API calls + const client = new ZoomAPIClient(testAccountId); + const users = await client.getUsers(); + console.log('Users:', users); + + // 5. Test webhook handling + await simulateWebhook('meeting.started', testPayload); +} + +// Test deauthorization +async function testDeauthorization() { + // Simulate deauth webhook + await simulateWebhook('app_deauthorized', { + account_id: testAccountId + }); + + // Verify cleanup + const installation = await getInstallation(testAccountId); + console.assert(installation === null, 'Installation should be deleted'); +} +``` + +--- + +## Data Residency & Compliance + +### Handle Regional Requirements + +```javascript +// Determine storage region based on user location +async function getStorageRegion(accountId) { + const accountInfo = await zoomClient.getAccountInfo(accountId); + + // Map Zoom data center to storage region + const regionMap = { + 'US': 'us-east-1', + 'EU': 'eu-west-1', + 'AU': 'ap-southeast-2', + 'IN': 'ap-south-1' + }; + + return regionMap[accountInfo.data_residency_region] || 'us-east-1'; +} + +// Store data in correct region +async function storeData(accountId, data) { + const region = await getStorageRegion(accountId); + const regionalStorage = getStorageClient(region); + + await regionalStorage.put(data); +} +``` + +--- + +## Rate Limiting for Multi-Tenant + +Implement per-customer rate limiting: + +```javascript +const rateLimit = require('express-rate-limit'); +const RedisStore = require('rate-limit-redis'); + +const apiLimiter = rateLimit({ + store: new RedisStore({ + client: redisClient, + prefix: 'rl:' + }), + windowMs: 60 * 1000, // 1 minute + max: 100, // 100 requests per minute per customer + keyGenerator: (req) => { + // Rate limit per customer account + return req.headers['x-account-id'] || req.ip; + } +}); + +app.use('/api/', apiLimiter); +``` + +--- + +## Publishing Steps + +1. **Development** - Build and test thoroughly +2. **Submit for Review** - In Marketplace portal +3. **Review Period** - 2-4 weeks typically +4. **Address Feedback** - Fix any issues found +5. **Approval** - App goes live +6. **Maintenance** - Monitor, update, support + +## Resources + +- **Marketplace Portal**: https://marketplace.zoom.us/ +- **Publishing Guide**: https://developers.zoom.us/docs/zoom-apps/publishing/ +- **App Review**: https://developers.zoom.us/docs/distribute/app-review/ +- **ISV Program**: https://zoom.us/partners/isv diff --git a/plugins/zoom-developers/skills/general/use-cases/meeting-automation.md b/plugins/zoom-developers/skills/general/use-cases/meeting-automation.md new file mode 100644 index 00000000..e7463987 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/meeting-automation.md @@ -0,0 +1,237 @@ +# Meeting Automation + +Schedule, update, and delete Zoom meetings programmatically. + +## Overview + +Use the Zoom REST API to automate meeting management - create meetings, update settings, manage participants, and delete meetings without manual intervention. + +If your primary goal is deterministic backend automation, stay on REST API. + +## Skills Needed + +- **zoom-rest-api** - Primary + +## Prerequisites + +- Server-to-Server OAuth or OAuth app +- `meeting:write` scope + +## Quick Start + +```bash +# Create a meeting +curl -X POST "https://api.zoom.us/v2/users/me/meetings" \ + -H "Authorization: Bearer {accessToken}" \ + -H "Content-Type: application/json" \ + -d '{ + "topic": "Automated Meeting", + "type": 2, + "start_time": "2024-01-15T10:00:00Z", + "duration": 60 + }' +``` + +## Common Tasks + +### Creating Recurring Meetings + +```javascript +const axios = require('axios'); + +// Daily recurring meeting +async function createDailyMeeting() { + const response = await axios.post( + 'https://api.zoom.us/v2/users/me/meetings', + { + topic: 'Daily Standup', + type: 8, // Recurring with fixed time + start_time: '2024-01-15T09:00:00Z', + duration: 15, + timezone: 'America/Los_Angeles', + recurrence: { + type: 1, // Daily + repeat_interval: 1, // Every day + end_times: 90 // 90 occurrences + }, + settings: { + join_before_host: true, + waiting_room: false, + mute_upon_entry: true + } + }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ); + + return response.data; +} + +// Weekly recurring meeting +async function createWeeklyMeeting() { + const response = await axios.post( + 'https://api.zoom.us/v2/users/me/meetings', + { + topic: 'Weekly Team Sync', + type: 8, + start_time: '2024-01-15T14:00:00Z', + duration: 60, + recurrence: { + type: 2, // Weekly + repeat_interval: 1, + weekly_days: '2,4', // Monday, Wednesday (1=Sun, 2=Mon, etc.) + end_date_time: '2024-12-31T00:00:00Z' + } + }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + + return response.data; +} +``` + +### Updating Meeting Settings + +```javascript +// Update meeting details +async function updateMeeting(meetingId, updates) { + await axios.patch( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { + topic: updates.topic, + start_time: updates.startTime, + duration: updates.duration, + settings: { + host_video: updates.hostVideo ?? true, + participant_video: updates.participantVideo ?? false, + join_before_host: updates.joinBeforeHost ?? false, + waiting_room: updates.waitingRoom ?? true, + mute_upon_entry: updates.muteOnEntry ?? true, + auto_recording: updates.autoRecording ?? 'none' // 'local', 'cloud', 'none' + } + }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); +} + +// Add meeting co-hosts +async function addCoHosts(meetingId, emails) { + // Co-hosts must be set before meeting starts + await axios.patch( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { + settings: { + alternative_hosts: emails.join(';'), + alternative_hosts_email_notification: true + } + }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); +} +``` + +### Managing Registrants + +```javascript +// Add registrant +async function addRegistrant(meetingId, registrant) { + const response = await axios.post( + `https://api.zoom.us/v2/meetings/${meetingId}/registrants`, + { + email: registrant.email, + first_name: registrant.firstName, + last_name: registrant.lastName, + custom_questions: [ + { title: 'Company', value: registrant.company }, + { title: 'Role', value: registrant.role } + ] + }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + + // Returns join_url for the registrant + return response.data; +} + +// List registrants +async function listRegistrants(meetingId, status = 'approved') { + // status: 'pending', 'approved', 'denied' + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}/registrants?status=${status}`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + + return response.data.registrants; +} + +// Approve/deny registrants +async function updateRegistrantStatus(meetingId, registrantId, action) { + // action: 'approve', 'deny', 'cancel' + await axios.put( + `https://api.zoom.us/v2/meetings/${meetingId}/registrants/status`, + { + action: action, + registrants: [{ id: registrantId }] + }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); +} +``` + +### Deleting/Canceling Meetings + +```javascript +// Delete meeting +async function deleteMeeting(meetingId, notifyHosts = true) { + await axios.delete( + `https://api.zoom.us/v2/meetings/${meetingId}?schedule_for_reminder=${notifyHosts}`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); +} + +// Delete specific occurrence of recurring meeting +async function deleteOccurrence(meetingId, occurrenceId) { + await axios.delete( + `https://api.zoom.us/v2/meetings/${meetingId}?occurrence_id=${occurrenceId}`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); +} + +// End a live meeting +async function endMeeting(meetingId) { + await axios.put( + `https://api.zoom.us/v2/meetings/${meetingId}/status`, + { action: 'end' }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); +} +``` + +### Rate Limit Considerations + +| Operation | Limit | +|-----------|-------| +| Create/Update meetings | 100 per user per day | +| API calls (Light) | 30/sec (Pro), 80/sec (Business+) | +| API calls (Heavy) | 10/sec (Pro), 40/sec (Business+) | + +```javascript +// Implement rate limiting +const Bottleneck = require('bottleneck'); + +const limiter = new Bottleneck({ + minTime: 100, // 10 requests per second max + maxConcurrent: 5 +}); + +const createMeetingLimited = limiter.wrap(createMeeting); +``` + +## Resources + +- **API Reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Meetings +- **Rate Limits**: https://developers.zoom.us/docs/api/rest/rate-limits/ diff --git a/plugins/zoom-developers/skills/general/use-cases/meeting-bots.md b/plugins/zoom-developers/skills/general/use-cases/meeting-bots.md new file mode 100644 index 00000000..de348ef6 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/meeting-bots.md @@ -0,0 +1,311 @@ +# Meeting Bots + +Build bots that join Zoom meetings for AI, transcription, and automation. + +## Overview + +Meeting bots are headless applications that join Zoom meetings as participants to perform tasks like recording, transcription, real-time AI processing, or automated interactions. + +## Skills Needed + +- **meeting-sdk/linux** - Visible bot join flow, raw recording, and raw media access +- **zoom-rest-api** - Meeting lookup plus OBF/ZAK retrieval, optional cloud-recording settings +- **zoom-webhooks** - Optional if you want Zoom-managed cloud recording download after the meeting +- **zoom-rtms** - Alternative when you need invisible media access instead of a visible participant bot + +## Architecture + +``` +Meeting Bot Architecture: +┌─────────────────┐ ┌─────────────────┐ +│ Zoom Meeting │────▶│ Bot (Linux) │ +│ │ │ - Meeting SDK │ +│ │◀────│ - Raw audio │ +└─────────────────┘ │ - Raw video │ + └────────┬────────┘ + │ + ┌────────▼────────┐ + │ AI Pipeline │ + │ - Transcription│ + │ - Analysis │ + └─────────────────┘ +``` + +## Platform + +| Platform | Recommended | +|----------|-------------| +| Linux | Yes - headless, server-side | +| Windows | Possible but not typical | +| macOS | Possible but not typical | + +## Key Features + +- Join meetings programmatically +- Access raw audio/video data +- Start and stop raw recording explicitly +- Real-time transcription +- AI processing (sentiment, summarization) + +## Automatic Join + Recording Pattern + +Use this chain when the user asks for a bot that automatically joins and records a meeting: + +```text +zoom-rest-api + -> fetch meeting metadata + -> mint OBF/ZAK token +meeting-sdk/linux + -> join as visible participant + -> StartRawRecording() + -> subscribe audio/video delegates + -> write PCM/YUV or send to downstream pipeline +optional zoom-webhooks + zoom-rest-api + -> receive recording.completed + -> download Zoom-managed cloud recording assets +``` + +### Raw Recording Control + +```cpp +void onMeetingStatusChanged(MeetingStatus status, int iResult) { + if (status != MEETING_STATUS_INMEETING) return; + + auto* recordCtrl = m_meetingService->GetMeetingRecordingController(); + if (!recordCtrl) { + throw std::runtime_error("recording_controller_unavailable"); + } + + if (recordCtrl->CanStartRawRecording() != SDKERR_SUCCESS) { + throw std::runtime_error("raw_recording_not_permitted"); + } + + SDKError err = recordCtrl->StartRawRecording(); + if (err != SDKERR_SUCCESS) { + throw std::runtime_error("start_raw_recording_failed"); + } + + GetAudioRawdataHelper()->subscribe(new AudioRawDataDelegate(), true); + + IZoomSDKRenderer* renderer = nullptr; + createRenderer(&renderer, new VideoRawDataDelegate()); + renderer->setRawDataResolution(ZoomSDKResolution_720P); + renderer->subscribe(activeSpeakerUserId, RAW_DATA_TYPE_VIDEO); +} +``` + +### Choose the Right Recording Output + +| Requirement | Correct path | +|-------------|--------------| +| Bot-owned audio/video files or real-time AI processing | Meeting SDK Linux raw recording | +| Zoom-hosted MP4/M4A/transcript files after meeting end | Cloud recording settings + webhooks + recordings REST API | + +`StartRawRecording()` enables raw media flow. It does not create a finished MP4 by itself. You still need to persist PCM/YUV or post-process it with your own pipeline. + +## Bot Implementation Patterns + +### 1. Bot Authentication Flow + +```cpp +// Step 1: Generate JWT for SDK authentication +void generateJWT(const string& key, const string& secret) { + auto iat = chrono::system_clock::now(); + auto exp = iat + chrono::hours{24}; + + m_jwt = jwt::create() + .set_type("JWT") + .set_issued_at(iat) + .set_expires_at(exp) + .set_payload_claim("appKey", claim(key)) + .set_payload_claim("tokenExp", claim(exp)) + .sign(algorithm::hs256{secret}); +} + +// Step 2: Authenticate SDK +AuthContext ctx; +ctx.jwt_token = m_jwt; +m_authService->SDKAuth(ctx); +// Wait for onAuthenticationReturn callback +``` + +### 2. Joining a Meeting as a Bot + +```cpp +JoinParam joinParam; +joinParam.userType = SDK_UT_WITHOUT_LOGIN; +JoinParam4WithoutLogin& param = joinParam.param.withoutloginuserJoin; +param.meetingNumber = meetingNumber; +param.userName = "My Transcription Bot"; // Display name +param.psw = password; +param.isVideoOff = true; // Bots typically don't need video +param.isAudioOff = false; // Need audio for transcription + +// For own meetings: Use ZAK token +param.userZAK = zakToken; + +// For external meetings (after Feb 2026): Use OBF token +param.onBehalfToken = obfToken; + +err = m_meetingService->Join(joinParam); +``` + +**Token Requirements:** + +| Meeting Type | Required Tokens | +|--------------|-----------------| +| Your own meetings | JWT + ZAK | +| External meetings (before Feb 2026) | JWT only | +| External meetings (after Feb 2026) | JWT + OBF | + +### 3. Capturing Audio Streams + +```cpp +class AudioRawDataDelegate : public IZoomSDKAudioRawDataDelegate { +public: + void onMixedAudioRawDataReceived(AudioRawData *data) override { + // Mixed audio from all participants + // Format: 16-bit PCM, 16kHz or 32kHz, mono + + // Send to transcription service + transcriptionService.process( + data->GetBuffer(), + data->GetBufferLen(), + data->GetSampleRate() + ); + } + + void onOneWayAudioRawDataReceived(AudioRawData* data, uint32_t node_id) override { + // Individual participant audio - useful for speaker identification + speakerIdentifier.process(node_id, data); + } +}; + +// Subscribe after joining meeting +auto* pRawDataHelper = GetAudioRawdataHelper(); +pRawDataHelper->subscribe(new AudioRawDataDelegate()); +``` + +### 4. Processing Video Frames + +```cpp +class VideoRawDataDelegate : public IZoomSDKRendererDelegate { +public: + void onRawDataFrameReceived(YUVRawDataI420 *data) override { + // Format: I420 (YUV 4:2:0) - contiguous planar data + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Option 1: Save raw YUV for later processing + yuvFile.write(data->GetYBuffer(), width * height); + yuvFile.write(data->GetUBuffer(), (width/2) * (height/2)); + yuvFile.write(data->GetVBuffer(), (width/2) * (height/2)); + + // Option 2: Convert to OpenCV for real-time processing + // (requires copying planes into contiguous buffer first) + } +}; + +// Subscribe to specific user's video +auto* pVideoHelper = GetRawdataRendererHelper(); +pVideoHelper->setRawDataResolution(ZoomSDKResolution_720P); +pVideoHelper->subscribe(userId, RAW_DATA_TYPE_VIDEO, new VideoRawDataDelegate()); +``` + +### 5. Handling Participant Events + +```cpp +class MeetingParticipantsDelegate : public IMeetingParticipantsCtrlEvent { +public: + void onUserJoin(IList* lstUserID, const wchar_t* strUserList) override { + // New participants joined + for (int i = 0; i < lstUserID->GetCount(); i++) { + unsigned int userId = lstUserID->GetItem(i); + auto userInfo = m_participantsCtrl->GetUserByUserID(userId); + log("User joined: " + userInfo->GetUserName()); + + // Subscribe to their video if needed + subscribeToUserVideo(userId); + } + } + + void onUserLeft(IList* lstUserID, const wchar_t* strUserList) override { + // Participants left + } + + void onHostChangeNotification(unsigned int userId) override { + // Host changed + } +}; +``` + +### 6. Graceful Disconnection + +```cpp +void Bot::leaveMeeting() { + // Stop raw data subscriptions + GetAudioRawdataHelper()->unSubscribe(); + + // Stop recording if active + auto recCtl = m_meetingService->GetMeetingRecordingController(); + recCtl->StopRawRecording(); + + // Leave meeting + m_meetingService->Leave(LEAVE_MEETING); + + // Wait for onMeetingStatusChanged(MEETING_STATUS_ENDED) +} + +// Handle unexpected disconnection +void onMeetingStatusChanged(MeetingStatus status, int iResult) { + if (status == MEETING_STATUS_ENDED || status == MEETING_STATUS_FAILED) { + cleanup(); + // Optionally reconnect + if (shouldReconnect) { + scheduleReconnect(); + } + } +} +``` + +## Scaling Considerations + +| Consideration | Recommendation | +|---------------|----------------| +| **Bot per meeting** | 1 bot instance per meeting (SDK limitation) | +| **Container deployment** | Use Kubernetes with 1 pod per bot | +| **Resource allocation** | 2-4 GB RAM, 1-2 CPU cores per bot | +| **Queue management** | Use message queue (Redis, RabbitMQ) for bot assignments | +| **Health monitoring** | Implement heartbeat checks for bot instances | + +## Meeting SDK vs Video SDK for Bots + +| Aspect | Meeting SDK | Video SDK | +|--------|-------------|-----------| +| Joins | Zoom meetings | Video SDK sessions | +| Features | Full meeting features | Core video features | +| Use case | Meeting bots, recording | Custom video apps, telehealth | + +**Choose Meeting SDK** for: Joining existing Zoom meetings, transcription bots +**Choose Video SDK** for: Custom video sessions you control, BYOS recording + +## Detailed Platform Guides + +### Meeting SDK (Linux) - Recommended for Bots +- **[Meeting SDK Linux - Quick Start](../../meeting-sdk/linux/linux.md)** - Complete setup guide +- **[High-Level Bot Scenarios](../../meeting-sdk/linux/concepts/high-level-scenarios.md)** - Production architectures +- **[Resilient Bot Pattern](../../meeting-sdk/linux/meeting-sdk-bot.md)** - Retry logic, OBF tokens +- **[Linux Platform Reference](../../meeting-sdk/linux/references/linux-reference.md)** - Dependencies, Docker, troubleshooting + +### Specific Use Cases +- **[Transcription Bot (Linux)](transcription-bot-linux.md)** - Step-by-step transcription bot guide +- **[AI Integration](ai-integration.md)** - AI-powered meeting analysis +- **[Real-time Media Streams](real-time-media-streams.md)** - RTMS alternative (invisible bots) + +## Resources + +- **Meeting SDK Linux Docs**: https://developers.zoom.us/docs/meeting-sdk/linux/ +- **Meeting SDK Linux API**: https://marketplacefront.zoom.us/sdk/meeting/linux/ +- **Headless Sample (Modern)**: https://github.com/zoom/meetingsdk-headless-linux-sample +- **Raw Recording Sample (Traditional)**: https://github.com/zoom/meetingsdk-linux-raw-recording-sample +- **RTMS (Alternative)**: https://developers.zoom.us/docs/rtms/ diff --git a/plugins/zoom-developers/skills/general/use-cases/meeting-details-with-events.md b/plugins/zoom-developers/skills/general/use-cases/meeting-details-with-events.md new file mode 100644 index 00000000..3bc292d4 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/meeting-details-with-events.md @@ -0,0 +1,630 @@ +# Meeting Details with Event Subscription + +Retrieve meeting details and subscribe to real-time meeting events. + +## Overview + +A common integration pattern: get meeting information via REST API, then receive real-time updates via webhooks when meeting events occur (started, ended, participants join/leave). + +For implementation-heavy orchestration patterns (token refresh locks, retries, queue-based webhook handling, circuit-breaker and reconciliation fallbacks), see: +- [../references/automatic-skill-chaining-rest-webhooks.md](../references/automatic-skill-chaining-rest-webhooks.md) +- [../references/meeting-webhooks-oauth-refresh-orchestration.md](../references/meeting-webhooks-oauth-refresh-orchestration.md) +- [../references/distributed-meeting-fallback-architecture.md](../references/distributed-meeting-fallback-architecture.md) + +## Skills Needed + +| Order | Skill | Purpose | +|-------|-------|---------| +| 1 | **zoom-rest-api** | Retrieve meeting details | +| 2 | **webhooks** | Subscribe to and receive meeting events | + +## Skill Chaining Flow + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ COMPLETE INTEGRATION FLOW │ +└─────────────────────────────────────────────────────────────────────────┘ + +SETUP PHASE (One-time): +┌─────────────────────────────────────────────────────────────────────────┐ +│ 1. Configure Event Subscriptions (Marketplace Portal or API) │ +│ └── Subscribe to: meeting.started, meeting.ended, │ +│ meeting.participant_joined, meeting.participant_left │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +RUNTIME PHASE: +┌─────────────────────────────────────────────────────────────────────────┐ +│ 2. GET Meeting Details (zoom-rest-api) │ +│ └── GET /meetings/{meetingId} │ +│ └── Store meeting info (topic, host, settings, join_url) │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 3. Receive Meeting Events (webhooks) │ +│ └── meeting.started → Update status, notify users │ +│ └── meeting.participant_joined → Track attendance │ +│ └── meeting.participant_left → Log departure time │ +│ └── meeting.ended → Finalize records, trigger post-processing │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Prerequisites + +- Zoom app with OAuth or Server-to-Server OAuth +- Scopes: `meeting:read` (for REST API) +- Event subscriptions configured for meeting events +- HTTPS endpoint for receiving webhooks +- **See [Authorization Patterns](../references/authorization-patterns.md)** for scope validation and permission checking + +## Step 1: Configure Event Subscriptions + +### Option A: Via Marketplace Portal (Recommended for most apps) + +1. Go to [marketplace.zoom.us](https://marketplace.zoom.us/) → Your App +2. Navigate to **Feature** → **Event Subscriptions** +3. Click **Add Event Subscription** +4. Configure: + - **Subscription name**: "Meeting Events" + - **Event notification endpoint URL**: `https://yourapp.com/webhooks/zoom` +5. Select events: + - ✅ `meeting.started` + - ✅ `meeting.ended` + - ✅ `meeting.participant_joined` + - ✅ `meeting.participant_left` +6. Save and activate + +### Option B: Programmatic Setup + +Event subscriptions are configured at app creation time in the Marketplace portal. However, you can verify your subscription status via API: + +```javascript +// List webhook subscriptions for your app +const response = await fetch( + 'https://api.zoom.us/v2/webhooks', + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } +); + +const webhooks = await response.json(); +console.log('Active webhooks:', webhooks); +``` + +## Step 2: Retrieve Meeting Details (zoom-rest-api) + +```javascript +const axios = require('axios'); + +/** + * Get meeting details from Zoom REST API + * @param {string} meetingId - The meeting ID + * @param {string} accessToken - Valid OAuth access token + * @returns {Promise} Meeting details + */ +async function getMeetingDetails(meetingId, accessToken) { + try { + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { + headers: { + 'Authorization': `Bearer ${accessToken}` + } + } + ); + + const meeting = response.data; + + return { + id: meeting.id, + uuid: meeting.uuid, + topic: meeting.topic, + type: meeting.type, + status: meeting.status, + start_time: meeting.start_time, + duration: meeting.duration, + timezone: meeting.timezone, + host_id: meeting.host_id, + host_email: meeting.host_email, + join_url: meeting.join_url, + password: meeting.password, + settings: meeting.settings + }; + } catch (error) { + if (error.response?.status === 404) { + throw new Error(`Meeting ${meetingId} not found`); + } + if (error.response?.status === 401) { + throw new Error('Invalid or expired access token'); + } + throw error; + } +} + +// Usage +const meeting = await getMeetingDetails('123456789', accessToken); +console.log(`Meeting: ${meeting.topic}`); +console.log(`Join URL: ${meeting.join_url}`); +console.log(`Host: ${meeting.host_email}`); +``` + +## Step 3: Handle Meeting Events (webhooks) + +```javascript +const express = require('express'); +const crypto = require('crypto'); + +const app = express(); +app.use(express.json()); + +// Store meeting state (use database in production) +const meetingState = new Map(); + +/** + * Verify Zoom webhook signature + */ +function verifyWebhookSignature(req, webhookSecret) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + const payload = `v0:${timestamp}:${JSON.stringify(req.body)}`; + + const expectedSignature = `v0=${crypto + .createHmac('sha256', webhookSecret) + .update(payload) + .digest('hex')}`; + + return signature === expectedSignature; +} + +/** + * Handle URL validation challenge (required for new subscriptions) + */ +function handleUrlValidation(req, res, webhookSecret) { + const hashForValidation = crypto + .createHmac('sha256', webhookSecret) + .update(req.body.payload.plainToken) + .digest('hex'); + + return res.json({ + plainToken: req.body.payload.plainToken, + encryptedToken: hashForValidation + }); +} + +// Webhook endpoint +app.post('/webhooks/zoom', async (req, res) => { + const WEBHOOK_SECRET = process.env.ZOOM_WEBHOOK_SECRET; + + // 1. Handle URL validation challenge + if (req.body.event === 'endpoint.url_validation') { + return handleUrlValidation(req, res, WEBHOOK_SECRET); + } + + // 2. Verify webhook signature + if (!verifyWebhookSignature(req, WEBHOOK_SECRET)) { + console.error('Invalid webhook signature'); + return res.status(401).send('Invalid signature'); + } + + // 3. Process meeting events + const { event, payload } = req.body; + const meetingId = String(payload.object.id); + + console.log(`Received event: ${event} for meeting ${meetingId}`); + + try { + switch (event) { + case 'meeting.started': + await handleMeetingStarted(meetingId, payload); + break; + case 'meeting.ended': + await handleMeetingEnded(meetingId, payload); + break; + case 'meeting.participant_joined': + await handleParticipantJoined(meetingId, payload); + break; + case 'meeting.participant_left': + await handleParticipantLeft(meetingId, payload); + break; + default: + console.log(`Unhandled event: ${event}`); + } + + res.status(200).send(); + } catch (error) { + console.error(`Error handling ${event}:`, error); + // Return 200 to prevent Zoom from retrying + // Log error for investigation + res.status(200).send(); + } +}); + +// Event handlers +async function handleMeetingStarted(meetingId, payload) { + const { object } = payload; + + // Initialize meeting state + meetingState.set(meetingId, { + topic: object.topic, + host_id: object.host_id, + start_time: object.start_time, + participants: [], + status: 'in_progress' + }); + + console.log(`✅ Meeting started: ${object.topic} (ID: ${meetingId})`); + + // Optional: Fetch full meeting details for additional context + // const fullDetails = await getMeetingDetails(meetingId, accessToken); +} + +async function handleMeetingEnded(meetingId, payload) { + const { object } = payload; + const state = meetingState.get(meetingId); + + if (state) { + state.status = 'ended'; + state.end_time = object.end_time; + state.duration = object.duration; + + console.log(`🏁 Meeting ended: ${state.topic}`); + console.log(` Duration: ${state.duration} minutes`); + console.log(` Total participants: ${state.participants.length}`); + + // Trigger post-meeting processing + await processMeetingRecords(meetingId, state); + } +} + +async function handleParticipantJoined(meetingId, payload) { + const { object } = payload; + const participant = object.participant; + const state = meetingState.get(meetingId); + + if (state) { + state.participants.push({ + user_id: participant.user_id, + user_name: participant.user_name, + email: participant.email, + join_time: participant.join_time, + status: 'in_meeting' + }); + + console.log(`👋 ${participant.user_name} joined meeting ${meetingId}`); + } +} + +async function handleParticipantLeft(meetingId, payload) { + const { object } = payload; + const participant = object.participant; + const state = meetingState.get(meetingId); + + if (state) { + const p = state.participants.find(p => p.user_id === participant.user_id); + if (p) { + p.leave_time = participant.leave_time; + p.status = 'left'; + } + + console.log(`👋 ${participant.user_name} left meeting ${meetingId}`); + } +} + +async function processMeetingRecords(meetingId, state) { + // Save to database, generate reports, notify stakeholders, etc. + console.log('Processing meeting records...'); + + // Example: Calculate attendance stats + const attendanceReport = { + meeting_id: meetingId, + topic: state.topic, + duration_minutes: state.duration, + total_participants: state.participants.length, + participants: state.participants.map(p => ({ + name: p.user_name, + email: p.email, + joined: p.join_time, + left: p.leave_time || state.end_time + })) + }; + + console.log(JSON.stringify(attendanceReport, null, 2)); + + // Clean up in-memory state + meetingState.delete(meetingId); +} + +app.listen(3000, () => { + console.log('Webhook server running on port 3000'); +}); +``` + +## Complete Integration Example + +```javascript +/** + * Complete example: Meeting Dashboard Integration + * + * Skills used: + * 1. zoom-rest-api - Get meeting details + * 2. webhooks - Real-time event updates + */ + +const express = require('express'); +const axios = require('axios'); +const crypto = require('crypto'); + +const app = express(); +app.use(express.json()); + +// In-memory store (use Redis/database in production) +const meetings = new Map(); + +// ============================================ +// AUTHENTICATION HELPER +// ============================================ + +async function getAccessToken() { + const credentials = Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64'); + + const response = await axios.post( + `https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${process.env.ZOOM_ACCOUNT_ID}`, + null, + { headers: { 'Authorization': `Basic ${credentials}` } } + ); + + return response.data.access_token; +} + +// ============================================ +// STEP 1: REST API - Get Meeting Details +// ============================================ + +async function getMeetingDetails(meetingId) { + const token = await getAccessToken(); + + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return response.data; +} + +// API endpoint to fetch and track a meeting +app.get('/api/meetings/:meetingId', async (req, res) => { + try { + const { meetingId } = req.params; + + // Get meeting details from Zoom API (zoom-rest-api skill) + const details = await getMeetingDetails(meetingId); + + // Store for tracking (webhook events will update this) + meetings.set(meetingId, { + ...details, + participants: [], + events: [], + tracking_status: 'active' + }); + + res.json({ + success: true, + meeting: { + id: details.id, + topic: details.topic, + start_time: details.start_time, + join_url: details.join_url, + host_email: details.host_email + }, + message: 'Meeting tracked. Events will be received via webhook.' + }); + } catch (error) { + res.status(error.response?.status || 500).json({ + success: false, + error: error.message + }); + } +}); + +// ============================================ +// STEP 2: WEBHOOKS - Receive Meeting Events +// ============================================ + +function verifyWebhook(req) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + const payload = `v0:${timestamp}:${JSON.stringify(req.body)}`; + const expected = `v0=${crypto + .createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET) + .update(payload) + .digest('hex')}`; + + return signature === expected; +} + +app.post('/webhooks/zoom', async (req, res) => { + // URL validation challenge + if (req.body.event === 'endpoint.url_validation') { + const hash = crypto + .createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET) + .update(req.body.payload.plainToken) + .digest('hex'); + return res.json({ + plainToken: req.body.payload.plainToken, + encryptedToken: hash + }); + } + + // Verify signature + if (!verifyWebhook(req)) { + return res.status(401).send('Invalid signature'); + } + + const { event, payload } = req.body; + const meetingId = String(payload.object.id); + + // Get or create meeting record + let meeting = meetings.get(meetingId); + if (!meeting) { + // Meeting wasn't pre-fetched, create minimal record + meeting = { + id: meetingId, + topic: payload.object.topic, + participants: [], + events: [], + tracking_status: 'webhook_only' + }; + meetings.set(meetingId, meeting); + } + + // Log event + meeting.events.push({ + type: event, + timestamp: new Date().toISOString(), + data: payload + }); + + // Update meeting state based on event + switch (event) { + case 'meeting.started': + meeting.status = 'in_progress'; + meeting.actual_start_time = payload.object.start_time; + console.log(`✅ Meeting started: ${meeting.topic}`); + break; + + case 'meeting.ended': + meeting.status = 'ended'; + meeting.end_time = payload.object.end_time; + meeting.actual_duration = payload.object.duration; + console.log(`🏁 Meeting ended: ${meeting.topic} (${meeting.actual_duration} min)`); + break; + + case 'meeting.participant_joined': + meeting.participants.push({ + ...payload.object.participant, + status: 'in_meeting' + }); + console.log(`👋 ${payload.object.participant.user_name} joined`); + break; + + case 'meeting.participant_left': + const p = meeting.participants.find( + p => p.user_id === payload.object.participant.user_id + ); + if (p) { + p.status = 'left'; + p.leave_time = payload.object.participant.leave_time; + } + console.log(`👋 ${payload.object.participant.user_name} left`); + break; + } + + res.status(200).send(); +}); + +// ============================================ +// STEP 3: Query Meeting Status +// ============================================ + +app.get('/api/meetings/:meetingId/status', (req, res) => { + const meeting = meetings.get(req.params.meetingId); + + if (!meeting) { + return res.status(404).json({ error: 'Meeting not tracked' }); + } + + res.json({ + id: meeting.id, + topic: meeting.topic, + status: meeting.status || 'scheduled', + participants_in_meeting: meeting.participants.filter(p => p.status === 'in_meeting').length, + total_participants: meeting.participants.length, + events_count: meeting.events.length, + last_event: meeting.events[meeting.events.length - 1]?.type + }); +}); + +// ============================================ +// START SERVER +// ============================================ + +app.listen(3000, () => { + console.log('Server running on port 3000'); + console.log(''); + console.log('Endpoints:'); + console.log(' GET /api/meetings/:id - Fetch & track meeting (zoom-rest-api)'); + console.log(' GET /api/meetings/:id/status - Get meeting status'); + console.log(' POST /webhooks/zoom - Webhook receiver (webhooks)'); +}); +``` + +## Meeting Event Types Reference + +| Event | Trigger | Key Payload Fields | +|-------|---------|-------------------| +| `meeting.started` | Host starts meeting | `id`, `topic`, `host_id`, `start_time` | +| `meeting.ended` | Meeting ends | `id`, `end_time`, `duration` | +| `meeting.participant_joined` | User joins | `participant.user_id`, `participant.user_name`, `participant.join_time` | +| `meeting.participant_left` | User leaves | `participant.user_id`, `participant.leave_time` | +| `meeting.sharing_started` | Screen share begins | `participant`, `sharing_details` | +| `meeting.sharing_ended` | Screen share ends | `participant` | + +## Error Handling + +### Common Errors and Solutions + +| Error | Cause | Solution | +|-------|-------|----------| +| 404 on GET /meetings | Invalid meeting ID | Verify meeting ID exists | +| 401 on API call | Expired token | Refresh access token | +| Invalid webhook signature | Wrong secret or modified payload | Verify WEBHOOK_SECRET matches app config | +| Missing events | Subscription not active | Check Event Subscriptions in Marketplace | + +### Retry Logic for REST API + +```javascript +async function getMeetingWithRetry(meetingId, maxRetries = 3) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await getMeetingDetails(meetingId); + } catch (error) { + if (error.response?.status === 429) { + // Rate limited - wait and retry + const retryAfter = error.response.headers['retry-after'] || 1; + await new Promise(r => setTimeout(r, retryAfter * 1000)); + continue; + } + if (error.response?.status === 401 && attempt < maxRetries) { + // Token expired - refresh and retry + await refreshAccessToken(); + continue; + } + throw error; + } + } +} +``` + +## Best Practices + +1. **Fetch meeting details first** - Get context before events arrive +2. **Handle events idempotently** - Webhooks may be delivered multiple times +3. **Use meeting UUID for tracking** - More reliable than meeting ID for recurring meetings +4. **Store events for audit** - Log all events for debugging and compliance +5. **Implement retry logic** - REST API calls may fail transiently +6. **Return 200 for webhooks** - Even on processing errors, to prevent retries + +## Related Use Cases + +- **[Recording & Transcription](recording-transcription.md)** - Download recordings after meeting ends +- **[Meeting Automation](meeting-automation.md)** - Create and manage meetings programmatically +- **[Real-Time Media Streams](real-time-media-streams.md)** - Access live audio/video during meeting + +## Resources + +- **Meetings API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Meetings +- **Webhook Events**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/events/ +- **Event Subscriptions**: https://developers.zoom.us/docs/api/rest/webhook-reference/ diff --git a/plugins/zoom-developers/skills/general/use-cases/meeting-links-vs-embedding.md b/plugins/zoom-developers/skills/general/use-cases/meeting-links-vs-embedding.md new file mode 100644 index 00000000..49b16c61 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/meeting-links-vs-embedding.md @@ -0,0 +1,38 @@ +# Meeting Links vs Embedding (REST `join_url` vs Meeting SDK) + +This is a high-frequency confusion cluster: + +- "Generate Zoom Meeting URLs server-side" +- "Join meeting via API" +- "Can I use the `join_url` inside Meeting SDK?" + +## The Decision + +### Use `join_url` (Meeting Link) When + +- You are sending a user to the Zoom client or Zoom web client. +- You do not need embedded UI inside your application. + +### Use Meeting SDK When + +- You must embed a meeting inside your app. +- You need SDK-level control over the experience. + +## Common Mistakes + +- Treating REST `join_url` as a way to join via SDK. +- Expecting an API endpoint to "join a user to a meeting". + +## Skills Needed + +| Order | Skill | Purpose | +|------:|------|---------| +| 1 | **zoom-rest-api** | Create meetings and understand what `join_url` is | +| 2 | **zoom-meeting-sdk** | Embed meetings correctly | + +## Links + +- `../../rest-api/concepts/meeting-urls-and-sdk-joining.md` +- `../../meeting-sdk/SKILL.md` +- `embed-meetings.md` + diff --git a/plugins/zoom-developers/skills/general/use-cases/minutes-calculation.md b/plugins/zoom-developers/skills/general/use-cases/minutes-calculation.md new file mode 100644 index 00000000..7d262a11 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/minutes-calculation.md @@ -0,0 +1,798 @@ +# Minutes Calculation for Billing + +Calculate usage minutes for Video SDK sessions and Meeting SDK meetings for billing and cost management. + +## Overview + +Zoom SDKs are billed based on **participant-minutes**. This guide covers how to track and calculate usage for accurate billing projections and cost optimization. + +## Skills Needed + +- **zoom-rest-api** - Reports API +- **webhooks** - Real-time tracking +- **zoom-video-sdk** - Session Quality API + +## Billing Models + +| SDK | Billing Unit | Calculation | +|-----|--------------|-------------| +| Video SDK | Participant-minutes | Sum of (each participant's session duration) | +| Meeting SDK | Host minutes | Based on meeting duration, not participant count | + +**Example**: A 30-minute Video SDK session with 4 participants = 120 participant-minutes. + +## Video SDK Usage Tracking + +### Method 1: Session Quality API (Recommended) + +Most accurate method using Zoom's built-in analytics. + +```javascript +const axios = require('axios'); + +// Get session details including participant minutes +async function getSessionUsage(sessionId) { + const response = await axios.get( + `https://api.zoom.us/v2/videosdk/sessions/${sessionId}`, + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + return { + sessionName: response.data.session_name, + startTime: response.data.start_time, + endTime: response.data.end_time, + totalMinutes: response.data.duration, // Total session minutes + participantCount: response.data.participant_count + }; +} + +// Get participant-level breakdown +async function getSessionParticipants(sessionId) { + const response = await axios.get( + `https://api.zoom.us/v2/videosdk/sessions/${sessionId}/participants`, + { + params: { page_size: 300 }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + // Calculate participant-minutes + const participants = response.data.participants.map(p => { + const joinTime = new Date(p.join_time); + const leaveTime = new Date(p.leave_time); + const durationMinutes = (leaveTime - joinTime) / 1000 / 60; + + return { + name: p.user_name, + joinTime: p.join_time, + leaveTime: p.leave_time, + durationMinutes: Math.round(durationMinutes * 100) / 100 + }; + }); + + const totalParticipantMinutes = participants.reduce( + (sum, p) => sum + p.durationMinutes, 0 + ); + + return { + participants, + totalParticipantMinutes: Math.round(totalParticipantMinutes * 100) / 100 + }; +} + +// Get all sessions in date range +async function getSessionsInRange(from, to) { + const response = await axios.get( + 'https://api.zoom.us/v2/videosdk/sessions', + { + params: { from, to, page_size: 300 }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + return response.data.sessions; +} + +// Calculate monthly usage +async function calculateMonthlyUsage(year, month) { + const from = `${year}-${String(month).padStart(2, '0')}-01`; + const lastDay = new Date(year, month, 0).getDate(); + const to = `${year}-${String(month).padStart(2, '0')}-${lastDay}`; + + const sessions = await getSessionsInRange(from, to); + + let totalParticipantMinutes = 0; + const sessionDetails = []; + + for (const session of sessions) { + const participants = await getSessionParticipants(session.id); + totalParticipantMinutes += participants.totalParticipantMinutes; + + sessionDetails.push({ + sessionId: session.id, + sessionName: session.session_name, + date: session.start_time, + participantMinutes: participants.totalParticipantMinutes + }); + } + + return { + period: `${year}-${String(month).padStart(2, '0')}`, + totalSessions: sessions.length, + totalParticipantMinutes: Math.round(totalParticipantMinutes), + sessions: sessionDetails + }; +} +``` + +### Method 2: Real-Time Webhook Tracking + +Track usage in real-time as sessions happen. + +```javascript +const express = require('express'); +const app = express(); + +// In-memory storage (use database in production) +const activeSessions = new Map(); +const usageRecords = []; + +app.post('/webhook', express.json(), (req, res) => { + const { event, payload } = req.body; + + switch (event) { + case 'session.started': + handleSessionStarted(payload); + break; + case 'session.ended': + handleSessionEnded(payload); + break; + case 'session.participant_joined': + handleParticipantJoined(payload); + break; + case 'session.participant_left': + handleParticipantLeft(payload); + break; + } + + res.status(200).send(); +}); + +function handleSessionStarted(payload) { + const { object } = payload; + activeSessions.set(object.id, { + sessionId: object.id, + sessionName: object.session_name, + startTime: new Date(object.start_time), + participants: new Map() + }); +} + +function handleSessionEnded(payload) { + const { object } = payload; + const session = activeSessions.get(object.id); + + if (session) { + // Calculate final usage for any remaining participants + const endTime = new Date(object.end_time); + let totalMinutes = 0; + + session.participants.forEach((participant, participantId) => { + if (!participant.leaveTime) { + participant.leaveTime = endTime; + } + const duration = (participant.leaveTime - participant.joinTime) / 1000 / 60; + totalMinutes += duration; + }); + + // Record usage + usageRecords.push({ + sessionId: object.id, + sessionName: session.sessionName, + startTime: session.startTime, + endTime: endTime, + totalParticipantMinutes: Math.round(totalMinutes * 100) / 100, + participantCount: session.participants.size + }); + + activeSessions.delete(object.id); + } +} + +function handleParticipantJoined(payload) { + const { object } = payload; + const session = activeSessions.get(object.session_id); + + if (session) { + session.participants.set(object.participant.participant_id, { + name: object.participant.user_name, + joinTime: new Date(object.participant.join_time), + leaveTime: null + }); + } +} + +function handleParticipantLeft(payload) { + const { object } = payload; + const session = activeSessions.get(object.session_id); + + if (session) { + const participant = session.participants.get(object.participant.participant_id); + if (participant) { + participant.leaveTime = new Date(object.participant.leave_time); + } + } +} + +// Get current month usage +app.get('/usage/current-month', (req, res) => { + const now = new Date(); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + + const monthlyRecords = usageRecords.filter(r => + new Date(r.startTime) >= startOfMonth + ); + + const totalMinutes = monthlyRecords.reduce( + (sum, r) => sum + r.totalParticipantMinutes, 0 + ); + + res.json({ + period: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`, + totalSessions: monthlyRecords.length, + totalParticipantMinutes: Math.round(totalMinutes), + records: monthlyRecords + }); +}); +``` + +### Method 3: Client-Side Tracking + +Track locally in your SDK application. + +```javascript +// React Native / JavaScript SDK tracking +class UsageTracker { + constructor() { + this.sessionStart = null; + this.participants = new Map(); + } + + onSessionJoin() { + this.sessionStart = new Date(); + } + + onUserJoin(user) { + this.participants.set(user.id, { + name: user.name, + joinTime: new Date(), + leaveTime: null + }); + } + + onUserLeave(user) { + const participant = this.participants.get(user.id); + if (participant) { + participant.leaveTime = new Date(); + } + } + + calculateUsage() { + const now = new Date(); + let totalMinutes = 0; + + this.participants.forEach(participant => { + const endTime = participant.leaveTime || now; + const duration = (endTime - participant.joinTime) / 1000 / 60; + totalMinutes += duration; + }); + + return { + sessionDuration: (now - this.sessionStart) / 1000 / 60, + totalParticipantMinutes: Math.round(totalMinutes * 100) / 100, + participantCount: this.participants.size + }; + } + + // Send to your backend periodically + async reportUsage() { + const usage = this.calculateUsage(); + await fetch('/api/usage', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(usage) + }); + } +} +``` + +```cpp +// Windows/C++ SDK tracking +class UsageTracker : public IZoomVideoSDKDelegate { +private: + std::chrono::system_clock::time_point sessionStart; + struct ParticipantUsage { + std::wstring name; + std::chrono::system_clock::time_point joinTime; + std::chrono::system_clock::time_point leaveTime; + bool hasLeft = false; + }; + std::map participants; + +public: + void onSessionJoin() override { + sessionStart = std::chrono::system_clock::now(); + } + + void onUserJoin(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* users) override { + for (int i = 0; i < users->GetCount(); i++) { + auto user = users->GetItem(i); + ParticipantUsage usage; + usage.name = user->getUserName(); + usage.joinTime = std::chrono::system_clock::now(); + participants[user->getUserID()] = usage; + } + } + + void onUserLeave(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* users) override { + for (int i = 0; i < users->GetCount(); i++) { + auto user = users->GetItem(i); + auto it = participants.find(user->getUserID()); + if (it != participants.end()) { + it->second.leaveTime = std::chrono::system_clock::now(); + it->second.hasLeft = true; + } + } + } + + double calculateTotalMinutes() { + auto now = std::chrono::system_clock::now(); + double totalMinutes = 0; + + for (const auto& [id, participant] : participants) { + auto endTime = participant.hasLeft ? participant.leaveTime : now; + auto duration = std::chrono::duration_cast( + endTime - participant.joinTime + ); + totalMinutes += duration.count(); + } + + return totalMinutes; + } +}; +``` + +## Meeting SDK Usage Tracking + +### Reports API for Past Meetings + +```javascript +// Get meeting usage from Reports API +async function getMeetingUsage(meetingId) { + // Note: Double-encode UUID if it contains / or // + const encodedId = meetingId.includes('/') + ? encodeURIComponent(encodeURIComponent(meetingId)) + : meetingId; + + const [meeting, participants] = await Promise.all([ + axios.get( + `https://api.zoom.us/v2/report/meetings/${encodedId}`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ), + axios.get( + `https://api.zoom.us/v2/report/meetings/${encodedId}/participants`, + { + params: { page_size: 300 }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ) + ]); + + // Calculate participant-minutes + const participantMinutes = participants.data.participants.map(p => { + const duration = p.duration; // Already in seconds + return { + name: p.name, + email: p.user_email, + durationMinutes: Math.round(duration / 60 * 100) / 100 + }; + }); + + const totalParticipantMinutes = participantMinutes.reduce( + (sum, p) => sum + p.durationMinutes, 0 + ); + + return { + meetingId: meetingId, + topic: meeting.data.topic, + startTime: meeting.data.start_time, + endTime: meeting.data.end_time, + hostMinutes: meeting.data.duration, // Meeting duration in minutes + totalParticipantMinutes: Math.round(totalParticipantMinutes), + participantCount: participants.data.participants.length, + participants: participantMinutes + }; +} + +// Get daily usage summary +async function getDailyUsageSummary(year, month) { + const response = await axios.get( + 'https://api.zoom.us/v2/report/daily', + { + params: { year, month }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + // Aggregate daily data + const summary = response.data.dates.reduce((acc, day) => { + acc.totalMeetings += day.meetings; + acc.totalMinutes += day.meeting_minutes; + acc.totalParticipants += day.participants; + return acc; + }, { totalMeetings: 0, totalMinutes: 0, totalParticipants: 0 }); + + return { + period: `${year}-${String(month).padStart(2, '0')}`, + ...summary, + dailyBreakdown: response.data.dates + }; +} + +// Get user-specific meeting report +async function getUserMeetingReport(userId, from, to) { + const response = await axios.get( + `https://api.zoom.us/v2/report/users/${userId}/meetings`, + { + params: { from, to, page_size: 300 }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + const totalMinutes = response.data.meetings.reduce( + (sum, m) => sum + m.duration, 0 + ); + + return { + userId, + period: { from, to }, + totalMeetings: response.data.meetings.length, + totalHostMinutes: totalMinutes, + meetings: response.data.meetings + }; +} +``` + +### Webhook-Based Tracking + +```javascript +// Meeting SDK webhook events +app.post('/meeting-webhook', express.json(), (req, res) => { + const { event, payload } = req.body; + + switch (event) { + case 'meeting.started': + handleMeetingStarted(payload); + break; + case 'meeting.ended': + handleMeetingEnded(payload); + break; + case 'meeting.participant_joined': + handleMeetingParticipantJoined(payload); + break; + case 'meeting.participant_left': + handleMeetingParticipantLeft(payload); + break; + } + + res.status(200).send(); +}); + +// Store in database for billing +async function handleMeetingEnded(payload) { + const { object } = payload; + + await db.meetings.insert({ + meetingId: object.id, + uuid: object.uuid, + topic: object.topic, + hostId: object.host_id, + startTime: object.start_time, + endTime: object.end_time, + durationMinutes: object.duration, + participantCount: object.participant_count + }); +} +``` + +## Cost Estimation + +### Video SDK Cost Calculator + +```javascript +// Pricing tiers (example - check current Zoom pricing) +const PRICING_TIERS = [ + { upTo: 10000, pricePerMinute: 0.0050 }, + { upTo: 50000, pricePerMinute: 0.0040 }, + { upTo: 100000, pricePerMinute: 0.0030 }, + { upTo: Infinity, pricePerMinute: 0.0025 } +]; + +function estimateMonthlyCost(participantMinutes) { + let remaining = participantMinutes; + let totalCost = 0; + let previousLimit = 0; + + for (const tier of PRICING_TIERS) { + const tierMinutes = Math.min(remaining, tier.upTo - previousLimit); + if (tierMinutes <= 0) break; + + totalCost += tierMinutes * tier.pricePerMinute; + remaining -= tierMinutes; + previousLimit = tier.upTo; + } + + return { + participantMinutes, + estimatedCost: Math.round(totalCost * 100) / 100, + currency: 'USD' + }; +} + +// Project usage for the month +function projectMonthlyUsage(currentUsage, dayOfMonth, daysInMonth) { + const dailyAverage = currentUsage / dayOfMonth; + const projectedTotal = dailyAverage * daysInMonth; + + return { + currentUsage, + dailyAverage: Math.round(dailyAverage), + projectedMonthlyUsage: Math.round(projectedTotal), + projectedCost: estimateMonthlyCost(projectedTotal) + }; +} +``` + +### Year-to-Date (YTD) Usage + +Calculate cumulative usage from the start of the year. + +```javascript +// Calculate YTD usage for Video SDK +async function getYTDUsage() { + const now = new Date(); + const year = now.getFullYear(); + const currentMonth = now.getMonth() + 1; + + // Fetch all months in parallel + const monthPromises = []; + for (let month = 1; month <= currentMonth; month++) { + monthPromises.push(calculateMonthlyUsage(year, month)); + } + + const monthlyResults = await Promise.all(monthPromises); + + // Aggregate YTD totals + const ytdTotals = monthlyResults.reduce((acc, month) => { + acc.totalSessions += month.totalSessions; + acc.totalParticipantMinutes += month.totalParticipantMinutes; + return acc; + }, { totalSessions: 0, totalParticipantMinutes: 0 }); + + // Monthly breakdown for trending + const monthlyBreakdown = monthlyResults.map(m => ({ + period: m.period, + sessions: m.totalSessions, + participantMinutes: m.totalParticipantMinutes + })); + + // Calculate month-over-month growth + const growthRates = []; + for (let i = 1; i < monthlyBreakdown.length; i++) { + const prev = monthlyBreakdown[i - 1].participantMinutes; + const curr = monthlyBreakdown[i].participantMinutes; + growthRates.push({ + period: monthlyBreakdown[i].period, + growthRate: prev > 0 ? ((curr - prev) / prev * 100).toFixed(1) + '%' : 'N/A' + }); + } + + return { + year, + asOfDate: now.toISOString().split('T')[0], + ytdTotals: { + totalSessions: ytdTotals.totalSessions, + totalParticipantMinutes: Math.round(ytdTotals.totalParticipantMinutes), + estimatedCost: estimateMonthlyCost(ytdTotals.totalParticipantMinutes) + }, + monthlyBreakdown, + growthRates, + averageMonthlyUsage: Math.round(ytdTotals.totalParticipantMinutes / currentMonth) + }; +} + +// Calculate YTD usage for Meeting SDK (via Reports API) +async function getMeetingSDKYTDUsage() { + const now = new Date(); + const year = now.getFullYear(); + const currentMonth = now.getMonth() + 1; + + // Fetch daily reports for each month + const monthPromises = []; + for (let month = 1; month <= currentMonth; month++) { + monthPromises.push(getDailyUsageSummary(year, month)); + } + + const monthlyResults = await Promise.all(monthPromises); + + // Aggregate YTD + const ytdTotals = monthlyResults.reduce((acc, month) => { + acc.totalMeetings += month.totalMeetings; + acc.totalMinutes += month.totalMinutes; + acc.totalParticipants += month.totalParticipants; + return acc; + }, { totalMeetings: 0, totalMinutes: 0, totalParticipants: 0 }); + + return { + year, + asOfDate: now.toISOString().split('T')[0], + ytdTotals, + monthlyBreakdown: monthlyResults.map(m => ({ + period: m.period, + meetings: m.totalMeetings, + minutes: m.totalMinutes, + participants: m.totalParticipants + })), + averageMonthly: { + meetings: Math.round(ytdTotals.totalMeetings / currentMonth), + minutes: Math.round(ytdTotals.totalMinutes / currentMonth), + participants: Math.round(ytdTotals.totalParticipants / currentMonth) + } + }; +} + +// Compare YTD to previous year +async function getYTDComparison() { + const now = new Date(); + const currentYear = now.getFullYear(); + const currentMonth = now.getMonth() + 1; + + // Get current YTD + const currentYTD = await getYTDUsage(); + + // Get same period last year (Jan through current month) + const lastYearPromises = []; + for (let month = 1; month <= currentMonth; month++) { + lastYearPromises.push(calculateMonthlyUsage(currentYear - 1, month)); + } + + const lastYearResults = await Promise.all(lastYearPromises); + const lastYearTotal = lastYearResults.reduce( + (sum, m) => sum + m.totalParticipantMinutes, 0 + ); + + const yearOverYearChange = lastYearTotal > 0 + ? ((currentYTD.ytdTotals.totalParticipantMinutes - lastYearTotal) / lastYearTotal * 100) + : null; + + return { + currentYear: { + year: currentYear, + totalParticipantMinutes: currentYTD.ytdTotals.totalParticipantMinutes + }, + previousYear: { + year: currentYear - 1, + totalParticipantMinutes: Math.round(lastYearTotal), + note: `Same period (Jan-${currentMonth})` + }, + yearOverYearChange: yearOverYearChange !== null + ? yearOverYearChange.toFixed(1) + '%' + : 'N/A (no prior year data)' + }; +} + +// Project full year usage based on YTD +function projectAnnualUsage(ytdMinutes, currentMonth) { + const monthsRemaining = 12 - currentMonth; + const monthlyAverage = ytdMinutes / currentMonth; + const projectedRemaining = monthlyAverage * monthsRemaining; + const projectedAnnual = ytdMinutes + projectedRemaining; + + return { + ytdMinutes: Math.round(ytdMinutes), + projectedAnnualMinutes: Math.round(projectedAnnual), + projectedAnnualCost: estimateMonthlyCost(projectedAnnual), + monthlyAverage: Math.round(monthlyAverage), + confidence: currentMonth >= 6 ? 'high' : currentMonth >= 3 ? 'medium' : 'low' + }; +} +``` + +### Usage Dashboard + +```javascript +// Build a usage dashboard +async function getUsageDashboard() { + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth() + 1; + const dayOfMonth = now.getDate(); + const daysInMonth = new Date(year, month, 0).getDate(); + + // Get current month usage + const usage = await calculateMonthlyUsage(year, month); + + // Calculate projections + const projection = projectMonthlyUsage( + usage.totalParticipantMinutes, + dayOfMonth, + daysInMonth + ); + + // Get previous month for comparison + const prevMonth = month === 1 ? 12 : month - 1; + const prevYear = month === 1 ? year - 1 : year; + const previousUsage = await calculateMonthlyUsage(prevYear, prevMonth); + + return { + currentMonth: { + period: usage.period, + totalSessions: usage.totalSessions, + totalParticipantMinutes: usage.totalParticipantMinutes, + estimatedCost: estimateMonthlyCost(usage.totalParticipantMinutes) + }, + projection: { + projectedMinutes: projection.projectedMonthlyUsage, + projectedCost: projection.projectedCost, + dailyAverage: projection.dailyAverage + }, + previousMonth: { + period: previousUsage.period, + totalParticipantMinutes: previousUsage.totalParticipantMinutes, + monthOverMonthChange: ( + (usage.totalParticipantMinutes - previousUsage.totalParticipantMinutes) / + previousUsage.totalParticipantMinutes * 100 + ).toFixed(1) + '%' + }, + topSessions: usage.sessions + .sort((a, b) => b.participantMinutes - a.participantMinutes) + .slice(0, 10) + }; +} +``` + +## Best Practices + +### Accurate Tracking + +1. **Use webhooks for real-time**: More accurate than periodic API polling +2. **Handle reconnections**: Participants may disconnect and rejoin +3. **Account for time zones**: Store all times in UTC +4. **Deduplicate participants**: Same user may rejoin multiple times + +### Cost Optimization + +1. **Monitor daily usage**: Set up alerts for unusual spikes +2. **Track by use case**: Identify which features consume most minutes +3. **Optimize session duration**: Encourage efficient meetings +4. **Review participant patterns**: Identify inactive participants + +### Data Retention + +| Data Source | Retention Period | +|-------------|------------------| +| Session Quality API | 30 days | +| Reports API (meetings) | 12 months | +| Reports API (participants) | 1 month | +| Webhook events | Store your own | + +## Related Resources + +- **Video SDK Session Quality**: https://developers.zoom.us/docs/video-sdk/session-quality/ +- **Reports API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Reports +- **Usage reporting guide**: See `usage-reporting-analytics.md` +- **Webhooks reference**: See `webhooks` skill diff --git a/plugins/zoom-developers/skills/general/use-cases/native-meeting-sdk-multi-platform.md b/plugins/zoom-developers/skills/general/use-cases/native-meeting-sdk-multi-platform.md new file mode 100644 index 00000000..446555b0 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/native-meeting-sdk-multi-platform.md @@ -0,0 +1,34 @@ +# Native Meeting SDK Multi-Platform Delivery + +Use this flow when you need the same embedded meeting product capability across multiple native stacks (Android, iOS, macOS, Unreal) while keeping behavior and auth patterns consistent. + +## When to Use + +- You are shipping a shared meeting feature set across multiple native clients. +- You need consistent auth/signature and role policy across platforms. +- You need version-drift guardrails and staged rollout checks per platform. + +## Skill Chain + +1. [meeting-sdk](../../meeting-sdk/SKILL.md) +2. [meeting-sdk/android](../../meeting-sdk/android/SKILL.md) +3. [meeting-sdk/ios](../../meeting-sdk/ios/SKILL.md) +4. [meeting-sdk/macos](../../meeting-sdk/macos/SKILL.md) +5. [meeting-sdk/unreal](../../meeting-sdk/unreal/SKILL.md) +6. [oauth](../../oauth/SKILL.md) + +## Typical Flow + +1. Standardize server-side signing and role policy once. +2. Validate default UI join/start baseline on each platform. +3. Add platform-specific custom UI features only after baseline parity. +4. Maintain a version matrix and run upgrade checks per platform before release. +5. Track contradictions between wrapper docs, package artifacts, and API references. + +## References + +- [Meeting SDK Root Skill](../../meeting-sdk/SKILL.md) +- [Android Reference Map](../../meeting-sdk/android/references/android-reference-map.md) +- [iOS Reference Map](../../meeting-sdk/ios/references/ios-reference-map.md) +- [macOS Reference Map](../../meeting-sdk/macos/references/macos-reference-map.md) +- [Unreal Reference Map](../../meeting-sdk/unreal/references/unreal-reference-map.md) diff --git a/plugins/zoom-developers/skills/general/use-cases/native-video-sdk-multi-platform.md b/plugins/zoom-developers/skills/general/use-cases/native-video-sdk-multi-platform.md new file mode 100644 index 00000000..7f853868 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/native-video-sdk-multi-platform.md @@ -0,0 +1,35 @@ +# Native Video SDK Multi-Platform Delivery + +## Goal + +Ship one product experience across Android, iOS, macOS, and Unity while keeping token auth, session behavior, and upgrade policies aligned. + +## Skills to chain + +- [zoom-video-sdk](../../video-sdk/SKILL.md) +- [zoom-video-sdk-android](../../video-sdk/android/SKILL.md) +- [zoom-video-sdk-ios](../../video-sdk/ios/SKILL.md) +- [zoom-video-sdk-macos](../../video-sdk/macos/SKILL.md) +- [zoom-video-sdk-unity](../../video-sdk/unity/SKILL.md) +- [zoom-oauth](../../oauth/SKILL.md) + +## Recommended delivery model + +1. Standardize backend token service contract (`sessionName`, `userName`, role/claims). +2. Keep platform session state machines consistent (init -> join -> media -> leave). +3. Version-lock each platform release and run compatibility checks before rollout. +4. Maintain per-platform fallback plans for renamed/deprecated APIs. + +## Failure modes to pre-plan + +- Wrapper/native feature mismatch (especially Unity). +- Event naming drift across SDK versions. +- Token claim changes that break only one platform. +- Permission/regression differences per OS release. + +## Output checklist + +- Shared auth/token contract spec +- Platform-specific session lifecycle docs +- Upgrade runbook with rollback plan +- Known incompatibility matrix diff --git a/plugins/zoom-developers/skills/general/use-cases/prebuilt-video-ui.md b/plugins/zoom-developers/skills/general/use-cases/prebuilt-video-ui.md new file mode 100644 index 00000000..54367947 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/prebuilt-video-ui.md @@ -0,0 +1,307 @@ +# Pre-built Video UI with UI Toolkit + +Build video conferencing apps in minutes using Zoom's ready-made UI components. + +## Use Case + +You need to add video conferencing to your web application quickly without building custom UI from scratch. The Zoom Video SDK UI Toolkit provides a complete, production-ready video interface that works across frameworks. + +## When to Use UI Toolkit + +- ✅ Need video conferencing fast (hours, not weeks) +- ✅ Want Zoom-like UI consistency +- ✅ Don't have resources to build custom video UI +- ✅ Need standard features (chat, share, participants, settings) +- ✅ Want framework-agnostic solution (React, Vue, Angular, vanilla JS) + +## When NOT to Use (Use Raw Video SDK Instead) + +- ❌ Need complete custom UI control +- ❌ Building non-standard video experiences +- ❌ Need access to raw video/audio data for processing +- ❌ Want custom rendering pipeline + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Your Web Application │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Your Frontend (React/Vue/Angular/Vanilla JS) │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────────┐ │ │ +│ │ │ Zoom UI Toolkit │ │ │ +│ │ │ ┌────────────────────────────────────────┐ │ │ │ +│ │ │ │ Pre-built UI Components │ │ │ │ +│ │ │ │ • Video Grid/Gallery │ │ │ │ +│ │ │ │ • Control Bar │ │ │ │ +│ │ │ │ • Chat Panel │ │ │ │ +│ │ │ │ • Participants List │ │ │ │ +│ │ │ │ • Settings Panel │ │ │ │ +│ │ │ └────────────────────────────────────────┘ │ │ │ +│ │ │ │ │ │ +│ │ │ ┌────────────────────────────────────────┐ │ │ │ +│ │ │ │ Zoom Video SDK (Underlying Engine) │ │ │ │ +│ │ │ │ • WebRTC │ │ │ │ +│ │ │ │ • Media Processing │ │ │ │ +│ │ │ │ • Session Management │ │ │ │ +│ │ │ └────────────────────────────────────────┘ │ │ │ +│ │ └──────────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Your Backend (Node.js/Python/Any) │ │ +│ │ ┌────────────────────────────────────────────────┐ │ │ +│ │ │ JWT Generation Endpoint │ │ │ +│ │ │ • Uses Video SDK Secret (NEVER expose!) │ │ │ +│ │ │ • Generates session tokens │ │ │ +│ │ └────────────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Implementation + +### 1. Install UI Toolkit + +```bash +npm install @zoom/videosdk-zoom-ui-toolkit +npm install react@18 react-dom@18 # Required peer dependency +``` + +### 2. Server-Side JWT Generation (Required) + +```typescript +// Backend: api/zoom-token/route.ts +import { KJUR } from 'jsrsasign'; + +export async function POST(request) { + const { sessionName, role, userName } = await request.json(); + + const payload = { + app_key: process.env.ZOOM_VIDEO_SDK_KEY, + role_type: role, // 0 = participant, 1 = host + tpc: sessionName, + version: 1, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 7200 // 2 hours + }; + + const token = KJUR.jws.JWS.sign( + 'HS256', + JSON.stringify({ alg: 'HS256', typ: 'JWT' }), + JSON.stringify(payload), + process.env.ZOOM_VIDEO_SDK_SECRET + ); + + return Response.json({ signature: token }); +} +``` + +### 3. Frontend Integration (React Example) + +```typescript +'use client'; +import { useEffect, useRef } from 'react'; + +export default function VideoSession({ sessionName, userName }) { + const containerRef = useRef(null); + const uitoolkitRef = useRef(null); + + useEffect(() => { + let mounted = true; + + const init = async () => { + // Fetch JWT from your backend + const response = await fetch('/api/zoom-token', { + method: 'POST', + body: JSON.stringify({ sessionName, userName, role: 1 }) + }); + const { signature } = await response.json(); + + // Import UI Toolkit + const uitoolkitModule = await import('@zoom/videosdk-zoom-ui-toolkit'); + const uitoolkit = uitoolkitModule.default; + uitoolkitRef.current = uitoolkit; + + // @ts-ignore + await import('@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css'); + + if (!mounted || !containerRef.current) return; + + // Configure session + const config = { + videoSDKJWT: signature, + sessionName, + userName, + featuresOptions: { + video: { enable: true }, + audio: { enable: true }, + share: { enable: true }, + chat: { enable: true }, + users: { enable: true }, + settings: { enable: true } + } + }; + + // Join session + uitoolkit.joinSession(containerRef.current, config); + + uitoolkit.onSessionJoined(() => console.log('Joined')); + uitoolkit.onSessionClosed(() => console.log('Closed')); + }; + + init(); + + return () => { + mounted = false; + if (uitoolkitRef.current && containerRef.current) { + uitoolkitRef.current.closeSession(containerRef.current); + uitoolkitRef.current.destroy(); + } + }; + }, [sessionName, userName]); + + return
; +} +``` + +That's it! You now have a fully functional video conferencing UI. + +## Features You Get Out-of-the-Box + +| Feature | Description | +|---------|-------------| +| **Video Grid** | Gallery and speaker views with automatic switching | +| **Audio Controls** | Mute/unmute, device selection, background noise suppression | +| **Video Controls** | Camera on/off, device selection, virtual backgrounds | +| **Screen Share** | Share screen/window with annotation support | +| **Chat** | In-session messaging with emoji support | +| **Participants** | User list with host controls (mute, remove, etc.) | +| **Settings** | Device management, quality statistics, theme selection | +| **Reactions** | Emoji reactions and raised hand | + +## Customization Options + +### Choose Which Features to Enable + +```javascript +const config = { + // ... other config + featuresOptions: { + preview: { enable: true }, // Pre-join device check + video: { enable: true }, + audio: { enable: true }, + share: { enable: true }, + chat: { enable: true }, + users: { enable: true }, + settings: { enable: true }, + virtualBackground: { + enable: true, + virtualBackgrounds: [ + { url: '/bg1.jpg', displayName: 'Office' } + ] + }, + recording: { enable: false }, // Requires paid plan + caption: { enable: false }, // Requires paid plan + theme: { + enable: true, + defaultTheme: 'dark' // 'light' | 'dark' | 'blue' | 'green' + } + } +}; +``` + +### Two UI Modes + +**Composite Mode** (Full UI - Easiest): +```javascript +// Single call gets you complete video UI +uitoolkit.joinSession(container, config); +``` + +**Component Mode** (Custom Layouts): +```javascript +// Show individual pieces where you want +uitoolkit.joinSession(container, config); +uitoolkit.showControlsComponent(controlsContainer); +uitoolkit.showChatComponent(chatContainer); +uitoolkit.showUsersComponent(usersContainer); +``` + +## Related Use Cases + +- **[Custom Video Experiences](custom-video.md)** - When you need raw Video SDK for custom UI +- **[Meeting SDK Integration](embed-meetings.md)** - For Zoom Meeting embedding +- **[Real-Time Media Streams](real-time-media-streams.md)** - When you need raw media access + +## Related Skills + +- **[zoom-ui-toolkit](../../ui-toolkit/SKILL.md)** - Complete UI Toolkit documentation +- **[zoom-video-sdk](../../video-sdk/web/SKILL.md)** - Raw Video SDK (when UI Toolkit isn't enough) +- **[zoom-general](../SKILL.md)** - General Zoom platform knowledge + +## Security Best Practices + +1. **NEVER expose Video SDK Secret** in frontend code +2. **ALWAYS generate JWT server-side** using the secret +3. **Set appropriate JWT expiration** (1-2 hours typical) +4. **Validate user identity** before generating tokens +5. **Use HTTPS** for production deployments + +## Production Checklist + +- [ ] JWT generation is server-side only +- [ ] Proper cleanup on component unmount (`uitoolkit.destroy()`) +- [ ] Error handling for network issues +- [ ] Loading states during JWT fetch +- [ ] Testing across browsers (Chrome, Firefox, Safari, Edge) +- [ ] Mobile responsive testing (if targeting mobile) +- [ ] HTTPS enabled for production +- [ ] Environment variables for SDK credentials + +## Development Time Comparison + +| Approach | Development Time | Effort | +|----------|-----------------|--------| +| **UI Toolkit** | 1-3 days | Low - Drop-in solution | +| **Raw Video SDK** | 2-4 weeks | High - Build all UI | +| **Meeting SDK** | 3-5 days | Medium - Embed Zoom Meetings | + +## When You Outgrow UI Toolkit + +If you need more customization than UI Toolkit provides: + +1. **Access underlying SDK**: + ```javascript + const client = uitoolkit.getClient(); // Get raw Video SDK client + uitoolkit.on('user-added', (payload) => { + // Listen to 80+ raw SDK events + }); + ``` + +2. **Migrate to raw Video SDK**: + - Keep your JWT generation + - Replace UI Toolkit with custom UI + - Use same session/token architecture + - See [zoom-video-sdk](../../video-sdk/web/SKILL.md) for migration guide + +## Common Gotchas + +1. **React 18 Required**: UI Toolkit needs React 18 specifically (not 17 or 19) +2. **CSS Import**: Must import `videosdk-zoom-ui-toolkit.css` or UI will be unstyled +3. **Cleanup Required**: Always call `destroy()` on unmount to prevent memory leaks +4. **JWT Security**: NEVER put SDK secret in frontend - always use server endpoint + +## Resources + +- **Skill**: [zoom-ui-toolkit](../../ui-toolkit/SKILL.md) +- **Live Demo**: https://sdk.zoom.com/videosdk-uitoolkit +- **Official Docs**: https://developers.zoom.us/docs/video-sdk/web/ui-toolkit/ +- **NPM Package**: https://www.npmjs.com/package/@zoom/videosdk-zoom-ui-toolkit +- **Sample Apps**: + - React: https://github.com/zoom/videosdk-zoom-ui-toolkit-react-sample + - Vue.js: https://github.com/zoom/videosdk-zoom-ui-toolkit-vuejs-sample + - Angular: https://github.com/zoom/videosdk-zoom-ui-toolkit-angular-sample + - JavaScript: https://github.com/zoom/videosdk-zoom-ui-toolkit-javascript-sample diff --git a/plugins/zoom-developers/skills/general/use-cases/probe-sdk-preflight-readiness-gate.md b/plugins/zoom-developers/skills/general/use-cases/probe-sdk-preflight-readiness-gate.md new file mode 100644 index 00000000..55e19fbc --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/probe-sdk-preflight-readiness-gate.md @@ -0,0 +1,34 @@ +# Probe SDK Preflight Readiness Gate + +Use Probe SDK before user join/start flows to detect device, browser, and network issues early. + +## When to Use + +- You want to reduce failed joins and low-quality first-minute experiences. +- You need a clear pass/warn/fail decision before launching Meeting SDK or Video SDK UX. +- You need structured diagnostics for support workflows. + +## Skill Chain + +- [probe-sdk](../../probe-sdk/SKILL.md) +- [zoom-meeting-sdk/web](../../meeting-sdk/web/SKILL.md) or [zoom-video-sdk/web](../../video-sdk/web/SKILL.md) +- [zoom-general](../SKILL.md) + +## High-Level Flow + +1. Request media permissions and enumerate devices. +2. Run targeted diagnostics for selected mic/camera/speaker. +3. Run comprehensive network probe and collect final report. +4. Apply readiness policy (`allow`, `warn`, `block`) and present next steps. +5. Launch meeting/session only when policy permits. + +## Risks + +- Renderer and report schema drift across versions. +- Browser support changes over time. +- Incomplete cleanup causing residual device/network usage. + +## See Also + +- [probe-sdk runbook](../../probe-sdk/RUNBOOK.md) +- [probe-sdk troubleshooting](../../probe-sdk/troubleshooting/common-issues.md) diff --git a/plugins/zoom-developers/skills/general/use-cases/qss-monitoring.md b/plugins/zoom-developers/skills/general/use-cases/qss-monitoring.md new file mode 100644 index 00000000..f1d36e56 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/qss-monitoring.md @@ -0,0 +1,112 @@ +# QSS Monitoring + +Quality of Service Subscription for real-time meeting quality monitoring. + +## Overview + +QSS (Quality of Service Subscription) provides near real-time QoS telemetry for Zoom Meetings, Webinars, and Phone. IT teams can monitor network quality, diagnose issues, and track performance at scale. + +## Skills Needed + +- **webhooks** - Receive QSS events +- **zoom-rest-api** - QSS API endpoints + +## What QSS Provides + +### Quality Metrics + +| Metric | Description | +|--------|-------------| +| Bitrate | Data transfer rate | +| Latency | Network delay | +| Jitter | Latency variation | +| Packet Loss | Lost data packets | +| Resolution | Video resolution | +| Frame Rate | Video FPS | +| CPU Usage | Client CPU load | + +### Usage Metrics + +| Metric | Description | +|--------|-------------| +| Device | Device type and model | +| Network | Network type and quality | +| Signaling Region | Connection region | +| Client Version | Zoom client version | +| Audio I/O | Audio device info | +| Video I/O | Camera info | + +## Data Delivery + +| Aspect | Details | +|--------|---------| +| **Frequency** | ~1 event per minute per participant | +| **Delivery** | Webhook events | +| **Retention** | 7 days via Webhook Logs API | +| **Products** | Meetings, Webinars, Phone | + +## Prerequisites + +- Business or Enterprise Zoom account +- QSS add-on subscription +- Webhook endpoint configured +- Internal dashboard system (recommended) + +## Setup + +### 1. Enable QSS + +Contact Zoom sales to add QSS to your account. + +### 2. Configure Webhooks + +Subscribe to QSS events in your app's Event Subscriptions. + +### 3. Handle Events + +```javascript +app.post('/webhook', (req, res) => { + const { event, payload } = req.body; + + if (event.startsWith('qss.')) { + // Process QSS data + const { meeting_id, participant_id, metrics } = payload; + + // Send to your monitoring dashboard + dashboard.ingest({ + meetingId: meeting_id, + participantId: participant_id, + bitrate: metrics.bitrate, + latency: metrics.latency, + jitter: metrics.jitter, + packetLoss: metrics.packet_loss + }); + } + + res.status(200).send(); +}); +``` + +## Use Cases + +| Use Case | Description | +|----------|-------------| +| **Network monitoring** | Track network quality across organization | +| **Troubleshooting** | Diagnose call quality issues in real-time | +| **Capacity planning** | Understand bandwidth usage patterns | +| **SLA compliance** | Monitor meeting quality for SLA reporting | +| **Proactive alerts** | Alert IT when quality degrades | + +## Integration + +QSS data can be integrated with: +- Splunk +- Datadog +- Grafana +- Custom dashboards +- ITSM tools + +## Resources + +- **QSS docs**: https://developers.zoom.us/docs/api/rest/qss-api/ +- **QSS API reference**: https://developers.zoom.us/docs/api/rest/reference/qss/methods/ diff --git a/plugins/zoom-developers/skills/general/use-cases/raw-recording.md b/plugins/zoom-developers/skills/general/use-cases/raw-recording.md new file mode 100644 index 00000000..2ba90bac --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/raw-recording.md @@ -0,0 +1,172 @@ +# Raw Recording + +Access raw audio and video data from Zoom meetings and sessions for custom processing. + +## Overview + +Raw recording allows you to capture unprocessed audio and video frames directly from the SDK, enabling custom recording solutions, AI processing, and media pipelines. + +## Platform Support + +| Platform | Support Level | SDK | +|----------|---------------|-----| +| **Linux** | Primary | Meeting SDK, Video SDK | +| **Windows** | Primary | Meeting SDK, Video SDK | +| **macOS** | Primary | Meeting SDK, Video SDK | +| **iOS** | Light | Meeting SDK, Video SDK | +| **Android** | Light | Meeting SDK, Video SDK | +| **Web** | No native support | Use 3rd party browser recording; must call recording API for compliance notification | + +## Desktop Platforms (Primary) + +### Linux + +```cpp +// Subscribe to raw audio +class AudioRawDataDelegate : public IZoomSDKAudioRawDataDelegate { +public: + void onMixedAudioRawDataReceived(AudioRawData *data) override { + // Format: 16-bit PCM, 16kHz or 32kHz, mono + std::ofstream file("meeting.pcm", std::ios::binary | std::ios::app); + file.write(data->GetBuffer(), data->GetBufferLen()); + } +}; + +// Subscribe to raw video +class VideoRawDataDelegate : public IZoomSDKRendererDelegate { +public: + void onRawDataFrameReceived(YUVRawDataI420 *data) override { + // Format: I420 (YUV 4:2:0) - contiguous planar data + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Write raw YUV to file (can convert with ffmpeg later) + yuvFile.write(data->GetYBuffer(), width * height); + yuvFile.write(data->GetUBuffer(), (width/2) * (height/2)); + yuvFile.write(data->GetVBuffer(), (width/2) * (height/2)); + } +}; + +// Enable raw recording +auto recCtl = m_meetingService->GetMeetingRecordingController(); +recCtl->StartRawRecording(); + +// Subscribe +GetAudioRawdataHelper()->subscribe(new AudioRawDataDelegate()); +GetRawdataRendererHelper()->subscribe(userId, RAW_DATA_TYPE_VIDEO, new VideoRawDataDelegate()); +``` + +**Playing/Converting Raw Files:** +```bash +# Play raw YUV video (adjust dimensions to match output) +ffplay -video_size 640x360 -pixel_format yuv420p -f rawvideo video.yuv + +# Convert YUV to MP4 +ffmpeg -video_size 640x360 -pixel_format yuv420p -f rawvideo -i video.yuv -c:v libx264 output.mp4 + +# Play raw PCM audio +ffplay -f s16le -ar 32000 -ac 1 audio.pcm + +# Combine video + audio +ffmpeg -video_size 640x360 -pixel_format yuv420p -f rawvideo -i video.yuv \ + -f s16le -ar 32000 -ac 1 -i audio.pcm \ + -c:v libx264 -c:a aac -shortest output.mp4 +``` + +### Windows + +```cpp +// Same API as Linux +// Subscribe to raw data after joining meeting +auto* pRawDataHelper = GetAudioRawdataHelper(); +pRawDataHelper->subscribe(new AudioRawDataDelegate()); + +auto* pVideoHelper = GetRawdataRendererHelper(); +pVideoHelper->setRawDataResolution(ZoomSDKResolution_720P); +pVideoHelper->subscribe(userId, RAW_DATA_TYPE_VIDEO, new VideoRawDataDelegate()); +``` + +### macOS + +```swift +// Get raw data controller +let rawDataCtrl = ZoomSDK.shared().getRawDataController() + +// Subscribe to audio +rawDataCtrl?.subscribeAudioRawData { audioData in + // Process PCM audio + let buffer = audioData.getBuffer() + let length = audioData.getBufferLen() +} + +// Subscribe to video +rawDataCtrl?.subscribeVideoRawData(forUser: userId) { videoData in + // Process YUV frames + let width = videoData.getStreamWidth() + let height = videoData.getStreamHeight() +} +``` + +## Mobile Platforms (Light) + +### iOS + +Raw data access on iOS is more limited than desktop: + +```swift +// Video raw data via ZoomVideoSDKVideoCanvas +let canvas = ZoomVideoSDKVideoCanvas() +canvas.delegate = self + +// Implement delegate +func onRawDataFrameReceived(_ pixelBuffer: CVPixelBuffer) { + // Process video frame + // Note: Performance-intensive on mobile +} +``` + +**Limitations:** +- Higher battery consumption +- May impact app performance +- Not recommended for long recordings + +### Android + +```kotlin +// Video raw data via ZoomVideoSDKVideoCanvas +val canvas = ZoomVideoSDKVideoCanvas(context) +canvas.setDelegate(object : ZoomVideoSDKRawDataPipeDelegate { + override fun onRawDataFrameReceived(rawData: ZoomVideoSDKVideoRawData) { + // Process YUV frame + // Note: Performance-intensive on mobile + } +}) +``` + +**Limitations:** +- Same as iOS - battery and performance concerns +- Consider cloud recording instead for mobile apps + +## Web Platform + +There is no native SDK support for raw recording on Web. For browser-based recording: + +1. Use 3rd party browser recording solutions +2. **Important**: Call the recording API to trigger the "This meeting is being recorded" notification for compliance + +## Common Use Cases + +- Meeting bots for transcription +- Custom recording pipelines +- AI/ML processing (sentiment analysis, summarization) +- Media archival solutions + +## Detailed Platform Guides + +- **[Video SDK Linux Guide](../../video-sdk/linux/linux.md)** - Complete C++ implementation for headless bots +- **[Meeting SDK Linux Guide](../../meeting-sdk/linux/linux.md)** - Meeting SDK raw data capture + +## Resources + +- **Meeting SDK docs**: https://developers.zoom.us/docs/meeting-sdk/ +- **Video SDK docs**: https://developers.zoom.us/docs/video-sdk/ diff --git a/plugins/zoom-developers/skills/general/use-cases/react-native-meeting-embed.md b/plugins/zoom-developers/skills/general/use-cases/react-native-meeting-embed.md new file mode 100644 index 00000000..a19048e7 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/react-native-meeting-embed.md @@ -0,0 +1,27 @@ +# React Native Meeting Embed + +Use this flow when you need Zoom meetings inside a mobile app built with React Native. + +## When to Use + +- iOS/Android app already uses React Native +- You need Zoom meeting join/start inside app navigation +- You want Zoom Meeting SDK UI from React Native wrapper, not a custom video stack + +## Skill Chain + +1. **[meeting-sdk/react-native](../../meeting-sdk/react-native/SKILL.md)** for wrapper APIs and platform setup +2. **[zoom-oauth](../../oauth/SKILL.md)** for backend token handling (SDK JWT and ZAK) + +## Typical Flow + +1. Backend issues SDK JWT for mobile client. +2. App initializes SDK with `initSDK`. +3. App joins by `joinMeeting` (attendee) or starts by `startMeeting` (host + ZAK). +4. App handles meeting lifecycle and calls `cleanup` during teardown. + +## References + +- [Meeting SDK React Native Skill](../../meeting-sdk/react-native/SKILL.md) +- [Auth and Token Model](../../meeting-sdk/react-native/concepts/auth-and-token-model.md) +- [Join Meeting Pattern](../../meeting-sdk/react-native/examples/join-meeting-pattern.md) diff --git a/plugins/zoom-developers/skills/general/use-cases/react-native-video-sessions.md b/plugins/zoom-developers/skills/general/use-cases/react-native-video-sessions.md new file mode 100644 index 00000000..507fd1c7 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/react-native-video-sessions.md @@ -0,0 +1,28 @@ +# React Native Video Sessions + +Use this flow for custom mobile video session products in React Native. + +## When to Use + +- You need full custom UX (not Zoom Meeting UI). +- You are building iOS/Android apps using `@zoom/react-native-videosdk`. +- You need helper-based features such as chat/share/recording/transcription. + +## Skill Chain + +1. [video-sdk/react-native](../../video-sdk/react-native/SKILL.md) +2. [zoom-oauth](../../oauth/SKILL.md) + +## Typical Flow + +1. Backend signs short-lived Video SDK JWT. +2. App initializes SDK provider and listeners. +3. App joins session with tokenized config. +4. App drives helper APIs and event-based UI state. +5. App leaves session and cleans up resources. + +## References + +- [React Native Video SDK Skill](../../video-sdk/react-native/SKILL.md) +- [Lifecycle Workflow](../../video-sdk/react-native/concepts/lifecycle-workflow.md) +- [Session Join Pattern](../../video-sdk/react-native/examples/session-join-pattern.md) diff --git a/plugins/zoom-developers/skills/general/use-cases/real-time-media-streams.md b/plugins/zoom-developers/skills/general/use-cases/real-time-media-streams.md new file mode 100644 index 00000000..d864b068 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/real-time-media-streams.md @@ -0,0 +1,237 @@ +# Real-Time Media Streams + +Access live audio, video, transcripts, chat, and screen share from Zoom meetings via WebSocket. + +## Overview + +Zoom RTMS (Realtime Media Streams) provides WebSocket-based access to live meeting media for real-time AI processing, transcription, analysis, and recording - **without meeting bots**. + +> **See the comprehensive RTMS skill**: [rtms/SKILL.md](../../rtms/SKILL.md) + +## Skills Needed + +- **[zoom-rtms](../../rtms/SKILL.md)** - Primary (comprehensive documentation) +- **webhooks** - Receive RTMS start/stop events + +## Two Approaches + +| Approach | Best For | Documentation | +|----------|----------|---------------| +| **SDK** (`@zoom/rtms`) | Most use cases | [SDK Quickstart](../../rtms/examples/sdk-quickstart.md) | +| **Manual WebSocket** | Full protocol control | [Manual WebSocket](../../rtms/examples/manual-websocket.md) | + +## How It Works + +``` +RTMS Flow: +1. Meeting starts with RTMS enabled + ↓ +2. Webhook: meeting.rtms_started + ↓ +3. Connect to Signaling WebSocket + ↓ +4. Connect to Media WebSocket + ↓ +5. Receive live audio/video/transcript/chat/share +``` + +## Media Types + +| Type | Format | Use Case | +|------|--------|----------| +| Audio | PCM 16-bit | Transcription, analysis | +| Video | H.264 | Visual AI, recording | +| Transcript | JSON | Real-time captions | + +## Prerequisites + +- RTMS feature enabled on app +- Webhook endpoint +- WebSocket client + +## Quick Start + +```javascript +// Handle RTMS webhook +app.post('/webhook', (req, res) => { + if (req.body.event === 'meeting.rtms_started') { + const { server_urls, signature } = req.body.payload; + + // Connect to RTMS + const ws = new WebSocket(server_urls); + ws.on('message', (data) => { + // Process live media + }); + } + res.status(200).send(); +}); +``` + +## Common Tasks + +### Connecting to RTMS WebSocket + +```javascript +const WebSocket = require('ws'); +const crypto = require('crypto'); + +// Webhook handler receives RTMS connection info +app.post('/webhook', (req, res) => { + const { event, payload } = req.body; + + if (event === 'meeting.rtms_started') { + const { server_urls, stream_id, signature } = payload; + + // Connect to RTMS WebSocket + connectToRTMS(server_urls[0], stream_id, signature); + } + + res.status(200).send(); +}); + +function connectToRTMS(url, streamId, signature) { + const ws = new WebSocket(url); + + ws.on('open', () => { + // Authenticate + ws.send(JSON.stringify({ + type: 'auth', + stream_id: streamId, + signature: signature + })); + }); + + ws.on('message', (data) => { + const message = JSON.parse(data); + handleRTMSMessage(message); + }); + + ws.on('error', (err) => { + console.error('RTMS error:', err); + // Implement reconnection logic + }); +} +``` + +### Processing Audio Streams + +```javascript +function handleRTMSMessage(message) { + switch (message.type) { + case 'audio': + processAudio(message); + break; + case 'video': + processVideo(message); + break; + case 'transcript': + processTranscript(message); + break; + } +} + +function processAudio(message) { + // Audio format: PCM 16-bit, 16kHz, mono + const audioBuffer = Buffer.from(message.data, 'base64'); + + // Send to transcription service (e.g., OpenAI Whisper, Deepgram) + transcriptionService.processChunk(audioBuffer, { + sampleRate: 16000, + channels: 1, + format: 'pcm_s16le' + }); +} +``` + +### Handling Video Frames + +```javascript +function processVideo(message) { + // Video format: H.264 encoded + const videoFrame = Buffer.from(message.data, 'base64'); + + // Decode H.264 frame (requires ffmpeg or similar) + const decodedFrame = h264Decoder.decode(videoFrame); + + // Process for AI (face detection, emotion analysis, etc.) + aiProcessor.analyzeFrame(decodedFrame, { + width: message.width, + height: message.height, + timestamp: message.timestamp + }); +} +``` + +### Real-Time Transcription Integration + +```javascript +// Using Zoom's built-in transcript stream +function processTranscript(message) { + const { text, speaker_id, timestamp, is_final } = message; + + if (is_final) { + // Final transcript segment + saveTranscript({ + speaker: speaker_id, + text: text, + timestamp: timestamp + }); + + // Optionally analyze with AI + analyzeWithAI(text); + } else { + // Partial transcript - update UI in real-time + updateLiveCaption(text); + } +} + +// Integration with external AI for summarization +async function analyzeWithAI(transcript) { + const response = await openai.chat.completions.create({ + model: 'gpt-4', + messages: [{ + role: 'system', + content: 'Extract action items from this meeting segment.' + }, { + role: 'user', + content: transcript + }] + }); + + return response.choices[0].message.content; +} +``` + +### Error Handling & Reconnection + +```javascript +class RTMSClient { + constructor(config) { + this.config = config; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 5; + } + + connect() { + this.ws = new WebSocket(this.config.url); + + this.ws.on('close', () => { + if (this.reconnectAttempts < this.maxReconnectAttempts) { + const delay = Math.pow(2, this.reconnectAttempts) * 1000; + setTimeout(() => this.connect(), delay); + this.reconnectAttempts++; + } + }); + + this.ws.on('open', () => { + this.reconnectAttempts = 0; + this.authenticate(); + }); + } +} +``` + +## Resources + +- **RTMS docs**: https://developers.zoom.us/docs/rtms/ +- **RTMS Quick Start**: https://developers.zoom.us/docs/rtms/getting-started/ diff --git a/plugins/zoom-developers/skills/general/use-cases/recording-download-pipeline.md b/plugins/zoom-developers/skills/general/use-cases/recording-download-pipeline.md new file mode 100644 index 00000000..ec90fe5d --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/recording-download-pipeline.md @@ -0,0 +1,308 @@ +# Recording Download Pipeline + +Automatically download Zoom Meeting cloud recordings to your own storage (S3, GCS, Azure Blob, etc.) using webhooks and REST API. + +> **Note:** This is NOT Video SDK BYOS (Bring Your Own Storage). Video SDK BYOS saves recordings **directly** to S3 without downloading through Zoom servers. See `zoom-video-sdk` for true BYOS. + +## Overview + +Set up automated pipelines to download Zoom Meeting cloud recordings to your own storage infrastructure for compliance, cost management, or integration with existing media workflows. + +## Skills Needed + +- **webhooks** - Receive recording events +- **zoom-rest-api** - Download recordings + +## Architecture + +``` +Recording Download Pipeline: +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Zoom │────▶│ Webhook │────▶│ Your │ +│ Cloud │ │ Handler │ │ Storage │ +│ Recording │ │ │ │ (S3, GCS) │ +└─────────────┘ └─────────────┘ └─────────────┘ +``` + +## Prerequisites + +- Cloud recording enabled +- Webhook endpoint +- Storage bucket (S3, GCS, Azure, etc.) +- `recording:read` scope + +## Workflow + +1. Meeting ends, cloud recording completes +2. Receive `recording.completed` webhook +3. Download recording via API +4. Upload to your storage +5. (Optional) Delete from Zoom cloud + +## Common Tasks + +### S3 Upload Integration + +```javascript +const axios = require('axios'); +const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); +const { Upload } = require('@aws-sdk/lib-storage'); + +const s3 = new S3Client({ region: 'us-east-1' }); + +// Handle recording.completed webhook +app.post('/webhook', async (req, res) => { + const { event, payload } = req.body; + + if (event === 'recording.completed') { + const { uuid, recording_files, host_email, topic } = payload.object; + + // Process each recording file + for (const file of recording_files) { + await uploadToS3(file, uuid, topic); + } + + // Optionally delete from Zoom after successful upload + // await deleteZoomRecording(uuid); + } + + res.status(200).send(); +}); + +async function uploadToS3(file, meetingUuid, topic) { + // Download from Zoom + const response = await axios({ + method: 'GET', + url: file.download_url, + headers: { 'Authorization': `Bearer ${accessToken}` }, + responseType: 'stream' + }); + + // Sanitize topic for filename + const safeTopic = topic.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 50); + const key = `recordings/${meetingUuid}/${safeTopic}_${file.file_type}.${file.file_extension}`; + + // Upload to S3 using multipart upload for large files + const upload = new Upload({ + client: s3, + params: { + Bucket: 'your-recordings-bucket', + Key: key, + Body: response.data, + ContentType: getContentType(file.file_extension), + Metadata: { + 'meeting-uuid': meetingUuid, + 'file-type': file.file_type, + 'recording-start': file.recording_start + } + } + }); + + await upload.done(); + return `s3://your-recordings-bucket/${key}`; +} +``` + +### GCS Upload Integration + +```javascript +const { Storage } = require('@google-cloud/storage'); +const storage = new Storage(); +const bucket = storage.bucket('your-recordings-bucket'); + +async function uploadToGCS(file, meetingUuid, topic) { + const response = await axios({ + method: 'GET', + url: file.download_url, + headers: { 'Authorization': `Bearer ${accessToken}` }, + responseType: 'stream' + }); + + const safeTopic = topic.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 50); + const filePath = `recordings/${meetingUuid}/${safeTopic}_${file.file_type}.${file.file_extension}`; + + const gcsFile = bucket.file(filePath); + + return new Promise((resolve, reject) => { + const writeStream = gcsFile.createWriteStream({ + metadata: { + contentType: getContentType(file.file_extension), + metadata: { + meetingUuid: meetingUuid, + fileType: file.file_type + } + }, + resumable: true // Important for large files + }); + + response.data.pipe(writeStream) + .on('finish', () => resolve(`gs://your-recordings-bucket/${filePath}`)) + .on('error', reject); + }); +} +``` + +### Handling Large Files + +```javascript +// Use streaming to avoid memory issues +async function streamDownload(downloadUrl, destination) { + const response = await axios({ + method: 'GET', + url: downloadUrl, + headers: { 'Authorization': `Bearer ${accessToken}` }, + responseType: 'stream', + maxContentLength: Infinity, // Allow large files + maxBodyLength: Infinity + }); + + // Track progress + const totalSize = parseInt(response.headers['content-length'], 10); + let downloadedSize = 0; + + response.data.on('data', (chunk) => { + downloadedSize += chunk.length; + const progress = ((downloadedSize / totalSize) * 100).toFixed(2); + console.log(`Download progress: ${progress}%`); + }); + + return response.data; +} + +// For very large files, use chunked upload +async function chunkedUploadToS3(stream, key) { + const upload = new Upload({ + client: s3, + params: { + Bucket: 'your-bucket', + Key: key, + Body: stream + }, + queueSize: 4, // Concurrent part uploads + partSize: 10 * 1024 * 1024 // 10MB parts + }); + + upload.on('httpUploadProgress', (progress) => { + console.log(`Upload progress: ${progress.loaded}/${progress.total}`); + }); + + await upload.done(); +} +``` + +### Retry Logic for Failed Downloads + +```javascript +const retry = require('async-retry'); + +async function downloadWithRetry(file, meetingUuid) { + return await retry( + async (bail, attemptNumber) => { + console.log(`Attempt ${attemptNumber} for ${file.file_type}`); + + try { + return await uploadToS3(file, meetingUuid); + } catch (error) { + // Don't retry on permanent errors + if (error.response?.status === 404) { + bail(new Error('Recording not found')); + return; + } + if (error.response?.status === 401) { + // Refresh token and retry + await refreshAccessToken(); + } + throw error; // Retry + } + }, + { + retries: 3, + factor: 2, + minTimeout: 1000, + maxTimeout: 10000, + onRetry: (error, attempt) => { + console.log(`Retry attempt ${attempt}: ${error.message}`); + } + } + ); +} + +// Queue system for processing +const Queue = require('bull'); +const recordingQueue = new Queue('recording-uploads', process.env.REDIS_URL || 'redis://YOUR_REDIS_HOST:6379'); + +recordingQueue.process(async (job) => { + const { file, meetingUuid, topic } = job.data; + return await downloadWithRetry(file, meetingUuid, topic); +}); + +// Add job with retry +app.post('/webhook', async (req, res) => { + if (req.body.event === 'recording.completed') { + const { uuid, recording_files, topic } = req.body.payload.object; + + for (const file of recording_files) { + await recordingQueue.add( + { file, meetingUuid: uuid, topic }, + { attempts: 3, backoff: { type: 'exponential', delay: 5000 }} + ); + } + } + res.status(200).send(); +}); +``` + +### Recording Lifecycle Management + +```javascript +// Delete from Zoom after successful upload +async function deleteZoomRecording(meetingUuid) { + // Double-encode UUID if it contains / or // + const encodedUuid = encodeURIComponent(encodeURIComponent(meetingUuid)); + + await axios.delete( + `https://api.zoom.us/v2/meetings/${encodedUuid}/recordings`, + { + params: { action: 'trash' }, // Move to trash (recoverable for 30 days) + // params: { action: 'delete' }, // Permanent delete + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); +} + +// Track what's been archived +const db = require('./db'); + +async function markAsArchived(meetingUuid, s3Path) { + await db.recordings.upsert({ + meeting_uuid: meetingUuid, + s3_path: s3Path, + archived_at: new Date(), + deleted_from_zoom: false + }); +} + +// Clean up old recordings from Zoom +async function cleanupOldRecordings() { + const archivedRecordings = await db.recordings.findAll({ + where: { + archived_at: { $lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }, + deleted_from_zoom: false + } + }); + + for (const recording of archivedRecordings) { + await deleteZoomRecording(recording.meeting_uuid); + await db.recordings.update( + { deleted_from_zoom: true }, + { where: { meeting_uuid: recording.meeting_uuid }} + ); + } +} +``` + +## Resources + +- **Recordings API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Cloud-Recording +- **AWS S3 SDK**: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/ +- **Google Cloud Storage**: https://cloud.google.com/storage/docs/reference/libraries diff --git a/plugins/zoom-developers/skills/general/use-cases/recording-transcription.md b/plugins/zoom-developers/skills/general/use-cases/recording-transcription.md new file mode 100644 index 00000000..93e4528b --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/recording-transcription.md @@ -0,0 +1,291 @@ +# Recording & Transcription + +Download cloud recordings and access transcripts from Zoom meetings. + +## Overview + +Access Zoom's cloud recordings and automated transcripts via webhooks and REST API for archival, compliance, or further processing. + +For deterministic archival pipelines, keep REST API + webhooks as the primary route. + +## Skills Needed + +- **zoom-webhooks** - Receive recording.completed events (pipeline mode) +- **zoom-rest-api** - Download recordings and transcripts (pipeline mode) + +## Flow + +``` +Recording Flow: +1. Meeting ends + ↓ +2. Cloud recording processes + ↓ +3. Webhook: recording.completed + ↓ +4. API: Download recording files + ↓ +5. API: Get transcript (if enabled) +``` + +## Prerequisites + +- Cloud recording enabled on account +- `recording:read` scope +- Webhook endpoint for `recording.completed` + +## Quick Start + +```javascript +// Handle recording.completed webhook +app.post('/webhook', async (req, res) => { + const { event, payload } = req.body; + + if (event === 'recording.completed') { + const { recording_files } = payload.object; + + for (const file of recording_files) { + // Download recording + const response = await fetch(file.download_url, { + headers: { 'Authorization': `Bearer ${token}` } + }); + } + } + + res.status(200).send(); +}); +``` + +## Common Tasks + +### Downloading Video Recordings + +```javascript +const axios = require('axios'); +const fs = require('fs'); + +// Handle recording.completed webhook +app.post('/webhook', async (req, res) => { + const { event, payload } = req.body; + + if (event === 'recording.completed') { + const { uuid, recording_files } = payload.object; + + for (const file of recording_files) { + if (file.file_type === 'MP4') { + await downloadRecording(file, uuid); + } + } + } + + res.status(200).send(); +}); + +async function downloadRecording(file, meetingUuid) { + // Download URL requires authentication + const downloadUrl = file.download_url; + + const response = await axios({ + method: 'GET', + url: downloadUrl, + headers: { + 'Authorization': `Bearer ${accessToken}` + }, + responseType: 'stream' + }); + + // Save to file + const writer = fs.createWriteStream(`recordings/${meetingUuid}.mp4`); + response.data.pipe(writer); + + return new Promise((resolve, reject) => { + writer.on('finish', resolve); + writer.on('error', reject); + }); +} +``` + +### Getting Audio-Only Files + +```javascript +// Recording files include multiple formats +const audioFormats = ['M4A']; +const videoFormats = ['MP4']; +const transcriptFormats = ['TRANSCRIPT', 'VTT']; + +app.post('/webhook', async (req, res) => { + const { recording_files } = req.body.payload.object; + + for (const file of recording_files) { + switch (file.file_type) { + case 'M4A': + // Audio-only recording + await saveFile(file, 'audio'); + break; + case 'MP4': + // Video recording (includes audio) + await saveFile(file, 'video'); + break; + case 'TRANSCRIPT': + // JSON transcript + await saveFile(file, 'transcript'); + break; + case 'VTT': + // WebVTT caption file + await saveFile(file, 'captions'); + break; + } + } +}); +``` + +### Accessing VTT Transcripts + +```javascript +// VTT file comes with recording.completed webhook +async function downloadTranscript(file, meetingUuid) { + const response = await axios.get(file.download_url, { + headers: { 'Authorization': `Bearer ${accessToken}` } + }); + + // Parse VTT format + const vttContent = response.data; + const parsed = parseVTT(vttContent); + + return parsed; +} + +// Simple VTT parser +function parseVTT(vttContent) { + const lines = vttContent.split('\n'); + const cues = []; + let currentCue = null; + + for (const line of lines) { + if (line.includes('-->')) { + const [start, end] = line.split(' --> '); + currentCue = { start, end, text: '' }; + } else if (currentCue && line.trim()) { + currentCue.text += line + ' '; + } else if (currentCue && !line.trim()) { + cues.push(currentCue); + currentCue = null; + } + } + + return cues; +} +``` + +### Getting Transcript via API + +```javascript +// Alternative: Get transcript via REST API +async function getTranscriptFromAPI(meetingUuid) { + // Double-encode UUID if it contains '/' or '//' + const encodedUuid = encodeURIComponent(encodeURIComponent(meetingUuid)); + + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${encodedUuid}/recordings`, + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + const transcriptFile = response.data.recording_files.find( + f => f.file_type === 'TRANSCRIPT' + ); + + if (transcriptFile) { + return await downloadTranscript(transcriptFile, meetingUuid); + } +} +``` + +### Handling Recording Expiration + +```javascript +// Recordings auto-delete based on account settings (30-120 days) +// Download and archive before expiration + +// Option 1: Download immediately on webhook +app.post('/webhook', async (req, res) => { + if (req.body.event === 'recording.completed') { + await archiveRecording(req.body.payload); + } +}); + +// Option 2: Batch download via scheduled job +async function downloadPendingRecordings() { + // List recordings from last 7 days + const from = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + const to = new Date().toISOString().split('T')[0]; + + const response = await axios.get( + `https://api.zoom.us/v2/users/me/recordings?from=${from}&to=${to}`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + + for (const meeting of response.data.meetings) { + if (!isArchived(meeting.uuid)) { + await archiveRecording(meeting); + } + } +} + +// Run daily +cron.schedule('0 0 * * *', downloadPendingRecordings); +``` + +### Upload to Cloud Storage (S3/GCS) + +```javascript +const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); +const { Upload } = require('@aws-sdk/lib-storage'); + +async function uploadToS3(stream, key) { + const s3 = new S3Client({ region: 'us-east-1' }); + + const upload = new Upload({ + client: s3, + params: { + Bucket: 'zoom-recordings', + Key: key, + Body: stream, + ContentType: 'video/mp4' + } + }); + + await upload.done(); + return `s3://zoom-recordings/${key}`; +} + +// Stream directly from Zoom to S3 +async function archiveToS3(file, meetingUuid) { + const response = await axios({ + method: 'GET', + url: file.download_url, + headers: { 'Authorization': `Bearer ${accessToken}` }, + responseType: 'stream' + }); + + const key = `recordings/${meetingUuid}/${file.file_type.toLowerCase()}.${file.file_extension}`; + return await uploadToS3(response.data, key); +} +``` + +## File Types Reference + +| File Type | Extension | Description | +|-----------|-----------|-------------| +| MP4 | .mp4 | Video recording (active speaker or gallery) | +| M4A | .m4a | Audio-only recording | +| CHAT | .txt | Chat messages | +| TRANSCRIPT | .json | JSON transcript with timestamps | +| VTT | .vtt | WebVTT captions file | +| TIMELINE | .json | Meeting timeline events | + +## Resources + +- **Recordings API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Cloud-Recording +- **Recording Webhooks**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/events/#recording-completed diff --git a/plugins/zoom-developers/skills/general/use-cases/retrieve-meeting-and-subscribe-events.md b/plugins/zoom-developers/skills/general/use-cases/retrieve-meeting-and-subscribe-events.md new file mode 100644 index 00000000..0b8ed0a7 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/retrieve-meeting-and-subscribe-events.md @@ -0,0 +1,943 @@ +# Retrieve Meeting Details and Subscribe to Events + +Comprehensive guide showing all ways to retrieve meeting details and subscribe to events in Zoom. The term "subscribe to events" has multiple meanings in Zoom, each requiring different skill combinations. + +## Overview + +When someone says "retrieve meeting details and subscribe to events," they could mean: + +| Pattern | Event Type | Skills | When to Use | +|---------|------------|--------|-------------| +| **Pattern 1** | Server-side webhooks (HTTP) | zoom-rest-api → webhooks | Account-level event monitoring, backend processing | +| **Pattern 2** | Server-side WebSockets (WS) | zoom-rest-api → zoom-websockets | Low-latency events, no exposed endpoint | +| **Pattern 3** | Client-side SDK events | zoom-rest-api → zoom-meeting-sdk | In-meeting participant events, real-time UI updates | +| **Pattern 4** | Real-time media events | zoom-rest-api → rtms | Bot-based transcription, recording, AI analysis | +| **Pattern 5** | Reports API (polling) | zoom-rest-api only | Historical data, batch processing | + +## Critical Distinction: Event Subscription Scope + +**Important**: Most Zoom event subscriptions are **account-level**, not meeting-specific: + +| Subscription Type | Scope | Filtering | +|-------------------|-------|-----------| +| **Webhooks** | Account-level | Filter events by meeting ID in handler | +| **WebSockets** | Account-level | Filter events by meeting ID in handler | +| **Meeting SDK** | Meeting-specific | Only events for joined meeting | +| **RTMS** | Meeting-specific | Only events/media for specific meeting | +| **Reports API** | Account-level | Query by meeting ID | + +**You cannot dynamically subscribe to webhooks/WebSockets for a single meeting**. These are configured at app creation time in the Marketplace portal and receive events for ALL meetings in the account. Your code filters events by meeting ID. + +--- + +## Pattern 1: REST API + Webhooks (Server-Side HTTP Events) + +**When to use:** +- Backend processing of meeting lifecycle events +- Attendance tracking, post-meeting workflows +- Recording download automation +- Standard event-driven architectures + +**Skills needed:** `zoom-rest-api` → `webhooks` + +### Flow Diagram + +``` +┌────────────────────────────────────────────────────────────────┐ +│ SETUP PHASE (One-time in Marketplace Portal): │ +│ │ +│ 1. Configure Event Subscriptions │ +│ └── Subscribe to: meeting.started, meeting.ended, etc. │ +│ └── Provide webhook endpoint URL │ +│ └── Get webhook secret token │ +└────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────┐ +│ RUNTIME PHASE: │ +│ │ +│ Step 1: GET /meetings/{meetingId} (zoom-rest-api) │ +│ └── Get meeting details (topic, join_url, host, etc.) │ +│ │ +│ Step 2: Store meeting ID for filtering │ +│ └── Your backend remembers which meetings to track │ +│ │ +│ Step 3: POST /webhooks/zoom (webhooks) │ +│ └── Zoom sends events for ALL meetings │ +│ └── Filter by meeting ID in your handler │ +│ └── Process: started, ended, participant events │ +└────────────────────────────────────────────────────────────────┘ +``` + +### Complete Implementation + +```javascript +const express = require('express'); +const axios = require('axios'); +const crypto = require('crypto'); + +const app = express(); +app.use(express.json()); + +// Track which meetings we care about (use Redis/DB in production) +const trackedMeetings = new Set(); + +// ============================================ +// STEP 1: Retrieve Meeting Details (zoom-rest-api) +// ============================================ + +async function getAccessToken() { + const credentials = Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64'); + + const response = await axios.post( + `https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${process.env.ZOOM_ACCOUNT_ID}`, + null, + { headers: { 'Authorization': `Basic ${credentials}` } } + ); + + return response.data.access_token; +} + +async function getMeetingDetails(meetingId) { + const token = await getAccessToken(); + + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return response.data; +} + +// API: Fetch meeting and start tracking it +app.get('/api/meetings/:meetingId', async (req, res) => { + try { + const { meetingId } = req.params; + + // Get meeting details + const meeting = await getMeetingDetails(meetingId); + + // Add to tracked set (events for this meeting will be processed) + trackedMeetings.add(String(meeting.id)); + + console.log(`✅ Now tracking meeting: ${meeting.topic} (${meeting.id})`); + + res.json({ + success: true, + meeting: { + id: meeting.id, + topic: meeting.topic, + start_time: meeting.start_time, + join_url: meeting.join_url + }, + message: 'Meeting tracked. Webhook events will be processed for this meeting.' + }); + } catch (error) { + res.status(error.response?.status || 500).json({ + success: false, + error: error.message + }); + } +}); + +// ============================================ +// STEP 2: Handle Webhook Events (webhooks) +// ============================================ + +function verifyWebhookSignature(req) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + const payload = `v0:${timestamp}:${JSON.stringify(req.body)}`; + + const expectedSignature = `v0=${crypto + .createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET) + .update(payload) + .digest('hex')}`; + + return signature === expectedSignature; +} + +app.post('/webhooks/zoom', async (req, res) => { + // Handle URL validation challenge + if (req.body.event === 'endpoint.url_validation') { + const hash = crypto + .createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET) + .update(req.body.payload.plainToken) + .digest('hex'); + + return res.json({ + plainToken: req.body.payload.plainToken, + encryptedToken: hash + }); + } + + // Verify signature + if (!verifyWebhookSignature(req)) { + console.error('❌ Invalid webhook signature'); + return res.status(401).send('Invalid signature'); + } + + const { event, payload } = req.body; + const meetingId = String(payload.object.id); + + // CRITICAL: Filter events - only process tracked meetings + if (!trackedMeetings.has(meetingId)) { + console.log(`⏭️ Ignoring event for untracked meeting ${meetingId}`); + return res.status(200).send(); + } + + // Process events for tracked meetings + console.log(`📩 Event: ${event} for meeting ${meetingId}`); + + switch (event) { + case 'meeting.started': + console.log(`✅ Meeting started: ${payload.object.topic}`); + // Handle meeting start + break; + + case 'meeting.ended': + console.log(`🏁 Meeting ended: ${payload.object.topic}`); + // Handle meeting end, cleanup + trackedMeetings.delete(meetingId); + break; + + case 'meeting.participant_joined': + console.log(`👋 ${payload.object.participant.user_name} joined`); + // Track attendance + break; + + case 'meeting.participant_left': + console.log(`👋 ${payload.object.participant.user_name} left`); + // Update attendance + break; + } + + res.status(200).send(); +}); + +app.listen(3000, () => { + console.log('Server running on port 3000'); + console.log(''); + console.log('Endpoints:'); + console.log(' GET /api/meetings/:id - Fetch & track meeting'); + console.log(' POST /webhooks/zoom - Webhook receiver'); +}); +``` + +**See also**: [meeting-details-with-events.md](meeting-details-with-events.md) for expanded webhook implementation. + +--- + +## Pattern 2: REST API + WebSockets (Server-Side Low-Latency Events) + +**When to use:** +- Lower latency than webhooks required +- Security-sensitive industries (no exposed endpoint) +- Bidirectional communication needed +- Real-time dashboards, live monitoring + +**Skills needed:** `zoom-rest-api` → `zoom-websockets` + +### Flow Diagram + +``` +┌────────────────────────────────────────────────────────────────┐ +│ SETUP PHASE (One-time in Marketplace Portal): │ +│ │ +│ 1. Create Server-to-Server OAuth app │ +│ 2. Configure WebSocket Event Subscription │ +│ └── Subscribe to: meeting.started, meeting.ended, etc. │ +│ └── Get subscription ID │ +└────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────┐ +│ RUNTIME PHASE: │ +│ │ +│ Step 1: GET /meetings/{meetingId} (zoom-rest-api) │ +│ └── Get meeting details │ +│ │ +│ Step 2: Connect to WebSocket (zoom-websockets) │ +│ └── wss://ws.zoom.us/ws?subscriptionId=... │ +│ └── Authenticate with S2S OAuth access token │ +│ │ +│ Step 3: Receive events via WebSocket │ +│ └── Filter by meeting ID in message handler │ +│ └── Lower latency than webhooks │ +└────────────────────────────────────────────────────────────────┘ +``` + +### Complete Implementation + +```javascript +const axios = require('axios'); +const WebSocket = require('ws'); + +// Track which meetings we care about +const trackedMeetings = new Set(); +let ws = null; + +// ============================================ +// STEP 1: Retrieve Meeting Details (zoom-rest-api) +// ============================================ + +async function getAccessToken() { + const credentials = Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64'); + + const response = await axios.post( + 'https://zoom.us/oauth/token', + new URLSearchParams({ + grant_type: 'account_credentials', + account_id: process.env.ZOOM_ACCOUNT_ID + }), + { + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + return response.data.access_token; +} + +async function getMeetingDetails(meetingId) { + const token = await getAccessToken(); + + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return response.data; +} + +// ============================================ +// STEP 2: Connect to WebSocket (zoom-websockets) +// ============================================ + +async function connectWebSocket() { + const accessToken = await getAccessToken(); + const subscriptionId = process.env.ZOOM_WEBSOCKET_SUBSCRIPTION_ID; + + const wsUrl = `wss://ws.zoom.us/ws?subscriptionId=${subscriptionId}&access_token=${accessToken}`; + + ws = new WebSocket(wsUrl); + + ws.on('open', () => { + console.log('✅ WebSocket connection established'); + }); + + ws.on('message', (data) => { + const event = JSON.parse(data); + handleWebSocketEvent(event); + }); + + ws.on('close', () => { + console.log('❌ WebSocket connection closed. Reconnecting...'); + setTimeout(connectWebSocket, 5000); + }); + + ws.on('error', (error) => { + console.error('WebSocket error:', error); + }); +} + +// ============================================ +// STEP 3: Handle WebSocket Events +// ============================================ + +function handleWebSocketEvent(event) { + const { event: eventType, payload } = event; + const meetingId = String(payload?.object?.id); + + // CRITICAL: Filter events - only process tracked meetings + if (!meetingId || !trackedMeetings.has(meetingId)) { + return; + } + + console.log(`📩 WebSocket Event: ${eventType} for meeting ${meetingId}`); + + switch (eventType) { + case 'meeting.started': + console.log(`✅ Meeting started: ${payload.object.topic}`); + break; + + case 'meeting.ended': + console.log(`🏁 Meeting ended: ${payload.object.topic}`); + trackedMeetings.delete(meetingId); + break; + + case 'meeting.participant_joined': + console.log(`👋 ${payload.object.participant.user_name} joined`); + break; + + case 'meeting.participant_left': + console.log(`👋 ${payload.object.participant.user_name} left`); + break; + } +} + +// ============================================ +// USAGE: Track Meeting and Receive Events +// ============================================ + +async function trackMeeting(meetingId) { + // Get meeting details + const meeting = await getMeetingDetails(meetingId); + + // Add to tracked set + trackedMeetings.add(String(meeting.id)); + + console.log(`✅ Now tracking meeting: ${meeting.topic} (${meeting.id})`); + console.log(` Events will arrive via WebSocket connection`); + + return meeting; +} + +// Initialize WebSocket connection +connectWebSocket(); + +// Track specific meetings +trackMeeting('123456789').catch(console.error); +trackMeeting('987654321').catch(console.error); +``` + +**Comparison with Webhooks:** + +| Aspect | WebSockets | Webhooks | +|--------|------------|----------| +| **Latency** | Lower (persistent connection) | Higher (new HTTP request per event) | +| **Setup** | More complex (S2S OAuth required) | Simpler (just endpoint URL) | +| **Security** | No exposed endpoint | Requires public endpoint + signature verification | +| **Best for** | Real-time dashboards, high-frequency events | Standard backend processing | + +--- + +## Pattern 3: REST API + Meeting SDK Events (Client-Side In-Meeting) + +**When to use:** +- Building custom meeting UI with real-time updates +- In-meeting participant tracking +- User joins meeting from your web app +- Real-time UI state updates (active speaker, video on/off, etc.) + +**Skills needed:** `zoom-rest-api` → `zoom-meeting-sdk` + +### Flow Diagram + +``` +┌────────────────────────────────────────────────────────────────┐ +│ RUNTIME PHASE (User-initiated): │ +│ │ +│ Step 1: GET /meetings/{meetingId} (zoom-rest-api) │ +│ └── Get meeting number, password │ +│ └── Generate SDK signature (JWT) │ +│ │ +│ Step 2: ZoomMtg.init() + join() (zoom-meeting-sdk) │ +│ └── User joins meeting in browser │ +│ └── Receive ZoomMtg instance for event listeners │ +│ │ +│ Step 3: ZoomMtg.inMeetingServiceListener() (zoom-meeting-sdk) │ +│ └── Listen to: onUserJoin, onUserLeave, etc. │ +│ └── Events ONLY for this specific meeting │ +│ └── Real-time UI updates │ +└────────────────────────────────────────────────────────────────┘ +``` + +### Complete Implementation + +**Backend: Generate Signature** + +```javascript +const KJUR = require('jsrsasign'); + +// Step 1: Get meeting details +async function getMeetingDetails(meetingId) { + const token = await getAccessToken(); + + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return { + id: response.data.id, + password: response.data.password, + topic: response.data.topic, + join_url: response.data.join_url + }; +} + +// Generate Meeting SDK signature +function generateSignature(meetingNumber, role) { + const iat = Math.floor(Date.now() / 1000) - 30; + const exp = iat + 60 * 60 * 2; // 2 hours + + const header = { alg: 'HS256', typ: 'JWT' }; + const payload = { + sdkKey: process.env.ZOOM_SDK_KEY, + mn: String(meetingNumber).replace(/\D/g, ''), + role: parseInt(role, 10), // 0 = participant, 1 = host + iat, + exp, + tokenExp: exp + }; + + return KJUR.jws.JWS.sign( + 'HS256', + JSON.stringify(header), + JSON.stringify(payload), + process.env.ZOOM_SDK_SECRET + ); +} + +// API endpoint: Get meeting details + signature +app.get('/api/meetings/:meetingId/join', async (req, res) => { + try { + const { meetingId } = req.params; + const { role = 0 } = req.query; + + // Get meeting details from Zoom API + const meeting = await getMeetingDetails(meetingId); + + // Generate signature for SDK + const signature = generateSignature(meeting.id, role); + + res.json({ + meetingNumber: meeting.id, + password: meeting.password, + signature: signature, + sdkKey: process.env.ZOOM_SDK_KEY, + topic: meeting.topic + }); + } catch (error) { + res.status(error.response?.status || 500).json({ + error: error.message + }); + } +}); +``` + +**Frontend: Join Meeting and Listen to Events** + +```javascript +import { ZoomMtg } from '@zoom/meetingsdk'; + +// Step 1: Get meeting details + signature from backend +async function getMeetingCredentials(meetingId) { + const response = await fetch(`/api/meetings/${meetingId}/join`); + return response.json(); +} + +// Step 2: Initialize Meeting SDK +async function joinMeeting(meetingId, userName) { + try { + // Get meeting details from backend (includes REST API call) + const credentials = await getMeetingCredentials(meetingId); + + // Preload SDK + ZoomMtg.preLoadWasm(); + ZoomMtg.prepareWebSDK(); + + // Initialize SDK + ZoomMtg.init({ + leaveUrl: window.location.origin, + patchJsMedia: true, + leaveOnPageUnload: true, + success: () => { + // Join meeting + ZoomMtg.join({ + signature: credentials.signature, + sdkKey: credentials.sdkKey, + meetingNumber: credentials.meetingNumber, + passWord: credentials.password, + userName: userName, + success: () => { + console.log('✅ Joined meeting:', credentials.topic); + + // Step 3: Subscribe to in-meeting events + subscribeToMeetingEvents(); + }, + error: (err) => { + console.error('Join error:', err); + } + }); + }, + error: (err) => { + console.error('Init error:', err); + } + }); + } catch (error) { + console.error('Failed to join meeting:', error); + } +} + +// Step 3: Listen to Meeting SDK events +function subscribeToMeetingEvents() { + // User join events + ZoomMtg.inMeetingServiceListener('onUserJoin', (data) => { + console.log('👋 User joined:', data); + // data.userId, data.userName + // Update UI: add participant to list + }); + + // User leave events + ZoomMtg.inMeetingServiceListener('onUserLeave', (data) => { + console.log('👋 User left:', data); + // Update UI: remove participant from list + }); + + // Active speaker detection + ZoomMtg.inMeetingServiceListener('onActiveSpeaker', (data) => { + console.log('🎤 Active speaker:', data); + // data: [{ userId, userName }] + // Update UI: highlight speaker + }); + + // Meeting status changes + ZoomMtg.inMeetingServiceListener('onMeetingStatus', (data) => { + console.log('📊 Meeting status:', data); + // status: 1=connecting, 2=connected, 3=disconnected, 4=reconnecting + }); + + // Chat messages + ZoomMtg.inMeetingServiceListener('onReceiveChatMsg', (data) => { + console.log('💬 Chat message:', data); + // Display in custom UI + }); + + // Recording status + ZoomMtg.inMeetingServiceListener('onRecordingChange', (data) => { + console.log('🔴 Recording status:', data); + // Show recording indicator + }); + + // User updates (audio/video state changes) + ZoomMtg.inMeetingServiceListener('onUserUpdate', (data) => { + console.log('🔄 User updated:', data); + // Update UI: show muted/video off indicators + }); +} + +// Usage +joinMeeting('123456789', 'John Doe'); +``` + +**Key Differences from Webhooks/WebSockets:** + +| Aspect | Meeting SDK Events | Webhooks/WebSockets | +|--------|-------------------|---------------------| +| **Scope** | Meeting-specific (only joined meeting) | Account-level (all meetings) | +| **Location** | Client-side (browser) | Server-side | +| **Authentication** | SDK signature (JWT) | OAuth access token | +| **Use case** | In-meeting UI updates | Backend processing | +| **Event types** | Participant, audio/video, chat, recording | Meeting lifecycle, participants | + +--- + +## Pattern 4: REST API + RTMS (Bot-Based Real-Time Media + Events) + +**When to use:** +- AI transcription, translation, sentiment analysis +- Meeting recording bots +- Real-time media processing (audio/video streams) +- Compliance monitoring + +**Skills needed:** `zoom-rest-api` → `rtms` + +### Flow Diagram + +``` +┌────────────────────────────────────────────────────────────────┐ +│ SETUP PHASE: │ +│ │ +│ 1. Configure webhook subscription for meeting.rtms_started │ +│ 2. Deploy bot infrastructure │ +└────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────┐ +│ RUNTIME PHASE: │ +│ │ +│ Step 1: GET /meetings/{meetingId} (zoom-rest-api) │ +│ └── Get meeting details │ +│ │ +│ Step 2: Wait for meeting.rtms_started webhook │ +│ └── Contains rtmsSessionID, meetingUUID │ +│ │ +│ Step 3: Connect to RTMS WebSocket (rtms) │ +│ └── wss://rtms.zoom.us/ws │ +│ └── Subscribe to: audio, video, transcript, chat │ +│ │ +│ Step 4: Receive real-time media + events │ +│ └── Audio chunks, video frames, transcripts │ +│ └── Events: participant joined/left, speaking status │ +└────────────────────────────────────────────────────────────────┘ +``` + +### Complete Implementation + +**See comprehensive implementation:** [rtms/examples/rtms-bot.md](../../rtms/examples/rtms-bot.md) + +**Quick Overview:** + +```javascript +const axios = require('axios'); +const WebSocket = require('ws'); + +// Step 1: Get meeting details +async function getMeetingDetails(meetingId) { + const token = await getAccessToken(); + + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return response.data; +} + +// Step 2: Wait for meeting.rtms_started webhook +app.post('/webhooks/zoom', (req, res) => { + const { event, payload } = req.body; + + if (event === 'meeting.rtms_started') { + const { rtmsSessionID, meetingUUID } = payload.object; + + // Step 3: Connect to RTMS + connectToRTMS(rtmsSessionID, meetingUUID); + } + + res.status(200).send(); +}); + +// Step 3: Connect to RTMS WebSocket +async function connectToRTMS(sessionID, meetingUUID) { + const token = await getAccessToken(); + + const ws = new WebSocket('wss://rtms.zoom.us/ws', { + headers: { + 'Authorization': `Bearer ${token}`, + 'X-Zoom-Session-ID': sessionID + } + }); + + ws.on('open', () => { + // Subscribe to media types + ws.send(JSON.stringify({ + type: 'subscribe', + mediaTypes: ['audio', 'transcript', 'chat'] + })); + }); + + ws.on('message', (data) => { + const message = JSON.parse(data); + + // Step 4: Handle real-time media and events + switch (message.type) { + case 'audio': + processAudioChunk(message.data); + break; + + case 'transcript': + console.log(`📝 Transcript: ${message.data.text}`); + break; + + case 'chat': + console.log(`💬 Chat: ${message.data.message}`); + break; + + case 'participant_joined': + console.log(`👋 ${message.data.user_name} joined`); + break; + + case 'participant_left': + console.log(`👋 ${message.data.user_name} left`); + break; + } + }); +} + +function processAudioChunk(audioData) { + // Process raw audio for transcription, analysis, etc. +} +``` + +**RTMS Events (Different from Webhooks):** + +| Event Type | Source | Data | +|------------|--------|------| +| `participant_joined` | RTMS WebSocket | Real-time when user joins | +| `participant_left` | RTMS WebSocket | Real-time when user leaves | +| `active_speaker_change` | RTMS WebSocket | Current speaker info | +| `audio` | RTMS WebSocket | Raw audio stream (PCM) | +| `video` | RTMS WebSocket | Raw video frames | +| `transcript` | RTMS WebSocket | Live transcription | + +--- + +## Pattern 5: REST API + Reports (Historical Data, Not Real Events) + +**When to use:** +- Historical analysis, batch processing +- Audit logs, compliance reports +- No real-time requirements +- Backup/recovery systems + +**Skills needed:** `zoom-rest-api` only + +### Implementation + +```javascript +// Get meeting details +async function getMeetingDetails(meetingId) { + const token = await getAccessToken(); + + const response = await axios.get( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return response.data; +} + +// Poll for meeting participants report (after meeting ends) +async function getMeetingParticipantsReport(meetingId) { + const token = await getAccessToken(); + + // Get past meeting details + const response = await axios.get( + `https://api.zoom.us/v2/past_meetings/${meetingId}`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return response.data; +} + +// Get detailed participant report +async function getParticipantsList(meetingId) { + const token = await getAccessToken(); + + const response = await axios.get( + `https://api.zoom.us/v2/report/meetings/${meetingId}/participants`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + + return response.data.participants; +} + +// Usage: Retrieve meeting + get historical data +async function analyzeMeeting(meetingId) { + // Get meeting metadata + const meeting = await getMeetingDetails(meetingId); + console.log(`Meeting: ${meeting.topic}`); + + // Wait for meeting to end, then get reports + // (typically poll or wait for meeting.ended webhook) + + const participants = await getParticipantsList(meetingId); + console.log(`Total participants: ${participants.length}`); + + participants.forEach(p => { + console.log(`- ${p.name}: ${p.duration} minutes`); + }); +} +``` + +**Not Real-Time Events:** +- Reports API only provides historical data after meeting ends +- Not suitable for real-time monitoring +- No WebSocket/webhook integration +- Use for: auditing, analytics, compliance + +--- + +## Comparison: When to Use Each Pattern + +| Pattern | Latency | Scope | Setup Complexity | Best For | +|---------|---------|-------|------------------|----------| +| **Webhooks** | Medium | Account-level | Low | Standard backend processing | +| **WebSockets** | Low | Account-level | Medium | Real-time dashboards, no exposed endpoint | +| **Meeting SDK** | Very Low | Meeting-specific | Low | In-meeting UI, participant views | +| **RTMS** | Very Low | Meeting-specific | High | AI transcription, recording bots | +| **Reports API** | N/A (historical) | Account-level | Low | Batch processing, analytics | + +--- + +## Skill Chaining Summary + +| Scenario | Skills Chain | Order | +|----------|--------------|-------| +| Backend event processing | `zoom-rest-api` → `webhooks` | 1. Get meeting, 2. Filter webhook events | +| Low-latency monitoring | `zoom-rest-api` → `zoom-websockets` | 1. Get meeting, 2. Filter WebSocket events | +| Custom meeting UI | `zoom-rest-api` → `zoom-meeting-sdk` | 1. Get details, 2. Join meeting, 3. Listen to SDK events | +| AI transcription bot | `zoom-rest-api` → `rtms` | 1. Get meeting, 2. Wait for webhook, 3. Connect to RTMS | +| Historical analysis | `zoom-rest-api` only | 1. Get meeting, 2. Poll reports after meeting ends | + +--- + +## Authorization Requirements + +**All patterns require different OAuth scopes:** + +| Pattern | Required Scopes | +|---------|----------------| +| REST API | `meeting:read:admin` or `meeting:read` | +| Webhooks | Event subscription in app config | +| WebSockets | Server-to-Server OAuth + event subscription | +| Meeting SDK | SDK Key/Secret + signature generation | +| RTMS | `meeting:read:admin` + RTMS-enabled account | + +**See also**: [Authorization Patterns](../references/authorization-patterns.md) for complete scope validation strategies. + +--- + +## Common Pitfalls + +### ❌ Misconception: "Subscribe to Events for a Specific Meeting" + +**Reality**: Most Zoom event systems are **account-level**, not meeting-specific. + +| What You Think | What Actually Happens | +|----------------|----------------------| +| "Subscribe to events for meeting 123456789" | You receive events for ALL meetings in the account | +| "Dynamically enable webhooks when meeting starts" | Webhooks are configured at app creation time | +| "Create a webhook subscription per meeting" | One subscription receives events for all meetings | + +**Solution**: Filter events by meeting ID in your handler code. + +### ❌ Misconception: "All Events Are the Same" + +**Reality**: Different event systems provide different event types: + +| Event System | Events | +|--------------|--------| +| **Webhooks** | Meeting lifecycle: started, ended, participant_joined/left, recording_completed | +| **WebSockets** | Same as webhooks, but lower latency | +| **Meeting SDK** | In-meeting: onUserJoin, onActiveSpeaker, onUserUpdate, onReceiveChatMsg | +| **RTMS** | Real-time media: participant events + audio/video streams + transcripts | + +**Solution**: Choose the right event system for your use case (see comparison table above). + +--- + +## Related Use Cases + +- **[Meeting Details with Events](meeting-details-with-events.md)** - Expanded webhook implementation +- **[Meeting Bots](meeting-bots.md)** - Building RTMS bots for transcription +- **[Recording & Transcription](recording-transcription.md)** - Download recordings after meeting ends + +--- + +## Resources + +- **REST API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Meetings +- **Webhooks**: https://developers.zoom.us/docs/api/rest/webhook-reference/ +- **WebSockets**: https://developers.zoom.us/docs/api/websockets/ +- **Meeting SDK**: https://developers.zoom.us/docs/meeting-sdk/web/ +- **RTMS**: https://developers.zoom.us/docs/rtms/ diff --git a/plugins/zoom-developers/skills/general/use-cases/rivet-event-driven-api-orchestrator.md b/plugins/zoom-developers/skills/general/use-cases/rivet-event-driven-api-orchestrator.md new file mode 100644 index 00000000..2f3d597a --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/rivet-event-driven-api-orchestrator.md @@ -0,0 +1,43 @@ +# Rivet Event-Driven API Orchestrator + +Use Rivet to build a Node.js backend that combines webhook event handling with Zoom REST API actions using typed module clients. + +## When to Use + +- You need both event-driven workflows and API calls in one service. +- You want to reduce custom OAuth/webhook boilerplate. +- You are composing multiple Zoom surfaces (Team Chat, Meetings, Users, Phone, Video SDK API). + +## Skill Chain + +- [rivet-sdk](../../rivet-sdk/SKILL.md) +- [oauth](../../oauth/SKILL.md) +- [rest-api](../../rest-api/SKILL.md) +- [team-chat](../../team-chat/SKILL.md) + +## Architecture + +```text +Zoom Events -> Rivet webEventConsumer -> business logic -> Rivet endpoints.* -> Zoom APIs +``` + +## High-Level Flow + +1. Instantiate one or more Rivet module clients. +2. Register webhook event handlers. +3. Start receiver(s) and expose `/zoom/events` endpoint(s). +4. Call typed endpoint wrappers from event handlers. +5. Persist state/tokens and return user-visible results. + +## Key Risks + +- Incorrect per-module endpoint port mapping. +- OAuth redirect/state mismatch. +- Scope mismatch for endpoint calls. +- Event payload drift across versions. + +## See Also + +- [rivet-sdk examples](../../rivet-sdk/examples/getting-started-pattern.md) +- [rivet-sdk multi-client pattern](../../rivet-sdk/examples/multi-client-pattern.md) +- [rivet-sdk runbook](../../rivet-sdk/RUNBOOK.md) diff --git a/plugins/zoom-developers/skills/general/use-cases/saas-app-oauth-integration.md b/plugins/zoom-developers/skills/general/use-cases/saas-app-oauth-integration.md new file mode 100644 index 00000000..e88e87ca --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/saas-app-oauth-integration.md @@ -0,0 +1,196 @@ +# Building a SaaS App with Zoom OAuth Integration + +Create a multi-tenant SaaS application that integrates with Zoom using User OAuth for per-user authorization. + +## Scenario + +You're building a meeting scheduling SaaS that needs to: +- Allow users to authorize your app to access their Zoom account +- Create meetings on behalf of users +- List user's meetings +- Store and refresh tokens securely per user +- Handle token expiration and refresh automatically + +## Required Skills + +1. **oauth** - User authorization flow, token management +2. **zoom-rest-api** - Meeting creation and management endpoints + +## Architecture + +``` +User Browser → Your SaaS App → Zoom OAuth → Zoom APIs + ↓ + Database (user tokens) +``` + +## Implementation Steps + +### 1. OAuth Setup (oauth) + +**Configure app in Zoom Marketplace:** +- App Type: OAuth +- Redirect URL: `https://yourapp.com/oauth/callback` +- Required scopes: `meeting:write`, `meeting:read`, `user:read` + +**See:** `oauth/concepts/oauth-flows.md#user-authorization-oauth` + +### 2. User Authorization Flow (oauth) + +```javascript +// Redirect user to Zoom authorization +app.get('/connect-zoom', (req, res) => { + const state = generateSecureState(); + req.session.oauthState = state; + + res.redirect( + `https://zoom.us/oauth/authorize?` + + `response_type=code&` + + `client_id=${CLIENT_ID}&` + + `redirect_uri=${REDIRECT_URI}&` + + `state=${state}` + ); +}); +``` + +**See:** `oauth/examples/user-oauth-mysql.md` + +### 3. Token Storage (oauth) + +Store user tokens with encryption: + +```javascript +// After OAuth callback +const { access_token, refresh_token } = await exchangeCode(code); + +await db.users.update({ + zoom_access_token: encrypt(access_token), + zoom_refresh_token: encrypt(refresh_token), + token_expiry: Date.now() + 3600000 +}, { where: { id: userId } }); +``` + +**See:** `oauth/concepts/token-lifecycle.md#user-oauth--device-flow-token-lifecycle` + +### 4. Auto-Refresh Middleware (oauth) + +```javascript +// Automatically refresh expired tokens +const zoomTokenMiddleware = async (req, res, next) => { + const user = await db.users.findByPk(req.session.userId); + + if (isTokenExpired(user.token_expiry)) { + const newTokens = await refreshToken(user.zoom_refresh_token); + await updateUserTokens(user.id, newTokens); + req.zoomToken = newTokens.access_token; + } else { + req.zoomToken = decrypt(user.zoom_access_token); + } + + next(); +}; +``` + +**See:** `oauth/examples/token-refresh.md` + +### 5. Create Meetings (zoom-rest-api) + +```javascript +app.post('/api/meetings', zoomTokenMiddleware, async (req, res) => { + const meeting = await axios.post( + 'https://api.zoom.us/v2/users/me/meetings', + { + topic: req.body.topic, + type: 2, // Scheduled meeting + start_time: req.body.start_time, + duration: req.body.duration + }, + { + headers: { Authorization: `Bearer ${req.zoomToken}` } + } + ); + + res.json(meeting.data); +}); +``` + +**See:** `zoom-rest-api` skill for endpoint documentation + +## Security Considerations + +### Token Encryption (oauth) + +**MUST encrypt tokens at rest:** +```javascript +const crypto = require('crypto'); + +function encrypt(text) { + const cipher = crypto.createCipher('aes-256-cbc', process.env.CIPHER_KEY); + return cipher.update(text, 'utf8', 'hex') + cipher.final('hex'); +} +``` + +**See:** `oauth/examples/user-oauth-mysql.md#token-encryption` + +### State Parameter (oauth) + +**Prevent CSRF attacks:** +```javascript +const state = crypto.randomBytes(16).toString('hex'); +req.session.oauthState = state; // Verify in callback +``` + +**See:** `oauth/concepts/state-parameter.md` + +## Handling Edge Cases + +### User Revokes Access (webhooks) + +Listen for deauthorization webhook: + +```javascript +app.post('/webhooks/zoom', (req, res) => { + if (req.body.event === 'app_deauthorized') { + const userId = req.body.payload.user_id; + await deleteUserZoomTokens(userId); + } +}); +``` + +**Chain to:** `webhooks` skill for webhook setup + +### Refresh Token Expired (oauth) + +```javascript +try { + await refreshToken(user.zoom_refresh_token); +} catch (error) { + if (error.code === 4735) { + // Refresh token expired - prompt re-authorization + res.redirect('/connect-zoom'); + } +} +``` + +**See:** `oauth/troubleshooting/token-issues.md` + +## Testing Checklist + +- [ ] User authorization flow works +- [ ] Tokens stored encrypted in database +- [ ] Tokens auto-refresh before expiration +- [ ] Meetings created successfully via API +- [ ] Deauthorization webhook handled +- [ ] Refresh token expiration handled + +## Related Use Cases + +- `meeting-automation.md` - Advanced meeting scheduling +- `user-and-meeting-creation.md` - Bulk user/meeting operations +- `recording-download-pipeline.md` - Download recordings via API + +## Skills Used + +- **oauth** (primary) - User OAuth, token management, PKCE +- **zoom-rest-api** - Meeting and user API endpoints +- **webhooks** - Deauthorization notifications diff --git a/plugins/zoom-developers/skills/general/use-cases/scribe-transcription-pipeline.md b/plugins/zoom-developers/skills/general/use-cases/scribe-transcription-pipeline.md new file mode 100644 index 00000000..d9007a07 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/scribe-transcription-pipeline.md @@ -0,0 +1,58 @@ +# Scribe Transcription Pipeline + +Use AI Services Scribe when the input is already a file or storage object and the output should be a transcript JSON payload or transcript files. + +## Skill Chain + +- Primary: [../../scribe/SKILL.md](../../scribe/SKILL.md) +- Optional storage/download source: [../../rest-api/SKILL.md](../../rest-api/SKILL.md) +- Optional webhook hardening: [../../webhooks/SKILL.md](../../webhooks/SKILL.md) + +## When to Use Scribe + +Use `scribe` for: +- one uploaded file that should be transcribed immediately +- S3 archive transcription in the background +- post-processing exported media files into searchable transcript data + +Do not use `scribe` for: +- live in-meeting media stream ingestion +- bot-style participant join and raw recording + +For those, use: +- [../../rtms/SKILL.md](../../rtms/SKILL.md) for live media streams +- [../../meeting-sdk/linux/SKILL.md](../../meeting-sdk/linux/SKILL.md) for visible meeting bots + +## Minimal Flow + +```text +input file or storage prefix + -> generate Build JWT + -> choose fast mode or batch mode + -> submit Scribe request + -> receive transcript JSON or batch job state + -> persist transcript output +``` + +## Typical Variants + +1. Fast mode + - one short file + - immediate response needed + - `POST /aiservices/scribe/transcribe` + +2. Batch mode + - long recordings or many files + - `POST /aiservices/scribe/jobs` + - monitor with polling or webhook notifications + +3. Zoom recording re-transcription + - use REST API to download or export recording files + - feed those files into Scribe for your own transcript settings + +## Common Failure Points + +- wrong credential type (Build JWT vs normal OAuth token) +- choosing RTMS for offline archive transcription +- expired S3 credentials for batch jobs +- webhook signature verification implemented after JSON parsing instead of on raw body diff --git a/plugins/zoom-developers/skills/general/use-cases/sdk-size-optimization.md b/plugins/zoom-developers/skills/general/use-cases/sdk-size-optimization.md new file mode 100644 index 00000000..c7bcf700 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/sdk-size-optimization.md @@ -0,0 +1,195 @@ +# SDK Size Optimization + +Reduce mobile app binary size when integrating Zoom Meeting SDK or Video SDK on iOS and Android. + +## Overview + +Zoom SDKs add significant size to mobile apps. This guide covers **Zoom-recommended techniques** from the official developer forum and blog to minimize the impact on your app's download size. + +## Skills Needed + +- **zoom-meeting-sdk** (iOS, Android) +- **zoom-video-sdk** (iOS, Android) + +## SDK Size Reference + +### Meeting SDK + +| Platform | Configuration | Size | +|----------|---------------|------| +| iOS | Universal (all architectures) | ~107 MB | +| Android | arm64-v8a + armeabi-v7a | ~97-108 MB | +| Android | arm64-v8a only | ~71 MB | +| Android | armeabi-v7a only | ~47 MB | + +### Video SDK + +| Platform | Size | +|----------|------| +| iOS/Android | ~75 MB | + +--- + +## Android: Zoom-Recommended Methods + +### 1. ABI Filtering (Official Recommendation) + +This is the **primary method recommended by Zoom** to reduce APK size. Filter to only the CPU architectures you need. + +```gradle +// build.gradle (app module) +android { + defaultConfig { + ndk { + // Option 1: Modern devices only (smallest size) + abiFilters 'arm64-v8a' + + // Option 2: Broader device support + // abiFilters 'arm64-v8a', 'armeabi-v7a' + } + } +} +``` + +**Size Impact (from Zoom Dev Forum):** + +| Configuration | APK Size | +|---------------|----------| +| No Zoom SDK | ~11 MB | +| arm64-v8a + armeabi-v7a | ~97 MB | +| **arm64-v8a only** | **~71 MB** | +| armeabi-v7a only | ~47 MB | + +**Note:** Most modern Android devices (2017+) use `arm64-v8a`. Only include `armeabi-v7a` if you need to support older 32-bit devices. + +### 2. Android App Bundle (AAB) + +Use Android App Bundles for Play Store distribution. Google Play automatically generates device-specific APKs: + +```gradle +android { + bundle { + abi { + enableSplit = true + } + } +} +``` + +Users only download the architecture matching their device, reducing download size. + +--- + +## iOS: Zoom-Recommended Methods + +### App Store App Thinning (Automatic) + +iOS App Store automatically applies App Thinning: + +- Delivers only the architecture slice needed for each device +- No manual configuration required +- Happens automatically when distributing through App Store + +**Verify in Xcode:** +Window → Organizer → Archives → App Thinning Report + +--- + +## What Does NOT Work + +Zoom has confirmed the following approaches are **NOT supported**: + +### No Feature/Module Exclusion + +From Zoom Developer Forum (August 2024): +> "There are **no new updates regarding reducing size of SDK**" + +- Cannot remove virtual background module +- Cannot remove screen sharing module +- Cannot exclude any bundled features +- All features are compiled together + +### No Dynamic Feature Module Support + +Zoom has confirmed they do **NOT support** Android Dynamic Feature Modules: + +- Resource linking errors occur +- `Resources$NotFoundException` at runtime +- Community workarounds are unsupported and may break + +### ProGuard/R8 Not Fully Supported + +**Warning:** ProGuard/R8 causes crashes with Zoom SDK, even with Zoom's provided rules. Users report runtime crashes. Use at your own risk. + +### Bitcode Not Supported (iOS) + +Zoom SDK does **NOT** support iOS Bitcode due to internal dependencies. However, Bitcode is no longer required by Apple (Xcode 15+). + +--- + +## Alternative Approaches + +If SDK size is prohibitive for your use case: + +### 1. Web SDK in WebView + +Load the Web SDK in a native WebView instead of using the native SDK: + +```swift +// iOS +let webView = WKWebView(frame: view.bounds) +webView.load(URLRequest(url: URL(string: "https://your-app.com/zoom-meeting")!)) +``` + +```kotlin +// Android +webView.loadUrl("https://your-app.com/zoom-meeting") +``` + +**Trade-offs:** +- No native SDK size impact +- Reduced native feature access +- Requires WebView setup and web hosting + +### 2. Deep Link to Zoom App + +Open meetings in the native Zoom app instead of embedding: + +```swift +// iOS +if let url = URL(string: "zoomus://zoom.us/join?confno=\(meetingNumber)") { + UIApplication.shared.open(url) +} +``` + +```kotlin +// Android +val intent = Intent(Intent.ACTION_VIEW, + Uri.parse("zoomus://zoom.us/join?confno=$meetingNumber")) +startActivity(intent) +``` + +**Trade-offs:** +- Zero SDK size impact +- Requires Zoom app to be installed +- Less integrated user experience + +--- + +## Summary + +| Platform | Recommended Method | Expected Savings | +|----------|-------------------|------------------| +| **Android** | `abiFilters 'arm64-v8a'` | ~26 MB | +| **Android** | App Bundle (AAB) | Automatic per-device | +| **iOS** | App Thinning (automatic) | Automatic per-device | + +**Key Limitation:** Zoom does not currently offer modular SDK downloads or feature exclusion. The SDK includes all features bundled together. + +--- + +## Resources + +- **Zoom Blog - Reduce APK Size**: https://developers.zoom.us/blog/reduce-apk-size-zoom-meeting-sdk-android/ +- **Zoom Developer Forum - SDK Size Discussion**: https://devforum.zoom.us/t/reducing-the-size-of-the-zoom-meeting-sdk-ios-android/94302 +- **Android ABI Management**: https://developer.android.com/ndk/guides/abis diff --git a/plugins/zoom-developers/skills/general/use-cases/sdk-wrappers-gui.md b/plugins/zoom-developers/skills/general/use-cases/sdk-wrappers-gui.md new file mode 100644 index 00000000..b4fedb6c --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/sdk-wrappers-gui.md @@ -0,0 +1,560 @@ +# SDK Wrappers and GUI Integration + +Building custom language wrappers and GUI applications with Zoom Meeting/Video SDKs. + +## Overview + +The native Zoom SDKs are written in C++. To use them from other languages or with GUI frameworks, you need wrappers or direct integration. + +| Platform | Wrapper/Integration | Use Case | +|----------|---------------------|----------| +| Windows | C++/CLI → C# | WPF, WinForms, .NET apps | +| Linux | Native C++ | Qt, GTK desktop apps | +| Linux | Direct C++ | Headless bots, server-side | + +## Windows: C++/CLI Wrapper for C#/.NET + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ C# Application Layer (WPF / WinForms / .NET) │ +│ - UI controls, business logic │ +│ - Uses managed ZOOM_SDK_DOTNET_WRAP namespace │ +└─────────────────────────────────────────────────────────────┘ + ↕ Managed/Unmanaged Boundary +┌─────────────────────────────────────────────────────────────┐ +│ C++/CLI Wrapper Layer (zoom_sdk_dotnet_wrap.dll) │ +│ - Managed ref classes with ^ handles │ +│ - Native C++ classes calling SDK │ +│ - Event bridging: std::bind → C# delegates │ +└─────────────────────────────────────────────────────────────┘ + ↕ Native C++ Interface +┌─────────────────────────────────────────────────────────────┐ +│ Native Zoom SDK (videosdk.dll / sdk.dll) │ +│ - Loaded dynamically via sdk_dll_path │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Why C++/CLI (Not P/Invoke)? + +| Aspect | P/Invoke | C++/CLI | +|--------|----------|---------| +| Complex C++ objects | ❌ Difficult | ✅ Native support | +| Callbacks/delegates | ❌ Manual marshaling | ✅ Automatic bridging | +| Object lifetime | ❌ Manual GC pinning | ✅ Automatic handling | +| std::function patterns | ❌ Not supported | ✅ Works with std::bind | + +### Project Structure + +``` +videosdk-windows-dotnet-quickstart/ +├── ZoomVideoSDK.CSharp.sln # Visual Studio solution +├── config.json # Runtime configuration +├── sdk/ # Zoom SDK files +│ └── x64/ +│ ├── h/ # C++ headers +│ ├── lib/ # Static libraries (.lib) +│ └── bin/ # Runtime DLLs (.dll) +├── ZoomVideoSDK.Wrapper/ # C++/CLI wrapper project +│ ├── ZoomVideoSDK.Wrapper.vcxproj # CLRSupport=true +│ ├── ZoomSDKManager.h # Managed wrapper class +│ └── ZoomSDKManager.cpp +├── ZoomVideoSDK.WinForms/ # C# WinForms app +│ ├── MainForm.cs +│ └── ZoomSDKInterop.cs +└── ZoomVideoSDK.WPF/ # C# WPF app + ├── MainWindow.xaml + └── MainWindow.xaml.cs +``` + +### C++/CLI Wrapper Implementation + +**Project Configuration (.vcxproj)**: +```xml + + DynamicLibrary + true + Unicode + +``` + +**Wrapper Class (C++/CLI)**: +```cpp +// ZoomSDKManager.h +#pragma once + +using namespace System; + +namespace ZoomVideoSDK { + namespace Wrapper { + + // Managed delegate (callable from C#) + public delegate void SessionStatusChangedHandler(String^ status, String^ message); + + // Managed wrapper class + public ref class ZoomSDKManager sealed { + public: + static property ZoomSDKManager^ Instance { + ZoomSDKManager^ get() { return m_Instance; } + } + + bool Initialize(String^ sdkDllPath); + bool JoinSession(String^ sessionName, String^ token, String^ userName); + void LeaveSession(); + + // Events exposed to C# + event SessionStatusChangedHandler^ SessionStatusChanged; + + private: + static ZoomSDKManager^ m_Instance = gcnew ZoomSDKManager; + + // Bridge to invoke C# events from native callbacks + void OnSessionStatusChanged(String^ status, String^ message) { + SessionStatusChanged(status, message); + } + }; + } +} +``` + +**Callback Bridging Pattern**: +```cpp +// Native C++ handler class (unmanaged) +class NativeEventHandler { +public: + static NativeEventHandler& GetInst() { + static NativeEventHandler inst; + return inst; + } + + void onSessionJoin() { + // Forward to managed wrapper using QMetaObject pattern + ZoomSDKManager::Instance->OnSessionStatusChanged("Joined", "Session joined successfully"); + } +}; + +// Binding native callbacks to handler +void ZoomSDKManager::BindEvents() { + // Use std::bind to connect native callbacks + ZOOM_SDK::GetSessionService().m_cbonSessionJoin = + std::bind(&NativeEventHandler::onSessionJoin, &NativeEventHandler::GetInst()); +} +``` + +### C# Application Usage + +**WPF Example**: +```csharp +using ZoomVideoSDK.Wrapper; + +public partial class MainWindow : Window +{ + private ZoomSDKManager _sdk; + + public MainWindow() + { + InitializeComponent(); + + _sdk = ZoomSDKManager.Instance; + _sdk.SessionStatusChanged += OnSessionStatusChanged; + } + + private void JoinButton_Click(object sender, RoutedEventArgs e) + { + bool initialized = _sdk.Initialize(@".\sdk\x64\bin"); + if (initialized) + { + _sdk.JoinSession( + sessionName: SessionNameTextBox.Text, + token: JwtTokenTextBox.Text, + userName: UserNameTextBox.Text + ); + } + } + + private void OnSessionStatusChanged(string status, string message) + { + // Must invoke on UI thread + Dispatcher.Invoke(() => { + StatusLabel.Content = $"{status}: {message}"; + }); + } +} +``` + +**WinForms Example**: +```csharp +public partial class MainForm : Form +{ + private ZoomSDKManager _sdk; + + private void OnSessionStatusChanged(string status, string message) + { + // Must invoke on UI thread + this.Invoke((MethodInvoker)delegate { + statusLabel.Text = $"{status}: {message}"; + }); + } +} +``` + +### Build Configuration + +**DLL Dependencies**: +``` +C# App (managed) + ↓ references +ZoomVideoSDK.Wrapper.dll (C++/CLI mixed-mode) + ↓ loads dynamically at runtime +videosdk.dll + dependencies (native) +``` + +**Post-Build Events**: +```batch +REM Copy SDK DLLs to output directory +xcopy /Y "$(SolutionDir)sdk\x64\bin\*.dll" "$(OutDir)" +``` + +--- + +## Linux: Qt GUI Integration + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Qt Application (C++) │ +│ - Qt Widgets / QML │ +│ - Signals/Slots for event handling │ +└─────────────────────────────────────────────────────────────┘ + ↕ Direct C++ calls +┌─────────────────────────────────────────────────────────────┐ +│ Native Zoom SDK (libvideosdk.so) │ +│ - Video/Audio processing │ +│ - Network communication │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Project Structure + +``` +videosdk-linux-qt-quickstart/ +├── README.md +├── run_qt_demo.sh # Run script +├── src/ +│ ├── CMakeLists.txt # Qt6/Qt5 build config +│ ├── config.json # Session config +│ ├── bin/ # Build output +│ │ └── VideoSDKQtDemo +│ ├── include/ # SDK headers +│ ├── lib/ # SDK libraries +│ └── Source Files: +│ ├── zoom_v-sdk_linux_bot_qt.cpp # Main entry +│ ├── QtMainWindow.h/cpp # Main window +│ ├── QtVideoWidget.h/cpp # Video display +│ ├── QtVideoRenderer.h/cpp # YUV→RGB rendering +│ ├── QtPreviewVideoHandler.h/cpp # Self video +│ └── QtRemoteVideoHandler.h/cpp # Remote video +``` + +### Qt Video Rendering + +```cpp +// QtVideoWidget.h +class QtVideoWidget : public QWidget { + Q_OBJECT + +public: + explicit QtVideoWidget(QWidget* parent = nullptr); + void updateFrame(const QImage& frame); + +protected: + void paintEvent(QPaintEvent* event) override; + +private: + QImage m_currentFrame; + QMutex m_mutex; +}; + +// QtVideoWidget.cpp +void QtVideoWidget::updateFrame(const QImage& frame) { + QMutexLocker lock(&m_mutex); + m_currentFrame = frame; + update(); // Trigger repaint +} + +void QtVideoWidget::paintEvent(QPaintEvent* event) { + QPainter painter(this); + QMutexLocker lock(&m_mutex); + + if (!m_currentFrame.isNull()) { + // Scale to fit with aspect ratio + QImage scaled = m_currentFrame.scaled( + size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + + // Center the image + int x = (width() - scaled.width()) / 2; + int y = (height() - scaled.height()) / 2; + painter.drawImage(x, y, scaled); + } +} +``` + +### YUV to RGB Conversion + +```cpp +// QtVideoRenderer.cpp +QImage convertYUV420ToRGB(const unsigned char* yuvData, int width, int height) { + QImage image(width, height, QImage::Format_RGB888); + + const unsigned char* yPlane = yuvData; + const unsigned char* uPlane = yuvData + width * height; + const unsigned char* vPlane = uPlane + (width * height / 4); + + 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 = yPlane[yIndex]; + int U = uPlane[uvIndex] - 128; + int V = vPlane[uvIndex] - 128; + + // ITU-R BT.601 conversion + int R = qBound(0, (int)(Y + 1.402 * V), 255); + int G = qBound(0, (int)(Y - 0.344 * U - 0.714 * V), 255); + int B = qBound(0, (int)(Y + 1.772 * U), 255); + + image.setPixel(x, y, qRgb(R, G, B)); + } + } + + return image; +} +``` + +### Qt Event Threading + +```cpp +// Invoke SDK callbacks on Qt main thread +class VideoCallback : public IZoomVideoSDKRawDataPipeDelegate { +public: + void onRawDataFrameReceived(YUVRawDataI420* data) override { + QImage frame = convertYUV420ToRGB(data->getBuffer(), + data->getWidth(), + data->getHeight()); + + // Thread-safe UI update using Qt's event system + QMetaObject::invokeMethod(m_videoWidget, [this, frame]() { + m_videoWidget->updateFrame(frame); + }, Qt::QueuedConnection); + } + +private: + QtVideoWidget* m_videoWidget; +}; +``` + +### Build with CMake + +```cmake +cmake_minimum_required(VERSION 3.14) +project(VideoSDKQtDemo) + +set(CMAKE_CXX_STANDARD 17) + +# Find Qt +find_package(Qt6 COMPONENTS Core Widgets REQUIRED) +# Or: find_package(Qt5 COMPONENTS Core Widgets REQUIRED) + +# Find ALSA for audio +find_package(ALSA REQUIRED) + +# Source files +set(SOURCES + zoom_v-sdk_linux_bot_qt.cpp + QtMainWindow.cpp + QtVideoWidget.cpp + QtVideoRenderer.cpp +) + +add_executable(VideoSDKQtDemo ${SOURCES}) + +target_link_libraries(VideoSDKQtDemo + Qt6::Core Qt6::Widgets + ${ALSA_LIBRARIES} + ${CMAKE_SOURCE_DIR}/lib/libvideosdk.so +) +``` + +--- + +## Linux: GTK GUI Integration + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ GTKmm Application (C++) │ +│ - GTK Widgets (Gtk::Window, Gtk::Box, etc.) │ +│ - Glib signals for event handling │ +│ - SDL2 for video rendering │ +└─────────────────────────────────────────────────────────────┘ + ↕ Direct C++ calls +┌─────────────────────────────────────────────────────────────┐ +│ Native Zoom SDK (libvideosdk.so) │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Project Structure + +``` +videosdk-linux-gtk-quickstart/ +├── README.md +├── src/ +│ ├── CMakeLists.txt +│ ├── config.json +│ ├── bin/ +│ │ └── SkeletonDemo +│ └── Source Files: +│ ├── zoom_v-sdk_linux_bot.cpp # Main entry + GTK UI +│ ├── VideoRenderer.h/cpp # SDL2 video rendering +│ ├── VideoDisplayBridge.h/cpp # SDK→Renderer bridge +│ ├── PreviewVideoHandler.h/cpp # Self video +│ └── RemoteVideoRawDataHandler.h/cpp # Remote video +``` + +### GTK Video Rendering (SDL2) + +```cpp +// VideoRenderer.h +class VideoRenderer { +public: + VideoRenderer(Gtk::Widget* container); + ~VideoRenderer(); + + void renderFrame(const unsigned char* yuvData, int width, int height); + +private: + SDL_Window* m_window; + SDL_Renderer* m_renderer; + SDL_Texture* m_texture; +}; + +// VideoRenderer.cpp +void VideoRenderer::renderFrame(const unsigned char* yuvData, int width, int height) { + // Update YUV texture directly (SDL handles conversion) + SDL_UpdateYUVTexture(m_texture, nullptr, + yuvData, // Y plane + width, // Y pitch + yuvData + width * height, // U plane + width / 2, // U pitch + yuvData + width * height * 5/4, // V plane + width / 2); // V pitch + + SDL_RenderClear(m_renderer); + SDL_RenderCopy(m_renderer, m_texture, nullptr, nullptr); + SDL_RenderPresent(m_renderer); +} +``` + +### GTK Main Window + +```cpp +// zoom_v-sdk_linux_bot.cpp +class MainWindow : public Gtk::Window { +public: + MainWindow() { + set_title("Zoom Video SDK Demo"); + set_default_size(1200, 800); + + // Create layout + auto mainBox = Gtk::make_managed(Gtk::ORIENTATION_VERTICAL); + + // Device selection + m_cameraCombo = Gtk::make_managed(); + m_micCombo = Gtk::make_managed(); + m_speakerCombo = Gtk::make_managed(); + + // Control buttons + m_joinButton = Gtk::make_managed("Join Session"); + m_leaveButton = Gtk::make_managed("Leave Session"); + + // Video areas (SDL embedded) + m_selfVideoArea = Gtk::make_managed(); + m_remoteVideoArea = Gtk::make_managed(); + + // Connect signals + m_joinButton->signal_clicked().connect( + sigc::mem_fun(*this, &MainWindow::on_join_clicked)); + + add(*mainBox); + show_all_children(); + } + +private: + void on_join_clicked() { + // Initialize SDK and join session + ZoomVideoSDKInitParams params; + params.domain = "https://zoom.us"; + m_sdk->initialize(params); + + ZoomVideoSDKSessionContext context; + context.sessionName = m_sessionEntry->get_text().c_str(); + context.token = m_tokenEntry->get_text().c_str(); + m_sdk->joinSession(context); + } +}; +``` + +### Build Dependencies + +```bash +# Ubuntu/Debian +sudo apt install build-essential cmake +sudo apt install libgtkmm-3.0-dev libsdl2-dev +sudo apt install libasound2-dev libcurl4-openssl-dev +``` + +```cmake +# CMakeLists.txt +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTKMM REQUIRED gtkmm-3.0) +pkg_check_modules(SDL2 REQUIRED sdl2) +pkg_check_modules(ALSA REQUIRED alsa) + +target_link_libraries(SkeletonDemo + ${GTKMM_LIBRARIES} + ${SDL2_LIBRARIES} + ${ALSA_LIBRARIES} + ${CMAKE_SOURCE_DIR}/lib/libvideosdk.so +) +``` + +--- + +## Comparison: Qt vs GTK + +| Aspect | Qt | GTK | +|--------|----|----| +| Language | C++ (native) | C++ (GTKmm wrapper) | +| Video Rendering | QPainter / QImage | SDL2 / Cairo | +| Threading | Signals/Slots + QMetaObject | Glib main loop + idle callbacks | +| Build System | CMake / qmake | CMake | +| Look & Feel | Platform-native | GTK theme | +| Learning Curve | Moderate | Moderate | + +## Resources + +### Sample Repositories + +- **Windows C#**: [videosdk-windows-dotnet-desktop-framework-quickstart](https://github.com/tanchunsiong/videosdk-windows-dotnet-desktop-framework-quickstart) +- **Linux Qt**: [videosdk-linux-qt-quickstart](https://github.com/tanchunsiong/videosdk-linux-qt-quickstart) +- **Linux GTK**: [videosdk-linux-gtk-quickstart](https://github.com/tanchunsiong/videosdk-linux-gtk-quickstart) + +### Official Documentation + +- [Windows Meeting SDK](https://developers.zoom.us/docs/meeting-sdk/windows/) +- [Windows Video SDK](https://developers.zoom.us/docs/video-sdk/windows/) +- [Linux Video SDK](https://developers.zoom.us/docs/video-sdk/linux/) diff --git a/plugins/zoom-developers/skills/general/use-cases/server-to-server-oauth-with-webhooks.md b/plugins/zoom-developers/skills/general/use-cases/server-to-server-oauth-with-webhooks.md new file mode 100644 index 00000000..8ad781d9 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/server-to-server-oauth-with-webhooks.md @@ -0,0 +1,43 @@ +# Server-to-Server OAuth With Webhooks (Common Enterprise Pattern) + +This pattern shows up constantly in high-frequency forum clusters: + +- "Can I use Server-to-Server OAuth with webhooks?" +- "How do I validate webhook requests?" +- "How do I automate meeting/user/report operations across the account?" + +## Skills Needed + +| Order | Skill | Purpose | +|------:|------|---------| +| 1 | **zoom-rest-api** | Make server-side API calls using S2S tokens | +| 2 | **zoom-oauth** | Correctly mint S2S access tokens (`account_credentials`) | +| 3 | **zoom-webhooks** | Receive events, handle URL validation, verify signatures | + +## Architecture + +1. Your backend periodically requests an S2S access token. +2. Your backend calls REST API endpoints to create/update resources. +3. Zoom calls your webhook endpoint for events (meeting/webinar/recording/etc). +4. Your webhook handler verifies authenticity and enqueues async work. + +## Key Clarifications + +- **Webhooks do not "use" your S2S token**. Webhooks are pushed to your endpoint and verified via webhook secrets/signatures. +- REST API calls and webhook ingestion are separate authentication planes. + +## Hard Requirements + +- Public HTTPS webhook endpoint +- Handle `endpoint.url_validation` +- Verify request signatures (and/or follow Zoom verification guidance) +- Respond `200` quickly; do heavy processing asynchronously + +## Links + +- `../../rest-api/concepts/authentication-flows.md` +- `../../rest-api/examples/webhook-server.md` +- `../../webhooks/references/verification.md` +- `../references/automatic-skill-chaining-rest-webhooks.md` +- `../references/meeting-webhooks-oauth-refresh-orchestration.md` +- `../references/distributed-meeting-fallback-architecture.md` diff --git a/plugins/zoom-developers/skills/general/use-cases/team-chat-llm-bot.md b/plugins/zoom-developers/skills/general/use-cases/team-chat-llm-bot.md new file mode 100644 index 00000000..23ad63d3 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/team-chat-llm-bot.md @@ -0,0 +1,266 @@ +# Use Case: AI-Powered Team Chat Bot with LLM Integration + +Build an intelligent Zoom Team Chat bot that uses an LLM provider for natural language understanding and can be chained with other Zoom skills. + +## Scenario + +Create a chatbot that: +1. Responds to natural language queries +2. Can trigger Zoom Meeting creation +3. Can search and retrieve chat history +4. Provides intelligent assistance across Zoom products + +## Skills Required + +- **zoom-team-chat** - Primary skill for chatbot functionality +- **zoom-rest-api** - For meeting creation, user management +- **oauth** - For user authentication flows (optional) +- **zoom-meeting-sdk** - For advanced meeting integrations (optional) + +## Architecture + +``` +User sends message → Team Chat Bot receives webhook + ↓ + Call LLM provider + ↓ + Parse LLM response for intent + ↓ + ┌──────────────────┼──────────────────┐ + │ │ │ + Create Meeting Get User Info Send Response + (REST API) (REST API) (Team Chat) +``` + +## Implementation Steps + +### 1. Setup Team Chat Bot + +**Skill**: `zoom-team-chat` + +```javascript +// Handle bot notification +case 'bot_notification': { + const { toJid, cmd, accountId } = payload; + + // Call LLM + const llmResponse = await callLLM(cmd); + + // Check for intents + const intent = parseIntent(llmResponse); + + if (intent.type === 'create_meeting') { + await handleCreateMeeting(toJid, accountId, intent); + } else { + await sendTextMessage(toJid, accountId, llmResponse); + } +} +``` + +### 2. Create Meeting Intent + +**Skills**: `zoom-team-chat` + `zoom-rest-api` + +```javascript +async function handleCreateMeeting(toJid, accountId, intent) { + // Extract meeting details from LLM response + const { topic, start_time, duration } = intent.details; + + // Create meeting using REST API + const meeting = await fetch('https://api.zoom.us/v2/users/me/meetings', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${userAccessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + topic, + type: 2, // Scheduled meeting + start_time, + duration + }) + }); + + const meetingData = await meeting.json(); + + // Send meeting details back to Team Chat + await sendChatbotMessage(toJid, accountId, { + head: { text: 'Meeting Created' }, + body: [ + { type: 'message', text: `Meeting "${topic}" created successfully!` }, + { + type: 'fields', + items: [ + { key: 'Start Time', value: start_time }, + { key: 'Duration', value: `${duration} minutes` }, + { key: 'Join URL', value: meetingData.join_url } + ] + }, + { + type: 'actions', + items: [ + { text: 'Join Meeting', value: `join_${meetingData.id}`, style: 'Primary' }, + { text: 'Share Link', value: `share_${meetingData.id}`, style: 'Default' } + ] + } + ] + }); +} +``` + +### 3. LLM Integration with Intent Parsing + +**Skill**: `zoom-team-chat` + +```javascript +const llmClient = createLLMClient({ + apiKey: process.env.LLM_API_KEY, + model: process.env.LLM_MODEL +}); + +async function callLLM(userMessage) { + const response = await llmClient.generate({ + max_tokens: 1024, + system: `You are a Zoom assistant bot. You can help users with: +- Creating meetings +- Finding user information +- Answering questions about Zoom +- General assistance + +When a user wants to create a meeting, respond with JSON: +{ + "intent": "create_meeting", + "details": { + "topic": "Meeting topic", + "start_time": "ISO 8601 format", + "duration": 60 + } +} + +Otherwise, provide a helpful text response.`, + messages: [{ role: 'user', content: userMessage }] + }); + + return response.content[0].text; +} + +function parseIntent(llmResponse) { + try { + // Check if response is JSON + if (llmResponse.trim().startsWith('{')) { + const intent = JSON.parse(llmResponse); + return intent; + } + } catch (e) { + // Not JSON, regular text response + } + + return { type: 'text_response', message: llmResponse }; +} +``` + +## Skill Chaining Examples + +### Example 1: Create Meeting from Chat + +**User**: `/bot schedule a team standup tomorrow at 10am for 30 minutes` + +**Flow**: +1. **zoom-team-chat**: Receives command via webhook +2. LLM parses: "create_meeting" intent +3. **zoom-rest-api**: Creates meeting +4. **zoom-team-chat**: Sends confirmation with buttons + +### Example 2: Find User and Start DM + +**User**: `/bot who is John Doe?` + +**Flow**: +1. **zoom-team-chat**: Receives query +2. LLM identifies: "find_user" intent +3. **zoom-rest-api**: Searches users +4. **zoom-team-chat**: Shows user info with "Send DM" button + +### Example 3: Search Chat History + +**User**: `/bot find messages about project alpha` + +**Flow**: +1. **zoom-team-chat**: Receives search query +2. **zoom-rest-api**: Searches chat messages +3. **zoom-team-chat**: Displays results with links + +## Environment Variables + +```bash +# Team Chat (from zoom-team-chat skill) +ZOOM_CLIENT_ID= +ZOOM_CLIENT_SECRET= +ZOOM_BOT_JID= +ZOOM_VERIFICATION_TOKEN= +ZOOM_ACCOUNT_ID= + +# LLM Integration +LLM_API_KEY= +LLM_MODEL= + +# Server +PORT=4000 +``` + +## Advanced: Multi-Skill Integration + +### With webhooks + +Subscribe to meeting events and notify in Team Chat: + +```javascript +// Webhook handler for meeting events +app.post('/meeting-webhook', (req, res) => { + const { event, payload } = req.body; + + if (event === 'meeting.started') { + // Notify in Team Chat + await sendChatbotMessage(channelJid, accountId, { + body: [ + { type: 'message', text: `Meeting "${payload.object.topic}" has started!` }, + { + type: 'actions', + items: [ + { text: 'Join Now', value: `join_${payload.object.id}`, style: 'Primary' } + ] + } + ] + }); + } + + res.status(200).send(); +}); +``` + +## Testing Checklist + +- [ ] Bot responds to natural language queries +- [ ] Can create meetings from chat commands +- [ ] Meeting details sent back to Team Chat +- [ ] Buttons trigger appropriate actions +- [ ] LLM intent parsing works correctly +- [ ] Error handling for failed API calls +- [ ] Multi-turn conversation support + +## Resources + +- [zoom-team-chat skill](../../team-chat/SKILL.md) +- [zoom-rest-api skill](../../rest-api/SKILL.md) +- [oauth skill](../../oauth/SKILL.md) +- [Chatbot Setup Example](../../team-chat/examples/chatbot-setup.md) +- [LLM Integration Example](../../team-chat/examples/llm-integration.md) + +## Next Steps + +1. Build basic chatbot using [Chatbot Setup](../../team-chat/examples/chatbot-setup.md) +2. Add LLM integration +3. Implement intent parsing +4. Add REST API calls for meetings +5. Test end-to-end flow +6. Deploy to production diff --git a/plugins/zoom-developers/skills/general/use-cases/testing-development.md b/plugins/zoom-developers/skills/general/use-cases/testing-development.md new file mode 100644 index 00000000..6c8023b3 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/testing-development.md @@ -0,0 +1,341 @@ +# Testing & Development Environment + +Set up development and testing environments for Zoom integrations. + +## Overview + +Zoom provides several options for development and testing: +- Development app credentials (separate from production) +- Test accounts +- Local webhook testing tools +- SDK sandbox modes + +## Skills Needed + +- **general** - App configuration +- **webhooks** - Webhook testing + +--- + +## Development vs Production Apps + +### Create Separate Apps + +**Always create separate apps for development and production:** + +| Environment | Purpose | Credentials | +|-------------|---------|-------------| +| Development | Testing, debugging | Dev Client ID/Secret | +| Production | Live users | Prod Client ID/Secret | + +1. Go to [Zoom Marketplace](https://marketplace.zoom.us/) +2. Create "MyApp - Development" for testing +3. Create "MyApp - Production" for live deployment +4. Use environment variables to switch between them + +```javascript +// .env.development +ZOOM_CLIENT_ID=dev_client_id_here +ZOOM_CLIENT_SECRET=dev_secret_here +ZOOM_ACCOUNT_ID=dev_account_id + +// .env.production +ZOOM_CLIENT_ID=prod_client_id_here +ZOOM_CLIENT_SECRET=prod_secret_here +ZOOM_ACCOUNT_ID=prod_account_id +``` + +--- + +## Test Accounts + +### Option 1: Developer Account + +Use your own Zoom account for initial development: +- Free tier works for basic API testing +- Pro account needed for SDK testing +- Create test meetings manually + +### Option 2: Zoom Developer Sandbox (ISV Partners) + +ISV partners can request sandbox accounts: +- Contact Zoom partnership team +- Isolated test environment +- Multiple test users + +### Option 3: Programmatic Test Users + +Create test users via API (requires admin account): + +```javascript +// Create a test user +const response = await axios.post( + 'https://api.zoom.us/v2/users', + { + action: 'create', + user_info: { + email: 'testuser+1@yourcompany.com', // Use + alias + type: 1, // Basic user + first_name: 'Test', + last_name: 'User' + } + }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} +); +``` + +**Tip**: Use email aliases (`testuser+1@company.com`, `testuser+2@company.com`) that all route to one inbox. + +--- + +## Local Webhook Testing + +### Option 1: ngrok (Recommended) + +Expose your local development webhook server to the internet for testing: + +```bash +# Install ngrok +npm install -g ngrok + +# Start your local server +node server.js # Running on port 3000 + +# In another terminal, create tunnel +ngrok http 3000 +``` + +Output: +``` +Forwarding https://abc123.ngrok.io -> http://YOUR_DEV_HOST:3000 +``` + +Use `https://abc123.ngrok.io/webhook` as your webhook URL in Zoom Marketplace. + +### Option 2: Cloudflare Tunnel + +```bash +# Install cloudflared +brew install cloudflare/cloudflare/cloudflared + +# Create tunnel +LOCAL_WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:3000" +cloudflared tunnel --url "$LOCAL_WEBHOOK_BASE_URL" +``` + +### Option 3: localtunnel + +```bash +npm install -g localtunnel +lt --port 3000 +``` + +### Webhook URL Validation + +Zoom requires validating your webhook endpoint. Your server must respond to the challenge: + +```javascript +app.post('/webhook', (req, res) => { + // Handle Zoom's endpoint validation + if (req.body.event === 'endpoint.url_validation') { + const hashForValidate = crypto + .createHmac('sha256', ZOOM_WEBHOOK_SECRET) + .update(req.body.payload.plainToken) + .digest('hex'); + + return res.json({ + plainToken: req.body.payload.plainToken, + encryptedToken: hashForValidate + }); + } + + // Handle actual events + // ... +}); +``` + +--- + +## SDK Development Mode + +### Meeting SDK Web + +Enable debug logging: + +```javascript +const client = ZoomMtgEmbedded.createClient(); + +client.init({ + debug: true, // Enable debug logs + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', +}); +``` + +### Video SDK Web + +```javascript +const client = ZoomVideo.createClient(); + +await client.init('en-US', 'CDN', { + enforceMultipleVideos: true, + stayAwake: true, + patchJsMedia: true, + leaveOnPageUnload: true, +}); + +// Enable debug mode +ZoomVideo.setLogLevel('debug'); +``` + +### Native SDKs (iOS/Android/Desktop) + +Enable verbose logging: + +```swift +// iOS +let initParams = ZoomVideoSDKInitParams() +initParams.enableLog = true +initParams.logFilePrefix = "videosdk_debug" +``` + +```kotlin +// Android +val initParams = ZoomVideoSDKInitParams().apply { + enableLog = true + logFilePrefix = "videosdk_debug" +} +``` + +--- + +## Mock Webhook Events + +### Manual Testing with curl + +Test your webhook handler locally: + +```bash +LOCAL_WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:3000" + +# Simulate meeting.started event +curl -X POST "$LOCAL_WEBHOOK_BASE_URL/webhook" \ + -H "Content-Type: application/json" \ + -H "x-zm-signature: v0=test" \ + -H "x-zm-request-timestamp: $(date +%s)" \ + -d '{ + "event": "meeting.started", + "payload": { + "account_id": "abc123", + "object": { + "id": "123456789", + "uuid": "abcd-1234-efgh", + "topic": "Test Meeting", + "host_id": "xyz789" + } + } + }' +``` + +### Webhook Replay Tool + +Build a simple replay tool for testing: + +```javascript +const fs = require('fs'); + +// Save incoming webhooks to file +app.post('/webhook', (req, res) => { + const filename = `webhooks/${Date.now()}_${req.body.event}.json`; + fs.writeFileSync(filename, JSON.stringify(req.body, null, 2)); + + // Process normally... +}); + +// Replay saved webhook +async function replayWebhook(filename) { + const payload = JSON.parse(fs.readFileSync(filename)); + await processWebhook(payload); +} +``` + +--- + +## Testing Checklist + +### Before Going Live + +- [ ] Test OAuth flow end-to-end +- [ ] Verify webhook signature validation +- [ ] Test with multiple user types (host, participant, admin) +- [ ] Handle rate limiting gracefully +- [ ] Test error scenarios (invalid tokens, network failures) +- [ ] Verify recording download permissions +- [ ] Test SDK on all target platforms +- [ ] Load test with expected user volume + +### API Testing + +```javascript +// Test helper for API calls +async function testAPICall(name, fn) { + console.log(`Testing: ${name}`); + try { + const result = await fn(); + console.log(`Pass: ${name}`); + return result; + } catch (error) { + console.error(`Fail: ${name}:`, error.message); + throw error; + } +} + +// Run tests +await testAPICall('Create meeting', () => + createMeeting({ topic: 'Test' }) +); +await testAPICall('Get meeting', () => + getMeeting(meetingId) +); +await testAPICall('Delete meeting', () => + deleteMeeting(meetingId) +); +``` + +--- + +## Debugging Tips + +### Enable Request Logging + +```javascript +const axios = require('axios'); + +// Log all requests +axios.interceptors.request.use(config => { + console.log(`-> ${config.method.toUpperCase()} ${config.url}`); + return config; +}); + +axios.interceptors.response.use( + response => { + console.log(`<- ${response.status} ${response.config.url}`); + return response; + }, + error => { + console.error(`<- ${error.response?.status} ${error.config?.url}`); + console.error('Error:', error.response?.data); + throw error; + } +); +``` + +### SDK Log Collection + +See [SDK Logs & Troubleshooting](../references/sdk-logs-troubleshooting.md) for collecting SDK debug logs. + +## Resources + +- **ngrok**: https://ngrok.com/ +- **Postman Collection**: https://developers.zoom.us/docs/api/rest/postman/ +- **Developer Forum**: https://devforum.zoom.us/ diff --git a/plugins/zoom-developers/skills/general/use-cases/token-and-scope-troubleshooting.md b/plugins/zoom-developers/skills/general/use-cases/token-and-scope-troubleshooting.md new file mode 100644 index 00000000..6747dd0f --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/token-and-scope-troubleshooting.md @@ -0,0 +1,71 @@ +# Token and Scope Troubleshooting (Highest-Frequency Pattern) + +This is the most common failure mode across Zoom REST API integrations and SDK backends: + +- `Invalid access token` +- `Access token is expired` +- `does not contain scopes:[...]` +- "works for me but not for other users/accounts" + +## Skills Needed + +| Order | Skill | Purpose | +|------:|------|---------| +| 1 | **zoom-oauth** | Understand which grant type you are using and why | +| 2 | **zoom-rest-api** | Tie the failing endpoint to required scopes and app type | +| 3 | **zoom-webhooks** (optional) | Verify you are validating webhook requests correctly | + +## Triage Checklist + +### 1. Identify Which Token You Are Using + +Ask: + +- Which app type: **Server-to-Server OAuth**, **General App (User OAuth)**, **Chatbot**, **Meeting SDK**, **Video SDK**? +- Which token: user OAuth access token, S2S OAuth access token, bot token, SDK JWT? + +Rule of thumb: + +- REST calls generally require **OAuth access tokens** (S2S or user-based depending on endpoint). +- SDK join flows require **SDK JWT/signature** plus product-specific tokens (for some cases). + +### 2. Confirm the Exact Endpoint and Operation + +Token scope errors are endpoint-specific. Capture: + +- HTTP method + path (example: `GET /v2/users/me/token?type=zak`) +- full error response body from Zoom +- the token’s `scope` string (if present in token response) + +### 3. Map Endpoint -> Required Scopes + +Do not guess scopes. + +- Use `zoom-rest-api` references for the endpoint. +- Use `general/references/authorization-patterns.md` for RBAC and scope validation strategies. + +### 4. Scope Changes Require Re-Consent (User OAuth) + +If you add scopes after users already installed/authorized: + +- existing users may need to reauthorize so the new scopes are granted + +### 5. “Works on My Account” Usually Means One of These + +- different account plan/features enabled +- missing admin role / privilege +- endpoint requires `:admin` scope but token only has user scope +- using the wrong `me` semantics for the app type + +## Common Fix Patterns + +- Add missing scopes, then reauthorize users (User OAuth). +- Ensure you are using the correct grant (S2S for backend automation across an account; User OAuth when acting on behalf of users). +- Validate that the endpoint actually supports your app type (some endpoints are not usable with some token types). + +## Links + +- `../references/authorization-patterns.md` +- `../../rest-api/troubleshooting/token-scope-playbook.md` +- `../../oauth/SKILL.md` + diff --git a/plugins/zoom-developers/skills/general/use-cases/transcription-bot-linux.md b/plugins/zoom-developers/skills/general/use-cases/transcription-bot-linux.md new file mode 100644 index 00000000..4d05efdc --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/transcription-bot-linux.md @@ -0,0 +1,428 @@ +# Transcription Bot (Linux) + +Build a production-ready transcription bot using Meeting SDK for Linux to automatically transcribe Zoom meetings. + +## Overview + +A transcription bot joins Zoom meetings as a visible participant, captures raw audio, and streams it to a transcription service (AssemblyAI, Whisper, etc.) for real-time or post-meeting transcription. + +## Skills Needed + +- **[meeting-sdk/linux](../../meeting-sdk/linux/SKILL.md)** - Primary (headless meeting bot) +- **[zoom-rest-api](../../rest-api/SKILL.md)** - Get meeting details, OBF tokens +- **[zoom-oauth](../../oauth/SKILL.md)** - JWT token generation for SDK auth + +## Architecture + +``` +┌──────────────┐ ┌─────────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ Meeting │───▶│ Meeting SDK Bot │───▶│ Raw Audio │───▶│ Transcription│ +│ Started │ │ (Linux/Docker) │ │ Stream (PCM) │ │ Service │ +│ │ │ │ │ 32kHz, 16-bit │ │ (AssemblyAI) │ +└──────────────┘ └─────────────────┘ └──────────────────┘ └──────────────┘ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────┐ ┌──────────────────┐ ┌──────────────┐ + │ 1. Get OBF Token│ │ 2. StartRaw │ │ 3. Store │ + │ (REST API) │ │ Recording │ │ Transcript│ + └─────────────────┘ └──────────────────┘ └──────────────┘ +``` + +## Implementation Steps + +### Step 1: Generate JWT & OBF Tokens + +**JWT Token** (SDK authentication): +```bash +# Using zoom-oauth skill +curl -X POST https://your-auth-service.com/generate-jwt \ + -d '{"client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET"}' +``` + +**OBF Token** (join external meetings): +```bash +# Via REST API +curl -X POST "https://api.zoom.us/v2/users/{userId}/token?type=obf&ttl=7200" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" +``` + +See: [zoom-oauth](../../oauth/SKILL.md), [bot-authentication](../../meeting-sdk/references/bot-authentication.md) + +### Step 2: Initialize Meeting SDK + +**Full implementation**: [meeting-sdk/linux](../../meeting-sdk/linux/linux.md) + +```cpp +#include "zoom_sdk.h" +#include + +USING_ZOOM_SDK_NAMESPACE + +// Initialize SDK +InitParam init_params; +init_params.strWebDomain = "https://zoom.us"; +init_params.enableLogByDefault = true; +init_params.rawdataOpts.audioRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap; + +SDKError err = InitSDK(init_params); +if (err != SDKERR_SUCCESS) { + std::cerr << "InitSDK failed: " << err << std::endl; + return 1; +} + +// Authenticate SDK with JWT +AuthContext auth_ctx; +auth_ctx.jwt_token = jwt_token_from_step1; + +IAuthService* auth_service; +CreateAuthService(&auth_service); +auth_service->SetEvent(new MyAuthDelegate()); +auth_service->SDKAuth(auth_ctx); +``` + +### Step 3: Join Meeting + +```cpp +// In onAuthenticationReturn callback +class MyAuthDelegate : public IAuthServiceEvent { + void onAuthenticationReturn(AuthResult ret) override { + if (ret == AUTHRET_SUCCESS) { + JoinParam join_param; + join_param.userType = SDK_UT_WITHOUT_LOGIN; + + auto& params = join_param.param.withoutloginuserJoin; + params.meetingNumber = 1234567890; + params.userName = "Transcription Bot"; + params.psw = meeting_password; + params.isVideoOff = true; // Bot doesn't need video + params.isAudioOff = false; // Need audio for transcription + params.app_privilege_token = obf_token; // From Step 1 + + meeting_service->Join(join_param); + } + } +}; +``` + +### Step 4: Start Raw Recording & Subscribe to Audio + +```cpp +class MyMeetingDelegate : public IMeetingServiceEvent { + void onMeetingStatusChanged(MeetingStatus status, int iResult) override { + if (status == MEETING_STATUS_INMEETING) { + std::cout << "[BOT] Joined meeting successfully" << std::endl; + + // Get recording controller + auto* record_ctrl = meeting_service->GetMeetingRecordingController(); + + // Check permission + SDKError can_record = record_ctrl->CanStartRawRecording(); + if (can_record != SDKERR_SUCCESS) { + std::cerr << "[ERROR] Cannot start raw recording: " << can_record << std::endl; + std::cerr << "Need: host/co-host OR recording token" << std::endl; + return; + } + + // Start raw recording (enables raw data access) + SDKError err = record_ctrl->StartRawRecording(); + if (err != SDKERR_SUCCESS) { + std::cerr << "[ERROR] StartRawRecording failed: " << err << std::endl; + return; + } + + std::cout << "[BOT] Raw recording started, subscribing to audio..." << std::endl; + + // Subscribe to audio + auto* audio_helper = GetAudioRawdataHelper(); + if (!audio_helper) { + std::cerr << "[ERROR] Failed to get audio helper" << std::endl; + return; + } + + SDKError audio_err = audio_helper->subscribe(new TranscriptionAudioDelegate()); + if (audio_err != SDKERR_SUCCESS) { + std::cerr << "[ERROR] Audio subscribe failed: " << audio_err << std::endl; + } else { + std::cout << "[BOT] Subscribed to audio successfully" << std::endl; + } + } + } +}; +``` + +### Step 5: Process Audio & Send to Transcription Service + +```cpp +class TranscriptionAudioDelegate : public IZoomSDKAudioRawDataDelegate { +private: + AssemblyAIClient transcription_client; + std::ofstream debug_file; // For debugging: save raw audio + +public: + TranscriptionAudioDelegate() { + // Initialize transcription service connection + transcription_client.connect(); + + // Optional: Save raw audio for debugging + debug_file.open("meeting_audio.pcm", std::ios::binary); + } + + void onMixedAudioRawDataReceived(AudioRawData* data) override { + // Get audio properties + uint32_t sample_rate = data->GetSampleRate(); // Typically 32000 Hz + uint32_t channels = data->GetChannelNum(); // 1 (mono) or 2 (stereo) + uint32_t buffer_len = data->GetBufferLen(); + char* buffer = data->GetBuffer(); + + // Send to transcription service + transcription_client.send_audio(buffer, buffer_len, sample_rate, channels); + + // Optional: Save for debugging + if (debug_file.is_open()) { + debug_file.write(buffer, buffer_len); + } + } + + void onOneWayAudioRawDataReceived(AudioRawData* data, uint32_t node_id) override { + // Per-user audio (optional - for speaker diarization) + // node_id identifies the speaker + } + + ~TranscriptionAudioDelegate() { + transcription_client.disconnect(); + if (debug_file.is_open()) { + debug_file.close(); + } + } +}; +``` + +### Step 6: Handle Transcription Results + +```cpp +class AssemblyAIClient { +private: + WebSocketClient ws; + std::string api_key; + +public: + void connect() { + ws.connect("wss://api.assemblyai.com/v2/realtime/ws?sample_rate=32000", { + {"Authorization": api_key} + }); + + // Listen for transcription results + ws.on_message([](const std::string& message) { + json result = json::parse(message); + if (result["message_type"] == "FinalTranscript") { + std::string text = result["text"]; + float confidence = result["confidence"]; + + std::cout << "[TRANSCRIPT] " << text << " (confidence: " << confidence << ")" << std::endl; + + // Store in database + save_to_database(text, timestamp); + } + }); + } + + void send_audio(char* buffer, size_t len, uint32_t sample_rate, uint32_t channels) { + // Convert PCM to base64 (AssemblyAI expects base64-encoded audio) + std::string encoded = base64_encode((unsigned char*)buffer, len); + + json audio_data = { + {"audio_data", encoded} + }; + + ws.send(audio_data.dump()); + } +}; +``` + +## Production Patterns + +### Retry Logic for Meeting Join + +**See**: [meeting-sdk-bot.md](../../meeting-sdk/linux/meeting-sdk-bot.md) + +```cpp +bool joinMeetingWithRetry(int max_attempts = 5, int retry_interval_ms = 60000) { + for (int attempt = 1; attempt <= max_attempts; attempt++) { + std::cout << "[JOIN] Attempt " << attempt << "/" << max_attempts << std::endl; + + SDKError err = meeting_service->Join(join_param); + + if (err == SDKERR_SUCCESS && waitForJoinCallback()) { + std::cout << "[JOIN] Success!" << std::endl; + return true; + } + + if (attempt < max_attempts) { + std::cout << "[JOIN] Retrying in " << (retry_interval_ms / 1000) << "s..." << std::endl; + std::this_thread::sleep_for(std::chrono::milliseconds(retry_interval_ms)); + } + } + + std::cerr << "[JOIN] Failed after " << max_attempts << " attempts" << std::endl; + return false; +} +``` + +### Docker Deployment + +**Dockerfile**: +```dockerfile +FROM ubuntu:22.04 + +# Install dependencies +RUN apt-get update && apt-get install -y \ + build-essential cmake \ + libx11-xcb1 libxcb-xfixes0 libxcb-shape0 libxcb-shm0 \ + libxcb-randr0 libxcb-image0 libxcb-keysyms1 libxcb-xtest0 \ + libglib2.0-dev libcurl4-openssl-dev \ + pulseaudio pulseaudio-utils + +# Setup PulseAudio for headless audio +RUN mkdir -p ~/.config && \ + echo "[General]\nsystem.audio.type=default" > ~/.config/zoomus.conf + +# Copy SDK and app +COPY zoom_meeting_sdk/ /app/lib/ +COPY transcription_bot /app/ + +# Setup PulseAudio virtual devices +COPY setup-pulseaudio.sh /app/ +RUN chmod +x /app/setup-pulseaudio.sh + +CMD ["/app/setup-pulseaudio.sh && /app/transcription_bot"] +``` + +**setup-pulseaudio.sh**: +```bash +#!/bin/bash +# Start PulseAudio daemon +pulseaudio --start --exit-idle-time=-1 + +# Create virtual speaker +pactl load-module module-null-sink sink_name=virtual_speaker + +# Create virtual microphone +pactl load-module module-null-sink sink_name=virtual_mic + +echo "PulseAudio configured for headless operation" +``` + +## Configuration Management + +**Environment Variables** (.env): +```bash +# Zoom SDK Credentials +ZOOM_CLIENT_ID=your_client_id +ZOOM_CLIENT_SECRET=your_client_secret + +# Meeting Info +ZOOM_MEETING_NUMBER=1234567890 +ZOOM_MEETING_PASSWORD=abc123 + +# Transcription Service +ASSEMBLYAI_API_KEY=your_api_key + +# Bot Config +BOT_NAME="Transcription Bot" +BOT_JOIN_RETRY_ATTEMPTS=5 +BOT_JOIN_RETRY_INTERVAL_MS=60000 +``` + +**Loading config**: +```cpp +#include + +struct BotConfig { + std::string client_id; + std::string client_secret; + uint64_t meeting_number; + std::string meeting_password; + std::string bot_name; + int join_retry_attempts; + int join_retry_interval_ms; +}; + +BotConfig loadConfig() { + BotConfig cfg; + cfg.client_id = getenv("ZOOM_CLIENT_ID") ?: ""; + cfg.client_secret = getenv("ZOOM_CLIENT_SECRET") ?: ""; + cfg.meeting_number = std::stoull(getenv("ZOOM_MEETING_NUMBER") ?: "0"); + cfg.meeting_password = getenv("ZOOM_MEETING_PASSWORD") ?: ""; + cfg.bot_name = getenv("BOT_NAME") ?: "Transcription Bot"; + cfg.join_retry_attempts = atoi(getenv("BOT_JOIN_RETRY_ATTEMPTS") ?: "5"); + cfg.join_retry_interval_ms = atoi(getenv("BOT_JOIN_RETRY_INTERVAL_MS") ?: "60000"); + return cfg; +} +``` + +## Common Issues & Solutions + +### Issue: Raw Recording Permission Denied + +**Error**: `CanStartRawRecording()` returns `SDKERR_NO_PERMISSION` + +**Solution**: +1. Bot needs to be **host/co-host**, OR +2. Use **recording token** from [REST API](https://developers.zoom.us/docs/meeting-sdk/apis/#operation/meetingLocalRecordingJoinToken), OR +3. Host grants recording permission manually + +**See**: [meeting-sdk-bot.md#raw-recording-permission-denied](../../meeting-sdk/linux/meeting-sdk-bot.md#raw-recording-permission-denied) + +### Issue: No Audio in Docker + +**Error**: Audio subscription succeeds but no audio callbacks + +**Solution**: Create `~/.config/zoomus.conf`: +```bash +mkdir -p ~/.config +echo "[General]\nsystem.audio.type=default" > ~/.config/zoomus.conf +``` + +**See**: [linux-reference.md#pulseaudio-setup](../../meeting-sdk/linux/references/linux-reference.md#pulseaudio-setup) + +### Issue: Callbacks Not Firing + +**Error**: `onMeetingStatusChanged()` never called after `Join()` + +**Solution**: Add GLib main loop: +```cpp +#include + +GMainLoop* loop = g_main_loop_new(NULL, FALSE); +g_main_loop_run(loop); // Blocks until quit +``` + +## Related Use Cases + +- **[AI Meeting Assistant](ai-integration.md)** - Add AI analysis on top of transcription +- **[Recording Bot](../../meeting-sdk/linux/concepts/high-level-scenarios.md#scenario-2-recording-bot)** - Record video + audio with sync +- **[Real-time Media Streams](real-time-media-streams.md)** - Alternative: RTMS for invisible bots + +## Related Skills + +- **[meeting-sdk/linux](../../meeting-sdk/linux/SKILL.md)** - Complete Meeting SDK Linux guide +- **[zoom-rest-api](../../rest-api/SKILL.md)** - Get meetings, OBF tokens +- **[zoom-oauth](../../oauth/SKILL.md)** - JWT token generation + +## Sample Code + +**Complete sample**: [meeting-sdk/linux/concepts/high-level-scenarios.md](../../meeting-sdk/linux/concepts/high-level-scenarios.md#scenario-1) + +**Official samples**: +- https://github.com/zoom/meetingsdk-linux-raw-recording-sample +- https://github.com/zoom/meetingsdk-headless-linux-sample + +## Key Takeaways + +✅ **Use OBF tokens** for joining external meetings (require owner present) +✅ **Setup PulseAudio** for Docker/headless audio access +✅ **Call StartRawRecording()** before subscribing to audio +✅ **Use heap memory mode** for raw data (`ZoomSDKRawDataMemoryModeHeap`) +✅ **Implement retry logic** for meeting join (OBF requires owner present) +✅ **Add GLib main loop** for callbacks to work +✅ **Stream audio in real-time** for best transcription latency diff --git a/plugins/zoom-developers/skills/general/use-cases/usage-reporting-analytics.md b/plugins/zoom-developers/skills/general/use-cases/usage-reporting-analytics.md new file mode 100644 index 00000000..27fbc8bc --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/usage-reporting-analytics.md @@ -0,0 +1,307 @@ +# Usage Reporting & Analytics + +Get meeting statistics, usage reports, and billing data from Zoom. + +## Overview + +Access Zoom's reporting APIs to track meeting usage, participant statistics, and generate analytics for billing, compliance, or business intelligence. + +## Skills Needed + +- **zoom-rest-api** - Primary + +## Report Types + +| Report | Description | +|--------|-------------| +| Daily usage | Meetings per day, minutes used | +| Meeting details | Participant list, join/leave times | +| Webinar reports | Attendee, Q&A, poll data | +| Billing reports | Usage for billing purposes | + +## Prerequisites + +- Admin or owner account +- `report:read` scope + +## Quick Start + +```bash +# Get daily usage report +curl -X GET "https://api.zoom.us/v2/report/daily?year=2024&month=1" \ + -H "Authorization: Bearer {accessToken}" + +# Get meeting participants +curl -X GET "https://api.zoom.us/v2/report/meetings/{meetingId}/participants" \ + -H "Authorization: Bearer {accessToken}" +``` + +## Common Tasks + +### Daily/Monthly Usage Summaries + +```javascript +const axios = require('axios'); + +// Get daily usage report +async function getDailyUsage(year, month) { + const response = await axios.get( + `https://api.zoom.us/v2/report/daily`, + { + params: { year, month }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + // Returns: dates[], total_meeting_minutes, total_meetings, total_participants + return response.data; +} + +// Aggregate monthly statistics +async function getMonthlyStats(year, month) { + const daily = await getDailyUsage(year, month); + + return { + totalMeetings: daily.dates.reduce((sum, d) => sum + d.meetings, 0), + totalMinutes: daily.dates.reduce((sum, d) => sum + d.meeting_minutes, 0), + totalParticipants: daily.dates.reduce((sum, d) => sum + d.participants, 0), + averageMeetingDuration: daily.dates.length > 0 + ? daily.total_meeting_minutes / daily.total_meetings + : 0, + peakDay: daily.dates.reduce((max, d) => + d.meetings > max.meetings ? d : max, { meetings: 0 } + ) + }; +} + +// Get user-level activity +async function getUserActivity(userId, fromDate, toDate) { + const response = await axios.get( + `https://api.zoom.us/v2/report/users/${userId}/meetings`, + { + params: { from: fromDate, to: toDate, page_size: 300 }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + return response.data.meetings; +} +``` + +### Per-Meeting Participant Reports + +```javascript +// Get meeting participants +async function getMeetingParticipants(meetingId) { + // Note: meetingId can be meeting ID or UUID + // If UUID contains / or //, double-encode it + const encodedId = meetingId.includes('/') + ? encodeURIComponent(encodeURIComponent(meetingId)) + : meetingId; + + const response = await axios.get( + `https://api.zoom.us/v2/report/meetings/${encodedId}/participants`, + { + params: { page_size: 300 }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + return response.data.participants; +} + +// Calculate meeting metrics +function calculateMeetingMetrics(participants) { + const uniqueParticipants = new Set(participants.map(p => p.user_email || p.name)); + + // Calculate duration per participant + const durations = participants.map(p => { + const join = new Date(p.join_time); + const leave = new Date(p.leave_time); + return (leave - join) / 1000 / 60; // minutes + }); + + return { + totalParticipants: uniqueParticipants.size, + peakConcurrent: calculatePeakConcurrent(participants), + averageAttendanceDuration: average(durations), + lateJoiners: participants.filter(p => /* logic for late join */).length, + earlyLeavers: participants.filter(p => /* logic for early leave */).length + }; +} + +function calculatePeakConcurrent(participants) { + const events = []; + participants.forEach(p => { + events.push({ time: new Date(p.join_time), delta: 1 }); + events.push({ time: new Date(p.leave_time), delta: -1 }); + }); + + events.sort((a, b) => a.time - b.time); + + let current = 0; + let peak = 0; + events.forEach(e => { + current += e.delta; + peak = Math.max(peak, current); + }); + + return peak; +} +``` + +### Webinar Analytics + +```javascript +// Get webinar participants (panelists + attendees) +async function getWebinarReport(webinarId) { + const [participants, absentees, qa, polls] = await Promise.all([ + getWebinarParticipants(webinarId), + getWebinarAbsentees(webinarId), + getWebinarQA(webinarId), + getWebinarPolls(webinarId) + ]); + + return { participants, absentees, qa, polls }; +} + +async function getWebinarParticipants(webinarId) { + const response = await axios.get( + `https://api.zoom.us/v2/report/webinars/${webinarId}/participants`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + return response.data.participants; +} + +async function getWebinarAbsentees(webinarId) { + const response = await axios.get( + `https://api.zoom.us/v2/report/webinars/${webinarId}/absentees`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + return response.data.registrants; +} + +async function getWebinarQA(webinarId) { + const response = await axios.get( + `https://api.zoom.us/v2/report/webinars/${webinarId}/qa`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + return response.data.questions; +} + +async function getWebinarPolls(webinarId) { + const response = await axios.get( + `https://api.zoom.us/v2/report/webinars/${webinarId}/polls`, + { headers: { 'Authorization': `Bearer ${accessToken}` }} + ); + return response.data.questions; +} + +// Calculate webinar engagement score +function calculateEngagementScore(report) { + const { participants, absentees, qa, polls } = report; + + const registeredCount = participants.length + absentees.length; + const attendedCount = participants.length; + const participatedInQA = new Set(qa.map(q => q.email)).size; + const participatedInPolls = new Set(polls.flatMap(p => p.email)).size; + + return { + attendanceRate: (attendedCount / registeredCount * 100).toFixed(1), + qaParticipation: (participatedInQA / attendedCount * 100).toFixed(1), + pollParticipation: (participatedInPolls / attendedCount * 100).toFixed(1), + totalQuestions: qa.length, + averageAttendanceDuration: average(participants.map(p => p.duration)) + }; +} +``` + +### Exporting Data for BI Tools + +```javascript +const { Parser } = require('json2csv'); +const fs = require('fs'); + +// Export to CSV for BI tools +async function exportMeetingsToCSV(fromDate, toDate, outputPath) { + // Get all meetings in date range + const meetings = []; + let nextPageToken = null; + + do { + const response = await axios.get( + 'https://api.zoom.us/v2/report/users/me/meetings', + { + params: { + from: fromDate, + to: toDate, + page_size: 300, + next_page_token: nextPageToken + }, + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + + meetings.push(...response.data.meetings); + nextPageToken = response.data.next_page_token; + } while (nextPageToken); + + // Flatten for CSV + const flatMeetings = meetings.map(m => ({ + id: m.id, + uuid: m.uuid, + topic: m.topic, + start_time: m.start_time, + end_time: m.end_time, + duration_minutes: m.duration, + participants_count: m.participants_count, + host_email: m.host_email, + has_recording: m.has_recording ? 'yes' : 'no' + })); + + const parser = new Parser(); + const csv = parser.parse(flatMeetings); + + fs.writeFileSync(outputPath, csv); + return outputPath; +} + +// Export to JSON for data warehouse +async function exportToDataWarehouse(fromDate, toDate) { + const meetings = await getAllMeetings(fromDate, toDate); + + // Transform for BigQuery/Snowflake + const records = meetings.map(m => ({ + ...m, + _ingested_at: new Date().toISOString(), + _source: 'zoom_api' + })); + + // Send to warehouse + await bigquery.dataset('zoom').table('meetings').insert(records); +} + +// Scheduled export job +const cron = require('node-cron'); + +cron.schedule('0 1 * * *', async () => { + // Run at 1 AM daily + const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000); + const from = yesterday.toISOString().split('T')[0]; + const to = from; + + await exportToDataWarehouse(from, to); + console.log(`Exported data for ${from}`); +}); +``` + +## Data Retention Notes + +- **Meeting/Webinar reports**: Available for 12 months +- **Participant reports**: Available for 1 month after meeting ends +- **QSS (Quality of Service)**: Available for 30 days + +## Resources + +- **Reports API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Reports +- **Dashboard API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Dashboards diff --git a/plugins/zoom-developers/skills/general/use-cases/user-and-meeting-creation.md b/plugins/zoom-developers/skills/general/use-cases/user-and-meeting-creation.md new file mode 100644 index 00000000..3920df8d --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/user-and-meeting-creation.md @@ -0,0 +1,512 @@ +# User and Meeting Creation Chain + +Create a user account and immediately schedule a meeting for that user. + +## Overview + +A common provisioning pattern: create a new Zoom user via REST API, wait for activation, then create a meeting for that user. This requires understanding user states and proper sequencing. + +## Skills Needed + +| Order | Skill | Purpose | +|-------|-------|---------| +| 1 | **zoom-rest-api** | Create user account | +| 2 | **zoom-rest-api** | Create meeting for user | +| (Optional) | **webhooks** | Receive user activation event | + +## Skill Chaining Flow + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ USER + MEETING CREATION FLOW │ +└─────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ 1. Create User (zoom-rest-api) │ +│ └── POST /users │ +│ └── action: "create" | "autoCreate" | "custCreate" | "ssoCreate" │ +│ └── Returns: user_id, status │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 2. Wait for User Activation │ +│ └── Option A: Poll GET /users/{userId} until status = "active" │ +│ └── Option B: Listen for user.activated webhook │ +│ └── Option C: Use autoCreate (auto-activates) │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 3. Create Meeting (zoom-rest-api) │ +│ └── POST /users/{userId}/meetings │ +│ └── Returns: meeting_id, join_url, start_url │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Prerequisites + +- Zoom app with Server-to-Server OAuth (recommended) or OAuth +- Scopes: `user:write:admin`, `meeting:write:admin` +- Admin privileges on the Zoom account +- **See [Authorization Patterns](../references/authorization-patterns.md)** for RBAC and permission validation middleware + +## User Action Types + +| Action | Description | Activation | Best For | +|--------|-------------|------------|----------| +| `create` | Sends activation email to user | User clicks email link | Standard provisioning | +| `autoCreate` | Creates pre-activated user | Immediate | Automated systems | +| `custCreate` | Creates user without email | Manual activation | Custom workflows | +| `ssoCreate` | Creates SSO user | SSO login | Enterprise SSO | + +## Step 1: Create User + +### Option A: Standard Creation (with email activation) + +```javascript +const axios = require('axios'); + +/** + * Create a new Zoom user + * @param {Object} userInfo - User information + * @param {string} accessToken - Valid OAuth access token + * @returns {Promise} Created user details + */ +async function createUser(userInfo, accessToken) { + try { + const response = await axios.post( + 'https://api.zoom.us/v2/users', + { + action: 'create', // Sends activation email + user_info: { + email: userInfo.email, + type: userInfo.type || 1, // 1=Basic, 2=Licensed + first_name: userInfo.firstName, + last_name: userInfo.lastName, + password: userInfo.password // Optional + } + }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ); + + return { + id: response.data.id, + email: response.data.email, + first_name: response.data.first_name, + last_name: response.data.last_name, + type: response.data.type, + status: 'pending' // User needs to activate via email + }; + } catch (error) { + if (error.response?.status === 409) { + throw new Error(`User ${userInfo.email} already exists`); + } + if (error.response?.status === 400) { + throw new Error(`Invalid user data: ${error.response.data.message}`); + } + throw error; + } +} +``` + +### Option B: Auto-Create (Immediate activation) + +```javascript +/** + * Create a pre-activated Zoom user (no email required) + * Recommended for automated provisioning + */ +async function createUserAutoActivate(userInfo, accessToken) { + try { + const response = await axios.post( + 'https://api.zoom.us/v2/users', + { + action: 'autoCreate', // User is immediately active + user_info: { + email: userInfo.email, + type: userInfo.type || 2, // 2=Licensed (required for autoCreate) + first_name: userInfo.firstName, + last_name: userInfo.lastName, + password: userInfo.password || generateSecurePassword() + } + }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ); + + return { + id: response.data.id, + email: response.data.email, + first_name: response.data.first_name, + last_name: response.data.last_name, + type: response.data.type, + status: 'active' // Ready immediately + }; + } catch (error) { + handleUserCreationError(error, userInfo.email); + } +} + +function generateSecurePassword() { + const crypto = require('crypto'); + return crypto.randomBytes(16).toString('base64') + '!Aa1'; +} +``` + +## Step 2: Wait for User Activation + +### Option A: Polling (Simple) + +```javascript +/** + * Wait for user to become active by polling + * @param {string} userId - User ID to check + * @param {string} accessToken - OAuth token + * @param {number} maxWaitMs - Maximum wait time (default 5 minutes) + * @param {number} pollIntervalMs - Poll interval (default 5 seconds) + */ +async function waitForUserActivation(userId, accessToken, maxWaitMs = 300000, pollIntervalMs = 5000) { + const startTime = Date.now(); + + while (Date.now() - startTime < maxWaitMs) { + const user = await getUser(userId, accessToken); + + if (user.status === 'active') { + console.log(`User ${userId} is now active`); + return user; + } + + console.log(`User ${userId} status: ${user.status}, waiting...`); + await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); + } + + throw new Error(`User ${userId} did not activate within ${maxWaitMs}ms`); +} + +async function getUser(userId, accessToken) { + const response = await axios.get( + `https://api.zoom.us/v2/users/${userId}`, + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); + return response.data; +} +``` + +### Option B: Webhook (Production recommended) + +```javascript +const express = require('express'); +const app = express(); +app.use(express.json()); + +// Store pending user creations +const pendingUsers = new Map(); + +/** + * Create user and wait for webhook activation + */ +async function createUserAndWait(userInfo, accessToken) { + // Create user + const user = await createUser(userInfo, accessToken); + + // Set up promise that resolves when webhook fires + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + pendingUsers.delete(user.id); + reject(new Error(`User ${user.id} activation timeout`)); + }, 300000); // 5 minute timeout + + pendingUsers.set(user.id, { resolve, reject, timeout, user }); + }); +} + +// Webhook handler for user activation +app.post('/webhooks/zoom', (req, res) => { + const { event, payload } = req.body; + + if (event === 'user.activated') { + const userId = payload.object.id; + const pending = pendingUsers.get(userId); + + if (pending) { + clearTimeout(pending.timeout); + pendingUsers.delete(userId); + pending.resolve({ ...pending.user, status: 'active' }); + } + } + + res.status(200).send(); +}); +``` + +## Step 3: Create Meeting for User + +```javascript +/** + * Create a meeting for a specific user + * @param {string} userId - User ID or email + * @param {Object} meetingInfo - Meeting details + * @param {string} accessToken - OAuth token + */ +async function createMeetingForUser(userId, meetingInfo, accessToken) { + try { + const response = await axios.post( + `https://api.zoom.us/v2/users/${userId}/meetings`, + { + topic: meetingInfo.topic, + type: meetingInfo.type || 2, // 2 = Scheduled + start_time: meetingInfo.startTime, + duration: meetingInfo.duration || 60, + timezone: meetingInfo.timezone || 'UTC', + agenda: meetingInfo.agenda, + settings: { + host_video: true, + participant_video: true, + join_before_host: false, + mute_upon_entry: true, + waiting_room: true, + ...meetingInfo.settings + } + }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ); + + return { + id: response.data.id, + topic: response.data.topic, + start_time: response.data.start_time, + join_url: response.data.join_url, + start_url: response.data.start_url, + password: response.data.password, + host_id: response.data.host_id, + host_email: response.data.host_email + }; + } catch (error) { + if (error.response?.status === 404) { + throw new Error(`User ${userId} not found or not active`); + } + if (error.response?.status === 429) { + throw new Error('Rate limit exceeded. Try again later.'); + } + throw error; + } +} +``` + +## Complete Chained Operation + +```javascript +/** + * Complete example: Create user and schedule their first meeting + * + * This demonstrates skill chaining: + * 1. zoom-rest-api (users) - Create user + * 2. zoom-rest-api (meetings) - Create meeting + */ + +async function provisionUserWithMeeting(userInfo, meetingInfo) { + console.log('Starting user provisioning...'); + + // Get access token + const accessToken = await getAccessToken(); + + try { + // Step 1: Create user (autoCreate for immediate activation) + console.log(`Creating user: ${userInfo.email}`); + const user = await createUserAutoActivate({ + email: userInfo.email, + firstName: userInfo.firstName, + lastName: userInfo.lastName, + type: 2 // Licensed user + }, accessToken); + + console.log(`User created: ${user.id} (status: ${user.status})`); + + // Step 2: Verify user is active (should be immediate with autoCreate) + if (user.status !== 'active') { + console.log('Waiting for user activation...'); + await waitForUserActivation(user.id, accessToken); + } + + // Step 3: Create meeting for the new user + console.log(`Creating meeting for user: ${user.id}`); + const meeting = await createMeetingForUser(user.id, { + topic: meetingInfo.topic || `${user.first_name}'s Meeting`, + type: 2, + startTime: meetingInfo.startTime || new Date(Date.now() + 3600000).toISOString(), + duration: meetingInfo.duration || 60, + timezone: meetingInfo.timezone || 'America/Los_Angeles' + }, accessToken); + + console.log(`Meeting created: ${meeting.id}`); + + // Return complete provisioning result + return { + success: true, + user: { + id: user.id, + email: user.email, + name: `${user.first_name} ${user.last_name}` + }, + meeting: { + id: meeting.id, + topic: meeting.topic, + join_url: meeting.join_url, + start_url: meeting.start_url, + start_time: meeting.start_time + } + }; + + } catch (error) { + console.error('Provisioning failed:', error.message); + + // Cleanup: If user was created but meeting failed, optionally delete user + // await deleteUser(user.id, accessToken); + + return { + success: false, + error: error.message + }; + } +} + +// Helper: Get access token (Server-to-Server OAuth) +async function getAccessToken() { + const credentials = Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64'); + + const response = await axios.post( + `https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${process.env.ZOOM_ACCOUNT_ID}`, + null, + { headers: { 'Authorization': `Basic ${credentials}` } } + ); + + return response.data.access_token; +} + +// Usage +const result = await provisionUserWithMeeting( + { + email: 'newuser@example.com', + firstName: 'John', + lastName: 'Doe' + }, + { + topic: 'Onboarding Meeting', + startTime: '2024-02-01T10:00:00Z', + duration: 30 + } +); + +console.log(result); +// { +// success: true, +// user: { id: 'abc123', email: 'newuser@example.com', name: 'John Doe' }, +// meeting: { id: '123456789', topic: 'Onboarding Meeting', join_url: '...', ... } +// } +``` + +## Error Handling + +### Error Recovery Pattern + +```javascript +/** + * Robust provisioning with rollback capability + */ +async function provisionUserWithMeetingSafe(userInfo, meetingInfo) { + const accessToken = await getAccessToken(); + let createdUser = null; + + try { + // Step 1: Create user + createdUser = await createUserAutoActivate(userInfo, accessToken); + + // Step 2: Create meeting + const meeting = await createMeetingForUser(createdUser.id, meetingInfo, accessToken); + + return { success: true, user: createdUser, meeting }; + + } catch (error) { + // Rollback: Delete user if meeting creation failed + if (createdUser && error.message.includes('meeting')) { + console.log(`Rolling back: deleting user ${createdUser.id}`); + try { + await deleteUser(createdUser.id, accessToken); + } catch (deleteError) { + console.error('Rollback failed:', deleteError.message); + } + } + + throw error; + } +} + +async function deleteUser(userId, accessToken) { + await axios.delete( + `https://api.zoom.us/v2/users/${userId}?action=delete`, + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } + ); +} +``` + +### Common Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| 409 Conflict | User email already exists | Use existing user or different email | +| 400 Bad Request | Invalid user data | Check email format, required fields | +| 404 Not Found | User not active/found | Wait for activation or verify user ID | +| 429 Rate Limit | Too many requests | Implement backoff, batch operations | +| 201 but pending | User needs to activate | Use autoCreate or wait for activation | + +## User Types Reference + +| Type | Value | Description | Can Host? | +|------|-------|-------------|-----------| +| Basic | 1 | Free user | Limited | +| Licensed | 2 | Paid license | Yes | +| On-prem | 3 | On-premise deployment | Yes | +| None | 99 | No license | No | + +## Best Practices + +1. **Use autoCreate for automation** - Avoids waiting for email activation +2. **Implement rollback logic** - Clean up if later steps fail +3. **Cache access tokens** - Tokens are valid for 1 hour +4. **Handle rate limits** - Implement exponential backoff +5. **Validate input early** - Check email format before API calls +6. **Log all operations** - Aids debugging and audit + +## Related Use Cases + +- **[Authorization Patterns](../references/authorization-patterns.md)** - RBAC, permission validation, and scope checking for multi-step workflows +- **[Meeting Automation](meeting-automation.md)** - More meeting management patterns +- **[Meeting Details with Events](meeting-details-with-events.md)** - Track meeting events +- **[Recording & Transcription](recording-transcription.md)** - Handle recordings + +## Resources + +- **Users API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Users +- **Meetings API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Meetings +- **User Types**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/userCreate diff --git a/plugins/zoom-developers/skills/general/use-cases/video-sdk-bring-your-own-storage.md b/plugins/zoom-developers/skills/general/use-cases/video-sdk-bring-your-own-storage.md new file mode 100644 index 00000000..22b419ac --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/video-sdk-bring-your-own-storage.md @@ -0,0 +1,220 @@ +# BYOS (Bring Your Own Storage) + +Video SDK feature that saves cloud recordings **directly** to your Amazon S3 bucket. No downloading required. + +> **Official docs:** https://developers.zoom.us/docs/build/storage/ + +## How BYOS Works + +``` +┌─────────────────┐ ┌─────────────────┐ +│ Video SDK │ │ Your AWS │ +│ Session │ Direct Upload │ S3 Bucket │ +│ │ ──────────────────►│ │ +│ Recording... │ (via IAM role │ .mp4 .m4a .txt │ +│ │ or access key) │ │ +└─────────────────┘ └─────────────────┘ +``` + +## BYOS vs Recording Download Pipeline + +| Aspect | BYOS (Video SDK) | Recording Download Pipeline | +|--------|------------------|----------------------------| +| How it works | Zoom writes directly to your S3 | You download from Zoom, upload to S3 | +| Products | Video SDK only | Zoom Meetings | +| Latency | During recording | After recording completes | +| Your infrastructure | Just S3 bucket + credentials | Webhook server + download code | +| Bandwidth cost | None (direct to S3) | You pay for download | + +## Prerequisites + +- Video SDK account with **Cloud Recording add-on plan** (Universal Credit plans include this) +- AWS account with administrator access +- Amazon S3 bucket + +## S3 File Path Structure + +BYOS recordings are stored at: + +``` +Buckets/{bucketName}/cmr/byos/{YYYY}/{MM}/{DD}/{GUID}/cmr_byos/ +``` + +## Setup + +### Step 1: Create S3 Bucket + +Create a private S3 bucket with "Block all public access" enabled. + +### Step 2: Enable BYOS in Zoom Portal + +1. Go to **Developer account web portal** +2. Navigate to **Account Settings** → **General** → **Communications Content Storage Location** +3. Toggle **Bring Your Own Storage** on +4. Click **Manage Storage** → **Add Storage** + +### Step 3: Choose Authentication Method + +#### Option A: AWS Access Key (Simpler) + +1. Enter your **Access Key ID** and **Access Secret Key** +2. Zoom encrypts these values +3. Click **Save** + +#### Option B: Cross Account Access (More Secure) + +1. **Enter Your ARN** in format: + ``` + arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/ZoomArchivingRole + ``` + +2. **Get Zoom Account ID** from the help text below the ARN field, or via [Get Account Settings API](https://developers.zoom.us/docs/api/accounts/#tag/accounts/get/accounts/{accountId}/settings) + +3. **Create IAM Policy** with these permissions: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "S3BucketList", + "Effect": "Allow", + "Action": [ + "s3:ListBucket", + "s3:GetBucketLocation" + ], + "Resource": "arn:aws:s3:::your_bucket_name" + }, + { + "Sid": "S3ObjectAccess", + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject" + ], + "Resource": "arn:aws:s3:::your_bucket_name/*" + } + ] +} +``` + +4. **Create Trust Relationship** for the IAM role: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "Zoom_ARN" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "YOUR_ZOOM_ACCOUNT_ID" + } + } + } + ] +} +``` + +### Step 4: Verify Configuration + +Zoom performs HTTP PUT, GET, and LIST operations against your bucket to validate credentials. + +### Step 5: Test + +Record a Video SDK session and verify recordings appear in your S3 bucket. + +## Managing Storage Locations + +- **Multiple locations:** You can add multiple storage locations, but only one is the default +- **Switch default:** Use the ellipsis menu to change the default location +- **Delete:** Cannot delete the only storage location; add another first + +### Via API + +Use the [Video SDK BYOS Storage APIs](https://developers.zoom.us/docs/api/video-sdk/#tag/byos-storage): + +| Endpoint | Description | +|----------|-------------| +| `GET /v2/videosdk/byos/storage` | List storage locations | +| `POST /v2/videosdk/byos/storage` | Add storage location | +| `DELETE /v2/videosdk/byos/storage/{storageId}` | Delete storage location | +| `PATCH /v2/videosdk/byos/storage/{storageId}` | Update storage location | + +## Managing Recordings + +Use the [Cloud Recording APIs](https://developers.zoom.us/docs/api/video-sdk/#tag/cloud-recording) to manage, play, and download BYOS recordings. + +**Important:** Cloud recordings have two components: + +| Component | Location | Managed by | +|-----------|----------|------------| +| Metadata | Zoom (portal) | Zoom APIs / Web Portal | +| Recording files | Your S3 bucket | You / AWS | + +## Web Portal Limitations + +The Zoom web portal only manages **metadata**, not S3 files: + +- **Delete in portal** → Only removes metadata, S3 files remain +- **Trash recovery** → Restore metadata within 30 days to re-enable playback +- **After 30 days** → Metadata permanently deleted, no portal playback (files still in S3) + +**Recommendation:** Use APIs for full control over BYOS recordings. + +## Effects of Disabling BYOS + +| Action | Metadata | S3 Files | +|--------|----------|----------| +| Toggle BYOS off | Deleted from Recordings page | Unaffected | +| Delete storage location | Deleted from Recordings page | Unaffected | + +**Warning:** Both actions permanently remove metadata. Re-adding the same storage location won't restore playback in the portal. + +## Troubleshooting + +### Verify Storage Location + +1. Click **Manage Storage** +2. Click **ellipsis** → **Verify** + +This tests region, bucket, and credentials. + +### Check AWS CloudTrail + +Look for `AssumeRole` or `PutObject` errors: + +```bash +aws cloudtrail lookup-events \ + --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole +``` + +### Common Issues + +| Issue | Cause | Fix | +|-------|-------|-----| +| Upload fails | Invalid credentials | Verify access key or IAM role | +| Permission denied | Missing S3 permissions | Check IAM policy has all required actions | +| Bucket not found | Wrong region/name | Verify bucket name and region match exactly | + +## Security Best Practices + +1. **Use Cross Account Access** over access keys when possible +2. **Set External ID** to your Zoom account ID (prevents confused deputy) +3. **Enable S3 encryption** (SSE-S3 or SSE-KMS) +4. **Enable bucket versioning** for recovery +5. **Audit IAM roles** regularly for least privilege +6. **Delete during off-peak hours** to avoid incomplete uploads + +## Resources + +- **BYOS Overview:** https://developers.zoom.us/docs/build/storage/ +- **Get Started:** https://developers.zoom.us/docs/build/storage-get-started/ +- **Manage Storage:** https://developers.zoom.us/docs/build/storage-manage/ +- **BYOS Storage APIs:** https://developers.zoom.us/docs/api/video-sdk/#tag/byos-storage +- **Cloud Recording APIs:** https://developers.zoom.us/docs/api/video-sdk/#tag/cloud-recording diff --git a/plugins/zoom-developers/skills/general/use-cases/virtual-agent-campaign-web-mobile-wrapper.md b/plugins/zoom-developers/skills/general/use-cases/virtual-agent-campaign-web-mobile-wrapper.md new file mode 100644 index 00000000..07cc9d37 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/virtual-agent-campaign-web-mobile-wrapper.md @@ -0,0 +1,32 @@ +# Virtual Agent Campaign Web and Mobile Wrapper + +Use this flow when you want one Virtual Agent campaign strategy across website and native mobile wrappers. + +## When to Use + +- You already run campaign-based web embed and need consistent mobile behavior. +- You need native app callbacks for exit or support handoff while keeping web bot logic. +- You want to avoid rebuilding bot UI natively on each platform. + +## Skill Chain + +1. [virtual-agent](../../virtual-agent/SKILL.md) +2. [virtual-agent/web](../../virtual-agent/web/SKILL.md) +3. [virtual-agent/android](../../virtual-agent/android/SKILL.md) or [virtual-agent/ios](../../virtual-agent/ios/SKILL.md) +4. [contact-center](../../contact-center/SKILL.md) + +## Typical Flow + +1. Configure campaign targeting and publish bot flow. +2. Validate web behavior with campaign controls and event listeners. +3. Embed the same campaign URL in Android/iOS WebView containers. +4. Inject bridge handlers for exit, common commands, and `support_handoff`. +5. Apply URL governance (`_self`, `_blank`, `window.open`) consistently. +6. Release with shared monitoring for engagement start/end metrics. + +## References + +- [Virtual Agent Root Skill](../../virtual-agent/SKILL.md) +- [Web Lifecycle and Events](../../virtual-agent/web/concepts/lifecycle-and-events.md) +- [Android JS Bridge Patterns](../../virtual-agent/android/examples/js-bridge-patterns.md) +- [iOS JS Bridge Patterns](../../virtual-agent/ios/examples/js-bridge-patterns.md) diff --git a/plugins/zoom-developers/skills/general/use-cases/virtual-agent-knowledge-base-sync-pipeline.md b/plugins/zoom-developers/skills/general/use-cases/virtual-agent-knowledge-base-sync-pipeline.md new file mode 100644 index 00000000..96c0e86f --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/virtual-agent-knowledge-base-sync-pipeline.md @@ -0,0 +1,30 @@ +# Virtual Agent Knowledge Base Sync Pipeline + +Use this flow when knowledge content lives outside Zoom and must stay synchronized for Virtual Agent responses. + +## When to Use + +- Your source-of-truth KB is in another CMS. +- Web sync alone is not enough for your content structure. +- You need repeatable ingestion and update automation. + +## Skill Chain + +1. [virtual-agent](../../virtual-agent/SKILL.md) +2. [zoom-rest-api](../../rest-api/SKILL.md) +3. [zoom-oauth](../../oauth/SKILL.md) + +## Typical Flow + +1. Decide sync mode: sitemap/link-discovery/manual URLs vs custom API connector. +2. Configure S2S OAuth app and required scopes. +3. Pull content from external source and transform to KB article schema. +4. Upsert articles, tags, and categories into Virtual Agent knowledge base. +5. Reconcile stale entries and monitor sync errors. +6. Re-run sync on release cadence. + +## References + +- [Virtual Agent Environment Variables](../../virtual-agent/references/environment-variables.md) +- [Virtual Agent Troubleshooting](../../virtual-agent/troubleshooting/common-drift-and-breaks.md) +- [REST API Skill](../../rest-api/SKILL.md) diff --git a/plugins/zoom-developers/skills/general/use-cases/web-sdk-embedding.md b/plugins/zoom-developers/skills/general/use-cases/web-sdk-embedding.md new file mode 100644 index 00000000..6ee94d1b --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/web-sdk-embedding.md @@ -0,0 +1,207 @@ +# Web SDK Embedding + +Embed Zoom SDKs in iframes with proper cross-origin configuration. + +## Overview + +Configure your web application to properly embed Zoom Meeting SDK or Video SDK, including iframe setup, CORS headers, and cross-origin requirements. + +## Skills Needed + +- **zoom-meeting-sdk** (Web) +- **zoom-video-sdk** (Web) + +## Embedding Options + +| Option | Description | +|--------|-------------| +| Same-origin | SDK loaded in main page | +| iframe (same-origin) | SDK in iframe, same domain | +| iframe (cross-origin) | SDK in iframe, different domain | + +## Required Headers + +For cross-origin embedding with SharedArrayBuffer: + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +## iframe Configuration + +```html + +``` + +## Common Tasks + +### Basic iframe Embedding + +```html + + +``` + +### Cross-Origin Setup with SharedArrayBuffer + +SharedArrayBuffer is required for: +- 720p sending +- Virtual backgrounds +- Gallery view + +**Server headers (Node.js/Express)**: +```javascript +app.use((req, res, next) => { + res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); + res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); + next(); +}); +``` + +**Nginx config**: +```nginx +location / { + add_header Cross-Origin-Opener-Policy same-origin; + add_header Cross-Origin-Embedder-Policy require-corp; +} +``` + +**Cloudflare Workers**: +```javascript +addEventListener('fetch', event => { + event.respondWith(handleRequest(event.request)); +}); + +async function handleRequest(request) { + const response = await fetch(request); + const newResponse = new Response(response.body, response); + newResponse.headers.set('Cross-Origin-Opener-Policy', 'same-origin'); + newResponse.headers.set('Cross-Origin-Embedder-Policy', 'require-corp'); + return newResponse; +} +``` + +### Permission Handling + +```javascript +// Request permissions before joining +async function requestMediaPermissions() { + try { + await navigator.mediaDevices.getUserMedia({ + video: true, + audio: true + }); + return true; + } catch (err) { + console.error('Permission denied:', err); + return false; + } +} + +// Check permission state +async function checkPermissions() { + const camera = await navigator.permissions.query({ name: 'camera' }); + const microphone = await navigator.permissions.query({ name: 'microphone' }); + + return { + camera: camera.state, // 'granted', 'denied', 'prompt' + microphone: microphone.state + }; +} +``` + +### Communication Between Parent and iframe + +**From parent to iframe**: +```javascript +// Parent page +const iframe = document.getElementById('zoom-frame'); +iframe.contentWindow.postMessage({ + type: 'JOIN_MEETING', + meetingNumber: '123456789', + password: 'pass' +}, 'https://your-app.com'); +``` + +**From iframe to parent**: +```javascript +// Inside iframe (meeting page) +window.parent.postMessage({ + type: 'MEETING_STATUS', + status: 'joined' +}, '*'); +``` + +**Receive messages**: +```javascript +// In parent or iframe +window.addEventListener('message', (event) => { + // Verify origin + if (event.origin !== 'https://trusted-domain.com') return; + + const { type, ...data } = event.data; + + switch (type) { + case 'MEETING_STATUS': + handleMeetingStatus(data); + break; + case 'LEAVE_MEETING': + handleLeaveMeeting(); + break; + } +}); +``` + +### Mobile Responsive Embedding + +```html + + +
+ +
+``` + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| Camera/mic blocked | Check `allow` attribute | +| SharedArrayBuffer error | Add COOP/COEP headers | +| Cross-origin errors | Configure CORS properly | + +## Resources + +- **Meeting SDK Web**: https://developers.zoom.us/docs/meeting-sdk/web/ +- **Video SDK Web**: https://developers.zoom.us/docs/video-sdk/web/ diff --git a/plugins/zoom-developers/skills/general/use-cases/zoom-phone-smart-embed-crm.md b/plugins/zoom-developers/skills/general/use-cases/zoom-phone-smart-embed-crm.md new file mode 100644 index 00000000..3365fe25 --- /dev/null +++ b/plugins/zoom-developers/skills/general/use-cases/zoom-phone-smart-embed-crm.md @@ -0,0 +1,31 @@ +# Zoom Phone Smart Embed CRM Integration + +Build CRM communication workflows by combining Zoom Phone Smart Embed, OAuth-authenticated Phone APIs, and webhook/event ingestion. + +## Skills Needed + +- `phone` (primary) +- `zoom-oauth` +- `zoom-rest-api` +- `zoom-webhooks` + +## Core Architecture + +1. Authenticate users/admins with OAuth. +2. Embed Zoom Phone in CRM side panel. +3. Capture Smart Embed events and correlate to CRM records. +4. Fetch call history/call element details from Phone APIs. +5. Store call outcomes, notes, and follow-up tasks. + +## High-Value Use Cases + +- Click-to-call from account/contact table. +- Post-call disposition and notes sync. +- SMS follow-up with status tracking. +- Real-time supervisor dashboards using webhook updates. + +## Where to Go Next + +- `../../phone/SKILL.md` +- `../../phone/RUNBOOK.md` +- `../../phone/references/deprecations-and-migrations.md` diff --git a/plugins/zoom-developers/skills/meeting-sdk/RUNBOOK.md b/plugins/zoom-developers/skills/meeting-sdk/RUNBOOK.md new file mode 100644 index 00000000..d22f4503 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/RUNBOOK.md @@ -0,0 +1,72 @@ +# Meeting SDK 5-Minute Preflight Runbook + +Use this before deep debugging. It catches high-frequency Meeting SDK failures quickly. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Integration Mode + +- Web Client View (CDN/global `ZoomMtg`) or Web Component View (npm `ZoomMtgEmbedded`). +- Do not mix APIs between modes. + +## 2) Confirm Signature Path + +- Generate signature server-side with SDK Secret. +- Never expose SDK Secret in browser code. +- Confirm `meetingNumber` and `role` in signature payload match join request. + +## 3) Confirm Join Payload Hygiene + +- Pass only valid values; avoid undefined optional fields. +- Ensure meeting number is normalized as digits string. +- If rendering issues appear, test with safer default view settings. + +## 4) Confirm Browser + Security Prereqs + +- If using advanced media features, validate cross-origin isolation setup (COOP/COEP) when required. +- Avoid global CSS resets that break Zoom UI layouts. +- Ensure page overlays/z-index do not hide meeting container. + +## 5) Confirm Routing and Base Path + +- Signature endpoint must be reachable from frontend (same origin proxy recommended). +- In subpath deployments, verify fetch URLs and reverse proxy rewrites. + +## 6) Quick Probes + +- Signature endpoint returns JSON with non-empty signature. +- Join call returns actionable SDK errors (not generic 404 HTML). +- Browser console has no obvious mixed-content/CORS blocks. + +### Copy/Paste Validation Commands + +```bash +# 1) Verify signature endpoint responds with JSON +curl -sS -i "$MEETING_SDK_BASE_URL/api/signature" + +# 2) Verify app page is reachable and returns HTML +curl -sS -i "$MEETING_SDK_BASE_URL" +``` + +Expected: endpoints return valid JSON/HTML (not generic 404/502 pages). + +## 7) Fast Decision Tree + +- **Black/blank UI** -> check CSS/z-index, mode mismatch, and payload field hygiene. +- **Join fails quickly** -> signature payload mismatch or expired signature. +- **Intermittent load issues** -> cross-origin isolation or browser extension interference. + +## 8) SDK Selection Guardrail + +- Use Meeting SDK when embedding Zoom meeting experiences. +- Use Video SDK when building fully custom video UX. + +## 9) Wrong-Path Detector (SDK vs REST) + +- If implementation is producing `join_url` links instead of SDK join calls, you are on REST path. +- If code depends on `GET/POST /v2/meetings` but user asked for embedded in-app join UX, you are on wrong path. +- For Meeting SDK MVP, require: signature endpoint + frontend `ZoomMtg`/`ZoomMtgEmbedded` join. diff --git a/plugins/zoom-developers/skills/meeting-sdk/SKILL.md b/plugins/zoom-developers/skills/meeting-sdk/SKILL.md new file mode 100644 index 00000000..4f89fcff --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/SKILL.md @@ -0,0 +1,27 @@ +--- +name: build-zoom-meeting-sdk-app +description: Use when using Meeting SDK. +--- + +# Build Zoom Meeting SDK App + +Use this skill when the user needs to join, start, or embed real Zoom meetings. If the user wants a fully custom non-meeting video experience, route to `build-zoom-video-sdk-app`. + +## Workflow + +1. Confirm product fit: Meeting SDK joins real Zoom meetings; Video SDK creates custom sessions; Zoom Apps SDK runs inside the Zoom client. +2. Choose the target platform: web, Android, iOS, macOS, Windows, Electron, React Native, Unreal, or Linux bot. +3. Validate the join or start path: meeting number, password, SDK signature, role, ZAK when hosting, waiting room behavior, and user identity. +4. Implement the smallest join flow first, then add meeting controls, custom UI, raw data, recording, or bot behavior. +5. Debug by isolating signature generation, SDK version, platform permissions, meeting settings, waiting room, raw data entitlement, and network constraints. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- Web: [web/SKILL.md](web/SKILL.md) +- Android: [android/SKILL.md](android/SKILL.md) +- iOS: [ios/SKILL.md](ios/SKILL.md) +- Windows: [windows/SKILL.md](windows/SKILL.md) +- Linux bots: [linux/SKILL.md](linux/SKILL.md) +- Signature playbook: [references/signature-playbook.md](references/signature-playbook.md) +- Troubleshooting: [references/troubleshooting.md](references/troubleshooting.md) diff --git a/plugins/zoom-developers/skills/meeting-sdk/android/RUNBOOK.md b/plugins/zoom-developers/skills/meeting-sdk/android/RUNBOOK.md new file mode 100644 index 00000000..3df8fc14 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/android/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting 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 Meeting SDK embed path for Android (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/android/ +- https://marketplacefront.zoom.us/sdk/meeting/android/index.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/android/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/android/` diff --git a/plugins/zoom-developers/skills/meeting-sdk/android/SKILL.md b/plugins/zoom-developers/skills/meeting-sdk/android/SKILL.md new file mode 100644 index 00000000..bef2c76a --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/android/SKILL.md @@ -0,0 +1,38 @@ +--- +name: zoom-meeting-sdk-android +description: | + Zoom Meeting SDK for Android native apps. Use when embedding Zoom meetings in Android with + default/custom UI, PKCE + SDK auth, join/start flows, and Meeting SDK API integration. +--- + +# Zoom Meeting SDK (Android) + +Use this skill when building Android apps with embedded Zoom meeting capabilities. + +## Start Here + +1. [android.md](android.md) +2. [concepts/lifecycle-workflow.md](concepts/lifecycle-workflow.md) +3. [concepts/architecture.md](concepts/architecture.md) +4. [examples/join-start-pattern.md](examples/join-start-pattern.md) +5. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +6. [references/android-reference-map.md](references/android-reference-map.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +9. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Routing Notes + +- Use **default UI** first for first successful join/start validation. +- Move to **custom UI** once auth, meeting state transitions, and permissions are stable. +- For signature/JWT mistakes, chain with [../../oauth/SKILL.md](../../oauth/SKILL.md) and [../references/signature-playbook.md](../references/signature-playbook.md). + +## Key Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/android/ +- API reference: https://marketplacefront.zoom.us/sdk/meeting/android/index.html +- Broader guide: [../SKILL.md](../SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/meeting-sdk/android/android.md b/plugins/zoom-developers/skills/meeting-sdk/android/android.md new file mode 100644 index 00000000..fbdacb64 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/android/android.md @@ -0,0 +1,19 @@ +# Meeting SDK Android Guide + +## Scope + +Android Meeting SDK integration for default UI, custom UI, auth, start/join, and in-meeting feature modules. + +## Validation Snapshot + +- Crawled docs path includes: `get-started`, `integrate`, `start-join-mtg-webinar`, `default-ui`, `custom-ui`, `resource/error-codes`. +- API reference snapshot includes class/interface/function maps from `index.html`, `annotated.html`, `classes.html`, `files.html`, and `functions*` pages. +- Local package checked: `zoom-sdk-android-6.7.5.37500` (contains `mobilertc.aar`, sample apps, dynamic sample modules). + +## Practical Guidance + +1. Initialize and authenticate SDK. +2. Get first successful join in default UI. +3. Add feature flags/settings and error handling. +4. Move to custom UI only for required UX control. +5. Add observability for meeting status and SDK callback failures. diff --git a/plugins/zoom-developers/skills/meeting-sdk/android/concepts/architecture.md b/plugins/zoom-developers/skills/meeting-sdk/android/concepts/architecture.md new file mode 100644 index 00000000..2f7ae63b --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/android/concepts/architecture.md @@ -0,0 +1,23 @@ +# Android Architecture + +## Layer Model + +- App UI layer (Activity/Fragment/Compose wrapper). +- Meeting orchestration layer (init/auth/join/start state machine). +- SDK facade layer (`mobilertc.aar` interfaces and controllers). +- Backend signing service (short-lived signature/JWT issuance). + +## Reference Flow + +```text +Android UI -> App Meeting Service -> Backend Signature API -> Zoom Meeting SDK + ^ | | | + | v v v + User actions Session state store Token/role policy Meeting callbacks +``` + +## Why this split + +- Keeps SDK secret server-side. +- Prevents UI from owning auth/security logic. +- Enables deterministic retry policy on join/start failures. diff --git a/plugins/zoom-developers/skills/meeting-sdk/android/concepts/lifecycle-workflow.md b/plugins/zoom-developers/skills/meeting-sdk/android/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..65cb2393 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/android/concepts/lifecycle-workflow.md @@ -0,0 +1,17 @@ +# Android Lifecycle Workflow + +## Core Sequence + +1. App boot + permissions precheck (camera, mic, notifications as needed). +2. SDK initialize (`InitSDK`-style flow in Android layer). +3. Auth for SDK access (non-login/API user paths or signed flow). +4. Join/start meeting request. +5. In-meeting event handling (audio/video/chat/share/BO/recording where enabled). +6. Leave/end meeting and release/cleanup. + +## Failure Domains + +- Initialization errors (SDK state, app config, package conflicts). +- Auth/signature mismatches (expired token, role mismatch, malformed payload). +- Join/start parameter mismatch (meeting number, passcode, meeting type). +- Device/media state mismatch (permissions, audio route, camera availability). diff --git a/plugins/zoom-developers/skills/meeting-sdk/android/examples/join-start-pattern.md b/plugins/zoom-developers/skills/meeting-sdk/android/examples/join-start-pattern.md new file mode 100644 index 00000000..12584d6a --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/android/examples/join-start-pattern.md @@ -0,0 +1,20 @@ +# Android Join/Start Pattern + +## Join (attendee) + +1. Backend creates short-lived SDK signature/JWT. +2. App initializes SDK and verifies init callback success. +3. App executes join with normalized meeting number + passcode. +4. App subscribes to meeting status callbacks before join call returns. + +## Start (host) + +1. Backend resolves host token (`ZAK`) + role-aware signature. +2. App executes start flow and validates host privilege errors explicitly. +3. App applies host-only features conditionally (recording, management controls). + +## Guardrails + +- Do not hardcode secret in app. +- Normalize meeting identifiers as strings of digits. +- Treat SDK callback thread behavior as asynchronous; avoid UI blocking. diff --git a/plugins/zoom-developers/skills/meeting-sdk/android/references/android-reference-map.md b/plugins/zoom-developers/skills/meeting-sdk/android/references/android-reference-map.md new file mode 100644 index 00000000..2eb77e12 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/android/references/android-reference-map.md @@ -0,0 +1,35 @@ +# Android Reference Map + +## Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/android/ +- API Reference: https://marketplacefront.zoom.us/sdk/meeting/android/index.html + +## Crawl Coverage Snapshot + +- Docs pages captured: `70` +- API reference pages captured: `1003` + +## Key API Entry Pages + +- `index.md` +- `annotated.md` +- `classes.md` +- `files.md` +- `hierarchy.md` +- `functions.md` and `functions_*` +- `functions_func_*` +- `functions_vars_*` + +## Notable API Surface Areas + +- Meeting lifecycle and service controllers +- Audio/video/share controllers +- Breakout room and webinar interfaces +- AI Companion / smart summary interfaces +- Raw data helpers and delegates + +## Drift Signals to Watch + +- Newly added `AI Companion`, `smart summary`, and `avatar` interfaces. +- Legacy helper names retained with new parallel interfaces. diff --git a/plugins/zoom-developers/skills/meeting-sdk/android/references/environment-variables.md b/plugins/zoom-developers/skills/meeting-sdk/android/references/environment-variables.md new file mode 100644 index 00000000..a3e1fecc --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/android/references/environment-variables.md @@ -0,0 +1,15 @@ +# Android Meeting SDK Environment Variables + +| Variable | Required | Purpose | Where to find | +| --- | --- | --- | --- | +| `ZOOM_SDK_KEY` | Yes | SDK signing identity | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_SDK_SECRET` | Yes | Server-side signing secret | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_MEETING_NUMBER` | Join/start | Meeting identifier | Zoom invite / web portal / Meetings API | +| `ZOOM_MEETING_PASSWORD` | Conditional | Meeting passcode | Zoom invite details / Meetings API | +| `ZOOM_ROLE` | Yes | Signature role (`0` attendee, `1` host) | App business logic | +| `ZOOM_ZAK` | Host start | Host authorization token | Zoom REST API token flow | + +## Notes + +- Generate signatures server-side only. +- Keep mobile client as consumer of short-lived tokens, not secret holder. diff --git a/plugins/zoom-developers/skills/meeting-sdk/android/references/versioning-and-compatibility.md b/plugins/zoom-developers/skills/meeting-sdk/android/references/versioning-and-compatibility.md new file mode 100644 index 00000000..d4cb5fcc --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/android/references/versioning-and-compatibility.md @@ -0,0 +1,17 @@ +# Android Versioning and Compatibility + +## Observed Versions + +- Local SDK package: `v6.7.5.37500` +- Docs baseline: current Meeting SDK Android docs tree captured on this crawl. + +## Compatibility Practices + +- Pin exact SDK artifact version in Gradle. +- Track enum/interface additions between releases. +- Validate custom UI flows after each SDK update (higher break risk vs default UI). + +## Contradiction/Drift Notes + +- Docs contain legacy and current path variants (`add-features` vs `custom-ui/default-ui` sections). +- Some page titles include suffix artifacts (for example `| MSDK | And`), indicating doc-generation inconsistencies, not functional API differences. diff --git a/plugins/zoom-developers/skills/meeting-sdk/android/scenarios/high-level-scenarios.md b/plugins/zoom-developers/skills/meeting-sdk/android/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..c9fd7635 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/android/scenarios/high-level-scenarios.md @@ -0,0 +1,19 @@ +# Android High-Level Scenarios + +## Scenario 1: Field-service meeting embed + +- Technician app embeds meeting in default UI. +- Uses attendee join tokens from backend. +- Adds device telemetry + service-quality diagnostics. + +## Scenario 2: Branded healthcare consult + +- Start from default UI for reliability. +- Phase in custom UI for constrained controls and guided workflows. +- Enforce strict permission and foreground-service handling. + +## Scenario 3: Training/coaching session app + +- Uses breakout room and in-meeting chat flows. +- Tracks lifecycle events for attendance and session analytics. +- Uses migration-safe handling for renamed SDK enums across upgrades. diff --git a/plugins/zoom-developers/skills/meeting-sdk/android/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/meeting-sdk/android/troubleshooting/common-issues.md new file mode 100644 index 00000000..be0acafa --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/android/troubleshooting/common-issues.md @@ -0,0 +1,25 @@ +# Android Common Issues + +## 1. Init succeeds, join fails immediately + +- Verify signature freshness and role correctness. +- Confirm meeting number/passcode are exact values. +- Confirm auth path aligns with join/start path (attendee vs host). + +## 2. Works in sample, fails in app + +- Compare manifest permissions and ProGuard/R8 rules. +- Compare Gradle dependency graph for collisions. +- Confirm lifecycle ownership (Activity recreation handling). + +## 3. Custom UI instability + +- Validate default UI parity first. +- Re-check event registration ordering before rendering operations. +- Guard against null/late user stream references. + +## 4. Version drift breakage + +- Rebuild against pinned SDK version. +- Revisit renamed interfaces in API reference map. +- Re-test breakout, raw data, and advanced feature modules after upgrade. diff --git a/plugins/zoom-developers/skills/meeting-sdk/electron/RUNBOOK.md b/plugins/zoom-developers/skills/meeting-sdk/electron/RUNBOOK.md new file mode 100644 index 00000000..ebd18625 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/electron/RUNBOOK.md @@ -0,0 +1,63 @@ +# Meeting SDK Electron 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 Meeting SDK embed path for Electron (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/electron/ + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/electron/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/electron/` diff --git a/plugins/zoom-developers/skills/meeting-sdk/electron/SKILL.md b/plugins/zoom-developers/skills/meeting-sdk/electron/SKILL.md new file mode 100644 index 00000000..a2c24ed1 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/electron/SKILL.md @@ -0,0 +1,78 @@ +--- +name: zoom-meeting-sdk-electron +description: | + Zoom Meeting SDK for Electron desktop applications. Use when embedding Zoom meetings in an Electron app + with the Node addon wrapper, JWT auth, join/start flows, settings controllers, and raw data integration. +--- + +# Zoom Meeting SDK (Electron) + +Use this skill when building Electron desktop apps that embed Zoom Meeting SDK capabilities through the Electron wrapper. + +## Start Here + +1. **[Lifecycle Workflow](concepts/lifecycle-workflow.md)** - init -> auth -> join/start -> in-meeting -> cleanup +2. **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - service/controller/event model in Electron +3. **[Setup Guide](examples/setup-guide.md)** - dependency and build expectations +4. **[Authentication Pattern](examples/authentication-pattern.md)** - SDK JWT generation and auth callbacks +5. **[Join Meeting Pattern](examples/join-meeting-pattern.md)** - start/join meeting execution flow +6. **[SKILL.md](SKILL.md)** - full navigation + +## Core Notes + +- Electron wrapper is built on top of native Meeting SDK with Node addon bridges. +- Keep SDK key/secret server-side; generate SDK JWT on backend. +- Feature support differs by platform/version; check module docs before implementation. +- Raw data and IPC patterns require explicit security hardening in production. + +## References + +- [Electron API Reference Index](references/electron-reference.md) +- [Module Map](references/module-map.md) +- [Deprecated and Contradictions](troubleshooting/deprecated-and-contradictions.md) + +## Related Skills + +- [zoom-meeting-sdk](../SKILL.md) +- [zoom-oauth](../../oauth/SKILL.md) +- [zoom-general](../../general/SKILL.md) + + +## Merged from meeting-sdk/electron/SKILL.md + +# Zoom Meeting SDK Electron - Documentation Index + +## Start Here + +1. [SKILL.md](SKILL.md) +2. [Lifecycle Workflow](concepts/lifecycle-workflow.md) +3. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) +4. [Setup Guide](examples/setup-guide.md) + +## Concepts + +- [Lifecycle Workflow](concepts/lifecycle-workflow.md) +- [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) +- [High-Level Scenarios](concepts/high-level-scenarios.md) + +## Examples + +- [Setup Guide](examples/setup-guide.md) +- [Authentication Pattern](examples/authentication-pattern.md) +- [Join Meeting Pattern](examples/join-meeting-pattern.md) +- [Raw Data Pattern](examples/raw-data-pattern.md) + +## References + +- [Electron API Reference](references/electron-reference.md) +- [Module Map](references/module-map.md) + +## Troubleshooting + +- [Common Issues](troubleshooting/common-issues.md) +- [Version Drift](troubleshooting/version-drift.md) +- [Deprecated and Contradictions](troubleshooting/deprecated-and-contradictions.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/meeting-sdk/electron/concepts/high-level-scenarios.md b/plugins/zoom-developers/skills/meeting-sdk/electron/concepts/high-level-scenarios.md new file mode 100644 index 00000000..e46ea3b7 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/electron/concepts/high-level-scenarios.md @@ -0,0 +1,26 @@ +# High-Level Scenarios + +## 1. Desktop internal meeting client + +- User signs in to your app. +- Backend mints SDK JWT. +- Electron app embeds join/start for scheduled meetings. +- Controllers handle mute/video/chat/share. + +## 2. Compliance-focused recorder assistant + +- Controlled join flow for operator accounts. +- Recording and raw data modules capture meeting artifacts. +- Data moves to internal compliance pipeline. + +## 3. Support operations dashboard + +- Agents join support sessions from desktop app. +- Use participants/chat/share modules for assistance workflows. +- Waiting room and host control automation for queue handling. + +## 4. AI-assisted desktop meeting copilot + +- Meeting join via Electron SDK. +- Raw data or AI-related modules feed local/remote AI services. +- Live summary/action extraction in side panel. diff --git a/plugins/zoom-developers/skills/meeting-sdk/electron/concepts/lifecycle-workflow.md b/plugins/zoom-developers/skills/meeting-sdk/electron/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..957d2a01 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/electron/concepts/lifecycle-workflow.md @@ -0,0 +1,31 @@ +# Lifecycle Workflow + +Recommended runtime sequence for Electron Meeting SDK integrations: + +1. Initialize SDK wrapper (`zoom_sdk` level init). +2. Authenticate with SDK JWT through auth service. +3. Configure meeting parameters and settings controllers. +4. Join or start meeting through meeting service. +5. Bind meeting controllers/events (audio, video, participants, chat, share). +6. Optional raw data and advanced modules. +7. Leave meeting and release SDK resources cleanly. + +## Sequence Diagram + +```text +Electron App + -> initSDK + -> authWithJwt + -> create/get meeting service + -> joinMeeting/startMeeting + -> subscribe callbacks + -> apply controller actions + -> leaveMeeting + -> cleanup +``` + +## Why this order matters + +- Controller operations before successful auth or join usually fail or no-op. +- Settings should be applied before meeting join where possible. +- Cleanup prevents stale state and callback leaks on app relaunch. diff --git a/plugins/zoom-developers/skills/meeting-sdk/electron/concepts/sdk-architecture-pattern.md b/plugins/zoom-developers/skills/meeting-sdk/electron/concepts/sdk-architecture-pattern.md new file mode 100644 index 00000000..a3eeff38 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/electron/concepts/sdk-architecture-pattern.md @@ -0,0 +1,24 @@ +# SDK Architecture Pattern + +Electron wrapper follows a service + controller + event callback model. + +## Core layers + +- `zoom_sdk` bootstrap/auth wrappers. +- Meeting service facade. +- Feature controllers (audio, video, participants, recording, share, chat, etc.). +- Settings service/controllers. +- Optional modules (raw data, webinar, AI companion, whiteboard, QA/polling). + +## Universal pattern + +1. Get service/controller. +2. Register event callback(s). +3. Invoke async action. +4. Handle callback result/error codes. + +## Implementation guidance + +- Centralize callback routing in one internal event bus. +- Use typed wrapper methods per module to reduce invocation mistakes. +- Log SDK return codes consistently for diagnostics. diff --git a/plugins/zoom-developers/skills/meeting-sdk/electron/examples/authentication-pattern.md b/plugins/zoom-developers/skills/meeting-sdk/electron/examples/authentication-pattern.md new file mode 100644 index 00000000..94c1ec95 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/electron/examples/authentication-pattern.md @@ -0,0 +1,20 @@ +# Authentication Pattern + +## Backend + +- Accept meeting context inputs. +- Generate short-lived Meeting SDK JWT. +- Return token to authenticated Electron client session. + +## Electron app + +1. Initialize SDK. +2. Send SDK JWT to auth module. +3. Wait for auth callback success. +4. Continue to meeting join/start. + +## Guardrails + +- Refresh token on expiry windows. +- Fail fast on auth callback errors and show actionable logs. +- Do not persist SDK secret or signing logic in Electron bundle. diff --git a/plugins/zoom-developers/skills/meeting-sdk/electron/examples/join-meeting-pattern.md b/plugins/zoom-developers/skills/meeting-sdk/electron/examples/join-meeting-pattern.md new file mode 100644 index 00000000..d7cd4e61 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/electron/examples/join-meeting-pattern.md @@ -0,0 +1,15 @@ +# Join Meeting Pattern + +## Flow + +1. Collect meeting number, display name, passcode/credential strategy. +2. Ensure SDK auth completed. +3. Call join/start meeting API via meeting service. +4. Wait for in-meeting callbacks. +5. Initialize required controllers (audio/video/chat/share/participants). + +## Operational checks + +- Validate meeting number format before SDK call. +- Normalize role-specific fields for attendee vs host start flows. +- Apply settings defaults (audio/video/share) before join when supported. diff --git a/plugins/zoom-developers/skills/meeting-sdk/electron/examples/raw-data-pattern.md b/plugins/zoom-developers/skills/meeting-sdk/electron/examples/raw-data-pattern.md new file mode 100644 index 00000000..3242ac87 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/electron/examples/raw-data-pattern.md @@ -0,0 +1,22 @@ +# Raw Data Pattern + +Raw data flows are advanced and require hardening. + +## Typical use cases + +- Local AI processing. +- Quality monitoring. +- Compliance capture. + +## Pattern + +1. Enable raw data module after meeting join. +2. Subscribe to relevant streams. +3. Transfer frames/samples through controlled IPC/data path. +4. Apply backpressure, buffering, and clean shutdown handling. + +## Risks + +- Performance overhead if frame handling is not bounded. +- Sensitive data exposure if raw buffers are not protected. +- Version mismatch risks in native addon dependencies. diff --git a/plugins/zoom-developers/skills/meeting-sdk/electron/examples/setup-guide.md b/plugins/zoom-developers/skills/meeting-sdk/electron/examples/setup-guide.md new file mode 100644 index 00000000..ea744bba --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/electron/examples/setup-guide.md @@ -0,0 +1,23 @@ +# Setup Guide + +This guide focuses on integration structure, not one-click install scripts. + +## Prerequisites + +- Electron + Node toolchain compatible with your chosen SDK package. +- Native build toolchain for Node addon compilation. +- Backend endpoint for SDK JWT signing. + +## Minimal setup checklist + +1. Add Meeting SDK Electron package and native artifacts. +2. Wire preload/main process APIs for SDK invocation. +3. Implement secure backend endpoint for JWT generation. +4. Add app-level init/auth/join lifecycle handlers. +5. Add structured logging around SDK callbacks and error codes. + +## Security baseline + +- Keep SDK secret off client. +- Gate any raw data transport with encryption and access controls. +- Validate all IPC boundaries between renderer and main process. diff --git a/plugins/zoom-developers/skills/meeting-sdk/electron/references/electron-reference.md b/plugins/zoom-developers/skills/meeting-sdk/electron/references/electron-reference.md new file mode 100644 index 00000000..b2e41407 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/electron/references/electron-reference.md @@ -0,0 +1,17 @@ +# Electron API Reference + +Primary crawled references: + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/electron.md` +- `raw-docs/developers.zoom.us/docs/meeting-sdk/electron/download-and-install.md` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/electron/index.md` + +Reference set includes module docs for: + +- auth and SDK bootstrap +- meeting service and feature controllers +- settings controllers +- raw data and advanced modules +- generated JS wrapper documentation + +Use [Module Map](module-map.md) to navigate quickly by feature area. diff --git a/plugins/zoom-developers/skills/meeting-sdk/electron/references/module-map.md b/plugins/zoom-developers/skills/meeting-sdk/electron/references/module-map.md new file mode 100644 index 00000000..17a0b2a3 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/electron/references/module-map.md @@ -0,0 +1,33 @@ +# Module Map + +## Bootstrap and auth + +- `module-zoom_electron_sdk.md` +- `module-zoom_auth.md` +- `module-zoom_meeting.md` + +## Core meeting controls + +- `module-zoom_meeting_audio.md` +- `module-zoom_meeting_video.md` +- `module-zoom_meeting_share.md` +- `module-zoom_meeting_participants_ctrl.md` +- `module-zoom_meeting_chat.md` +- `module-zoom_meeting_recording.md` + +## Settings + +- `module-zoom_setting.md` +- `module-zoom_setting_audio.md` +- `module-zoom_setting_video.md` +- `module-zoom_setting_share.md` +- `module-zoom_setting_recording.md` + +## Advanced/optional + +- `module-zoom_rawdata.md` +- `module-zoom_meeting_webinar.md` +- `module-zoom_meeting_ai_companion.md` +- `module-zoom_meeting_whiteboard.md` +- `module-zoom_meeting_polling.md` +- `module-zoom_meeting_qa.md` diff --git a/plugins/zoom-developers/skills/meeting-sdk/electron/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/meeting-sdk/electron/troubleshooting/common-issues.md new file mode 100644 index 00000000..c35b42aa --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/electron/troubleshooting/common-issues.md @@ -0,0 +1,25 @@ +# Common Issues + +## SDK initializes but join fails + +- Verify auth callback success before join. +- Validate meeting number/passcode format. +- Confirm role and host-specific fields for start flow. + +## Native addon load failures + +- Check Electron/Node ABI compatibility. +- Rebuild native modules for your Electron version. +- Confirm platform binaries are present in package/runtime paths. + +## Callback silence or partial feature behavior + +- Ensure controller callbacks are registered before invoking actions. +- Avoid duplicate singleton initialization in multiple processes. +- Confirm feature availability for account type/meeting type. + +## Raw data instability + +- Apply bounded queues and worker separation. +- Reduce frame sampling rates if renderer/main process saturates. +- Ensure graceful unsubscription on leave/cleanup. diff --git a/plugins/zoom-developers/skills/meeting-sdk/electron/troubleshooting/deprecated-and-contradictions.md b/plugins/zoom-developers/skills/meeting-sdk/electron/troubleshooting/deprecated-and-contradictions.md new file mode 100644 index 00000000..4eb467ec --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/electron/troubleshooting/deprecated-and-contradictions.md @@ -0,0 +1,28 @@ +# Deprecated and Contradictions + +Observed from crawled docs and provided SDK package context: + +## 1. Electron version guidance contradiction + +- Package README mentions installing Electron `33.0.0`. +- Same README also says sample app currently does not support Electron 10 or above. + +Action: treat sample README version text as inconsistent and validate against official current compatibility guidance before rollout. + +## 2. Deprecated module flags in API reference + +Crawled API reference marks deprecations in several areas, including webinar and some setting-related modules. + +Action: avoid new dependencies on deprecated modules; isolate behind adapter interfaces if legacy support is required. + +## 3. Feature availability caveats + +Multiple module docs include "not supported" notes depending on platform/account/meeting context. + +Action: gate feature use with runtime capability checks and fail gracefully. + +## 4. Python/build toolchain caveat in sample notes + +Sample package notes mention build issues with newer Python (distutils-related) and suggest older Python versions. + +Action: pin build environment versions in CI and document exact toolchain used for your release branch. diff --git a/plugins/zoom-developers/skills/meeting-sdk/electron/troubleshooting/version-drift.md b/plugins/zoom-developers/skills/meeting-sdk/electron/troubleshooting/version-drift.md new file mode 100644 index 00000000..4fe990dd --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/electron/troubleshooting/version-drift.md @@ -0,0 +1,17 @@ +# Version Drift + +Electron SDK integrations are sensitive to dependency drift. + +## Drift vectors + +- Electron runtime upgrades +- Node ABI changes +- Native addon compiler toolchain changes +- Meeting SDK wrapper package updates + +## Control strategy + +1. Pin tested Electron + SDK versions. +2. Keep a compatibility matrix in your project docs. +3. Rebuild and smoke test on every runtime change. +4. Run callback/error-code regression tests for join/start/audio/video/share. diff --git a/plugins/zoom-developers/skills/meeting-sdk/ios/RUNBOOK.md b/plugins/zoom-developers/skills/meeting-sdk/ios/RUNBOOK.md new file mode 100644 index 00000000..72a69b49 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/ios/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting 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 Meeting SDK embed path for iOS (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/ios/ +- https://marketplacefront.zoom.us/sdk/meeting/ios/annotated.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/ios/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/ios/` diff --git a/plugins/zoom-developers/skills/meeting-sdk/ios/SKILL.md b/plugins/zoom-developers/skills/meeting-sdk/ios/SKILL.md new file mode 100644 index 00000000..8448e523 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/ios/SKILL.md @@ -0,0 +1,32 @@ +--- +name: zoom-meeting-sdk-ios +description: | + Zoom Meeting SDK for iOS native apps. Use when embedding Zoom meetings in iOS with + default/custom UI, PKCE + SDK auth, host start with ZAK, and mobile lifecycle handling. +--- + +# Zoom Meeting SDK (iOS) + +Use this skill when building iOS apps with embedded Zoom meeting capabilities. + +## Start Here + +1. [ios.md](ios.md) +2. [concepts/lifecycle-workflow.md](concepts/lifecycle-workflow.md) +3. [concepts/architecture.md](concepts/architecture.md) +4. [examples/join-start-pattern.md](examples/join-start-pattern.md) +5. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +6. [references/ios-reference-map.md](references/ios-reference-map.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +9. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Key Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/ios/ +- API reference: https://marketplacefront.zoom.us/sdk/meeting/ios/annotated.html +- Broader guide: [../SKILL.md](../SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/meeting-sdk/ios/concepts/architecture.md b/plugins/zoom-developers/skills/meeting-sdk/ios/concepts/architecture.md new file mode 100644 index 00000000..63b8a516 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/ios/concepts/architecture.md @@ -0,0 +1,23 @@ +# iOS Architecture + +## Layer Model + +- UIKit/SwiftUI app layer. +- Meeting orchestration layer (state machine + delegate fan-out). +- SDK adapter layer (MobileRTC service wrappers). +- Backend signature/token service. + +## Reference Flow + +```text +iOS UI -> Meeting Coordinator -> Backend Sign Service -> Meeting SDK + ^ | | | + | v v v +User intents Local state store Role/token policy SDK delegates/events +``` + +## Why this split + +- Keeps security-critical logic server-side. +- Stabilizes delegate/event ordering. +- Makes upgrade drift easier to isolate. diff --git a/plugins/zoom-developers/skills/meeting-sdk/ios/concepts/lifecycle-workflow.md b/plugins/zoom-developers/skills/meeting-sdk/ios/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..04cceadb --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/ios/concepts/lifecycle-workflow.md @@ -0,0 +1,17 @@ +# iOS Lifecycle Workflow + +## Core Sequence + +1. App launch and permission strategy (camera/mic, optional share path prep). +2. SDK initialization and auth callbacks. +3. Join/start decision branch. +4. In-meeting event wiring (audio/video/chat/share/webinar where needed). +5. Background/foreground transitions and interruption recovery. +6. Leave/end and cleanup. + +## Failure Domains + +- Expired or mismatched signature data. +- Role/host-token mismatch in start flow. +- App lifecycle interruption (phone call/audio route/background). +- Incomplete delegate registration before meeting transitions. diff --git a/plugins/zoom-developers/skills/meeting-sdk/ios/examples/join-start-pattern.md b/plugins/zoom-developers/skills/meeting-sdk/ios/examples/join-start-pattern.md new file mode 100644 index 00000000..03a804c3 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/ios/examples/join-start-pattern.md @@ -0,0 +1,20 @@ +# iOS Join/Start Pattern + +## Join (attendee) + +1. Request short-lived signature from backend. +2. Initialize/auth SDK and verify callback success. +3. Call join with meeting number/passcode/display name. +4. Observe meeting status and user/video delegate events. + +## Start (host) + +1. Backend provides host `ZAK` + role-aware signature. +2. Call start path with host token. +3. Validate host-only feature permissions before showing controls. + +## Guardrails + +- Do not put SDK secret in iOS app bundle. +- Register delegates before initiating meeting transitions. +- Persist minimal state needed for background recovery. diff --git a/plugins/zoom-developers/skills/meeting-sdk/ios/ios.md b/plugins/zoom-developers/skills/meeting-sdk/ios/ios.md new file mode 100644 index 00000000..8e95dd58 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/ios/ios.md @@ -0,0 +1,18 @@ +# Meeting SDK iOS Guide + +## Scope + +iOS Meeting SDK integration for init/auth, default/custom UI, meeting join/start, and in-meeting features. + +## Validation Snapshot + +- Docs coverage includes: setup/get-started, default UI and custom UI feature tracks, PKCE/start/join/auth, FAQ/error-code pages. +- API reference snapshot includes class/protocol maps, file references, and member lists. +- Local package checked: `zoom-sdk-ios-6.7.5.33005` with `MobileRTCSample` and Objective-C sample presenters. + +## Practical Guidance + +1. Achieve stable default UI join path first. +2. Add host-start and advanced features after baseline is stable. +3. Move to custom UI only where UX requires it. +4. Add explicit permission and audio route diagnostics. diff --git a/plugins/zoom-developers/skills/meeting-sdk/ios/references/environment-variables.md b/plugins/zoom-developers/skills/meeting-sdk/ios/references/environment-variables.md new file mode 100644 index 00000000..b58c4c2f --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/ios/references/environment-variables.md @@ -0,0 +1,15 @@ +# iOS Meeting SDK Environment Variables + +| Variable | Required | Purpose | Where to find | +| --- | --- | --- | --- | +| `ZOOM_SDK_KEY` | Yes | SDK signing identity | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_SDK_SECRET` | Yes | Server-side signing secret | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_MEETING_NUMBER` | Join/start | Meeting identifier | Zoom invite / web portal / Meetings API | +| `ZOOM_MEETING_PASSWORD` | Conditional | Meeting passcode | Zoom invite details / Meetings API | +| `ZOOM_ROLE` | Yes | Signature role (`0` attendee, `1` host) | App business logic | +| `ZOOM_ZAK` | Host start | Host authorization token | Zoom REST API token flow | + +## Notes + +- Keep secrets server-side only. +- Consider storing only short-lived meeting tokens in app memory. diff --git a/plugins/zoom-developers/skills/meeting-sdk/ios/references/ios-reference-map.md b/plugins/zoom-developers/skills/meeting-sdk/ios/references/ios-reference-map.md new file mode 100644 index 00000000..475618f9 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/ios/references/ios-reference-map.md @@ -0,0 +1,33 @@ +# iOS Reference Map + +## Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/ios/ +- API Reference: https://marketplacefront.zoom.us/sdk/meeting/ios/annotated.html + +## Crawl Coverage Snapshot + +- Docs pages captured: `55` +- API reference pages captured: `643` + +## Key API Entry Pages + +- `annotated.md` +- `classes.md` +- `files.md` +- `hierarchy.md` +- `functions*` +- `globals*` +- `pages.md` + +## Notable API Surface Areas + +- `MobileRTCMeetingService` category extensions +- Protocol-heavy delegate architecture +- Audio/video/share/raw-data helpers +- BO/webinar/AI companion interfaces + +## Drift Signals to Watch + +- New AI Companion and smart summary handlers. +- Category-level method movement between releases. diff --git a/plugins/zoom-developers/skills/meeting-sdk/ios/references/versioning-and-compatibility.md b/plugins/zoom-developers/skills/meeting-sdk/ios/references/versioning-and-compatibility.md new file mode 100644 index 00000000..62f7bee2 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/ios/references/versioning-and-compatibility.md @@ -0,0 +1,17 @@ +# iOS Versioning and Compatibility + +## Observed Versions + +- Local SDK package: `v6.7.5.33005` +- Docs baseline: current iOS Meeting SDK docs tree captured on this crawl. + +## Compatibility Practices + +- Pin exact SDK package release. +- Re-verify category/protocol method availability on upgrades. +- Re-test background/audio interruption flows every upgrade. + +## Contradiction/Drift Notes + +- Package `README.md` points to generic Meeting SDK docs root, while platform docs live at `/meeting-sdk/ios/`. +- Package changelog file only links externally; keep your own integration delta notes per release. diff --git a/plugins/zoom-developers/skills/meeting-sdk/ios/scenarios/high-level-scenarios.md b/plugins/zoom-developers/skills/meeting-sdk/ios/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..b843efe9 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/ios/scenarios/high-level-scenarios.md @@ -0,0 +1,19 @@ +# iOS High-Level Scenarios + +## Scenario 1: Telehealth client app + +- Attendee join with strict permission prompts. +- Uses waiting-room and status callbacks for guided UX. +- Records app-level diagnostic events for support triage. + +## Scenario 2: Internal host app + +- Uses host start flow with `ZAK`. +- Enables moderator tools only after host privilege confirmation. +- Applies policy checks for recording and participant management. + +## Scenario 3: Branded custom in-meeting experience + +- Starts from default UI parity baseline. +- Adds custom UI modules for selected controls/views. +- Maintains fallback path when advanced features regress after SDK update. diff --git a/plugins/zoom-developers/skills/meeting-sdk/ios/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/meeting-sdk/ios/troubleshooting/common-issues.md new file mode 100644 index 00000000..dcaf6c86 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/ios/troubleshooting/common-issues.md @@ -0,0 +1,22 @@ +# iOS Common Issues + +## 1. Join/start fails with generic error + +- Validate signature expiry and role. +- Confirm meeting number/passcode mapping. +- Confirm host flow has valid `ZAK`. + +## 2. Delegate callbacks missing or late + +- Register delegates before invoking join/start. +- Verify lifecycle transitions do not deallocate coordinator/service objects. + +## 3. Audio route or interruption issues + +- Handle route changes explicitly. +- Re-sync meeting media state after interruption/background return. + +## 4. Upgrade regressions + +- Compare protocol/category signatures against prior version. +- Re-test custom UI extensions first; they are usually most drift-prone. diff --git a/plugins/zoom-developers/skills/meeting-sdk/linux/RUNBOOK.md b/plugins/zoom-developers/skills/meeting-sdk/linux/RUNBOOK.md new file mode 100644 index 00000000..2a9d994b --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/linux/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting 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 Meeting SDK embed path for Linux (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/linux/ +- https://marketplacefront.zoom.us/sdk/meeting/linux/ + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/linux/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/linux/` diff --git a/plugins/zoom-developers/skills/meeting-sdk/linux/SKILL.md b/plugins/zoom-developers/skills/meeting-sdk/linux/SKILL.md new file mode 100644 index 00000000..90036383 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/linux/SKILL.md @@ -0,0 +1,419 @@ +--- +name: zoom-meeting-sdk-linux +description: "Zoom Meeting SDK for Linux - C++ headless meeting bots with raw audio/video access, transcription, recording, and AI integration for server-side automation" +--- + +# Zoom Meeting SDK - Linux Development + +Expert guidance for building headless meeting bots with the Zoom Meeting SDK on Linux. This SDK enables server-side meeting participation, raw media capture, transcription, and AI-powered meeting automation. + +## How to Build a Meeting Bot That Automatically Joins and Records + +Use this skill when the requirement is: +- visible bot joins a real Zoom meeting +- the bot records raw media itself +- or the bot triggers a Zoom-managed cloud-recording workflow after join + +Skill chain: +- primary: `meeting-sdk/linux` +- add `zoom-rest-api` for OBF/ZAK lookup, scheduling, or cloud-recording settings +- add `zoom-webhooks` when post-meeting cloud recording retrieval is required + +Minimal raw-recording flow: + +```cpp +JoinParam join_param; +join_param.userType = SDK_UT_WITHOUT_LOGIN; +auto& params = join_param.param.withoutloginuserJoin; +params.meetingNumber = meeting_number; +params.userName = "Recording Bot"; +params.psw = meeting_password.c_str(); +params.app_privilege_token = obf_token.c_str(); + +SDKError join_err = meeting_service->Join(join_param); +if (join_err != SDKERR_SUCCESS) { + throw std::runtime_error("join_failed"); +} + +// In MEETING_STATUS_INMEETING callback: +auto* record_ctrl = meeting_service->GetMeetingRecordingController(); +if (!record_ctrl) { + throw std::runtime_error("recording_controller_unavailable"); +} + +if (record_ctrl->CanStartRawRecording() != SDKERR_SUCCESS) { + throw std::runtime_error("raw_recording_not_permitted"); +} + +SDKError record_err = record_ctrl->StartRawRecording(); +if (record_err != SDKERR_SUCCESS) { + throw std::runtime_error("start_raw_recording_failed"); +} + +GetAudioRawdataHelper()->subscribe(new MyAudioDelegate()); +``` + +Use **raw recording** when the bot must own PCM/YUV media or feed an AI pipeline directly. +Use **cloud recording + webhooks** when the requirement is Zoom-managed MP4/M4A/transcript assets after the meeting. + +**Official Documentation**: https://developers.zoom.us/docs/meeting-sdk/linux/ +**API Reference**: https://marketplacefront.zoom.us/sdk/meeting/linux/ +**Sample Repository (Raw Recording)**: https://github.com/zoom/meetingsdk-linux-raw-recording-sample +**Sample Repository (Headless)**: https://github.com/zoom/meetingsdk-headless-linux-sample + +## Quick Links + +**New to Meeting SDK Linux? Follow this path:** + +1. **[linux.md](linux.md)** - Quick start guide with complete workflow +2. **[concepts/high-level-scenarios.md](concepts/high-level-scenarios.md)** - Production bot architectures +3. **[meeting-sdk-bot.md](meeting-sdk-bot.md)** - Resilient bot with retry logic +4. **[references/linux-reference.md](references/linux-reference.md)** - Dependencies, Docker, CMake + +**Common Use Cases:** +- **Transcription Bot** → [high-level-scenarios.md#scenario-1-transcription-bot](concepts/high-level-scenarios.md) +- **Recording Bot** → [high-level-scenarios.md#scenario-2-recording-bot](concepts/high-level-scenarios.md) +- **AI Meeting Assistant** → [high-level-scenarios.md#scenario-3-ai-meeting-assistant](concepts/high-level-scenarios.md) +- **Quality Monitoring** → [high-level-scenarios.md#scenario-4-monitoring-quality-bot](concepts/high-level-scenarios.md) +- **Auto-Join + Recording Bot** → [meeting-sdk-bot.md](meeting-sdk-bot.md) for raw recording orchestration, retry, and cloud-recording handoff + +**Having issues?** +- Docker audio issues → [references/linux-reference.md#pulseaudio-setup](references/linux-reference.md) +- Raw recording permission denied → [meeting-sdk-bot.md#raw-recording-permission-denied](meeting-sdk-bot.md) +- Build errors → [references/linux-reference.md#troubleshooting](references/linux-reference.md) + +## Routing Rule for Bots + +If the user asks to build a bot that **automatically joins a Zoom meeting and records it**, start with +[meeting-sdk-bot.md](meeting-sdk-bot.md). + +- Use **Meeting SDK Linux** for the visible participant, join flow, and raw recording control. +- Chain **zoom-rest-api** when the bot must fetch OBF/ZAK tokens, schedule meetings, or enable account-side recording settings. +- Chain **zoom-webhooks** when the requirement is Zoom cloud recording retrieval after meeting end. + +## SDK Overview + +The Zoom Meeting SDK for Linux is a **C++ library optimized for headless server environments**: +- **Headless Operation**: No GUI required, perfect for Docker/cloud +- **Raw Data Access**: YUV420 video, PCM audio at 32kHz +- **GLib Event Loop**: Async event handling for callbacks +- **Docker-Ready**: Pre-configured Dockerfiles for CentOS/Ubuntu +- **PulseAudio Integration**: Virtual audio devices for headless environments + +### Key Differences from Video SDK + +| Feature | Meeting SDK (Linux) | Video SDK | +|---------|-------------------|-----------| +| **Primary Use** | Join existing meetings as bot | Host custom video sessions | +| **Visibility** | Visible participant | Session participant | +| **UI** | Headless (no UI) | Optional custom UI | +| **Authentication** | JWT + OBF/ZAK for external meetings | JWT only | +| **Recording Control** | `StartRawRecording()` required | Direct raw data access | +| **Platform** | Linux only | Windows, macOS, iOS, Android | + +## Prerequisites + +### System Requirements +- **OS**: Ubuntu 22+, CentOS 8/9, Oracle Linux 8 +- **Architecture**: x86_64 +- **Compiler**: gcc/g++ with C++11 support +- **Build Tools**: cmake 3.16+ + +### Development Dependencies +```bash +# Ubuntu +apt-get install -y build-essential cmake \ + libx11-xcb1 libxcb-xfixes0 libxcb-shape0 libxcb-shm0 \ + libxcb-randr0 libxcb-image0 libxcb-keysyms1 libxcb-xtest0 \ + libglib2.0-dev libcurl4-openssl-dev pulseaudio + +# CentOS +yum install -y cmake gcc gcc-c++ \ + libxcb-devel xcb-util-image xcb-util-keysyms \ + glib2-devel libcurl-devel pulseaudio +``` + +### Required Credentials +1. **Zoom Meeting SDK App** (Client ID & Secret) → [Create at Marketplace](https://marketplace.zoom.us/) +2. **JWT Token** → Generate from Client ID/Secret +3. **For External Meetings**: OBF token OR ZAK token → [Get via REST API](../references/bot-authentication.md) +4. **For Raw Recording**: Meeting Recording Token (optional) → [Get via API](https://developers.zoom.us/docs/meeting-sdk/apis/#operation/meetingLocalRecordingJoinToken) + +## Quick Start + +### 1. Download & Extract SDK + +```bash +# Download from https://marketplace.zoom.us/ +tar xzf zoom-meeting-sdk-linux_x86_64-{version}.tar + +# Organize files +mkdir -p demo/include/h demo/lib/zoom_meeting_sdk +cp -r h/* demo/include/h/ +cp lib*.so demo/lib/zoom_meeting_sdk/ +cp -r qt_libs demo/lib/zoom_meeting_sdk/ +cp translation.json demo/lib/zoom_meeting_sdk/json/ + +# Create required symlink +cd demo/lib/zoom_meeting_sdk && ln -s libmeetingsdk.so libmeetingsdk.so.1 +``` + +### 2. Initialize & Auth + +```cpp +#include "zoom_sdk.h" + +USING_ZOOM_SDK_NAMESPACE + +// Initialize SDK +InitParam init_params; +init_params.strWebDomain = "https://zoom.us"; +init_params.enableLogByDefault = true; +init_params.rawdataOpts.audioRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap; +InitSDK(init_params); + +// Authenticate with JWT +AuthContext auth_ctx; +auth_ctx.jwt_token = your_jwt_token; +CreateAuthService(&auth_service); +auth_service->SDKAuth(auth_ctx); +``` + +### 3. Join Meeting + +```cpp +// In onAuthenticationReturn callback +void onAuthenticationReturn(AuthResult ret) { + if (ret == AUTHRET_SUCCESS) { + JoinParam join_param; + join_param.userType = SDK_UT_WITHOUT_LOGIN; + + auto& params = join_param.param.withoutloginuserJoin; + params.meetingNumber = 1234567890; + params.userName = "Bot"; + params.psw = "password"; + params.isVideoOff = true; + params.isAudioOff = false; + + meeting_service->Join(join_param); + } +} +``` + +### 4. Access Raw Data + +```cpp +// In onMeetingStatusChanged callback +void onMeetingStatusChanged(MeetingStatus status, int iResult) { + if (status == MEETING_STATUS_INMEETING) { + auto* record_ctrl = meeting_service->GetMeetingRecordingController(); + + // Start raw recording (enables raw data access) + if (record_ctrl->CanStartRawRecording() == SDKERR_SUCCESS) { + record_ctrl->StartRawRecording(); + + // Subscribe to audio + auto* audio_helper = GetAudioRawdataHelper(); + audio_helper->subscribe(new MyAudioDelegate()); + + // Subscribe to video + IZoomSDKRenderer* video_renderer; + createRenderer(&video_renderer, new MyVideoDelegate()); + video_renderer->setRawDataResolution(ZoomSDKResolution_720P); + video_renderer->subscribe(user_id, RAW_DATA_TYPE_VIDEO); + } + } +} +``` + +## Key Features + +| Feature | Description | +|---------|-------------| +| **Headless Operation** | No GUI, perfect for Docker/server deployments | +| **Raw Audio (PCM)** | Capture mixed or per-user audio at 32kHz | +| **Raw Video (YUV420)** | Capture video frames in contiguous planar format | +| **GLib Event Loop** | Async callback handling | +| **Docker Support** | Pre-built Dockerfiles for CentOS/Ubuntu | +| **PulseAudio Virtual Devices** | Audio in headless environments | +| **Breakout Rooms** | Programmatic breakout room management | +| **Chat** | Send/receive in-meeting chat | +| **Recording Control** | Local, cloud, and raw recording | + +## Critical Gotchas & Best Practices + +### ⚠️ CRITICAL: PulseAudio for Docker/Headless + +**The #1 issue for raw audio in Docker:** + +Raw audio requires PulseAudio and a config file, even in headless environments. + +**Solution**: +```bash +# Install PulseAudio +apt-get install -y pulseaudio pulseaudio-utils + +# Create config file +mkdir -p ~/.config +cat > ~/.config/zoomus.conf << EOF +[General] +system.audio.type=default +EOF + +# Start PulseAudio with virtual devices +pulseaudio --start --exit-idle-time=-1 +pactl load-module module-null-sink sink_name=virtual_speaker +pactl load-module module-null-sink sink_name=virtual_mic +``` + +See: [references/linux-reference.md#pulseaudio-setup](references/linux-reference.md) + +### Raw Recording Permission Required + +Unlike Video SDK, Meeting SDK requires explicit permission to access raw data: + +```cpp +// MUST call StartRawRecording() first +auto* record_ctrl = meeting_service->GetMeetingRecordingController(); + +SDKError can_record = record_ctrl->CanStartRawRecording(); +if (can_record == SDKERR_SUCCESS) { + record_ctrl->StartRawRecording(); + // NOW you can subscribe to audio/video +} else { + // Need: host/co-host status OR recording token +} +``` + +**Ways to get permission**: +1. Bot is host/co-host +2. Host grants recording permission +3. Use `recording_token` parameter (get via [REST API](https://developers.zoom.us/docs/meeting-sdk/apis/#operation/meetingLocalRecordingJoinToken)) +4. Use `app_privilege_token` (OBF) when joining + +### GLib Main Loop Required + +SDK callbacks execute via GLib event loop: + +```cpp +#include + +// Setup main loop +GMainLoop* loop = g_main_loop_new(NULL, FALSE); + +// Add timeout for periodic tasks +g_timeout_add_seconds(10, check_meeting_status, NULL); + +// Run loop (blocks until quit) +g_main_loop_run(loop); +``` + +**Without GLib loop**: Callbacks never fire, join hangs indefinitely. + +### Heap Memory Mode Recommended + +Always use heap mode for raw data to avoid stack overflow with large frames: + +```cpp +init_params.rawdataOpts.videoRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap; +init_params.rawdataOpts.shareRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap; +init_params.rawdataOpts.audioRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap; +``` + +### Thread Safety + +SDK callbacks execute on SDK threads: +- Don't perform heavy operations in callbacks +- Don't call `CleanUPSDK()` from within callbacks +- Use thread-safe queues for data passing +- Use mutexes for shared state + +## Production Architectures + +### Transcription Bot + +**See**: [concepts/high-level-scenarios.md#scenario-1](concepts/high-level-scenarios.md) + +``` +Join Meeting → StartRawRecording → Subscribe Audio → +Stream to AssemblyAI/Whisper → Generate Real-time Transcript +``` + +### Recording Bot + +**See**: [concepts/high-level-scenarios.md#scenario-2](concepts/high-level-scenarios.md) + +``` +Join Meeting → StartRawRecording → Subscribe Audio+Video → +Write YUV+PCM → FFmpeg Merge → Upload to Storage +``` + +### AI Meeting Assistant + +**See**: [concepts/high-level-scenarios.md#scenario-3](concepts/high-level-scenarios.md) + +``` +Join Meeting → Real-time Transcription → AI Analysis → +Extract Action Items → Generate Summary +``` + +## Sample Applications + +**Official Repositories**: + +| Sample | Description | Link | +|--------|-------------|------| +| **Raw Recording Sample** | Traditional C++ approach with config.txt | [GitHub](https://github.com/zoom/meetingsdk-linux-raw-recording-sample) | +| **Headless Sample** | Modern TOML-based with CLI, Docker Compose | [GitHub](https://github.com/zoom/meetingsdk-headless-linux-sample) | + +**What Each Demonstrates**: + +- **Raw Recording Sample**: Complete raw data workflow, PulseAudio setup, Docker multi-distro support +- **Headless Sample**: AssemblyAI integration, LLM analysis, production-ready config management + +## Documentation Library + +### Core Concepts +- **[linux.md](linux.md)** - Quick start & core workflow +- **[concepts/high-level-scenarios.md](concepts/high-level-scenarios.md)** - Production bot architectures +- **[meeting-sdk-bot.md](meeting-sdk-bot.md)** - Resilient bot with retry logic + +### Platform Reference +- **[references/linux-reference.md](references/linux-reference.md)** - Dependencies, CMake, Docker, troubleshooting + +### Authentication +- **[../references/authorization.md](../references/authorization.md)** - SDK JWT generation +- **[../references/bot-authentication.md](../references/bot-authentication.md)** - Bot token types (ZAK, OBF, JWT) + +### Advanced Features +- **[../references/breakout-rooms.md](../references/breakout-rooms.md)** - Programmatic breakout rooms +- **[../references/ai-companion.md](../references/ai-companion.md)** - AI Companion controls + +## Common Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| **No audio in Docker** | Missing PulseAudio config | Create `~/.config/zoomus.conf` | +| **Raw recording denied** | No permission | Use host/co-host OR recording token | +| **Callbacks not firing** | Missing GLib main loop | Add `g_main_loop_run()` | +| **Build errors (XCB)** | Missing X11 libraries | Install libxcb packages | +| **Segfault on auth** | OpenSSL version mismatch | Install libssl1.1 | + +**Complete troubleshooting**: [references/linux-reference.md#troubleshooting](references/linux-reference.md) + +## Resources + +- **Official Docs**: https://developers.zoom.us/docs/meeting-sdk/linux/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/meeting/linux/ +- **Dev Forum**: https://devforum.zoom.us/ +- **GitHub Samples**: + - https://github.com/zoom/meetingsdk-linux-raw-recording-sample + - https://github.com/zoom/meetingsdk-headless-linux-sample + +--- + +**Need help?** Start with [linux.md](linux.md) for quick start, then explore [high-level-scenarios.md](concepts/high-level-scenarios.md) for production patterns. + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/meeting-sdk/linux/concepts/high-level-scenarios.md b/plugins/zoom-developers/skills/meeting-sdk/linux/concepts/high-level-scenarios.md new file mode 100644 index 00000000..770487b1 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/linux/concepts/high-level-scenarios.md @@ -0,0 +1,406 @@ +# High-Level Scenarios for Meeting SDK Linux + +This guide covers common bot architectures and patterns that remain stable across SDK versions, with flexible approaches to handle API changes. + +## Scenario 1: Transcription Bot + +**Goal**: Join meetings to transcribe conversations in real-time. + +### Architecture + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ +│ Meeting │───►│ Meeting SDK │───►│ Audio Stream │───►│ Transcription│ +│ Starts │ │ Join + Auth │ │ (PCM 32kHz) │ │ Service │ +└─────────────┘ └──────────────┘ └─────────────────┘ └──────────────┘ + │ │ │ + ▼ ▼ ▼ + ┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ + │ StartRaw │ │ onMixedAudio │ │ Store │ + │ Recording │ │ RawDataReceived │ │ Transcript │ + └──────────────┘ └─────────────────┘ └──────────────┘ +``` + +### Core Pattern (Version-Flexible) + +```cpp +// STEP 1: Initialize (stable across versions) +InitParam init_params; +init_params.strWebDomain = "https://zoom.us"; +init_params.enableLogByDefault = true; +init_params.rawdataOpts.audioRawDataMemoryMode = ZoomSDKRawDataMemoryModeHeap; +InitSDK(init_params); + +// STEP 2: Authenticate (JWT token - stable) +AuthContext auth_ctx; +auth_ctx.jwt_token = getJWTToken(); // Generate from SDK credentials +CreateAuthService(&auth_service); +auth_service->SDKAuth(auth_ctx); + +// STEP 3: Join meeting (flexible - check SDK version for exact field names) +JoinParam join_param; +join_param.userType = SDK_UT_WITHOUT_LOGIN; +auto& params = join_param.param.withoutloginuserJoin; +params.meetingNumber = meeting_number; +params.userName = "Transcription Bot"; +params.psw = meeting_password; + +// Version-flexible: Check if these fields exist in your SDK version +// params.app_privilege_token = obf_token; // v5.15+ +// params.onBehalfToken = obf_token; // Some versions +// params.userZAK = zak_token; // Alternative + +meeting_service->Join(join_param); + +// STEP 4: In onMeetingStatusChanged callback +if (status == MEETING_STATUS_INMEETING) { + // Get recording controller (pattern stable) + auto* record_ctrl = meeting_service->GetMeetingRecordingController(); + + // Start raw recording (enables raw data access) + if (record_ctrl->CanStartRawRecording() == SDKERR_SUCCESS) { + record_ctrl->StartRawRecording(); + } + + // Subscribe to audio + auto* audio_helper = GetAudioRawdataHelper(); + audio_helper->subscribe(new TranscriptionAudioDelegate()); +} + +// STEP 5: Process audio +class TranscriptionAudioDelegate : public IZoomSDKAudioRawDataDelegate { + void onMixedAudioRawDataReceived(AudioRawData* data) override { + // Send PCM data to transcription service + sendToTranscriptionAPI(data->GetBuffer(), data->GetBufferLen()); + } +}; +``` + +### Version Handling Strategy + +**Flexible field access**: +```cpp +// Define a macro to check field existence at compile time +#ifdef HAS_APP_PRIVILEGE_TOKEN + params.app_privilege_token = token; +#elif defined(HAS_ON_BEHALF_TOKEN) + params.onBehalfToken = token; +#else + params.userZAK = token; +#endif +``` + +**Runtime capability detection**: +```cpp +SDKError canRecord = record_ctrl->CanStartRawRecording(); +if (canRecord != SDKERR_SUCCESS) { + // Fallback: Try local recording + if (record_ctrl->CanStartRecording(true) == SDKERR_SUCCESS) { + record_ctrl->StartRecording(time, path); + } +} +``` + +### Key Resilience Patterns + +1. **Always check CanXXX() before calling XXX()** +2. **Have fallback authentication methods** (OBF → ZAK → password-only) +3. **Detect capabilities**, don't assume +4. **Use error codes** to adapt behavior + +--- + +## Scenario 2: Recording Bot (Video + Audio) + +**Goal**: Record meetings with synchronized audio and video. + +### Architecture + +``` +┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ +│ Join Meeting │───►│ StartRawRecording│───►│ Subscribe │ +│ │ │ │ │ Audio+Video │ +└──────────────┘ └─────────────────┘ └──────────────┘ + │ + ┌──────────────────────────┴─────────────────┐ + ▼ ▼ + ┌──────────────┐ ┌──────────────┐ + │ Write YUV420 │ │ Write PCM │ + │ video.yuv │ │ audio.pcm │ + └──────────────┘ └──────────────┘ + │ │ + └──────────────────────────────────────────┬─┘ + ▼ + ┌──────────────┐ + │ FFmpeg Merge │ + │ output.mp4 │ + └──────────────┘ +``` + +### Core Pattern + +```cpp +// After joining and starting raw recording... + +// Video subscription +class VideoRecorderDelegate : public IZoomSDKRendererDelegate { + std::ofstream videoFile; + + void onRawDataFrameReceived(YUVRawDataI420* data) override { + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // YUV420 format: Y + U + V planes (contiguous) + videoFile.write(data->GetYBuffer(), width * height); + videoFile.write(data->GetUBuffer(), (width/2) * (height/2)); + videoFile.write(data->GetVBuffer(), (width/2) * (height/2)); + } +}; + +IZoomSDKRenderer* video_renderer; +createRenderer(&video_renderer, new VideoRecorderDelegate()); +video_renderer->setRawDataResolution(ZoomSDKResolution_720P); +video_renderer->subscribe(user_id, RAW_DATA_TYPE_VIDEO); + +// Audio subscription +class AudioRecorderDelegate : public IZoomSDKAudioRawDataDelegate { + std::ofstream audioFile; + + void onMixedAudioRawDataReceived(AudioRawData* data) override { + audioFile.write((char*)data->GetBuffer(), data->GetBufferLen()); + } +}; + +auto* audio_helper = GetAudioRawdataHelper(); +audio_helper->subscribe(new AudioRecorderDelegate()); + +// Post-processing with FFmpeg +system("ffmpeg -video_size 1280x720 -pixel_format yuv420p -f rawvideo -i video.yuv " + "-f s16le -ar 32000 -ac 1 -i audio.pcm " + "-c:v libx264 -c:a aac -shortest output.mp4"); +``` + +### Handling Resolution Changes + +**Flexible resolution selection** (adapt to SDK version): +```cpp +// Try highest resolution available, fall back gracefully +ZoomSDKResolution resolutions[] = { + ZoomSDKResolution_1080P, + ZoomSDKResolution_720P, + ZoomSDKResolution_360P +}; + +for (auto res : resolutions) { + SDKError err = video_renderer->setRawDataResolution(res); + if (err == SDKERR_SUCCESS) { + std::cout << "Using resolution: " << res << std::endl; + break; + } +} +``` + +--- + +## Scenario 3: AI Meeting Assistant + +**Goal**: Real-time meeting analysis with AI (sentiment, action items, summaries). + +### Architecture + +``` +┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Audio Stream │───►│ Transcription │───►│ AI Analysis │───►│ Actions │ +│ (Real-time) │ │ (AssemblyAI) │ │ (LLM) │ │ Identified │ +└──────────────┘ └─────────────────┘ └──────────────┘ └──────────────┘ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ + │ Speaker │ │ Sentiment │ │ Generate │ + │ Diarization │ │ Analysis │ │ Summary │ + └─────────────────┘ └─────────────────┘ └──────────────┘ +``` + +### Core Pattern + +```cpp +// Real-time streaming transcription +class AIAssistantAudioDelegate : public IZoomSDKAudioRawDataDelegate { + WebSocketClient transcription_ws; // AssemblyAI WebSocket + AIClient ai_client; // LLM provider + + void onMixedAudioRawDataReceived(AudioRawData* data) override { + // Stream audio to real-time transcription + transcription_ws.send_audio(data->GetBuffer(), data->GetBufferLen()); + } +}; + +// Process transcription results +void onTranscriptReceived(const std::string& text, const std::string& speaker) { + // Accumulate context + meeting_context += speaker + ": " + text + "\n"; + + // Every 30 seconds, analyze with AI + if (should_analyze()) { + std::string analysis = ai_client.analyze(meeting_context, { + "extract_action_items", + "detect_decisions", + "identify_blockers" + }); + + // Store results + meeting_insights.push_back(analysis); + } +} + +// Post-meeting summary +void onMeetingEnded() { + std::string summary = ai_client.summarize(meeting_context, meeting_insights); + save_to_database(meeting_id, summary); + send_email_summary(participants, summary); +} +``` + +### Version Flexibility: Chat Integration + +Some SDK versions support in-meeting chat: +```cpp +// Try to send AI insights to meeting chat +auto* chat_ctrl = meeting_service->GetMeetingChatController(); +if (chat_ctrl) { + // Check if available + if (chat_ctrl->CanSendChat()) { + chat_ctrl->SendChatTo("AI detected action item: ..."); + } +} +``` + +--- + +## Scenario 4: Monitoring & Quality Bot + +**Goal**: Monitor meeting quality metrics and alert on issues. + +### Core Pattern + +```cpp +// Get service quality info +auto* meeting_info = meeting_service->GetMeetingInfo(); + +// Polling loop (GLib timeout) +gboolean check_quality(gpointer data) { + // Audio stats + auto* audio_stats = meeting_info->GetAudioStatistics(); + if (audio_stats) { + int jitter = audio_stats->jitter; + int packet_loss = audio_stats->packet_loss_percent; + + if (packet_loss > 5) { + alert("High packet loss: " + std::to_string(packet_loss) + "%"); + } + } + + // Video stats + auto* video_stats = meeting_info->GetVideoStatistics(); + if (video_stats) { + int fps = video_stats->fps; + int resolution_width = video_stats->width; + + if (fps < 15) { + alert("Low FPS: " + std::to_string(fps)); + } + } + + return TRUE; // Continue polling +} + +// Setup polling +g_timeout_add_seconds(10, check_quality, nullptr); +``` + +--- + +## Version Migration Guide + +### When SDK Updates + +1. **Read release notes** for breaking changes +2. **Check header files** for actual method signatures +3. **Test compilation** with `-Werror=deprecated` +4. **Update authentication** if new token types added +5. **Verify raw data flow** still works + +### Common Breaking Changes + +| Change Type | Example | Mitigation | +|-------------|---------|------------| +| Struct field rename | `withoutloginuserJoin` → `withoutLoginUserJoin` | Use `#ifdef` or update | +| Method signature change | `subscribe(user_id)` → `subscribe(user_id, type)` | Check return codes | +| Enum value rename | `SDKERR_NORECORDINGINPROCESS` → `SDKERR_NO_RECORDING_IN_PROCESS` | Map old → new | +| New required field | `app_privilege_token` becomes mandatory | Add to config | + +### Testing Strategy + +```cpp +// Version detection at compile time +#if ZOOM_SDK_VERSION >= 51500 // v5.15.0 + #define USE_OBF_TOKEN +#endif + +// Runtime capability testing +bool test_raw_recording() { + auto* ctrl = meeting_service->GetMeetingRecordingController(); + return ctrl && ctrl->CanStartRawRecording() == SDKERR_SUCCESS; +} +``` + +--- + +## Docker Deployment Patterns + +### Multi-Stage Build (Version-Flexible) + +```dockerfile +FROM ubuntu:22.04 AS builder + +# Install dependencies (stable) +RUN apt-get update && apt-get install -y \ + build-essential cmake \ + libx11-xcb1 libxcb-xfixes0 libxcb-shape0 \ + libglib2.0-dev libcurl4-openssl-dev + +# Copy SDK (any version) +COPY zoom-meeting-sdk-linux_*.tar.gz /tmp/ +RUN cd /tmp && tar xzf zoom-meeting-sdk-linux_*.tar.gz + +# Build app +COPY . /app +WORKDIR /app +RUN cmake -B build && cd build && make + +# Runtime stage +FROM ubuntu:22.04 +COPY --from=builder /app/build/meetingBot /usr/local/bin/ +COPY --from=builder /tmp/lib*.so /usr/local/lib/ + +# PulseAudio setup (required for raw audio) +RUN apt-get update && apt-get install -y pulseaudio && \ + mkdir -p ~/.config && \ + echo "[General]\nsystem.audio.type=default" > ~/.config/zoomus.conf + +CMD ["meetingBot"] +``` + +--- + +## Summary: Resilient Bot Checklist + +✅ **Always check capabilities** before calling methods +✅ **Use heap memory mode** for raw data +✅ **Handle authentication fallbacks** (OBF → ZAK → password) +✅ **Detect SDK version** at compile/runtime +✅ **Test with actual SDK headers**, not documentation examples +✅ **Setup PulseAudio** for Docker/headless environments +✅ **Parse error codes** to adapt behavior +✅ **Keep authentication tokens fresh** (regenerate before expiry) +✅ **Log everything** for debugging version-specific issues diff --git a/plugins/zoom-developers/skills/meeting-sdk/linux/linux.md b/plugins/zoom-developers/skills/meeting-sdk/linux/linux.md new file mode 100644 index 00000000..34cc9e9a --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/linux/linux.md @@ -0,0 +1,363 @@ +# Zoom Meeting SDK (Linux) + +Embed Zoom meeting capabilities into Linux applications for headless meeting bots and server-side integrations. + +## Prerequisites + +- Zoom app with Meeting SDK credentials (Client ID & Secret) +- Docker (recommended) or Linux environment (Ubuntu 22+, CentOS 8/9) +- C++ development toolchain (cmake, gcc, g++) +- GLib 2.0, libcurl, pthread + +> **Need help with authentication?** See the **[zoom-oauth](../../oauth/SKILL.md)** skill for JWT token generation. + +## Overview + +The Linux SDK is a **C++ native SDK** designed for: +- **Headless meeting bots** - Join meetings without UI +- **Raw media access** - Capture/send audio/video streams +- **Server-side automation** - Scalable meeting integrations + +## Quick Start + +### 1. Download Linux SDK + +Download from [Zoom Marketplace](https://marketplace.zoom.us/): +- Extract `zoom-meeting-sdk-linux_x86_64-{version}.tar` + +### 2. Setup Project Structure + +``` +your-project/ + demo/ + include/h/ # SDK headers + lib/zoom_meeting_sdk/ + libmeetingsdk.so + libmeetingsdk.so.1 # symlink + qt_libs/ + json/translations.json + meeting_sdk_demo.cpp + CMakeLists.txt + config.txt +``` + +Copy SDK files: +```bash +cp -r h/* demo/include/h/ +cp lib*.so demo/lib/zoom_meeting_sdk/ +cp -r qt_libs demo/lib/zoom_meeting_sdk/ +cp translation.json demo/lib/zoom_meeting_sdk/json/ + +# Create required symlink +cd demo/lib/zoom_meeting_sdk/ +ln -s libmeetingsdk.so libmeetingsdk.so.1 +``` + +### 3. Configure Credentials + +Create `config.txt`: +``` +meeting_number: "1234567890" +token: "YOUR_JWT_TOKEN" +meeting_password: "password123" +recording_token: "" +GetVideoRawData: "true" +GetAudioRawData: "true" +SendVideoRawData: "false" +SendAudioRawData: "false" +``` + +### 4. Build & Run + +```bash +cd demo +cmake -B build +cd build +make +cd ../bin +./meetingSDKDemo +``` + +## Core Workflow + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ InitSDK │───►│ AuthSDK │───►│ JoinMeeting │───►│ Raw Data │ +│ │ │ (JWT) │ │ │ │ Subscribe │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ + │ │ + ▼ ▼ + OnAuthComplete onInMeeting + callback callback +``` + +## Code Examples + +### 1. Initialize SDK + +```cpp +#include "zoom_sdk.h" + +USING_ZOOM_SDK_NAMESPACE + +void InitMeetingSDK() { + SDKError err(SDKERR_SUCCESS); + InitParam initParam; + + initParam.strWebDomain = "https://zoom.us"; + initParam.strSupportUrl = "https://zoom.us"; + initParam.emLanguageID = LANGUAGE_English; + initParam.enableLogByDefault = true; + initParam.enableGenerateDump = true; + + err = InitSDK(initParam); + if (err != SDKERR_SUCCESS) { + std::cerr << "Init meetingSdk:error" << std::endl; + } +} +``` + +### 2. Authenticate with JWT + +```cpp +#include "auth_service_interface.h" + +IAuthService* m_pAuthService; + +void OnAuthenticationComplete() { + JoinMeeting(); // Called on successful auth +} + +void AuthMeetingSDK() { + CreateAuthService(&m_pAuthService); + + m_pAuthService->SetEvent( + new AuthServiceEventListener(&OnAuthenticationComplete) + ); + + AuthContext param; + param.jwt_token = token.c_str(); // Your JWT token + m_pAuthService->SDKAuth(param); +} +``` + +### 3. Join Meeting + +```cpp +#include "meeting_service_interface.h" + +IMeetingService* m_pMeetingService; +ISettingService* m_pSettingService; + +void JoinMeeting() { + CreateMeetingService(&m_pMeetingService); + CreateSettingService(&m_pSettingService); + + // Set event listeners + m_pMeetingService->SetEvent( + new MeetingServiceEventListener(&onMeetingJoined, &onMeetingEnds, &onInMeeting) + ); + + // Prepare join parameters + JoinParam joinParam; + joinParam.userType = SDK_UT_WITHOUT_LOGIN; + + JoinParam4WithoutLogin& params = joinParam.param.withoutloginuserJoin; + params.meetingNumber = std::stoull(meeting_number); + params.userName = "BotUser"; + params.psw = meeting_password.c_str(); + params.isVideoOff = false; + params.isAudioOff = false; + + m_pMeetingService->Join(joinParam); +} +``` + +### 4. Subscribe to Raw Video + +```cpp +#include "rawdata/rawdata_renderer_interface.h" +#include "rawdata/zoom_rawdata_api.h" + +class ZoomSDKRenderer : public IZoomSDKRendererDelegate { +public: + void onRawDataFrameReceived(YUVRawDataI420* data) override { + // YUV420 (I420) format - contiguous planar data (no strides) + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Y plane: width * height bytes + outputFile.write(data->GetYBuffer(), width * height); + // U plane: (width/2) * (height/2) bytes + outputFile.write(data->GetUBuffer(), (width / 2) * (height / 2)); + // V plane: (width/2) * (height/2) bytes + outputFile.write(data->GetVBuffer(), (width / 2) * (height / 2)); + } + + void onRawDataStatusChanged(RawDataStatus status) override {} + void onRendererBeDestroyed() override {} +}; + +// Subscribe after joining +IZoomSDKRenderer* videoHelper; +ZoomSDKRenderer* videoSource = new ZoomSDKRenderer(); + +createRenderer(&videoHelper, videoSource); +videoHelper->setRawDataResolution(ZoomSDKResolution_720P); +videoHelper->subscribe(userID, RAW_DATA_TYPE_VIDEO); +``` + +### 5. Subscribe to Raw Audio + +```cpp +#include "rawdata/rawdata_audio_helper_interface.h" + +class ZoomSDKAudioRawData : public IZoomSDKAudioRawDataDelegate { +public: + void onMixedAudioRawDataReceived(AudioRawData* data) override { + // Process PCM audio (mixed from all participants) + pcmFile.write((char*)data->GetBuffer(), data->GetBufferLen()); + } + + void onOneWayAudioRawDataReceived(AudioRawData* data, uint32_t node_id) override { + // Process audio from specific participant + } +}; + +// Subscribe after joining +IZoomSDKAudioRawDataHelper* audioHelper = GetAudioRawdataHelper(); +audioHelper->subscribe(new ZoomSDKAudioRawData()); +``` + +### 6. GLib Main Loop + +```cpp +#include + +GMainLoop* loop; + +gboolean timeout_callback(gpointer data) { + return TRUE; // Keep running +} + +int main(int argc, char* argv[]) { + InitMeetingSDK(); + AuthMeetingSDK(); + + loop = g_main_loop_new(NULL, FALSE); + g_timeout_add(1000, timeout_callback, loop); + g_main_loop_run(loop); + + return 0; +} +``` + +## Available Examples + +| Example | Description | +|---------|-------------| +| **SkeletonExample** | Minimal join meeting - start here | +| **GetRawVideoAndAudioExample** | Subscribe to raw audio/video streams | +| **GetRawVideoAndAudioAPIExample** | API-based raw data access | +| **SendRawVideoAndAudioExample** | Send custom video/audio as virtual camera/mic | +| **ChatExample** | In-meeting chat functionality | +| **BreakoutExample** | Breakout room management | +| **AllInOneExample** | Complete demo with all features | +| **SendRawVideoAndAudioWithRTMSExample** | Raw data with RTMS integration | + +## Detailed References + +### High-Level Guides +- **[concepts/high-level-scenarios.md](concepts/high-level-scenarios.md)** - Production bot architectures (transcription, recording, AI assistant) +- **[meeting-sdk-bot.md](meeting-sdk-bot.md)** - Resilient bot pattern with retry logic + +### Platform Guide +- **[references/linux-reference.md](references/linux-reference.md)** - Dependencies, Docker setup, troubleshooting + +### Authentication +- **[../references/authorization.md](../references/authorization.md)** - SDK JWT generation +- **[../references/bot-authentication.md](../references/bot-authentication.md)** - Bot token types (ZAK, OBF, JWT) + +### Features +- **[../references/breakout-rooms.md](../references/breakout-rooms.md)** - Programmatic breakout room management +- **[../references/ai-companion.md](../references/ai-companion.md)** - AI Companion controls + +## Sample Repositories + +| Repository | Description | +|------------|-------------| +| [meetingsdk-headless-linux-sample](https://github.com/zoom/meetingsdk-headless-linux-sample) | Official headless bot with Docker | +| [meetingsdk-linux-raw-recording-sample](https://github.com/zoom/meetingsdk-linux-raw-recording-sample) | Raw audio/video access | + +## Playing Raw Video/Audio Files + +Raw YUV/PCM files have no headers - you must specify format explicitly. + +### Play Raw YUV Video +```bash +ffplay -video_size 640x360 -pixel_format yuv420p -f rawvideo video.yuv +``` + +### Convert YUV to MP4 +```bash +ffmpeg -video_size 640x360 -pixel_format yuv420p -f rawvideo -i video.yuv -c:v libx264 output.mp4 +``` + +### Play Raw PCM Audio +```bash +ffplay -f s16le -ar 32000 -ac 1 audio.pcm +``` + +### Convert PCM to WAV +```bash +ffmpeg -f s16le -ar 32000 -ac 1 -i audio.pcm output.wav +``` + +### Combine Video + Audio +```bash +ffmpeg -video_size 640x360 -pixel_format yuv420p -f rawvideo -i video.yuv \ + -f s16le -ar 32000 -ac 1 -i audio.pcm \ + -c:v libx264 -c:a aac -shortest output.mp4 +``` + +**Key flags:** +| Flag | Description | +|------|-------------| +| `-video_size WxH` | Frame dimensions (check output filename) | +| `-pixel_format yuv420p` | I420/YUV420 planar format | +| `-f rawvideo` | Raw video input (no container) | +| `-f s16le` | Signed 16-bit little-endian PCM | +| `-ar 32000` | Sample rate (Zoom uses 32kHz) | +| `-ac 1` | Mono (use `-ac 2` for stereo) | + +## Compilation Tips + +> **Note**: These are general patterns - specific methods/types may vary by SDK version. + +### Event Listener Interfaces +SDK event listener interfaces (`IMeetingServiceEvent`, `IMeetingParticipantsCtrlEvent`, etc.) have **many pure virtual methods**. You must implement ALL of them, even with empty bodies, or you'll get "invalid new-expression of abstract class type" errors. Always check the SDK headers for the complete list. + +### Include Order Issues +Some SDK headers don't include their own dependencies. If you encounter undefined type errors (like `AudioType` or `time_t`), try: +- Adding standard library includes (``, ``) before SDK headers +- Including related SDK headers in dependency order +- Checking which header defines the missing type + +### Method Signature Mismatches +Reference samples may have outdated method names. Always verify against the actual SDK header files - they are the authoritative source. + +## Authentication Requirements (2026 Update) + +> **Important**: Beginning **March 2, 2026**, apps joining meetings outside their account must be authorized. + +Use one of: +- **App Privilege Token (OBF)** - Recommended for bots (`app_privilege_token` in JoinParam) +- **ZAK Token** - Zoom Access Key (`userZAK` in JoinParam) +- **On Behalf Token** - For specific use cases (`onBehalfToken` in JoinParam) + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/linux/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/meeting/linux/index.html +- **Developer forum**: https://devforum.zoom.us/ +- **SDK download**: https://marketplace.zoom.us/ diff --git a/plugins/zoom-developers/skills/meeting-sdk/linux/meeting-sdk-bot.md b/plugins/zoom-developers/skills/meeting-sdk/linux/meeting-sdk-bot.md new file mode 100644 index 00000000..fed4aa83 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/linux/meeting-sdk-bot.md @@ -0,0 +1,825 @@ +# Meeting SDK Bot (Linux) + +Build resilient meeting bots that join Zoom meetings as visible participants using the Meeting SDK. + +## Overview + +Meeting SDK bots join as real participants (visible in participant list) and can access raw audio/video data for recording, transcription, or AI processing. + +**Use this approach when:** +- You need to be visible in the participant list +- You want to interact with meeting features (chat, reactions, etc.) +- You need local recording control +- You're processing your own meetings or have user consent + +**Alternative:** See [rtms/examples/rtms-bot.md](../../rtms/examples/rtms-bot.md) for invisible, read-only access via RTMS. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ MEETING SDK BOT FLOW │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ 1. Pre-Join: REST API │ +│ └── Get meeting schedule (number, password, start time) │ +│ └── Get OBF token for user (bot joins "on behalf of" user) │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 2. Join with Retry (OBF requires owner present) │ +│ └── Retry with configurable interval until owner joins │ +│ └── Circuit breaker: Stop after N attempts │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 3. Start Raw Recording (Meeting SDK singleton) │ +│ └── IMeetingRecordingController::StartRawRecording() │ +│ └── Subscribe to raw audio/video │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 4. Process Media Streams │ +│ └── Audio: PCM data via IZoomSDKAudioRawDataDelegate │ +│ └── Video: YUV420 frames via IZoomSDKVideoRawDataDelegate │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 5. Mid-Meeting: Connection Monitoring │ +│ └── Detect disconnections → Exponential backoff retry │ +│ └── Stop after N reconnection attempts │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Skills Required + +| Skill | Purpose | +|-------|---------| +| **zoom-rest-api** | Get meeting schedule, retrieve OBF token | +| **zoom-meeting-sdk** (Linux) | Join meeting, control recording, access raw media | + +## Choose the Recording Mode + +| Goal | Primary path | Skills | +|------|--------------|--------| +| Bot writes its own audio/video files | `StartRawRecording()` + raw audio/video delegates | `zoom-meeting-sdk` (Linux) | +| Zoom-hosted MP4/M4A/transcript assets after meeting end | Meeting/account cloud recording settings + `recording.completed` webhook + recordings download API | `zoom-rest-api` + `zoom-webhooks` | + +Use raw recording when the bot must process or persist media itself. Use the cloud-recording path when the requirement is post-meeting retrieval of Zoom-managed recording assets. + +## Automatic Join + Recording Flow + +```text +zoom-rest-api + -> get meeting metadata + -> get OBF or ZAK token +Meeting SDK Linux bot + -> join with retry + -> CanStartRawRecording() + -> StartRawRecording() + -> subscribe raw audio/video + -> write PCM/YUV or forward to AI pipeline +Optional post-meeting cloud path + -> zoom-webhooks recording.completed + -> zoom-rest-api recordings download +``` + +## Prerequisites + +- Zoom Meeting SDK v5.15+ (Linux) +- Meeting SDK JWT credentials (SDK Key + Secret) +- Server-to-Server OAuth app or OAuth app for REST API access +- REST API scopes: `meeting:read`, `user:read`, optionally `meeting:write` if triggering meetings +- Raw Data entitlement enabled (Admin → Account Settings → Meeting → "Allow access to raw data") + +## Configuration + +### Retry Parameters (Customizable) + +```cpp +// config.h or environment variables +struct BotConfig { + // Join retry (waiting for owner to be present) + int join_retry_attempts = 5; // Max join attempts (default: 5) + int join_retry_interval_ms = 60000; // Constant interval: 60s (default: 1min) + + // Mid-meeting reconnection (network failures) + int reconnect_max_attempts = 3; // Max reconnection attempts (default: 3) + int reconnect_base_delay_ms = 2000; // Initial delay: 2s (default: 2s) + // Exponential backoff: 2s, 4s, 8s... + + // Meeting schedule polling (if webhook unavailable) + int schedule_poll_interval_ms = 30000; // Poll every 30s (default: 30s) + + // Timeout settings + int auth_timeout_ms = 10000; // SDK auth timeout (default: 10s) + int join_timeout_ms = 30000; // Single join attempt timeout (default: 30s) +}; + +// Load from environment variables (recommended for production) +BotConfig loadConfig() { + BotConfig cfg; + + // Override defaults from env vars if present + const char* env; + + if ((env = getenv("BOT_JOIN_RETRY_ATTEMPTS"))) { + cfg.join_retry_attempts = atoi(env); + } + if ((env = getenv("BOT_JOIN_RETRY_INTERVAL_MS"))) { + cfg.join_retry_interval_ms = atoi(env); + } + if ((env = getenv("BOT_RECONNECT_MAX_ATTEMPTS"))) { + cfg.reconnect_max_attempts = atoi(env); + } + if ((env = getenv("BOT_RECONNECT_BASE_DELAY_MS"))) { + cfg.reconnect_base_delay_ms = atoi(env); + } + + return cfg; +} +``` + +### Customization Guide + +| Parameter | Default | When to Increase | When to Decrease | +|-----------|---------|------------------|------------------| +| `join_retry_attempts` | 5 | High-priority meetings, owner often late | Testing, short-lived meetings | +| `join_retry_interval_ms` | 60000 (1min) | Meetings with long pre-join buffer | Need faster failure detection | +| `reconnect_max_attempts` | 3 | Unstable networks, critical meetings | Batch processing, cost-sensitive | +| `reconnect_base_delay_ms` | 2000 (2s) | Network latency high (international) | Local network, low latency | + +**Recommended Ranges:** +- Join retry attempts: 3-10 +- Join retry interval: 30s-5min +- Reconnect attempts: 2-5 +- Reconnect base delay: 1s-5s + +**Examples:** + +```bash +# High-priority production bot (aggressive retries) +export BOT_JOIN_RETRY_ATTEMPTS=10 +export BOT_JOIN_RETRY_INTERVAL_MS=30000 # 30s +export BOT_RECONNECT_MAX_ATTEMPTS=5 +export BOT_RECONNECT_BASE_DELAY_MS=1000 # 1s + +# Cost-sensitive batch processing (conservative) +export BOT_JOIN_RETRY_ATTEMPTS=3 +export BOT_JOIN_RETRY_INTERVAL_MS=120000 # 2min +export BOT_RECONNECT_MAX_ATTEMPTS=2 +export BOT_RECONNECT_BASE_DELAY_MS=5000 # 5s + +# Development/testing (fail fast) +export BOT_JOIN_RETRY_ATTEMPTS=2 +export BOT_JOIN_RETRY_INTERVAL_MS=10000 # 10s +export BOT_RECONNECT_MAX_ATTEMPTS=1 +export BOT_RECONNECT_BASE_DELAY_MS=1000 # 1s +``` + +## Step 1: Get Meeting Info + OBF Token (REST API) + +### Get Meeting Schedule + +```bash +# Get meeting details +curl "https://api.zoom.us/v2/meetings/{meetingId}" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" +``` + +**Response:** +```json +{ + "id": 1234567890, + "topic": "Team Standup", + "start_time": "2026-02-09T15:00:00Z", + "password": "abc123", + "pmi": false +} +``` + +**Store:** meeting number, password, start time. + +### Get OBF Token + +The bot joins "on behalf of" a Zoom user. Get the user's OBF token: + +```bash +# Get OBF token for user +curl -X POST "https://api.zoom.us/v2/users/{userId}/token?type=obf&ttl=7200" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" +``` + +**Response:** +```json +{ + "token": "eyJhbGc...", + "expire_in": 7200 +} +``` + +**CRITICAL:** The bot cannot join until the **owner** (the user whose OBF token you're using) is present in the meeting. This is why retry logic is essential. + +**Error if REST API fails:** ABORT. No meeting info = cannot proceed. + +## Step 2: Join Meeting with Retry Logic + +### Join Implementation + +```cpp +#include +#include + +class MeetingBot { +private: + BotConfig config; + IMeetingService* meetingService; + bool joinSuccessful = false; + bool ownerNotPresentError = false; + +public: + // Attempt to join with retry logic + bool joinMeetingWithRetry( + uint64_t meetingNumber, + const string& password, + const string& obfToken, + const string& botDisplayName + ) { + for (int attempt = 1; attempt <= config.join_retry_attempts; attempt++) { + cout << "[JOIN] Attempt " << attempt << "/" + << config.join_retry_attempts << endl; + + // Attempt join + JoinParam joinParam; + joinParam.userType = SDK_UT_WITHOUT_LOGIN; + + JoinParam4WithoutLogin& param = joinParam.param.withoutloginuserJoin; + param.meetingNumber = meetingNumber; + param.userName = botDisplayName.c_str(); + param.psw = password.c_str(); + param.isVideoOff = true; // Bots typically don't send video + param.isAudioOff = false; // Need audio for transcription + param.app_privilege_token = obfToken.c_str(); // OBF token + + SDKError err = meetingService->Join(joinParam); + + if (err != SDKERR_SUCCESS) { + cerr << "[JOIN] Join() call failed: " << err << endl; + + // Check if it's a retriable error + if (shouldRetryJoin(err)) { + if (attempt < config.join_retry_attempts) { + cout << "[JOIN] Retrying in " + << (config.join_retry_interval_ms / 1000) + << "s..." << endl; + this_thread::sleep_for( + chrono::milliseconds(config.join_retry_interval_ms) + ); + continue; + } + } + + // Non-retriable error or out of attempts + cerr << "[JOIN] Giving up after " << attempt << " attempts" << endl; + return false; + } + + // Join() call succeeded, wait for callback + if (waitForJoinCallback()) { + cout << "[JOIN] Successfully joined meeting!" << endl; + return true; + } else { + cerr << "[JOIN] Join callback indicated failure" << endl; + + // If owner not present, retry + if (ownerNotPresentError && attempt < config.join_retry_attempts) { + cout << "[JOIN] Owner not present. Retrying in " + << (config.join_retry_interval_ms / 1000) << "s..." << endl; + this_thread::sleep_for( + chrono::milliseconds(config.join_retry_interval_ms) + ); + continue; + } + } + } + + cerr << "[JOIN] Failed to join after " + << config.join_retry_attempts << " attempts" << endl; + return false; + } + +private: + bool shouldRetryJoin(SDKError err) { + switch (err) { + case SDKERR_WRONG_USAGE: + case SDKERR_INVALID_PARAMETER: + case SDKERR_MODULE_LOAD_FAILED: + return false; // Configuration errors, don't retry + + case SDKERR_NETWORK_ERROR: + case SDKERR_SERVICE_FAILED: + return true; // Network/server issues, retry + + default: + return true; // Unknown error, retry to be safe + } + } + + bool waitForJoinCallback() { + // Wait for onMeetingStatusChanged callback + // Implementation depends on your callback handling + // Typical: use condition variable with timeout + + std::unique_lock lock(callbackMutex); + bool success = callbackCV.wait_for( + lock, + std::chrono::milliseconds(config.join_timeout_ms), + [this]{ return joinSuccessful || ownerNotPresentError; } + ); + + return success && joinSuccessful; + } + +public: + // Callback from SDK + void onMeetingStatusChanged(MeetingStatus status, int iResult) { + std::lock_guard lock(callbackMutex); + + if (status == MEETING_STATUS_INMEETING) { + joinSuccessful = true; + callbackCV.notify_one(); + } else if (status == MEETING_STATUS_FAILED) { + // Check error code for "owner not present" + if (iResult == MEETING_FAIL_OBF_OWNER_NOT_IN_MEETING) { + ownerNotPresentError = true; + } + joinSuccessful = false; + callbackCV.notify_one(); + } + } + +private: + std::mutex callbackMutex; + std::condition_variable callbackCV; +}; +``` + +### Common Join Errors + +| Error Code | Meaning | Action | +|------------|---------|--------| +| `MEETING_FAIL_OBF_OWNER_NOT_IN_MEETING` | OBF token owner not in meeting yet | RETRY (owner might join soon) | +| `MEETING_FAIL_MEETING_NOT_EXIST` | Meeting not started | RETRY if before end time | +| `MEETING_FAIL_INCORRECT_MEETING_NUMBER` | Wrong meeting ID | ABORT (config error) | +| `MEETING_FAIL_MEETING_NOT_START` | Meeting hasn't started | RETRY until start time | +| `MEETING_FAIL_INVALID_TOKEN` | OBF token invalid/expired | ABORT (need new token) | + +## Step 3: Start Raw Recording + +Once joined, request permission to access raw audio/video: + +```cpp +void onMeetingStatusChanged(MeetingStatus status, int iResult) { + if (status == MEETING_STATUS_INMEETING) { + cout << "[BOT] Joined successfully, starting raw recording..." << endl; + + // Get recording controller + IMeetingRecordingController* recordCtrl = + meetingService->GetMeetingRecordingController(); + + if (!recordCtrl) { + cerr << "[BOT] Failed to get recording controller" << endl; + return; + } + + // Check permission + SDKError canRecord = recordCtrl->CanStartRawRecording(); + if (canRecord != SDKERR_SUCCESS) { + cerr << "[BOT] Cannot start raw recording: " << canRecord << endl; + cerr << "[BOT] Check: Raw Data entitlement enabled in Admin settings?" << endl; + return; + } + + // Start raw recording (enables raw data flow) + SDKError err = recordCtrl->StartRawRecording(); + if (err != SDKERR_SUCCESS) { + cerr << "[BOT] StartRawRecording failed: " << err << endl; + return; + } + + cout << "[BOT] Raw recording started, subscribing to media..." << endl; + + // Subscribe to audio/video + subscribeToRawMedia(); + } +} +``` + +**IMPORTANT:** `StartRawRecording()` does NOT create a file. It enables access to raw audio/video data streams. + +### Manage Recording Lifecycle Explicitly + +The bot should treat raw recording as a capability switch plus media subscriptions: + +```cpp +class RecordingSession { +public: + void start(IMeetingService* meetingService) { + auto* recordCtrl = meetingService->GetMeetingRecordingController(); + if (!recordCtrl) { + throw std::runtime_error("recording_controller_unavailable"); + } + + SDKError canRecord = recordCtrl->CanStartRawRecording(); + if (canRecord != SDKERR_SUCCESS) { + throw std::runtime_error("raw_recording_not_permitted"); + } + + SDKError err = recordCtrl->StartRawRecording(); + if (err != SDKERR_SUCCESS) { + throw std::runtime_error("start_raw_recording_failed"); + } + + audioHelper = GetAudioRawdataHelper(); + audioHelper->subscribe(&audioDelegate, true); + + createRenderer(&videoRenderer, &videoDelegate); + videoRenderer->setRawDataResolution(ZoomSDKResolution_720P); + videoRenderer->subscribe(activeUserId, RAW_DATA_TYPE_VIDEO); + } + + void stop(IMeetingService* meetingService) { + if (audioHelper) { + audioHelper->unSubscribe(); + } + if (videoRenderer) { + videoRenderer->unSubscribe(); + } + + auto* recordCtrl = meetingService->GetMeetingRecordingController(); + if (recordCtrl) { + recordCtrl->StopRawRecording(); + } + } + +private: + IZoomSDKAudioRawDataHelper* audioHelper = nullptr; + IZoomSDKRenderer* videoRenderer = nullptr; + MyAudioDelegate audioDelegate; + MyVideoDelegate videoDelegate; + uint32_t activeUserId = 0; +}; +``` + +Persisting a recording is your job after raw data arrives. Typical outputs are: + +- PCM audio -> WAV/FLAC encoder or streaming transcription pipeline +- YUV420 video -> FFmpeg transcode to MP4 or frame-by-frame CV pipeline +- Mixed bot pipeline -> raw capture first, then post-process after leave + +## Step 4: Subscribe to Raw Audio/Video + +```cpp +void subscribeToRawMedia() { + // Subscribe to raw audio + IZoomSDKAudioRawDataDelegate* audioDelegate = new MyAudioDelegate(); + SDKError audioErr = meetingService->GetMeetingAudioController() + ->GetMeetingAudioHelper() + ->subscribe(audioDelegate, true); // true = mixed audio + + if (audioErr != SDKERR_SUCCESS) { + cerr << "[AUDIO] Subscribe failed: " << audioErr << endl; + } else { + cout << "[AUDIO] Subscribed to mixed audio" << endl; + } + + // Subscribe to raw video + IZoomSDKVideoRawDataDelegate* videoDelegate = new MyVideoDelegate(); + SDKError videoErr = meetingService->GetMeetingVideoController() + ->GetMeetingVideoHelper() + ->subscribe(videoDelegate); + + if (videoErr != SDKERR_SUCCESS) { + cerr << "[VIDEO] Subscribe failed: " << videoErr << endl; + } else { + cout << "[VIDEO] Subscribed to video streams" << endl; + } +} +``` + +### Cloud Recording Alternative + +If the requirement is **Zoom-managed cloud recording** instead of raw media capture, use Meeting SDK only for the joining bot and use API/webhook skills for the recording workflow: + +```bash +curl -X POST "https://api.zoom.us/v2/users/{userId}/meetings" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "topic": "Bot Recorded Meeting", + "type": 2, + "start_time": "2026-03-06T18:00:00Z", + "settings": { + "auto_recording": "cloud" + } + }' +``` + +Then subscribe to `recording.completed` and download assets through the recordings APIs: + +- `zoom-webhooks` -> receive `recording.completed` +- `zoom-rest-api` -> `GET /meetings/{meetingId}/recordings` or `GET /users/{userId}/recordings` + +Use this path when the desired output is Zoom-hosted MP4/M4A/transcript files rather than bot-owned raw PCM/YUV. + +## Step 5: Mid-Meeting Reconnection + +### Connection Monitoring + +```cpp +class MeetingBot { +private: + BotConfig config; + int reconnectionAttempt = 0; + +public: + void onMeetingStatusChanged(MeetingStatus status, int iResult) { + switch (status) { + case MEETING_STATUS_RECONNECTING: + cout << "[BOT] Connection lost, SDK is reconnecting..." << endl; + break; + + case MEETING_STATUS_FAILED: + case MEETING_STATUS_DISCONNECTING: + handleDisconnection(iResult); + break; + + case MEETING_STATUS_INMEETING: + // Reconnection successful + if (reconnectionAttempt > 0) { + cout << "[BOT] Reconnected successfully!" << endl; + reconnectionAttempt = 0; // Reset counter + } + break; + } + } + +private: + void handleDisconnection(int errorCode) { + reconnectionAttempt++; + + cout << "[RECONNECT] Disconnected (error: " << errorCode + << "), attempt " << reconnectionAttempt << "/" + << config.reconnect_max_attempts << endl; + + if (reconnectionAttempt >= config.reconnect_max_attempts) { + cerr << "[RECONNECT] Giving up after " + << reconnectionAttempt << " attempts" << endl; + cleanup(); + notifyFailure("Max reconnection attempts exceeded"); + return; + } + + // Exponential backoff: 2s, 4s, 8s... + int delay_ms = config.reconnect_base_delay_ms + * (1 << (reconnectionAttempt - 1)); + + cout << "[RECONNECT] Retrying in " << (delay_ms / 1000) << "s..." << endl; + + // Schedule reconnection + std::thread([this, delay_ms]() { + std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms)); + attemptRejoin(); + }).detach(); + } + + void attemptRejoin() { + // Re-use same meeting number, password, OBF token + // If OBF token expired, fetch new one from REST API first + + if (isOBFTokenExpired()) { + cout << "[RECONNECT] OBF token expired, fetching new one..." << endl; + // TODO: Call REST API to get fresh OBF token + } + + // Call join again + bool success = joinMeetingWithRetry( + cachedMeetingNumber, + cachedPassword, + cachedOBFToken, + cachedBotName + ); + + if (!success) { + cerr << "[RECONNECT] Rejoin failed" << endl; + cleanup(); + notifyFailure("Reconnection failed"); + } + } +}; +``` + +### Customizing Reconnection Behavior + +```cpp +// Example: Linear backoff instead of exponential +int delay_ms = config.reconnect_base_delay_ms * reconnectionAttempt; + +// Example: Capped exponential backoff (max 30s) +int delay_ms = std::min( + config.reconnect_base_delay_ms * (1 << (reconnectionAttempt - 1)), + 30000 // Cap at 30s +); + +// Example: Jittered backoff (avoid thundering herd) +int base_delay = config.reconnect_base_delay_ms * (1 << (reconnectionAttempt - 1)); +int jitter = rand() % 1000; // Random 0-1000ms +int delay_ms = base_delay + jitter; +``` + +## Recording Fallback: Local vs Cloud + +If raw recording fails, fall back to local or cloud recording: + +```cpp +void startRecordingWithFallback() { + IMeetingRecordingController* ctrl = + meetingService->GetMeetingRecordingController(); + + // Try raw recording first + if (ctrl->CanStartRawRecording() == SDKERR_SUCCESS) { + SDKError err = ctrl->StartRawRecording(); + if (err == SDKERR_SUCCESS) { + cout << "[RECORDING] Using raw recording" << endl; + return; + } + } + + // Fallback: Local recording + if (ctrl->CanStartRecording(true) == SDKERR_SUCCESS) { + SDKError err = ctrl->StartRecording( + chrono::system_clock::now(), + "/tmp/bot_recording.mp4" + ); + if (err == SDKERR_SUCCESS) { + cout << "[RECORDING] Using local recording" << endl; + return; + } + } + + // Fallback: Cloud recording + if (ctrl->CanStartCloudRecording() == SDKERR_SUCCESS) { + SDKError err = ctrl->StartCloudRecording(); + if (err == SDKERR_SUCCESS) { + cout << "[RECORDING] Using cloud recording" << endl; + return; + } + } + + cerr << "[RECORDING] All recording methods failed" << endl; +} +``` + +## Complete Resilient Bot Example + +```cpp +int main() { + // 1. Load configuration + BotConfig config = loadConfig(); + + // 2. Initialize Meeting SDK + InitParam initParam; + initParam.strWebDomain = "https://zoom.us"; + initParam.emLanguageID = LANGUAGE_English; + initParam.enableLogByDefault = true; + + SDKError err = InitSDK(initParam); + if (err != SDKERR_SUCCESS) { + cerr << "InitSDK failed: " << err << endl; + return 1; + } + + // 3. Authenticate SDK with JWT + AuthContext authCtx; + authCtx.jwt_token = generateJWT(SDK_KEY, SDK_SECRET); + + IAuthService* authService = CreateAuthService(); + authService->SDKAuth(authCtx); + + // Wait for auth callback... + + // 4. Fetch meeting info + OBF token from REST API + MeetingInfo meetingInfo = fetchMeetingInfoFromAPI(MEETING_ID); + string obfToken = fetchOBFTokenFromAPI(USER_ID); + + if (meetingInfo.empty() || obfToken.empty()) { + cerr << "ABORT: Failed to get meeting info or OBF token" << endl; + return 1; + } + + // 5. Join meeting with retry + MeetingBot bot(config); + bool joined = bot.joinMeetingWithRetry( + meetingInfo.number, + meetingInfo.password, + obfToken, + "Transcription Bot" + ); + + if (!joined) { + cerr << "ABORT: Failed to join meeting" << endl; + return 1; + } + + // 6. SDK callbacks handle: StartRawRecording, subscribe, reconnection + + // 7. Keep running until meeting ends + bot.runEventLoop(); + + // 8. Cleanup + bot.cleanup(); + CleanUPSDK(); + + return 0; +} +``` + +## Comparison: Meeting SDK Bot vs RTMS Bot + +| Aspect | Meeting SDK Bot | RTMS Bot | +|--------|----------------|----------| +| **Visibility** | Visible participant | Invisible (read-only service) | +| **Authentication** | JWT + OBF token | REST API trigger + webhook | +| **Join Dependency** | Owner must be present | No dependency on participants | +| **Retry Logic** | Required (owner presence) | Not applicable (webhook-based) | +| **Media Access** | Raw audio/video/share via SDK | Audio/video/text/share/chat via WebSocket | +| **Recording Control** | Full (local, cloud, raw) | None (read-only) | +| **Interaction** | Can send chat, reactions | Cannot interact | +| **Resource Usage** | Higher (full SDK) | Lower (WebSocket only) | +| **Use Case** | Interactive bots, recording, moderation | Passive transcription, analytics | + +**Choose Meeting SDK Bot when:** +- You need to interact with the meeting (chat, reactions) +- You need local recording control +- You want to be visible in participant list +- You're processing your own meetings + +**Choose RTMS Bot when:** +- You only need to observe/transcribe +- You want minimal resource usage +- You prefer invisible operation +- You're processing external meetings + +## Troubleshooting + +### Bot Never Joins (OBF Owner Not Present) + +**Symptom:** All join attempts fail with "owner not in meeting" + +**Solution:** +1. Verify owner (OBF token holder) has joined the meeting +2. Increase `join_retry_attempts` or `join_retry_interval_ms` +3. Check meeting start time - don't attempt join before meeting starts +4. Consider webhook: Listen for `meeting.participant_joined` event for owner + +### Raw Recording Permission Denied + +**Symptom:** `CanStartRawRecording()` returns error + +**Solution:** +1. Admin → Account Settings → Meeting → In Meeting (Advanced) +2. Enable "Allow access to raw data (audio, video, sharing) for Meeting SDK" +3. Requires "Raw Data" entitlement from Zoom support + +### Frequent Disconnections During Meeting + +**Symptom:** Bot reconnects multiple times, then gives up + +**Solution:** +1. Increase `reconnect_max_attempts` (e.g., 5 instead of 3) +2. Increase `reconnect_base_delay_ms` if network is slow +3. Check server network stability +4. Monitor CPU/memory usage (insufficient resources can cause disconnects) + +### OBF Token Expires During Meeting + +**Symptom:** Reconnection fails with "invalid token" + +**Solution:** +1. Fetch fresh OBF token in `attemptRejoin()` before rejoining +2. Monitor token expiration (`expire_in` from API response) +3. Request longer-lived tokens (max TTL: 7200s = 2 hours) + +## Resources + +- **Meeting SDK Linux**: https://developers.zoom.us/docs/meeting-sdk/linux/ +- **Raw Data Guide**: https://developers.zoom.us/docs/meeting-sdk/linux/add-features/raw-data/ +- **OBF FAQ**: https://developers.zoom.us/docs/meeting-sdk/obf-faq/ +- **REST API - Get Meeting**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meeting +- **REST API - Get OBF Token**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/userToken +- **RTMS Bot Alternative**: [../../rtms/examples/rtms-bot.md](../../rtms/examples/rtms-bot.md) diff --git a/plugins/zoom-developers/skills/meeting-sdk/linux/references/linux-reference.md b/plugins/zoom-developers/skills/meeting-sdk/linux/references/linux-reference.md new file mode 100644 index 00000000..3eef9000 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/linux/references/linux-reference.md @@ -0,0 +1,712 @@ +# Meeting SDK - Linux Reference + +Complete reference for Zoom Meeting SDK on Linux including dependencies, Docker setup, and troubleshooting. + +## System Requirements + +- **OS**: Ubuntu 22+, CentOS 8/9, Oracle Linux 8 +- **Architecture**: x86_64 +- **Build Tools**: cmake 3.16+, gcc/g++ with C++11 support + +## Dependencies + +### Ubuntu 22/23 + +```bash +# Build essentials +apt-get update +apt-get install -y build-essential cmake + +# X11/XCB libraries (required by SDK) +apt-get install -y --no-install-recommends --no-install-suggests \ + libx11-xcb1 \ + libxcb-xfixes0 \ + libxcb-shape0 \ + libxcb-shm0 \ + libxcb-randr0 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-xtest0 + +# Optional but recommended +apt-get install -y --no-install-recommends --no-install-suggests \ + libdbus-1-3 \ + libglib2.0-0 \ + libgbm1 \ + libxfixes3 \ + libgl1 \ + libdrm2 \ + libgssapi-krb5-2 + +# For curl-based JWT fetching +apt-get install -y \ + libcurl4-openssl-dev \ + openssl \ + ca-certificates \ + pkg-config + +# Audio support (PulseAudio) +apt-get install -y \ + libasound2 \ + libasound2-plugins \ + alsa \ + alsa-utils \ + alsa-oss \ + pulseaudio \ + pulseaudio-utils + +# Optional: FFmpeg for video processing +apt-get install -y libavformat-dev libavfilter-dev libavdevice-dev ffmpeg + +# If SDL2 errors occur +apt-get install -y libegl-mesa0 libsdl2-dev g++-multilib +``` + +### CentOS 8/9 + +```bash +# Build essentials +sudo yum install cmake gcc gcc-c++ + +# Enable required repos (CentOS 9) +sudo dnf config-manager --set-enabled crb +sudo dnf install epel-release epel-next-release + +# XCB libraries +sudo yum install \ + libxcb-devel \ + xcb-util-devel \ + xcb-util-image \ + xcb-util-keysyms + +# OpenGL/Mesa (for runtime) +sudo yum install \ + mesa-libGL \ + mesa-libGL-devel \ + mesa-dri-drivers + +# Curl support +sudo yum install -y openssl-devel libcurl-devel + +# PulseAudio +sudo yum install -y pulseaudio pulseaudio-utils + +# Optional: FFmpeg +sudo yum install -y libavformat-dev libavfilter-dev libavdevice-dev ffmpeg + +# If SDL2 errors occur +sudo yum install -y SDL2-devel +``` + +## CMakeLists.txt Template + +```cmake +cmake_minimum_required(VERSION 3.16) + +project(meetingSDKDemo CXX) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -g -O0") +add_definitions(-std=c++11) +set(CMAKE_BUILD_TYPE Debug) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) + +find_package(PkgConfig REQUIRED) +find_package(ZLIB REQUIRED) + +# GLib (required for main loop) +pkg_check_modules(GLIB REQUIRED glib-2.0) +pkg_check_modules(GIO REQUIRED gio-2.0) + +# Include directories +include_directories(${CMAKE_SOURCE_DIR}/include) +include_directories(${CMAKE_SOURCE_DIR}/include/h) +include_directories(${GLIB_INCLUDE_DIRS} ${GIO_INCLUDE_DIRS}) + +# Common GLib paths +include_directories(/usr/include/glib-2.0/) +include_directories(/usr/include/glib-2.0/glib) +include_directories(/usr/lib/x86_64-linux-gnu/glib-2.0/include/) +include_directories(/usr/lib64/glib-2.0/include/) + +# Link directories +link_directories(${GLIB_LIBRARY_DIRS} ${GIO_LIBRARY_DIRS}) +link_directories(${CMAKE_SOURCE_DIR}/lib/zoom_meeting_sdk) +link_directories(${CMAKE_SOURCE_DIR}/lib/zoom_meeting_sdk/qt_libs) +link_directories(${CMAKE_SOURCE_DIR}/lib/zoom_meeting_sdk/qt_libs/Qt/lib) + +add_definitions(${GLIB_CFLAGS_OTHER} ${GIO_CFLAGS_OTHER}) + +# Source files +add_executable(meetingSDKDemo + ${CMAKE_SOURCE_DIR}/meeting_sdk_demo.cpp + ${CMAKE_SOURCE_DIR}/AuthServiceEventListener.cpp + ${CMAKE_SOURCE_DIR}/MeetingServiceEventListener.cpp + ${CMAKE_SOURCE_DIR}/MeetingReminderEventListener.cpp + ${CMAKE_SOURCE_DIR}/MeetingParticipantsCtrlEventListener.cpp + ${CMAKE_SOURCE_DIR}/MeetingRecordingCtrlEventListener.cpp + # Raw data handlers (if needed) + ${CMAKE_SOURCE_DIR}/ZoomSDKRenderer.cpp + ${CMAKE_SOURCE_DIR}/ZoomSDKAudioRawData.cpp + ${CMAKE_SOURCE_DIR}/ZoomSDKVideoSource.cpp + ${CMAKE_SOURCE_DIR}/ZoomSDKVirtualAudioMicEvent.cpp +) + +# Link libraries +target_link_libraries(meetingSDKDemo ${GLIB_LIBRARIES} ${GIO_LIBRARIES}) +target_link_libraries(meetingSDKDemo gcc_s gcc) +target_link_libraries(meetingSDKDemo meetingsdk) +target_link_libraries(meetingSDKDemo glib-2.0) +target_link_libraries(meetingSDKDemo curl) +target_link_libraries(meetingSDKDemo pthread) + +# Create symlink for SDK +execute_process(COMMAND ln -sf libmeetingsdk.so libmeetingsdk.so.1 + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/lib/zoom_meeting_sdk +) + +# Copy config to bin +configure_file(${CMAKE_SOURCE_DIR}/config.txt ${CMAKE_SOURCE_DIR}/bin/config.txt COPYONLY) + +# Copy SDK files to bin +file(COPY ${CMAKE_SOURCE_DIR}/lib/zoom_meeting_sdk/ DESTINATION ${CMAKE_SOURCE_DIR}/bin) +``` + +## Configuration File + +### config.txt Format + +``` +meeting_number: "1234567890" +token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +meeting_password: "abc123" +recording_token: "" +onBehalfOf_Token: "" +GetVideoRawData: "true" +GetAudioRawData: "true" +SendVideoRawData: "false" +SendAudioRawData: "false" +``` + +### config.json Format (Alternative) + +```json +{ + "meeting_number": "1234567890", + "token": "YOUR_JWT_TOKEN", + "meeting_password": "abc123", + "recording_token": "", + "remote_url": "https://your-auth-endpoint.com", + "useJWTTokenFromWebService": "false", + "useRecordingTokenFromWebService": "false" +} +``` + +## Docker Setup + +### Dockerfile (Ubuntu) + +```dockerfile +FROM ubuntu:22.04 + +# Prevent interactive prompts +ENV DEBIAN_FRONTEND=noninteractive + +# Install dependencies +RUN apt-get update && apt-get install -y \ + build-essential cmake \ + libx11-xcb1 libxcb-xfixes0 libxcb-shape0 libxcb-shm0 \ + libxcb-randr0 libxcb-image0 libxcb-keysyms1 libxcb-xtest0 \ + libdbus-1-3 libglib2.0-0 libgbm1 libxfixes3 libgl1 libdrm2 \ + libcurl4-openssl-dev openssl ca-certificates pkg-config \ + pulseaudio pulseaudio-utils \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy project files +COPY . . + +# Build +RUN cd demo && cmake -B build && cd build && make + +# Setup audio config +RUN mkdir -p ~/.config && \ + echo "[General]" > ~/.config/zoomus.conf && \ + echo "system.audio.type=default" >> ~/.config/zoomus.conf + +CMD ["./demo/bin/meetingSDKDemo"] +``` + +### PulseAudio Setup (Headless) + +Create `setup-pulseaudio.sh`: + +```bash +#!/bin/bash + +# Start PulseAudio daemon +pulseaudio --start --exit-idle-time=-1 + +# Create virtual speaker +pactl load-module module-null-sink sink_name=virtual_speaker sink_properties=device.description="Virtual_Speaker" + +# Create virtual microphone +pactl load-module module-null-sink sink_name=virtual_mic sink_properties=device.description="Virtual_Microphone" + +# Create zoomus.conf +mkdir -p ~/.config +cat > ~/.config/zoomus.conf << EOF +[General] +system.audio.type=default +EOF + +echo "PulseAudio configured for headless operation" +``` + +### Docker Compose + +```yaml +version: '3.8' +services: + meeting-bot: + build: . + environment: + - PULSE_SERVER=unix:/run/user/1000/pulse/native + volumes: + - ./config.txt:/app/demo/bin/config.txt + - /run/user/1000/pulse:/run/user/1000/pulse + network_mode: host +``` + +## Event Listeners + +### AuthServiceEventListener + +```cpp +// AuthServiceEventListener.h +#include "auth_service_interface.h" + +class AuthServiceEventListener : public IAuthServiceEvent { +public: + AuthServiceEventListener(void (*onComplete)()) + : onAuthComplete(onComplete) {} + + void onAuthenticationReturn(AuthResult ret) override { + if (ret == AUTHRET_SUCCESS && onAuthComplete) { + onAuthComplete(); + } + } + + void onLoginReturnWithReason(LOGINSTATUS ret, IAccountInfo* info, LoginFailReason reason) override {} + void onLogout() override {} + void onZoomIdentityExpired() override {} + void onZoomAuthIdentityExpired() override {} + +private: + void (*onAuthComplete)(); +}; +``` + +### MeetingServiceEventListener + +```cpp +// MeetingServiceEventListener.h +#include "meeting_service_interface.h" + +class MeetingServiceEventListener : public IMeetingServiceEvent { +public: + MeetingServiceEventListener( + void (*onJoined)(), + void (*onEnded)(), + void (*onInMeeting)() + ) : onMeetingJoined(onJoined), + onMeetingEnded(onEnded), + onInMeetingCallback(onInMeeting) {} + + void onMeetingStatusChanged(MeetingStatus status, int iResult) override { + if (status == MEETING_STATUS_CONNECTING) { + if (onMeetingJoined) onMeetingJoined(); + } + else if (status == MEETING_STATUS_INMEETING) { + if (onInMeetingCallback) onInMeetingCallback(); + } + else if (status == MEETING_STATUS_ENDED) { + if (onMeetingEnded) onMeetingEnded(); + } + } + + void onMeetingStatisticsWarningNotification(StatisticsWarningType type) override {} + void onMeetingParameterNotification(const MeetingParameter* param) override {} + void onSuspendParticipantsActivities() override {} + void onAICompanionActiveChangeNotice(bool isActive) override {} + +private: + void (*onMeetingJoined)(); + void (*onMeetingEnded)(); + void (*onInMeetingCallback)(); +}; +``` + +### MeetingParticipantsCtrlEventListener + +```cpp +// MeetingParticipantsCtrlEventListener.h +#include "meeting_service_components/meeting_participants_ctrl_interface.h" + +class MeetingParticipantsCtrlEventListener : public IMeetingParticipantsCtrlEvent { +public: + MeetingParticipantsCtrlEventListener( + void (*onHost)(), + void (*onCoHost)() + ) : onIsHost(onHost), onIsCoHost(onCoHost) {} + + void onUserJoin(IList* lstUserID, const zchar_t* strUserList) override {} + void onUserLeft(IList* lstUserID, const zchar_t* strUserList) override {} + void onHostChangeNotification(unsigned int userId) override { + if (onIsHost) onIsHost(); + } + void onCoHostChangeNotification(unsigned int userId, bool isCoHost) override { + if (isCoHost && onIsCoHost) onIsCoHost(); + } + void onLowOrRaiseHandStatusChanged(bool bLow, unsigned int userid) override {} + +private: + void (*onIsHost)(); + void (*onIsCoHost)(); +}; +``` + +## Raw Data Requirements + +### Recording Permission + +Raw data access requires one of: +1. **Host** status +2. **Co-host** status +3. **Recording permission** from host +4. **Recording token** (app_privilege_token) + +```cpp +// Check and start raw recording +void CheckAndStartRawRecording() { + IMeetingRecordingController* recordCtrl = + m_pMeetingService->GetMeetingRecordingController(); + + SDKError canStart = recordCtrl->CanStartRawRecording(); + + if (canStart == SDKERR_SUCCESS) { + recordCtrl->StartRawRecording(); + // Now subscribe to raw data + } else { + std::cout << "Need host/cohost/recording permission" << std::endl; + } +} +``` + +### Video Resolutions + +```cpp +videoHelper->setRawDataResolution(ZoomSDKResolution_90P); +videoHelper->setRawDataResolution(ZoomSDKResolution_180P); +videoHelper->setRawDataResolution(ZoomSDKResolution_360P); +videoHelper->setRawDataResolution(ZoomSDKResolution_720P); +videoHelper->setRawDataResolution(ZoomSDKResolution_1080P); +``` + +### Audio Format + +- **Format**: PCM (raw) +- **Sample Rate**: 32000 Hz +- **Channels**: Mono or Stereo +- **Bit Depth**: 16-bit + +## Compilation Tips + +> **Note**: Specific methods/types may vary by SDK version. Always check SDK headers. + +### Event Listener Interfaces +SDK event listener interfaces have many pure virtual methods. **Implement ALL of them** (even with empty bodies) or you'll get "invalid new-expression of abstract class type" errors. Check the SDK header for the complete interface definition. + +### Include Order Issues +Some SDK headers don't include their dependencies. If you get undefined type errors: +- Add standard includes (``, ``) before SDK headers +- Include related SDK headers in dependency order (e.g., `meeting_audio_interface.h` before `meeting_participants_ctrl_interface.h`) + +### Method Signatures +Reference samples may have outdated method names. The SDK header files are the authoritative source - always verify method signatures there. + +## Troubleshooting + +### Segmentation Fault on AuthSDK + +**Cause**: OpenSSL version incompatibility (needs 1.1.x) + +**Fix** (Ubuntu): +```bash +cd /tmp +wget http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2.20_amd64.deb +sudo dpkg -i libssl1.1_1.1.1f-1ubuntu2.20_amd64.deb +``` + +### libGL Error (Mesa) + +``` +libGL error: MESA-LOADER: failed to open swrast +``` + +**Fix**: +```bash +# Ubuntu +apt-get install mesa-libGL mesa-dri-drivers + +# CentOS +yum install mesa-libGL mesa-libGL-devel mesa-dri-drivers +``` + +### XCB Libraries Missing + +``` +libxcb-image.so.0 not found +libxcb-keysyms.so.1 not found +``` + +**Fix**: +```bash +# Ubuntu +apt-get install libxcb-image0 libxcb-keysyms1 + +# CentOS +yum install xcb-util-image xcb-util-keysyms +``` + +### No Audio in Docker + +**Fix**: Create zoomus.conf before running: +```bash +mkdir -p ~/.config +echo -e "[General]\nsystem.audio.type=default" > ~/.config/zoomus.conf +``` + +### Cannot Start Raw Recording + +**Cause**: No recording permission + +**Fix**: Either: +1. Wait for host to grant recording permission +2. Use `recording_token` in config (get from [Recording Token API](https://developers.zoom.us/docs/meeting-sdk/apis/#operation/meetingLocalRecordingJoinToken)) +3. Join as host using `onBehalfOf_Token` + +## Cleanup + +```cpp +void CleanSDK() { + if (videoHelper) videoHelper->unSubscribe(); + if (audioHelper) audioHelper->unSubscribe(); + + if (m_pAuthService) { + DestroyAuthService(m_pAuthService); + m_pAuthService = NULL; + } + if (m_pSettingService) { + DestroySettingService(m_pSettingService); + m_pSettingService = NULL; + } + if (m_pMeetingService) { + DestroyMeetingService(m_pMeetingService); + m_pMeetingService = NULL; + } + + CleanUPSDK(); +} +``` + +## API Reference + +### InitParam Structure + +```cpp +struct tagInitParam { + const zchar_t* strWebDomain; // "https://zoom.us" + const zchar_t* strBrandingName; // Custom branding name + const zchar_t* strSupportUrl; // Support URL + SDK_LANGUAGE_ID emLanguageID; // LANGUAGE_English, etc. + bool enableGenerateDump; // Enable crash dump + bool enableLogByDefault; // Enable logging + unsigned int uiLogFileSize; // Log file size (MB, default: 5) + RawDataOptions rawdataOpts; // Raw data options + ConfigurableOptions obConfigOpts; // Config options + int wrapperType; // SDK wrapper type +}; +``` + +### IAuthService Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `SetEvent(IAuthServiceEvent*)` | Set auth callback | `SDKError` | +| `SDKAuth(AuthContext&)` | Authenticate with JWT | `SDKError` | +| `GetAuthResult()` | Get auth status | `AuthResult` | +| `GetSDKIdentity()` | Get SDK identity | `const zchar_t*` | +| `LogOut()` | Logout | `SDKError` | +| `GetAccountInfo()` | Get account info | `IAccountInfo*` | +| `GetLoginStatus()` | Get login status | `LOGINSTATUS` | + +### IMeetingService Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `SetEvent(IMeetingServiceEvent*)` | Set meeting callback | `SDKError` | +| `Join(JoinParam&)` | Join meeting | `SDKError` | +| `Start(StartParam&)` | Start meeting | `SDKError` | +| `Leave(LeaveMeetingCmd)` | Leave meeting | `SDKError` | +| `GetMeetingStatus()` | Get status | `MeetingStatus` | +| `GetMeetingInfo()` | Get meeting info | `IMeetingInfo*` | +| `GetMeetingVideoController()` | Video control | `IMeetingVideoController*` | +| `GetMeetingAudioController()` | Audio control | `IMeetingAudioController*` | +| `GetMeetingRecordingController()` | Recording control | `IMeetingRecordingController*` | +| `GetMeetingParticipantsController()` | Participants | `IMeetingParticipantsController*` | +| `GetMeetingChatController()` | Chat control | `IMeetingChatController*` | + +### JoinParam4WithoutLogin Structure + +```cpp +struct JoinParam4WithoutLogin { + UINT64 meetingNumber; // Meeting number + const zchar_t* userName; // Display name + const zchar_t* psw; // Meeting password + const zchar_t* vanityID; // Personal link name + const zchar_t* customer_key; // Customer key + const zchar_t* webinarToken; // Webinar token + const zchar_t* userZAK; // Zoom Access Key + const zchar_t* app_privilege_token; // App privilege token (for raw data) + const zchar_t* onBehalfToken; // On behalf token + bool isVideoOff; // Start with video off + bool isAudioOff; // Start with audio off +}; +``` + +### IZoomSDKRenderer Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `setRawDataResolution(ZoomSDKResolution)` | Set resolution | `SDKError` | +| `subscribe(uint32_t userId, ZoomSDKRawDataType)` | Subscribe to video | `SDKError` | +| `unSubscribe()` | Unsubscribe | `SDKError` | +| `getResolution()` | Get resolution | `ZoomSDKResolution` | +| `getSubscribeId()` | Get user ID | `uint32_t` | + +**ZoomSDKResolution values:** `ZoomSDKResolution_90P`, `_180P`, `_360P`, `_720P`, `_1080P` + +### YUVRawDataI420 Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `GetStreamWidth()` | Video width | `uint32_t` | +| `GetStreamHeight()` | Video height | `uint32_t` | +| `GetYBuffer()` | Y plane buffer | `char*` | +| `GetUBuffer()` | U plane buffer | `char*` | +| `GetVBuffer()` | V plane buffer | `char*` | +| `GetBufferLen()` | Total buffer length | `uint32_t` | +| `GetRotation()` | Rotation angle | `int` | +| `GetAlphaBuffer()` | Alpha channel | `char*` | + +**Video Format:** +- YUV420 (I420) contiguous planar format (no strides) +- Y plane: `width * height` bytes +- U plane: `(width/2) * (height/2)` bytes +- V plane: `(width/2) * (height/2)` bytes +- Total size: `width * height * 1.5` bytes + +### AudioRawData Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `GetBuffer()` | Audio buffer | `char*` | +| `GetBufferLen()` | Buffer length (bytes) | `uint32_t` | +| `GetSampleRate()` | Sample rate (Hz) | `uint32_t` | +| `GetChannelNum()` | Number of channels | `uint32_t` | + +**Audio Format:** +- PCM (uncompressed), 16-bit, little-endian +- Sample rate: 32000 Hz (typical) +- Channels: 1 (mono) or 2 (stereo) + +### Playing Raw Files with FFmpeg + +Raw files have no headers - specify format explicitly: + +```bash +# Play YUV video (adjust dimensions to match your output) +ffplay -video_size 640x360 -pixel_format yuv420p -f rawvideo video.yuv + +# Convert YUV to MP4 +ffmpeg -video_size 640x360 -pixel_format yuv420p -f rawvideo -i video.yuv -c:v libx264 output.mp4 + +# Play PCM audio +ffplay -f s16le -ar 32000 -ac 1 audio.pcm + +# Convert PCM to WAV +ffmpeg -f s16le -ar 32000 -ac 1 -i audio.pcm output.wav + +# Combine video + audio into MP4 +ffmpeg -video_size 640x360 -pixel_format yuv420p -f rawvideo -i video.yuv \ + -f s16le -ar 32000 -ac 1 -i audio.pcm \ + -c:v libx264 -c:a aac -shortest output.mp4 +``` + +### Error Codes (SDKError) + +| Code | Description | +|------|-------------| +| `SDKERR_SUCCESS` | Success | +| `SDKERR_INVALID_PARAMETER` | Invalid parameter | +| `SDKERR_UNINITIALIZE` | SDK not initialized | +| `SDKERR_UNAUTHENTICATION` | Not authenticated | +| `SDKERR_NO_PERMISSION` | No permission | +| `SDKERR_NO_AUDIODEVICE_ISFOUND` | No audio device | +| `SDKERR_NO_VIDEODEVICE_ISFOUND` | No video device | +| `SDKERR_INTERNAL_ERROR` | Internal error | +| `SDKERR_SERVICE_FAILED` | Service failed | +| `SDKERR_MEMORY_FAILED` | Memory allocation failed | +| `SDKERR_TOO_FREQUENT_CALL` | API called too frequently | + +### Authentication Results (AuthResult) + +| Result | Description | +|--------|-------------| +| `AUTHRET_SUCCESS` | Authentication successful | +| `AUTHRET_KEYORSECRETEMPTY` | Key or secret empty | +| `AUTHRET_JWTTOKENWRONG` | JWT token invalid | +| `AUTHRET_OVERTIME` | Operation timed out | + +### Meeting Status (MeetingStatus) + +| Status | Description | +|--------|-------------| +| `MEETING_STATUS_IDLE` | No meeting | +| `MEETING_STATUS_CONNECTING` | Connecting | +| `MEETING_STATUS_INMEETING` | In meeting | +| `MEETING_STATUS_RECONNECTING` | Reconnecting | +| `MEETING_STATUS_FAILED` | Failed | +| `MEETING_STATUS_ENDED` | Meeting ended | +| `MEETING_STATUS_WAITINGFORHOST` | Waiting for host | + +## Authentication Requirements (2026 Update) + +> **Important**: Beginning **March 2, 2026**, apps joining meetings outside their account must be authorized. + +Options: +- **App Privilege Token (OBF)** - Recommended for bots +- **ZAK Token** - Zoom Access Key +- **On Behalf Token** - For specific use cases + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/linux/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/meeting/linux/index.html +- **Headless sample**: https://github.com/zoom/meetingsdk-headless-linux-sample +- **Raw recording sample**: https://github.com/zoom/meetingsdk-linux-raw-recording-sample +- **Auth endpoint**: https://github.com/zoom/meetingsdk-auth-endpoint-sample diff --git a/plugins/zoom-developers/skills/meeting-sdk/macos/RUNBOOK.md b/plugins/zoom-developers/skills/meeting-sdk/macos/RUNBOOK.md new file mode 100644 index 00000000..667b93be --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/macos/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK macOS 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 Meeting SDK embed path for macOS (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/macos/ +- https://marketplacefront.zoom.us/sdk/meeting/macos/annotated.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/macos/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/macos/` diff --git a/plugins/zoom-developers/skills/meeting-sdk/macos/SKILL.md b/plugins/zoom-developers/skills/meeting-sdk/macos/SKILL.md new file mode 100644 index 00000000..71cf1df6 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/macos/SKILL.md @@ -0,0 +1,32 @@ +--- +name: zoom-meeting-sdk-macos +description: | + Zoom Meeting SDK for macOS native apps. Use when embedding Zoom meetings in macOS with + default/custom UI, PKCE + SDK auth, host start/join flows, and desktop meeting feature controllers. +--- + +# Zoom Meeting SDK (macOS) + +Use this skill when building macOS apps with embedded Zoom meeting capabilities. + +## Start Here + +1. [macos.md](macos.md) +2. [concepts/lifecycle-workflow.md](concepts/lifecycle-workflow.md) +3. [concepts/architecture.md](concepts/architecture.md) +4. [examples/join-start-pattern.md](examples/join-start-pattern.md) +5. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +6. [references/macos-reference-map.md](references/macos-reference-map.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +9. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Key Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/macos/ +- API reference: https://marketplacefront.zoom.us/sdk/meeting/macos/annotated.html +- Broader guide: [../SKILL.md](../SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/meeting-sdk/macos/concepts/architecture.md b/plugins/zoom-developers/skills/meeting-sdk/macos/concepts/architecture.md new file mode 100644 index 00000000..7058d3a9 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/macos/concepts/architecture.md @@ -0,0 +1,23 @@ +# macOS Architecture + +## Layer Model + +- App shell (AppKit/Swift UI integration layer). +- Meeting coordinator (join/start state machine). +- SDK service/controller layer (ZoomSDK class + service delegates). +- Backend signing/token service. + +## Reference Flow + +```text +macOS App -> Meeting Coordinator -> Backend Signature Service -> Meeting SDK + ^ | | | + | v v v +User actions State + retry policy Role/token policy Service delegates +``` + +## Why this split + +- Isolates security logic from desktop client. +- Makes controller/delegate ordering explicit. +- Reduces upgrade regression blast radius. diff --git a/plugins/zoom-developers/skills/meeting-sdk/macos/concepts/lifecycle-workflow.md b/plugins/zoom-developers/skills/meeting-sdk/macos/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..23b34298 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/macos/concepts/lifecycle-workflow.md @@ -0,0 +1,17 @@ +# macOS Lifecycle Workflow + +## Core Sequence + +1. App startup and SDK initialization. +2. SDK auth callback success. +3. Join/start flow selection. +4. Meeting controller registration and in-meeting feature activation. +5. Leave/end handling. +6. SDK cleanup and process teardown. + +## Failure Domains + +- Auth/signature mismatch. +- Join/start parameter mismatch. +- Delegate/controller ordering issues in custom UI mode. +- Feature-level permission or role mismatch (recording, breakout, webinar). diff --git a/plugins/zoom-developers/skills/meeting-sdk/macos/examples/join-start-pattern.md b/plugins/zoom-developers/skills/meeting-sdk/macos/examples/join-start-pattern.md new file mode 100644 index 00000000..38104502 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/macos/examples/join-start-pattern.md @@ -0,0 +1,20 @@ +# macOS Join/Start Pattern + +## Join (attendee) + +1. Get short-lived signature from backend. +2. Initialize/auth SDK and verify callback result. +3. Join with meeting number + passcode. +4. Register required meeting delegates before user interaction. + +## Start (host) + +1. Backend provides host `ZAK` + role-aware signature. +2. Start flow executes with host token. +3. Host-only controls enabled after privilege verification. + +## Guardrails + +- Keep SDK secret off client. +- Validate delegate callbacks under both default and custom UI. +- Explicitly handle leave/end transitions for cleanup. diff --git a/plugins/zoom-developers/skills/meeting-sdk/macos/macos.md b/plugins/zoom-developers/skills/meeting-sdk/macos/macos.md new file mode 100644 index 00000000..dbe9fae9 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/macos/macos.md @@ -0,0 +1,17 @@ +# Meeting SDK macOS Guide + +## Scope + +macOS Meeting SDK integration for default/custom UI, auth, join/start, and in-meeting feature controllers. + +## Validation Snapshot + +- Docs coverage includes: get-started, integrate, start/join/auth paths, default UI, custom UI, service quality, and error-code pages. +- API reference snapshot includes class/protocol maps, globals/functions pages, and file-level references. +- Local package checked: `zoom-sdk-macos-6.7.6.75900` with `ZoomSDKSample` and native macOS app sources. + +## Practical Guidance + +1. Get stable default UI flow first. +2. Add custom UI and advanced controls incrementally. +3. Validate immersive/share/annotation feature paths after each SDK upgrade. diff --git a/plugins/zoom-developers/skills/meeting-sdk/macos/references/environment-variables.md b/plugins/zoom-developers/skills/meeting-sdk/macos/references/environment-variables.md new file mode 100644 index 00000000..afd063fb --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/macos/references/environment-variables.md @@ -0,0 +1,15 @@ +# macOS Meeting SDK Environment Variables + +| Variable | Required | Purpose | Where to find | +| --- | --- | --- | --- | +| `ZOOM_SDK_KEY` | Yes | SDK signing identity | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_SDK_SECRET` | Yes | Server-side signing secret | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_MEETING_NUMBER` | Join/start | Meeting identifier | Zoom invite / web portal / Meetings API | +| `ZOOM_MEETING_PASSWORD` | Conditional | Meeting passcode | Zoom invite details / Meetings API | +| `ZOOM_ROLE` | Yes | Signature role (`0` attendee, `1` host) | App business logic | +| `ZOOM_ZAK` | Host start | Host authorization token | Zoom REST API token flow | + +## Notes + +- Keep signing on backend. +- For desktop distribution, keep runtime token handling outside checked-in configs. diff --git a/plugins/zoom-developers/skills/meeting-sdk/macos/references/macos-reference-map.md b/plugins/zoom-developers/skills/meeting-sdk/macos/references/macos-reference-map.md new file mode 100644 index 00000000..eee0c2fe --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/macos/references/macos-reference-map.md @@ -0,0 +1,33 @@ +# macOS Reference Map + +## Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/macos/ +- API Reference: https://marketplacefront.zoom.us/sdk/meeting/macos/annotated.html + +## Crawl Coverage Snapshot + +- Docs pages captured: `45` +- API reference pages captured: `528` + +## Key API Entry Pages + +- `annotated.md` +- `classes.md` +- `files.md` +- `hierarchy.md` +- `functions*` +- `globals*` +- `pages.md` + +## Notable API Surface Areas + +- ZoomSDK service/controller interfaces +- Meeting/audio/video/share/webinar/breakout modules +- AI companion, smart summary, and avatar related interfaces +- Raw data helper/delegate interfaces + +## Drift Signals to Watch + +- Frequent growth in `globals*` and controller interfaces. +- Added AI Companion and smart-summary-related surfaces across recent versions. diff --git a/plugins/zoom-developers/skills/meeting-sdk/macos/references/versioning-and-compatibility.md b/plugins/zoom-developers/skills/meeting-sdk/macos/references/versioning-and-compatibility.md new file mode 100644 index 00000000..9a7b7c5b --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/macos/references/versioning-and-compatibility.md @@ -0,0 +1,17 @@ +# macOS Versioning and Compatibility + +## Observed Versions + +- Local SDK package: `v6.7.6.75900` +- Docs baseline: current macOS Meeting SDK docs tree captured on this crawl. + +## Compatibility Practices + +- Pin exact SDK package and re-test controller/delegate contracts on upgrade. +- Verify custom UI features (annotation/share/immersive) first during upgrade testing. +- Maintain a release checklist for host-only and webinar-specific flows. + +## Contradiction/Drift Notes + +- Docs contain both top-level and nested advanced-feature paths; treat them as parallel documentation organization, not separate APIs. +- Changelog in package is external-link based; maintain local upgrade notes for exact behavior changes. diff --git a/plugins/zoom-developers/skills/meeting-sdk/macos/scenarios/high-level-scenarios.md b/plugins/zoom-developers/skills/meeting-sdk/macos/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..b31d07f7 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/macos/scenarios/high-level-scenarios.md @@ -0,0 +1,19 @@ +# macOS High-Level Scenarios + +## Scenario 1: Enterprise desktop collaboration app + +- Default UI embed for fast rollout. +- Adds policy-based host controls. +- Logs meeting lifecycle and quality stats for support analytics. + +## Scenario 2: Media-rich custom meeting app + +- Uses custom UI for branded layout and controlled tool access. +- Integrates share/annotation/immersive features. +- Keeps default UI fallback for release safety. + +## Scenario 3: Training operations console + +- Hosts meetings with role-aware controls. +- Uses breakout and participant controllers. +- Adds upgrade compatibility checks before broad deployment. diff --git a/plugins/zoom-developers/skills/meeting-sdk/macos/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/meeting-sdk/macos/troubleshooting/common-issues.md new file mode 100644 index 00000000..41bbdb9a --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/macos/troubleshooting/common-issues.md @@ -0,0 +1,22 @@ +# macOS Common Issues + +## 1. Join/start flow errors + +- Validate signature freshness and role. +- Confirm meeting identifier and passcode mapping. +- Validate host token (`ZAK`) for start path. + +## 2. Delegate callback gaps + +- Ensure delegate/controller registration happens before feature usage. +- Keep coordinator/service objects strongly referenced through session lifecycle. + +## 3. Custom UI regressions + +- Verify default UI still works to isolate custom layer issues. +- Re-check rendering and feature-controller dependencies after SDK upgrades. + +## 4. Version drift + +- Re-run feature-level tests on breakout, share, annotation, and AI companion modules. +- Compare API reference map for renamed or newly required interfaces. diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/RUNBOOK.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/RUNBOOK.md new file mode 100644 index 00000000..a991ccd6 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK React Native 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 Meeting SDK embed path for React Native (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/react-native/ +- https://marketplacefront.zoom.us/sdk/meeting/reactnative/modules.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/react-native/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/reactnative/` diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/SKILL.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/SKILL.md new file mode 100644 index 00000000..e2672bb6 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/SKILL.md @@ -0,0 +1,98 @@ +--- +name: zoom-meeting-sdk-react-native +description: Zoom Meeting SDK for React Native. Use when embedding Zoom meetings in React Native iOS/Android apps with @zoom/meetingsdk-react-native, JWT auth, join/start flows, platform setup, and native bridge troubleshooting. +--- + +# Zoom Meeting SDK (React Native) + +Use this skill when building React Native apps that need embedded Zoom meeting join/start flows. + +## Quick Links + +1. **[Lifecycle Workflow](concepts/lifecycle-workflow.md)** - init -> auth -> join/start -> in-meeting -> cleanup +2. **[Architecture](concepts/architecture.md)** - JS wrapper, native bridge, iOS/Android SDK layers +3. **[High-Level Scenarios](concepts/high-level-scenarios.md)** - practical product patterns +4. **[Setup Guide](examples/setup-guide.md)** - install package + platform requirements +5. **[Join Meeting Pattern](examples/join-meeting-pattern.md)** - JWT + meetingNumber + password +6. **[Start Meeting Pattern](examples/start-meeting-pattern.md)** - ZAK-based host start +7. **[SKILL.md](SKILL.md)** - full navigation + +## Core APIs (Wrapper) + +From `@zoom/meetingsdk-react-native` wrapper surface: + +- `initSDK(config)` +- `isInitialized()` +- `updateMeetingSetting(config)` +- `joinMeeting(config)` +- `startMeeting(config)` +- `cleanup()` + +See: **[Wrapper API](references/wrapper-api.md)** + +## Critical Notes + +- You still need native iOS/Android Meeting SDK dependencies configured. +- `joinMeeting` and `startMeeting` return numeric status/error codes from native layer. +- For host start flow, pass `zoomAccessToken` (ZAK). +- Keep JWT generation on backend, never embed SDK secret in app. +- Current docs note React Native support up to `0.75.4`; Expo is not supported. + +## Platform Guides + +- **[iOS Setup](references/ios-setup.md)** - Podfile, optional ReplayKit/app group fields +- **[Android Setup](references/android-setup.md)** - Gradle dependency + options mapping +- **[Native Bridge Notes](references/native-bridge-notes.md)** - behavior differences and gotchas + +## Troubleshooting + +- **[Common Issues](troubleshooting/common-issues.md)** +- **[Version Drift](troubleshooting/version-drift.md)** +- **[Deprecated/Contradictions](troubleshooting/deprecated-and-contradictions.md)** + +## Related Skills + +- **[zoom-meeting-sdk](../SKILL.md)** - parent Meeting SDK hub +- **[zoom-oauth](../../oauth/SKILL.md)** - auth flow and token management +- **[zoom-general](../../general/SKILL.md)** - cross-product architecture decisions + +## Documentation Index + +### Start Here + +1. [SKILL.md](SKILL.md) +2. [Lifecycle Workflow](concepts/lifecycle-workflow.md) +3. [Architecture](concepts/architecture.md) +4. [Setup Guide](examples/setup-guide.md) + +### Concepts + +- [Lifecycle Workflow](concepts/lifecycle-workflow.md) +- [Architecture](concepts/architecture.md) +- [Auth and Token Model](concepts/auth-and-token-model.md) +- [High-Level Scenarios](concepts/high-level-scenarios.md) + +### Examples + +- [Setup Guide](examples/setup-guide.md) +- [Join Meeting Pattern](examples/join-meeting-pattern.md) +- [Start Meeting Pattern](examples/start-meeting-pattern.md) +- [Provider Hook Pattern](examples/provider-hook-pattern.md) + +### References + +- [Wrapper API](references/wrapper-api.md) +- [Android Setup](references/android-setup.md) +- [iOS Setup](references/ios-setup.md) +- [Native Bridge Notes](references/native-bridge-notes.md) +- [Official Sources](references/official-sources.md) + +### Troubleshooting + +- [Common Issues](troubleshooting/common-issues.md) +- [Version Drift](troubleshooting/version-drift.md) +- [Deprecated and Contradictions](troubleshooting/deprecated-and-contradictions.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/concepts/architecture.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/concepts/architecture.md new file mode 100644 index 00000000..86154deb --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/concepts/architecture.md @@ -0,0 +1,16 @@ +# Architecture + +The React Native package is a wrapper around native Meeting SDKs. + +## Layers + +- JS API layer: `@zoom/meetingsdk-react-native` +- Context/hook layer: `ZoomSDKProvider`, `useZoom` +- Native module layer: `RNZoomSDK` (iOS Obj-C, Android Java) +- Zoom native SDK layer: MobileRTC (iOS) and ZoomSDK (Android) + +## Why this matters + +- Wrapper updates can change JS signatures while native SDK versions evolve independently. +- Some options are platform-specific (`logSize` Android, `bundleResPath` iOS). +- Numeric error codes from native must be interpreted by platform docs. diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/concepts/auth-and-token-model.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/concepts/auth-and-token-model.md new file mode 100644 index 00000000..94a1b0f3 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/concepts/auth-and-token-model.md @@ -0,0 +1,17 @@ +# Auth and Token Model + +## Token types used by wrapper + +- `jwtToken` in `initSDK`: Meeting SDK JWT (for SDK authorization) +- `zoomAccessToken` in `startMeeting`: ZAK token for host start + +## Security model + +- Generate tokens server-side only. +- Never ship SDK secret in the app. +- Keep JWT short-lived and rotate aggressively. + +## Flow guidance + +- Participant join: `initSDK(jwtToken)` + `joinMeeting(meetingNumber, password)` +- Host start: `initSDK(jwtToken)` + `startMeeting(zoomAccessToken=ZAK, meetingNumber)` diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/concepts/high-level-scenarios.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/concepts/high-level-scenarios.md new file mode 100644 index 00000000..bd92bc3c --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/concepts/high-level-scenarios.md @@ -0,0 +1,25 @@ +# High-Level Scenarios + +## 1. Mobile attendee app (customer-facing) + +- User opens iOS/Android app and joins meetings from in-app schedule. +- Backend provides short-lived Meeting SDK JWT. +- App calls `joinMeeting` with meeting number and passcode when required. + +## 2. Mobile host operations app + +- Authenticated operator starts scheduled sessions from mobile. +- Backend retrieves host ZAK via Zoom APIs. +- App uses `startMeeting` with `zoomAccessToken`. + +## 3. Kiosk-style controlled join flow + +- App runs in constrained device mode. +- Meeting controls are reduced via meeting flags/settings. +- App enforces deterministic init -> join -> cleanup sequence on each session. + +## 4. Support/field workforce app + +- Agents join support calls and optionally use share/chat controls. +- Per-platform settings are tuned independently (Android/iOS parity is not guaranteed). +- Runtime fallback handling is added for unsupported feature flags. diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/concepts/lifecycle-workflow.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..9b19462a --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/concepts/lifecycle-workflow.md @@ -0,0 +1,20 @@ +# Lifecycle Workflow + +Recommended runtime flow: + +1. App bootstraps and wraps tree with `ZoomSDKProvider`. +2. `initSDK` runs once with `jwtToken`, `domain`, logging options. +3. App checks `isInitialized()` before meeting actions. +4. User chooses: + - `joinMeeting` (participant) + - `startMeeting` (host, with ZAK) +5. Native Meeting SDK UI/session runs. +6. Call `cleanup()` on app shutdown/logout. + +```text +React UI -> ZoomSDKProvider -> JS Wrapper (ZoomSDK.ts) + -> Native Bridge (RNZoomSDK) + -> iOS MobileRTC / Android ZoomSDK +``` + +If initialization/auth fails, stop and rotate token before retrying. diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/examples/join-meeting-pattern.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/examples/join-meeting-pattern.md new file mode 100644 index 00000000..5a3e9e59 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/examples/join-meeting-pattern.md @@ -0,0 +1,19 @@ +# Join Meeting Pattern + +```tsx +import { useZoom } from '@zoom/meetingsdk-react-native'; + +const zoom = useZoom(); + +await zoom.joinMeeting({ + userName: 'participant-name', + meetingNumber: '123456789', + password: 'meeting-password', + userType: 1, +}); +``` + +Notes: + +- `meetingNumber` and `userName` are required by wrapper validation. +- `password` is optional in API shape, but may be mandatory by meeting settings. diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/examples/provider-hook-pattern.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/examples/provider-hook-pattern.md new file mode 100644 index 00000000..b5526d3c --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/examples/provider-hook-pattern.md @@ -0,0 +1,14 @@ +# Provider Hook Pattern + +Use wrapper context consistently. + +```tsx +import { ZoomSDKProvider, useZoom } from '@zoom/meetingsdk-react-native'; + +function MeetingActions() { + const zoom = useZoom(); + // zoom.joinMeeting / zoom.startMeeting / zoom.cleanup +} +``` + +Do not call wrapper methods before provider initialization is complete. diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/examples/setup-guide.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/examples/setup-guide.md new file mode 100644 index 00000000..9193dfe2 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/examples/setup-guide.md @@ -0,0 +1,43 @@ +# Setup Guide + +## 1. Install package + +```bash +npm install @zoom/meetingsdk-react-native +``` + +## 2. Respect documented support boundaries + +- React Native support currently documented up to `0.75.4`. +- Expo is currently not supported. +- Android baseline from docs: `minSdkVersion = 26`, `targetSdkVersion = 35`. + +## 3. Align platform SDK versions + +- This wrapper does not bundle native iOS/Android Meeting SDK artifacts for all workflows. +- Keep wrapper and native Meeting SDK versions aligned. +- For older wrapper versions (pre-`6.4.5`), docs note manual native SDK placement may be required. + +## 4. Initialize provider + +```tsx +import { ZoomSDKProvider } from '@zoom/meetingsdk-react-native'; + +', + domain: 'zoom.us', + enableLog: true, + logSize: 5, + }} +> + + +``` + +## 5. Platform prerequisites + +- Android: include Zoom Meeting SDK dependency in gradle and required permissions. +- iOS: ensure Podfile and framework setup are aligned with package expectations. + +See [Android Setup](../references/android-setup.md) and [iOS Setup](../references/ios-setup.md). diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/examples/start-meeting-pattern.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/examples/start-meeting-pattern.md new file mode 100644 index 00000000..fde3048d --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/examples/start-meeting-pattern.md @@ -0,0 +1,18 @@ +# Start Meeting Pattern + +```tsx +import { useZoom } from '@zoom/meetingsdk-react-native'; + +const zoom = useZoom(); + +await zoom.startMeeting({ + userName: 'host-name', + meetingNumber: '123456789', + zoomAccessToken: '', +}); +``` + +Notes: + +- `zoomAccessToken` is required for host start in wrapper validation. +- Missing/expired ZAK returns native start failure code. diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/references/android-setup.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/references/android-setup.md new file mode 100644 index 00000000..8988b6d7 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/references/android-setup.md @@ -0,0 +1,15 @@ +# Android Setup Notes + +Representative dependency from SDK package example: + +```gradle +implementation('us.zoom.meetingsdk:zoomsdk:6.7.2') +``` + +Other observed setup details: + +- Java/Kotlin target 17 in sample. +- Wrapper maps many `JoinMeetingOptions` / `StartMeetingOptions` flags. +- `language` setting is consumed by native bridge during `updateMeetingSetting`. + +Always verify against your app's RN/Gradle/Kotlin compatibility matrix. diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/references/ios-setup.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/references/ios-setup.md new file mode 100644 index 00000000..47165da4 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/references/ios-setup.md @@ -0,0 +1,13 @@ +# iOS Setup Notes + +Observed in package sample project: + +- Podfile includes React Native integration and required permissions pods. +- Wrapper supports optional init fields: + - `bundleResPath` + - `appGroupId` + - `replaykitBundleIdentifier` + +These are relevant for custom resource path and screen-share style setups. + +Confirm iOS deployment target and Podfile settings against your RN version. diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/references/native-bridge-notes.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/references/native-bridge-notes.md new file mode 100644 index 00000000..477499b0 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/references/native-bridge-notes.md @@ -0,0 +1,16 @@ +# Native Bridge Notes + +## Android bridge + +- Uses `ZoomSDK.initialize(...)` with `wrapperType = 2`. +- `joinMeeting` and `startMeeting` resolve numeric result codes. +- Exposes lifecycle hooks but event emitter list is currently empty in bridge. + +## iOS bridge + +- Initializes `MobileRTC` + auth service (`sdkAuth` with JWT). +- `joinMeeting`/`startMeeting` call native meeting service methods and resolve/reject promises. + +## Practical implication + +The wrapper currently behaves as command-based API with limited cross-platform event exposure. Build app-level state handling around command results and native UI transitions. diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/references/official-sources.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/references/official-sources.md new file mode 100644 index 00000000..2e47ab55 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/references/official-sources.md @@ -0,0 +1,14 @@ +# Official Sources + +Primary sources used for this skill: + +- Zoom docs (React Native Meeting SDK): https://developers.zoom.us/docs/meeting-sdk/react-native/ +- Zoom reference (React Native modules): https://marketplacefront.zoom.us/sdk/meeting/reactnative/modules.html +- Zoom Meeting SDK React Native package 6.7.2 (local archive analysis) +- Crawled docs snapshots under `skills/raw-docs/developers.zoom.us/docs/meeting-sdk/react-native/` +- Crawled reference snapshots under `skills/raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/reactnative/` + +Related: + +- Meeting SDK auth: https://developers.zoom.us/docs/meeting-sdk/auth/ +- Quickstart repo: https://github.com/zoom/MeetingSDK-ReactNative-Quickstart diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/references/wrapper-api.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/references/wrapper-api.md new file mode 100644 index 00000000..c87b98af --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/references/wrapper-api.md @@ -0,0 +1,30 @@ +# Wrapper API Reference (React Native package) + +## Init config + +- `jwtToken?: string` +- `domain?: string` +- `enableLog?: boolean` +- `logSize?: number` (Android) +- `bundleResPath?: string` (iOS) +- `appGroupId?: string` (iOS) +- `replaykitBundleIdentifier?: string` (iOS) + +## Methods + +- `initSDK(config): Promise` +- `isInitialized(): Promise` +- `joinMeeting(config): Promise` +- `startMeeting(config): Promise` +- `updateMeetingSetting(config): void` +- `cleanup(): void` + +## Join config highlights + +- Required: `userName`, `meetingNumber` +- Optional: `password`, `zoomAccessToken`, `vanityID`, `webinarToken`, `joinToken`, `appPrivilegeToken` + +## Start config highlights + +- Required: `userName`, `zoomAccessToken` +- Optional: `meetingNumber`, `vanityID`, `inviteContactId` diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/troubleshooting/common-issues.md new file mode 100644 index 00000000..2c4fdb9e --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/troubleshooting/common-issues.md @@ -0,0 +1,24 @@ +# Common Issues + +## `joinMeeting` fails immediately + +- Validate meeting number format and password. +- Confirm SDK initialization succeeded first. +- Check JWT validity window. + +## `startMeeting` fails + +- Verify `zoomAccessToken` (ZAK) is present and unexpired. +- Ensure host account and meeting ownership match ZAK context. + +## Provider/hook misuse + +- Ensure components calling `useZoom()` are wrapped in `ZoomSDKProvider`. + +## iOS-specific init issues + +- Confirm optional fields (`bundleResPath`, `appGroupId`, `replaykitBundleIdentifier`) only when needed and correctly configured. + +## Android language crash risk + +- Avoid passing partial/invalid language values to `updateMeetingSetting`. diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/troubleshooting/deprecated-and-contradictions.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/troubleshooting/deprecated-and-contradictions.md new file mode 100644 index 00000000..8e90b493 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/troubleshooting/deprecated-and-contradictions.md @@ -0,0 +1,27 @@ +# Deprecated and Contradictions Notes + +Observed from analyzed package/source artifacts: + +1. Reference URL naming inconsistency +- Package README references `react-native/annotated.html` for full API list, while current typed reference entrypoint is `modules.html`. +- Treat this as documentation routing inconsistency across versions. + +2. Meeting SDK vs Video SDK wording inconsistency in docs +- Crawled React Native docs include wording that the Meeting SDK wrapper is based on the native Video SDK version. +- Treat this as a likely docs wording issue; align implementation to Meeting SDK package/version compatibility docs. + +3. Example UX vs API shape mismatch +- Example UI prompts "Password Optional" but code blocks join if password is empty. +- Wrapper type allows optional `password`; runtime behavior depends on meeting config. + +4. Android bridge fragility +- `updateMeetingSetting` uses `config.getString("language")` without robust null checks before split. +- Passing missing/invalid language can crash or throw. + +5. Event model limitations +- Android bridge includes emitter plumbing but empty supported event set. +- Do not assume parity with native event coverage without custom extension. + +6. Version/toolchain caveat in demo docs +- Demo notes include version-conditional setup (for example pre-`6.4.5` artifact handling) and non-Expo limitation. +- Keep integration guides version-scoped to avoid false assumptions during upgrades. diff --git a/plugins/zoom-developers/skills/meeting-sdk/react-native/troubleshooting/version-drift.md b/plugins/zoom-developers/skills/meeting-sdk/react-native/troubleshooting/version-drift.md new file mode 100644 index 00000000..1b637293 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/react-native/troubleshooting/version-drift.md @@ -0,0 +1,17 @@ +# Version Drift Guidance + +Because wrapper and native SDKs evolve: + +- Reconfirm option names on each wrapper upgrade. +- Treat returned numeric meeting codes as versioned behavior. +- Re-test both join and host-start flows after SDK bump. +- Validate platform-specific flags (Android/iOS) separately. +- Reconfirm React Native framework compatibility window and Expo support status. + +Upgrade checklist: + +1. Compare `src/native/ZoomSDK.ts` API types between versions. +2. Compare Android `RNZoomSDKModule.java` option mapping. +3. Compare iOS `RNZoomSDK.m` auth/join/start implementations. +4. Re-run smoke tests: init -> isInitialized -> join/start -> cleanup. +5. Re-validate Android/iOS setup requirements from docs (permissions, min/target SDK, pod/gradle notes). diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/ai-companion.md b/plugins/zoom-developers/skills/meeting-sdk/references/ai-companion.md new file mode 100644 index 00000000..622d8e7a --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/ai-companion.md @@ -0,0 +1,225 @@ +# AI Companion in Meeting SDK + +Control AI Companion features in embedded Zoom meetings. + +## Overview + +The Meeting SDK provides `InMeetingAICompanionController` to manage AI Companion features within meetings. This allows you to check status and available features. + +**Important**: The Meeting SDK is for human use cases only. It does NOT support bots or AI notetakers. For bot/automated AI processing, use **rtms**. + +## Availability + +| Platform | Controller Available | +|----------|---------------------| +| Web | Limited (settings-based) | +| Android | ✅ `InMeetingAICompanionController` | +| iOS | ✅ `MobileRTCAICompanionController` | +| Windows | ✅ `IMeetingAICompanionController` | +| macOS | ✅ `ZoomSDKMeetingAICompanionController` | + +## AI Companion Features + +| Feature | Constant | Description | +|---------|----------|-------------| +| Query | `QUERY` | Ask AI Companion questions during meeting | +| Smart Summary | `SMART_SUMMARY` | Auto-generate meeting summary | +| Smart Recording | `SMART_RECORDING` | Highlight key moments in recording | + +## Android + +```java +import us.zoom.sdk.*; + +// Get AI Companion controller +InMeetingService inMeetingService = ZoomSDK.getInstance().getInMeetingService(); +InMeetingAICompanionController aiController = inMeetingService.getInMeetingAICompanionController(); + +// Check if AI Companion is enabled for this meeting +boolean isEnabled = aiController.isAICompanionEnabled(); +Log.d("AICompanion", "Enabled: " + isEnabled); + +// Get available features +List features = aiController.getAvailableFeatures(); +for (AICompanionFeature feature : features) { + Log.d("AICompanion", "Feature available: " + feature.name()); +} + +// Check specific feature +boolean hasSummary = aiController.isFeatureAvailable(AICompanionFeature.SMART_SUMMARY); +``` + +### AI Companion Events (Android) + +```java +// Listen for AI Companion status changes +inMeetingService.addListener(new InMeetingServiceListener() { + @Override + public void onAICompanionActiveChanged(boolean isActive) { + Log.d("AICompanion", "Active state changed: " + isActive); + } + + @Override + public void onAICompanionFeaturesChanged() { + // Re-check available features + List features = aiController.getAvailableFeatures(); + } +}); +``` + +## iOS (Swift) + +```swift +import MobileRTC + +// Get AI Companion controller +guard let meetingService = MobileRTC.shared().getMeetingService(), + let aiController = meetingService.getInMeetingAICompanionController() else { + return +} + +// Check if AI Companion is enabled +let isEnabled = aiController.isAICompanionEnabled() +print("AI Companion enabled: \(isEnabled)") + +// Get available features +if let features = aiController.getAvailableFeatures() as? [MobileRTCAICompanionFeature] { + for feature in features { + print("Feature: \(feature.rawValue)") + } +} + +// Check specific feature +let hasSummary = aiController.isFeatureAvailable(.smartSummary) +``` + +### AI Companion Events (iOS) + +```swift +// Implement delegate +class MeetingDelegate: NSObject, MobileRTCMeetingServiceDelegate { + func onAICompanionActiveChanged(_ isActive: Bool) { + print("AI Companion active: \(isActive)") + } + + func onAICompanionFeaturesChanged() { + // Refresh available features + } +} +``` + +## Windows (C++) + +```cpp +#include "zoom_sdk.h" + +// Get AI Companion controller +IMeetingService* meetingService = SDKInterfaceWrap::GetInst().GetMeetingService(); +IMeetingAICompanionController* aiController = meetingService->GetMeetingAICompanionController(); + +// Check status +bool isEnabled = aiController->IsAICompanionEnabled(); + +// Get features +IList* features = aiController->GetAvailableFeatures(); +for (int i = 0; i < features->GetCount(); i++) { + AICompanionFeature feature = features->GetItem(i); + // Process feature +} +``` + +## macOS (Objective-C) + +```objc +#import + +// Get AI Companion controller +ZoomSDKMeetingService *meetingService = [[ZoomSDK sharedSDK] getMeetingService]; +ZoomSDKMeetingAICompanionController *aiController = [meetingService getAICompanionController]; + +// Check status +BOOL isEnabled = [aiController isAICompanionEnabled]; +NSLog(@"AI Companion enabled: %d", isEnabled); + +// Get features +NSArray *features = [aiController getAvailableFeatures]; +for (NSNumber *feature in features) { + NSLog(@"Feature: %@", feature); +} +``` + +## Web SDK + +The Web SDK doesn't expose direct AI Companion controls. AI Companion behavior is determined by: +- Account settings +- Meeting settings +- Host controls + +```javascript +// AI Companion features are controlled by meeting/account settings +// The Web SDK respects these settings automatically + +// You can check meeting info for AI-related settings +ZoomMtg.getCurrentMeetingInfo(function(result) { + console.log('Meeting info:', result); +}); +``` + +## Use Cases + +### Display AI Companion Status + +```java +// Android: Show UI indicator based on AI Companion status +public void updateAICompanionUI() { + InMeetingAICompanionController aiController = getAIController(); + + if (aiController.isAICompanionEnabled()) { + aiIndicator.setVisibility(View.VISIBLE); + + if (aiController.isFeatureAvailable(AICompanionFeature.SMART_SUMMARY)) { + summaryBadge.setVisibility(View.VISIBLE); + } + } else { + aiIndicator.setVisibility(View.GONE); + } +} +``` + +### Inform Users About AI Features + +```swift +// iOS: Show alert about AI features +func showAICompanionInfo() { + guard let aiController = getAIController() else { return } + + var features: [String] = [] + + if aiController.isFeatureAvailable(.smartSummary) { + features.append("Meeting Summary") + } + if aiController.isFeatureAvailable(.smartRecording) { + features.append("Smart Recording") + } + + if !features.isEmpty { + let message = "AI Companion features active: \(features.joined(separator: ", "))" + showAlert(title: "AI Companion", message: message) + } +} +``` + +## Limitations + +| Limitation | Notes | +|------------|-------| +| No bot support | Meeting SDK is for human use only | +| Read-only | Cannot enable/disable features via SDK | +| Settings-dependent | Features depend on account/meeting settings | +| No transcript access | Use REST API or RTMS for transcripts | + +## Related + +- **[AI Companion Integration Use Case](../../general/use-cases/ai-companion-integration.md)** - Full integration guide +- **rtms** - For real-time transcript access +- **zoom-rest-api** - For meeting summaries and transcripts after meeting diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/android.md b/plugins/zoom-developers/skills/meeting-sdk/references/android.md new file mode 100644 index 00000000..072da674 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/android.md @@ -0,0 +1,19 @@ +# Meeting SDK (Android) Pointers + +This file provides a stable link target for Android Meeting SDK questions. + +Start here: +- [../android/SKILL.md](../android/SKILL.md) + +## Common Forum Questions + +- How do I integrate the SDK with Gradle and resolve dependency conflicts? +- How do I implement custom UI (vs default UI)? +- How do I get audio/video/raw data access? + +## Practical Guidance + +- Verify you can join a meeting with default UI first. +- Collect logs and the specific error code (many issues are version or parameter mismatches). +- For "Invalid signature" questions: use `signature-playbook.md`. +- Use [../android/references/android-reference-map.md](../android/references/android-reference-map.md) for interface discovery and version drift checks. diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/authorization.md b/plugins/zoom-developers/skills/meeting-sdk/references/authorization.md new file mode 100644 index 00000000..4da0d9bb --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/authorization.md @@ -0,0 +1,89 @@ +# Meeting SDK - Authorization + +Generate JWT signatures for Meeting SDK authentication. + +## Overview + +Meeting SDK uses JWT (JSON Web Token) signatures to authenticate users joining meetings. Signatures must be generated server-side to protect your SDK Secret. + +## Prerequisites + +- Meeting SDK Key and Secret from [Marketplace](https://marketplace.zoom.us/) (sign-in required) +- Server-side code to generate signatures + +## JWT Structure + +| Claim | Description | +|-------|-------------| +| `sdkKey` | Your SDK Key | +| `mn` | Meeting number | +| `role` | 0 = participant, 1 = host | +| `iat` | Issued at timestamp | +| `exp` | Expiration timestamp | +| `tokenExp` | Token expiration timestamp | + +## Signature Generation Best Practices + +### Short-Lived Tokens (Recommended) + +For security, generate tokens with short expiry: + +```javascript +const iat = Math.floor(Date.now() / 1000) - 7200; // 2 hours in the past +const exp = Math.floor(Date.now() / 1000) + 10; // 10 seconds from now + +const payload = { + sdkKey: SDK_KEY, + mn: meetingNumber, + role: role, + iat: iat, + exp: exp, + tokenExp: exp +}; +``` + +**Why this works:** +- `exp` is only 10 seconds after generation (short-lived for security) +- `iat` is set 2 hours in the past to satisfy Zoom's requirement that `exp - iat >= 2 hours` +- Token is generated just before joining, so 10 second window is sufficient + +### Server-Side Example (Node.js) + +```javascript +const jwt = require('jsonwebtoken'); + +function generateSignature(sdkKey, sdkSecret, meetingNumber, role) { + const iat = Math.floor(Date.now() / 1000) - 7200; // 2 hours ago + const exp = Math.floor(Date.now() / 1000) + 10; // 10 seconds from now + + const payload = { + sdkKey: sdkKey, + mn: meetingNumber, + role: role, + iat: iat, + exp: exp, + tokenExp: exp + }; + + return jwt.sign(payload, sdkSecret, { algorithm: 'HS256' }); +} +``` + +## Role Values + +| Role | Value | Description | +|------|-------|-------------| +| Participant | 0 | Join as attendee | +| Host | 1 | Join as host (requires host key or being meeting owner) | + +## Security Guidelines + +| Do | Don't | +|----|-------| +| Generate signatures server-side | Expose SDK Secret in client code | +| Use short expiry times | Use long-lived tokens | +| Validate user before generating | Generate for unauthenticated users | + +## Resources + +- **Auth docs**: https://developers.zoom.us/docs/meeting-sdk/auth/ diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/bot-authentication.md b/plugins/zoom-developers/skills/meeting-sdk/references/bot-authentication.md new file mode 100644 index 00000000..7ed7d426 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/bot-authentication.md @@ -0,0 +1,385 @@ +# Bot Authentication - JWT, ZAK, and OBF Tokens + +Understanding the different token types for Meeting SDK authentication, especially for bots joining meetings. + +## Overview + +Meeting SDK authentication involves multiple token types that serve different purposes. This guide clarifies the confusion between JWT signatures, ZAK tokens, and OBF tokens. + +## Token Types Summary + +| Token | Purpose | Always Required? | Deprecated? | +|-------|---------|------------------|-------------| +| **JWT Signature** | Initialize/authenticate Meeting SDK | **Yes** | **No** | +| **ZAK Token** | Authenticate as a Zoom user | No (situational) | **No** | +| **OBF Token** | Join external meetings with user attribution | No (required Feb 2026) | **No** | + +## Common Confusion + +### JWT App Type vs JWT Signature + +**This is the #1 source of confusion.** + +| Term | What It Is | Status | +|------|------------|--------| +| **JWT App Type** | A Zoom app type for REST API authentication | **Deprecated** (migrated to Server-to-Server OAuth) | +| **JWT Signature** | A token generated using SDK credentials to authenticate Meeting SDK | **Still required and NOT deprecated** | + +**Key Point:** The deprecation of JWT App Type does NOT affect Meeting SDK. You still need JWT signatures for SDK authentication. + +--- + +## 1. JWT Signature (Always Required) + +### What Is It? + +A JWT (JSON Web Token) signature authenticates your application to use the Meeting SDK. Generated server-side using your SDK Client ID and Client Secret. + +### When to Use + +**Always.** Every Meeting SDK join requires a JWT signature. + +### How to Generate + +```javascript +// Server-side (Node.js) +const KJUR = require('jsrsasign'); + +function generateSignature(sdkKey, sdkSecret, meetingNumber, role) { + const iat = Math.round(Date.now() / 1000) - 30; // 30 seconds ago + const exp = iat + 60 * 60 * 2; // 2 hours from iat + + const payload = { + sdkKey: sdkKey, // Your SDK Client ID + mn: meetingNumber, // Meeting number to join + role: role, // 0 = participant, 1 = host + iat: iat, + exp: exp, + tokenExp: exp + }; + + const header = { alg: 'HS256', typ: 'JWT' }; + + return KJUR.jws.JWS.sign('HS256', + JSON.stringify(header), + JSON.stringify(payload), + sdkSecret // Your SDK Client Secret + ); +} +``` + +### Best Practice: Short-Lived Tokens + +```javascript +// Generate token just before joining +const iat = Math.floor(Date.now() / 1000) - 7200; // 2 hours in past +const exp = Math.floor(Date.now() / 1000) + 10; // 10 seconds from now + +// Why this works: +// - exp is short-lived (security) +// - exp - iat >= 2 hours (Zoom requirement) +// - Token generated right before use +``` + +### Role Values + +| Role | Value | Description | +|------|-------|-------------| +| Participant | 0 | Join as attendee | +| Host | 1 | Join as host (requires being meeting owner or having host key) | + +--- + +## 2. ZAK Token (Zoom Access Key) + +### What Is It? + +A short-lived credential that proves your bot/app is authenticated as a specific Zoom user. Generated via the Zoom REST API. + +### When to Use + +| Scenario | ZAK Required? | +|----------|---------------| +| Meeting has "Only Authenticated Users Can Join" enabled | **Yes** | +| Starting a meeting as the host (when host not present) | **Yes** (host's ZAK) | +| Changing bot's profile picture to match a user | **Yes** | +| Regular meeting join | No | + +### How to Get ZAK Token + +**Step 1: Get OAuth Access Token** + +```bash +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic {BASE64(client_id:client_secret)}" \ + -d "grant_type=authorization_code&code={auth_code}&redirect_uri={redirect_uri}" +``` + +**Step 2: Generate ZAK Token** + +```bash +curl -X GET "https://api.zoom.us/v2/users/me/token?type=zak&ttl=7200" \ + -H "Authorization: Bearer {access_token}" +``` + +**Response:** +```json +{ + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." +} +``` + +### Required OAuth Scope + +``` +user:read:zak +``` + +### Using ZAK in SDK Join + +```javascript +// Web SDK +ZoomMtg.join({ + signature: signature, // JWT signature (always required) + sdkKey: clientId, + meetingNumber: meetingNumber, + passWord: password, + userName: "Meeting Bot", + zak: zakToken, // ZAK token for authenticated join + success: (success) => console.log('Joined'), + error: (error) => console.error(error) +}); +``` + +### Key Properties + +- **Short-lived**: Configurable TTL (typically 1-2 hours) +- **Any ZAK works**: Doesn't need to be from a meeting participant +- **No concurrency limit**: One service account can generate unlimited tokens +- **Expiry checked at join**: If already in meeting, bot stays connected even if ZAK expires + +### Common Mistake + +**Wrong:** Thinking ZAK must be from a meeting participant. + +**Right:** Any Zoom account's ZAK satisfies "Only Authenticated Users" requirement. Create one service account (e.g., `meeting-bot@company.com`) for all your bots. + +--- + +## 3. OBF Token (On-Behalf-Of Token) + +### What Is It? + +A new credential that ties your bot to a specific user who is present in the meeting. Required for external meetings starting **February 23, 2026**. + +### Why Introduced? + +Zoom introduced OBF tokens for accountability and transparency. It makes clear that an SDK app belongs to a specific person in the meeting. + +### When to Use + +| Date | External Meeting Requirement | +|------|------------------------------| +| Before Feb 23, 2026 | ZAK or OBF (optional) | +| **After Feb 23, 2026** | **ZAK or OBF required** | + +### Critical Difference: OBF vs ZAK + +| Aspect | ZAK Token | OBF Token | +|--------|-----------|-----------| +| User presence required | No | **Yes** - user MUST be in meeting | +| Bot disconnection | Stays if token owner leaves | **Immediately disconnected** if user leaves | +| Meeting scope | Any meeting | Specific meeting ID only | +| Attribution | Generic authentication | Tied to specific attending user | + +### How to Get OBF Token + +**Step 1: Get OAuth Access Token** (same as ZAK) + +**Step 2: Generate OBF Token** + +```bash +curl -X GET "https://api.zoom.us/v2/users/me/token?type=onbehalf&meeting_id={meeting_id}" \ + -H "Authorization: Bearer {access_token}" +``` + +**Response:** +```json +{ + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." +} +``` + +### Required OAuth Scope + +``` +user:read:token +``` + +### Using OBF in SDK Join + +```javascript +// Web SDK +ZoomMtg.join({ + signature: signature, // JWT signature (always required) + sdkKey: clientId, + meetingNumber: meetingNumber, + passWord: password, + userName: "Meeting Bot", + obfToken: obfToken, // OBF token (NOT zak) + success: (success) => console.log('Joined'), + error: (error) => console.error(error) +}); +``` + +**Important:** `zak` and `obfToken` are **mutually exclusive**. Use only one. + +### Handling Join Failures + +If bot joins before authorizing user is in meeting: + +```javascript +// SDK v6.6.10+ returns specific error code +// MEETING_FAIL_AUTHORIZED_USER_NOT_INMEETING + +async function joinWithRetry(joinOptions, maxRetries = 5) { + for (let i = 0; i < maxRetries; i++) { + try { + await ZoomMtg.join(joinOptions); + return; // Success + } catch (error) { + if (error.code === 'MEETING_FAIL_AUTHORIZED_USER_NOT_INMEETING') { + console.log(`User not in meeting yet. Retry ${i + 1}/${maxRetries}`); + await sleep(3000); // Wait 3 seconds + } else { + throw error; // Different error, don't retry + } + } + } + throw new Error('Max retries exceeded - user never joined meeting'); +} +``` + +### OBF Token Mapping + +You must map users to their meetings: + +1. **Zoom Meetings API**: `GET /users/{userId}/meetings` +2. **Calendar integration**: Parse meeting invites from Google Calendar/Outlook + +--- + +## Complete Bot Join Flow + +### Current (Pre-Feb 2026) + +```javascript +// 1. Generate JWT signature (always required) +const signature = await generateSignature(sdkKey, sdkSecret, meetingNumber, 0); + +// 2. Optionally get ZAK for authenticated-only meetings +let zakToken = null; +if (meetingRequiresAuth) { + zakToken = await getZAKToken(accessToken); +} + +// 3. Join meeting +await ZoomMtg.join({ + signature: signature, + sdkKey: sdkKey, + meetingNumber: meetingNumber, + passWord: password, + userName: "Meeting Bot", + zak: zakToken, // Optional +}); +``` + +### Post-Feb 2026 (External Meetings) + +```javascript +// 1. Generate JWT signature (always required) +const signature = await generateSignature(sdkKey, sdkSecret, meetingNumber, 0); + +// 2. For external meetings, get OBF token +const obfToken = await getOBFToken(accessToken, meetingNumber); + +// 3. Wait for authorizing user to join (if using OBF) +// ... implement retry logic ... + +// 4. Join meeting +await ZoomMtg.join({ + signature: signature, + sdkKey: sdkKey, + meetingNumber: meetingNumber, + passWord: password, + userName: "Meeting Bot", + obfToken: obfToken, // For external meetings +}); +``` + +--- + +## Linux Bot Implementation + +For headless Linux bots, use the official sample: + +```bash +# Clone sample repository +git clone git@github.com:zoom/meetingsdk-headless-linux-sample.git + +# Configure (sample.config.toml) +[credentials] +client_id = "YOUR_CLIENT_ID" +client_secret = "YOUR_CLIENT_SECRET" + +[meeting] +join_url = "https://zoom.us/j/123456789?pwd=xxx" +# OR +meeting_id = 123456789 +password = "abc123" + +# Optional tokens +zak_token = "..." # For authenticated joins +obf_token = "..." # For external meetings + +# Run with Docker +docker compose up +``` + +--- + +## Common Mistakes + +| Mistake | Reality | Fix | +|---------|---------|-----| +| JWT signatures are deprecated | Only JWT App Type is deprecated | Continue using JWT signatures for SDK | +| ZAK must be from meeting participant | Any Zoom account's ZAK works | Use single service account | +| Using both ZAK and OBF together | They're mutually exclusive | Use only one | +| Generating OBF before user in meeting | OBF requires user presence | Implement retry logic | +| Development credentials for external meetings | Dev credentials only work for your account | Get production credentials (4-6 week review) | + +--- + +## Timeline + +| Date | Change | +|------|--------| +| **Now** | JWT signatures required; ZAK optional | +| **Nov 2025** | SDK v6.6.10 with OBF-specific error codes | +| **Feb 23, 2026** | **OBF or ZAK required for external meetings** | + +--- + +## OAuth Scopes Summary + +| Token | Required Scope | +|-------|----------------| +| ZAK Token | `user:read:zak` | +| OBF Token | `user:read:token` | + +## Resources + +- **Meeting SDK Auth**: https://developers.zoom.us/docs/meeting-sdk/auth/ +- **OBF Token Announcement**: https://developers.zoom.us/blog/transition-to-obf-token-meetingsdk-apps/ +- **Linux SDK Sample**: https://github.com/zoom/meetingsdk-headless-linux-sample +- **Developer Forum**: https://devforum.zoom.us/ diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/breakout-rooms.md b/plugins/zoom-developers/skills/meeting-sdk/references/breakout-rooms.md new file mode 100644 index 00000000..c8560823 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/breakout-rooms.md @@ -0,0 +1,490 @@ +# Breakout Rooms + +Programmatically manage breakout rooms in Zoom meetings across all platforms. + +## Overview + +Breakout rooms allow hosts to split meeting participants into smaller groups. This guide covers SDK APIs for creating, managing, and controlling breakout rooms. + +## Platform Support + +| Platform | Support Level | Notes | +|----------|---------------|-------| +| **Web SDK** | Full | Complete API | +| **iOS SDK** | Full | Creator + Admin helpers | +| **Android SDK** | Full | Creator + Admin helpers | +| **Windows SDK** | Full | Controller interface | +| **macOS SDK** | Full | Controller interface | +| **Linux SDK** | Limited | Basic functionality only | +| **Video SDK** | Different | Uses "Subsessions" - not native breakout rooms | + +**Important:** Video SDK does NOT have native breakout rooms. It uses a "Subsessions" concept requiring manual session management. + +## REST API: Pre-assigned Rooms + +Create meetings with pre-assigned breakout rooms: + +```bash +POST /v2/users/{userId}/meetings +``` + +```json +{ + "topic": "Team Workshop", + "type": 2, + "settings": { + "breakout_room": { + "enable": true, + "rooms": [ + { + "name": "Team Alpha", + "participants": ["user1@example.com", "user2@example.com"] + }, + { + "name": "Team Beta", + "participants": ["user3@example.com", "user4@example.com"] + } + ] + } + } +} +``` + +**Limitation:** Pre-assigned rooms are NOT auto-opened. The host must manually open breakout rooms when the meeting starts. There is NO REST API to auto-open rooms. + +--- + +## Web SDK Implementation + +### Create Breakout Rooms + +```javascript +// Create 5 rooms with auto-generated names (Room 1, Room 2, etc.) +ZoomMtg.BreakoutRoom.createBreakoutRoom({ + data: 5, + success: (response) => console.log('Rooms created:', response), + error: (error) => console.error('Error:', error) +}); + +// Create named rooms +ZoomMtg.BreakoutRoom.createBreakoutRoom({ + data: [ + { name: 'Engineering' }, + { name: 'Design' }, + { name: 'Product' } + ], + success: (response) => console.log('Rooms created:', response), + error: (error) => console.error('Error:', error) +}); +``` + +### Get Breakout Rooms + +```javascript +ZoomMtg.BreakoutRoom.getBreakoutRooms({ + success: (response) => { + const rooms = response.result.rooms; + rooms.forEach(room => { + console.log(`Room ID: ${room.boId}, Name: ${room.name}`); + }); + }, + error: (error) => console.error('Error:', error) +}); +``` + +### Assign Participants + +```javascript +// Get unassigned attendees first +ZoomMtg.BreakoutRoom.getUnassignedAttendeeList({ + success: (response) => { + const unassigned = response.result.unassignedAttendeeList; + console.log('Unassigned:', unassigned); + } +}); + +// Assign user to a room +ZoomMtg.BreakoutRoom.assignUserToBreakoutRoom({ + targetRoomId: 'room-id-here', + userId: 12345678, + success: (response) => console.log('Assigned:', response), + error: (error) => console.error('Error:', error) +}); +``` + +### Move Participants Between Rooms + +```javascript +ZoomMtg.BreakoutRoom.moveUserToBreakoutRoom({ + targetRoomId: 'destination-room-id', + userId: 12345678, + success: (response) => console.log('Moved:', response), + error: (error) => console.error('Error:', error) +}); +``` + +### Open Breakout Rooms + +```javascript +ZoomMtg.BreakoutRoom.openBreakoutRooms({ + options: { + isAutoJoinRoom: false, // Let participants choose + isBackToMainSessionEnabled: true, // Allow returning to main + isTimerEnabled: true, // Enable countdown + timerDuration: 1800, // 30 minutes (seconds) + needCountDown: true, // Show countdown + waitSeconds: 60 // Wait before auto-join + }, + success: (response) => console.log('Rooms opened:', response), + error: (error) => console.error('Error:', error) +}); +``` + +### Close Breakout Rooms + +```javascript +ZoomMtg.BreakoutRoom.closeBreakoutRooms({ + success: (response) => console.log('Rooms closed:', response), + error: (error) => console.error('Error:', error) +}); +``` + +### Broadcast Message + +```javascript +ZoomMtg.BreakoutRoom.broadcast({ + message: 'Please return to the main room in 2 minutes', + success: (response) => console.log('Broadcast sent:', response), + error: (error) => console.error('Error:', error) +}); +``` + +### Check User Status + +```javascript +// Get current user's breakout room +ZoomMtg.BreakoutRoom.getCurrentBreakoutRoom({ + success: (response) => { + const { roomId, name, attendeeStatus } = response.result; + console.log(`Current room: ${name}, Status: ${attendeeStatus}`); + } +}); + +// attendeeStatus values: +// 1: UNASSIGNED - Not assigned to any room +// 2: ASSIGNED_NOT_JOIN - Assigned but hasn't joined yet +// 3: IN_BO - Currently in breakout room +``` + +--- + +## iOS SDK Implementation + +### Get Helpers + +```objc +#import + +// Get meeting service +MobileRTCMeetingService *meetingService = [[MobileRTC sharedRTC] getMeetingService]; + +// Get breakout room creator (for creating rooms) +MobileRTCBOCreator *boCreator = [meetingService getCreatorHelper]; + +// Get breakout room admin (for managing rooms) +MobileRTCBOAdmin *boAdmin = [meetingService getAdminHelper]; +``` + +### Create Rooms + +```objc +// Create 3 breakout rooms +[boCreator createBreakoutRoom:3 completion:^(NSError *error) { + if (error) { + NSLog(@"Error: %@", error.localizedDescription); + } else { + NSLog(@"Rooms created"); + } +}]; + +// Create room with specific name +[boCreator createBreakoutRoomWithName:@"Engineering" completion:^(NSError *error) { + // Handle result +}]; +``` + +### Manage Rooms + +```objc +// Open all rooms +[boAdmin openAllRoomsCompletion:^(NSError *error) { + if (!error) { + NSLog(@"Rooms opened"); + } +}]; + +// Assign user to room +[boAdmin assignUser:userId toRoom:roomId completion:^(NSError *error) { + if (!error) { + NSLog(@"User assigned"); + } +}]; + +// Close all rooms +[boAdmin closeAllRoomsCompletion:^(NSError *error) { + if (!error) { + NSLog(@"Rooms closed"); + } +}]; +``` + +### Event Handling + +```objc +@interface MyDelegate : NSObject +@end + +@implementation MyDelegate + +- (void)onMeetingBreakoutRoomStatusChanged:(MobileRTCBreakoutRoomStatus)status { + switch (status) { + case MobileRTCBreakoutRoomStatusNotStarted: + NSLog(@"Breakout rooms not started"); + break; + case MobileRTCBreakoutRoomStatusStarted: + NSLog(@"Breakout rooms started"); + break; + case MobileRTCBreakoutRoomStatusClosed: + NSLog(@"Breakout rooms closed"); + break; + } +} + +@end +``` + +--- + +## Android SDK Implementation + +### Get Helpers + +```kotlin +import us.zoom.sdk.ZoomSDK + +val zoomSDK = ZoomSDK.getInstance() +val meetingService = zoomSDK.meetingService +val boController = meetingService?.inMeetingBreakoutRoomController + +// Get creator (for creating rooms) +val creator = boController?.getCreatorHelper() + +// Get admin (for managing rooms) +val admin = boController?.getAdminHelper() +``` + +### Create Rooms + +```kotlin +// Create breakout rooms +val error = creator?.createBreakoutRoom(5) // Create 5 rooms +if (error == SDKError.SDKERR_SUCCESS) { + Log.d("Breakout", "Rooms created") +} + +// Create room with name +creator?.createBreakoutRoomWithName("Engineering") +``` + +### Manage Rooms + +```kotlin +// Open all rooms +admin?.openAllRooms() + +// Assign user to room +admin?.assignUser(userId, roomId) + +// Move user between rooms +admin?.assignUser(userId, newRoomId) // Removes from old room + +// Broadcast message +admin?.broadcastToAll("Please return in 2 minutes") + +// Close all rooms +admin?.closeAllRooms() +``` + +--- + +## Windows/macOS SDK Implementation + +### Windows (C++) + +```cpp +#include "meeting_breakout_rooms_interface.h" + +class MyBreakoutRoomsEvent : public IMeetingBreakoutRoomsEvent { +public: + void OnBreakoutRoomsStartedNotification(const wchar_t* stBID) override { + // Handle breakout rooms started + wprintf(L"Breakout rooms started: %s\n", stBID); + } +}; + +// Get controller +IMeetingBreakoutRoomsController* pController = + pMeetingService->GetBreakoutRoomsController(nullptr); + +// Set event handler +pController->SetEvent(new MyBreakoutRoomsEvent()); + +// Get list of rooms +IList* pRoomList = pController->GetBreakoutRoomsInfoList(); +for (int i = 0; i < pRoomList->GetItemCount(); ++i) { + IBreakoutRoomsInfo* pRoom = pRoomList->GetItem(i); + wprintf(L"Room: %s (ID: %s)\n", + pRoom->GetBreakoutRoomName(), + pRoom->GetBID()); +} + +// Join a breakout room +pController->JoinBreakoutRoom(L"room-id"); + +// Leave breakout room +pController->LeaveBreakoutRoom(); +``` + +### macOS (Objective-C) + +```objc +#import + +// Get controller +ZoomSDKBreakoutRoomsController *boController = + [[ZoomSDK sharedSDK] getMeetingService] getBreakoutRoomsController]; + +// Join breakout room +[boController requestJoinBreakoutRoom:@"room-id"]; + +// Leave breakout room +[boController requestLeaveBreakoutRoom]; + +// Close all rooms (host only) +[boController requestCloseAllBreakoutRooms]; +``` + +--- + +## Permissions + +### Host/Co-Host Restrictions + +| Action | Host | Co-Host | Participant | +|--------|------|---------|-------------| +| Create breakout rooms | ✅ | ✅ | ❌ | +| Open breakout rooms | ✅ | ✅ | ❌ | +| Close breakout rooms | ✅ | ✅ | ❌ | +| Assign participants | ✅ | ✅ | ❌ | +| Move participants | ✅ | ❌ | ❌ | +| Broadcast messages | ✅ | ✅ | ❌ | +| Join any room | ✅ | ✅* | ❌ | + +*Co-hosts can only join rooms assigned by host. + +--- + +## Limitations + +### Capacity Limits + +| Account Type | Max Rooms | Max Participants | +|--------------|-----------|------------------| +| Standard | 50 rooms | 500 total | +| Large Meeting Add-on | 100 rooms | 1,000 total | + +### Recording Limitations + +- **Cloud Recording**: Only records the **main session** +- **Local Recording**: Records only the room the recorder is in +- **Host cannot record** breakout rooms they're not in + +### Cannot Auto-Open Pre-Assigned Rooms + +**Critical:** There is NO API to auto-open pre-assigned breakout rooms. The host MUST manually open rooms when the meeting starts. + +### Session Timeout + +If no participant remains in the main session during breakout rooms, the main session may close after timeout. Ensure at least one participant (host or bot) stays in main session. + +--- + +## Best Practices + +### 1. Check Support Before Creating + +```javascript +ZoomMtg.BreakoutRoom.getBreakoutRoomOptions({ + success: (response) => { + if (response.result.isSupportBreakoutRoom) { + // Proceed to create rooms + } + } +}); +``` + +### 2. Handle User Status + +```javascript +// Check before assigning +ZoomMtg.BreakoutRoom.getUserStatus({ + userId: userId, + success: (response) => { + const { attendeeStatus } = response.result; + if (attendeeStatus === 3) { // IN_BO + // User already in a room - move instead of assign + } + } +}); +``` + +### 3. Error Handling + +```javascript +function handleBreakoutError(error) { + switch (error.method) { + case 'createBreakoutRoom': + if (error.errorMessage.includes('not support')) { + alert('Breakout rooms not enabled for this meeting'); + } + break; + case 'assignUserToBreakoutRoom': + if (error.errorMessage.includes('not host')) { + alert('Only host/co-host can assign participants'); + } + break; + } +} +``` + +--- + +## Video SDK Note + +Video SDK does **NOT** have native breakout rooms. Instead, use "Subsessions": + +1. Create separate Video SDK sessions +2. Move participants between sessions programmatically +3. Implement your own room management logic + +See: https://developers.zoom.us/blog/build-breakout-rooms-for-video-sdk/ + +--- + +## Resources + +- **Web SDK Docs**: https://developers.zoom.us/docs/meeting-sdk/web/ +- **iOS SDK Docs**: https://developers.zoom.us/docs/meeting-sdk/ios/ +- **Android SDK Docs**: https://developers.zoom.us/docs/meeting-sdk/android/ +- **REST API - Meetings**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Meetings +- **Developer Forum**: https://devforum.zoom.us/ diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/environment-variables.md b/plugins/zoom-developers/skills/meeting-sdk/references/environment-variables.md new file mode 100644 index 00000000..a56bc589 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/environment-variables.md @@ -0,0 +1,29 @@ +# Zoom Meeting SDK Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_SDK_KEY` | Yes | Meeting SDK signature issuer identity | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_SDK_SECRET` | Yes | Signature generation secret | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_MEETING_NUMBER` | Per flow | Meeting to join/start | Zoom meeting invite, Zoom web portal, or Meetings API | +| `ZOOM_MEETING_PASSWORD` | Conditional | Meeting passcode | Meeting invite details / Meetings API | +| `ZOOM_ROLE` | Conditional | Signature role (`0` attendee, `1` host) | Set by your app logic | +| `ZOOM_ZAK` | Host flows only | Host authorization token for start flows | Generate via Zoom REST API user token endpoint | + +## Runtime-only values + +- `MEETING_SDK_JWT` (generated signature) + +Generate server-side and keep short-lived. + +## Notes + +- Never expose `ZOOM_SDK_SECRET` in frontend/mobile clients. + +## Platform-Specific References + +- Android: [../android/references/environment-variables.md](../android/references/environment-variables.md) +- iOS: [../ios/references/environment-variables.md](../ios/references/environment-variables.md) +- macOS: [../macos/references/environment-variables.md](../macos/references/environment-variables.md) +- Unreal: [../unreal/references/environment-variables.md](../unreal/references/environment-variables.md) diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/forum-top-questions.md b/plugins/zoom-developers/skills/meeting-sdk/references/forum-top-questions.md new file mode 100644 index 00000000..efe16b4d --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/forum-top-questions.md @@ -0,0 +1,100 @@ +--- +title: "Forum-Derived Top Questions (Meeting SDK)" +--- + +# Forum-Derived Top Questions (Meeting SDK) + +Use this as a high-signal checklist of what developers repeatedly ask about the **Zoom Meeting SDK** (Web, Mobile, Desktop, Linux). + +## Fast Routing Questions (Ask First) + +- Platform: **Web** vs **Android/iOS** vs **Windows/macOS** vs **Linux headless** +- Web integration: **Client View (CDN + `ZoomMtg`)** vs **Component View (npm + `ZoomMtgEmbedded`)** +- Join type: **join** as participant vs **start** as host +- Auth inputs you have: `sdkKey`, `sdkSecret` (server only), `meetingNumber`, `role`, `zak` (if starting as host), `passcode` +- Exact error: full error code/message + SDK version + +## Signatures (Most Common Root Cause) + +- **Generate signature server-side only** (never expose SDK Secret in browser/mobile client code). +- Use the right payload fields: + - `sdkKey`, `mn` (meeting number), `role`, `iat`, `exp`, `tokenExp` +- Typical mistakes: + - Wrong meeting number format (non-digits; strip formatting) + - `exp` too long/short, or client/server clock skew + - Mixing Meeting SDK signature with REST API OAuth/JWT app-type tokens (different things) + +## Web SDK: Client View vs Component View Confusion + +- **Client View (CDN)** uses `ZoomMtg.*` callback style. +- **Component View (npm)** uses `ZoomMtgEmbedded.createClient()` with promise-based APIs. +- When a question is about “hide UI”, “toolbar”, “meeting info”, first confirm which view they are using: + - Some UI changes are only possible in **one** of the views, or not supported at all. + +## “Hide Meeting Password / Invite URL / Meeting Info” + +Common ask: “How do I hide passcode / meeting info / invite URL in Meeting SDK?” + +What to cover in answers: +- What’s supported by the SDK (official flags/APIs) vs what is not. +- If the goal is “don’t leak passcode”, the most reliable approach is usually: + - Use meeting settings that reduce exposure (and avoid displaying it in your own UI) + - Don’t log it client-side + - Don’t render invite UI if you control that surface (Component View) + +## Join Failures and Timeouts (Web) + +Common asks: +- “Join meeting failed” +- “Joining meeting timeout” +- “Browser doesn’t support gallery view” + +Checklist: +- Confirm `crossOriginIsolated` / SharedArrayBuffer requirements (if using features that need it) +- Confirm HTTPS + correct COOP/COEP headers (when required) +- Confirm ad blockers / CSP / corporate proxies aren’t blocking Zoom assets +- Confirm correct `passWord` casing (Web Client View uses `passWord`) + +## Captions / Transcript UI + +Common asks: +- Enabling captions +- Hiding captions but transcript still shows + +Answer pattern: +- Separate “what the host/account policy controls” from “what SDK UI controls” +- Call out constraints: some transcript/caption behaviors are server-side policy and not fully suppressible from SDK UI alone + +## Waiting Room and Admission Flow + +Common asks: +- Enabling/disabling waiting room +- Notifications and UI behavior + +Answer pattern: +- Distinguish Meeting settings (host/account) vs SDK client behavior +- If the goal is “auto-admit” or “control admission”, you likely need the host controls (and sometimes REST API) rather than just SDK UI changes + +## Raw Data / Raw Recording + +Common asks: +- Start raw recording fails (permissions) +- Raw data availability varies by platform + +Answer pattern: +- Always ask platform + SDK variant and whether they’re using supported raw-data APIs for that platform +- If it’s permission-related: + - confirm required entitlements/features + - confirm app permissions / OS permissions + +## Performance: “Save CPU”, Gallery View, Low-End Devices + +Common asks: +- Gallery view availability +- High CPU usage + +Checklist: +- Reduce subscribed video streams / lower quality where supported +- Ensure you’re not rendering unnecessary DOM/video elements (Component View) +- Confirm the device/browser constraints (some behavior is expected) + diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/full-guide.md b/plugins/zoom-developers/skills/meeting-sdk/references/full-guide.md new file mode 100644 index 00000000..682f38f3 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/full-guide.md @@ -0,0 +1,234 @@ +# /build-zoom-meeting-sdk-app + +Background reference for embedded Zoom meetings across web, mobile, desktop, and Linux bot environments. Prefer `build-zoom-meeting-app` or `build-zoom-bot` first, then route here for platform detail. + +# Zoom Meeting SDK + +Embed the full Zoom meeting experience into web, mobile, desktop, and headless integrations. + +## Hard Routing Guardrail (Read First) + +- If the user asks to embed/join meetings inside their app UI, route to Meeting SDK implementation. +- Do not switch to REST-only meeting link flow unless the user explicitly asks for meeting resource management or browser `join_url` links. +- Meeting SDK join path requires SDK signature + SDK join call; REST `join_url` is not a Meeting SDK join payload. + +## Prerequisites + +- Zoom app with Meeting SDK credentials +- SDK Key and Secret from Marketplace +- Platform-specific development environment (Web, Android, iOS, macOS, Unreal, Electron, Linux, or Windows) + +> **Need help with OAuth or signatures?** See the **[zoom-oauth](../../oauth/SKILL.md)** skill for authentication flows. + +> **Need pre-join diagnostics on web?** Use **[probe-sdk](../../probe-sdk/SKILL.md)** before Meeting SDK init/join to gate low-readiness devices/networks. + +> **Start troubleshooting fast:** Use the **[5-Minute Runbook](../RUNBOOK.md)** before deep debugging. + +## Quick Start (Web - Client View via CDN) + +```html + + + + + + + + +``` + +## Critical Notes (Web) + +### 1. CDN vs npm - Different APIs! + +| Distribution | Global Object | View Type | API Style | +|--------------|---------------|-----------|-----------| +| CDN (`zoom-meeting-{ver}.min.js`) | `ZoomMtg` | Client View (full-page) | Callbacks | +| npm (`@zoom/meetingsdk`) | `ZoomMtgEmbedded` | Component View (embeddable) | Promises | + +### 2. Backend Required for Production + +**Never expose SDK Secret in client code.** Generate signatures server-side: + +```javascript +// server.js (Node.js example) +const KJUR = require('jsrsasign'); + +app.post('/api/signature', (req, res) => { + const { meetingNumber, role } = req.body; + const iat = Math.floor(Date.now() / 1000) - 30; + const exp = iat + 60 * 60 * 2; + + const header = { alg: 'HS256', typ: 'JWT' }; + const payload = { + sdkKey: process.env.ZOOM_SDK_KEY, + mn: String(meetingNumber).replace(/\D/g, ''), + role: parseInt(role, 10), + iat, exp, tokenExp: exp + }; + + const signature = KJUR.jws.JWS.sign('HS256', + JSON.stringify(header), + JSON.stringify(payload), + process.env.ZOOM_SDK_SECRET + ); + + res.json({ signature, sdkKey: process.env.ZOOM_SDK_KEY }); +}); +``` + +### 3. CSS Conflicts - Avoid Global Resets + +Global `* { margin: 0; }` breaks Zoom's UI. Scope your styles: + +```css +/* BAD */ +* { margin: 0; padding: 0; } + +/* GOOD */ +.your-app, .your-app * { box-sizing: border-box; } +``` + +### 4. Client View Toolbar Cropping Fix + +If toolbar falls off screen, scale down the Zoom UI: + +```css +#zmmtg-root { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100vw !important; + height: 100vh !important; + /* Critical for SPAs (React/Next/etc): ensure Zoom UI isn't behind your app shell/overlays. */ + z-index: 9999 !important; + transform: scale(0.95) !important; + transform-origin: top center !important; +} +``` + +### 5. Hide Your App When Meeting Starts + +Client View takes over full page. Hide your UI: + +```javascript +// In ZoomMtg.init success callback: +document.documentElement.classList.add('meeting-active'); +document.body.classList.add('meeting-active'); +``` + +```css +body.meeting-active .your-app { display: none !important; } +body.meeting-active { background: #000 !important; } +``` + +## UI Options (Web) + +Meeting SDK provides **Zoom's UI with customization options**: + +| View | Description | +|------|-------------| +| **Component View** | Extractable, customizable UI - embed meeting in a div | +| **Client View** | Full-page Zoom UI experience | + +**Note**: Unlike Video SDK where you build the UI from scratch, Meeting SDK uses Zoom's UI as the base with customization on top. + +## Key Concepts + +| Concept | Description | +|---------|-------------| +| SDK Key/Secret | Credentials from Marketplace | +| Signature | JWT signed with SDK Secret | +| Component View | Extractable, customizable UI (Web) | +| Client View | Full-page Zoom UI (Web) | + +## Detailed References + +### Platform Guides +- **[android/SKILL.md](../android/SKILL.md)** - Android SDK (default/custom UI, join/start/auth lifecycle, mobile integration) +- **[android/references/android-reference-map.md](../android/references/android-reference-map.md)** - Android API surface map and drift watchpoints +- **[ios/SKILL.md](../ios/SKILL.md)** - iOS SDK (default/custom UI, join/start/auth lifecycle, mobile integration) +- **[ios/references/ios-reference-map.md](../ios/references/ios-reference-map.md)** - iOS API surface map and drift watchpoints +- **[macos/SKILL.md](../macos/SKILL.md)** - macOS SDK (desktop default/custom UI, service controllers, host flows) +- **[macos/references/macos-reference-map.md](../macos/references/macos-reference-map.md)** - macOS API surface map and drift watchpoints +- **[unreal/SKILL.md](../unreal/SKILL.md)** - Unreal Engine wrapper (C++/Blueprint wrapper behavior and SDK mapping) +- **[unreal/references/unreal-reference-map.md](../unreal/references/unreal-reference-map.md)** - Unreal wrapper reference map and version-lag notes +- **[references/android.md](../references/android.md)** - Android pointer doc for fast routing from broad Meeting SDK queries +- **[references/ios.md](../references/ios.md)** - iOS pointer doc for fast routing from broad Meeting SDK queries +- **[references/macos.md](../references/macos.md)** - macOS pointer doc for fast routing from broad Meeting SDK queries +- **[references/unreal.md](../references/unreal.md)** - Unreal pointer doc for fast routing from broad Meeting SDK queries +- **[linux/SKILL.md](../linux/SKILL.md)** - Linux SDK headless bot skill entrypoint +- **[linux/linux.md](../linux/linux.md)** - Linux SDK (C++ headless bots, raw media access) +- **[linux/references/linux-reference.md](../linux/references/linux-reference.md)** - Linux dependencies, Docker, troubleshooting +- **[react-native/SKILL.md](../react-native/SKILL.md)** - React Native SDK (iOS/Android wrapper, join/start flows, bridge setup) +- **[react-native/SKILL.md](../react-native/SKILL.md)** - React Native complete navigation +- **[electron/SKILL.md](../electron/SKILL.md)** - Electron SDK (desktop wrapper, auth/join flows, module controllers, raw data) +- **[electron/SKILL.md](../electron/SKILL.md)** - Electron complete navigation +- **[windows/SKILL.md](../windows/SKILL.md)** - Windows SDK (C++ desktop applications, raw media access) +- **[windows/references/windows-reference.md](../windows/references/windows-reference.md)** - Windows dependencies, Visual Studio setup, troubleshooting +- **[web/references/web.md](../web/references/web.md)** - Web SDK (Component + Client View) +- **[web/references/web-tracking-id.md](../web/references/web-tracking-id.md)** - Tracking ID configuration + +### Features +- **[references/authorization.md](../references/authorization.md)** - SDK JWT generation +- **[references/bot-authentication.md](../references/bot-authentication.md)** - ZAK vs OBF vs JWT tokens for bots +- **[references/breakout-rooms.md](../references/breakout-rooms.md)** - Programmatic breakout room management +- **[references/ai-companion.md](../references/ai-companion.md)** - AI Companion controls in meetings +- **[references/webinars.md](../references/webinars.md)** - Webinar SDK features +- **[references/forum-top-questions.md](../references/forum-top-questions.md)** - Common forum question patterns (what to cover) +- **[references/triage-intake.md](../references/triage-intake.md)** - What to ask first (turn vague reports into answers) +- **[references/signature-playbook.md](../references/signature-playbook.md)** - Signature/root-cause playbook +- **[references/multiple-meetings.md](../references/multiple-meetings.md)** - Joining multiple meetings / multiple instances +- **[references/troubleshooting.md](../references/troubleshooting.md)** - Common issues and solutions + +## Sample Repositories + +### Official (by Zoom) + +| Type | Repository | Stars | +|------|------------|-------| +| Linux Headless | [meetingsdk-headless-linux-sample](https://github.com/zoom/meetingsdk-headless-linux-sample) | 4 | +| Linux Raw Data | [meetingsdk-linux-raw-recording-sample](https://github.com/zoom/meetingsdk-linux-raw-recording-sample) | 0 | +| Web | [meetingsdk-web-sample](https://github.com/zoom/meetingsdk-web-sample) | 643 | +| Web NPM | [meetingsdk-web](https://github.com/zoom/meetingsdk-web) | 324 | +| React | [meetingsdk-react-sample](https://github.com/zoom/meetingsdk-react-sample) | 177 | +| Auth | [meetingsdk-auth-endpoint-sample](https://github.com/zoom/meetingsdk-auth-endpoint-sample) | 124 | +| Angular | [meetingsdk-angular-sample](https://github.com/zoom/meetingsdk-angular-sample) | 60 | +| Vue.js | [meetingsdk-vuejs-sample](https://github.com/zoom/meetingsdk-vuejs-sample) | 42 | + +**Full list**: See [general/references/community-repos.md](../../general/references/community-repos.md) + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/ +- **Developer forum**: https://devforum.zoom.us/ + +## Environment Variables + +- See [references/environment-variables.md](../references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/ios.md b/plugins/zoom-developers/skills/meeting-sdk/references/ios.md new file mode 100644 index 00000000..6149a7d0 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/ios.md @@ -0,0 +1,27 @@ +# Meeting SDK (iOS) Pointers + +Use this page as a quick starting point for iOS Meeting SDK questions before diving into the deeper iOS references. + +Start here: +- [../ios/SKILL.md](../ios/SKILL.md) + +## Common Forum Questions + +- How do I initialize the iOS Meeting SDK? +- What permissions/entitlements do I need for camera/mic? +- How do I customize UI vs default UI? + +## Practical Guidance + +- Start with default UI until basic join/start works. +- Confirm camera/mic permission flows. +- When debugging join failures, capture: + - SDK version + - init/join return codes + - meeting number vs meeting UUID confusion +- Use [../ios/references/ios-reference-map.md](../ios/references/ios-reference-map.md) to confirm current API surface before assuming wrapper/sample parity. + +## Next + +- `android.md` for Android equivalents. +- `authorization.md` and `signature-playbook.md` for auth/signature concepts shared across platforms. diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/macos.md b/plugins/zoom-developers/skills/meeting-sdk/references/macos.md new file mode 100644 index 00000000..dc70aa4d --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/macos.md @@ -0,0 +1,16 @@ +# Meeting SDK (macOS) Pointers + +Stable pointer for macOS Meeting SDK questions. + +Start here: +- [../macos/SKILL.md](../macos/SKILL.md) + +Common question patterns: +- How do I choose default UI vs custom UI? +- How do I handle start/join flows with host privileges? +- Which service/controller delegates must be registered first? + +Practical guidance: +- Validate default UI join path before custom UI. +- Confirm auth/signature and role alignment before host actions. +- Use [../macos/references/macos-reference-map.md](../macos/references/macos-reference-map.md) for API discovery and upgrade drift checks. diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/multiple-meetings.md b/plugins/zoom-developers/skills/meeting-sdk/references/multiple-meetings.md new file mode 100644 index 00000000..dd103e2e --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/multiple-meetings.md @@ -0,0 +1,28 @@ +--- +title: "Multiple Meetings and Multiple Instances" +--- + +# Multiple Meetings and Multiple Instances + +Common forum question: “How do I attend two meeting rooms at the same time?” + +## Core Constraint + +In most SDK/client integrations, a single Meeting SDK client instance is designed around **one active meeting session at a time**. + +If you need “two meetings at once”, you typically need **two separate instances** (and often separate process/browser contexts/devices). + +## Practical Options (What Usually Works) + +- **Two devices** (simplest operationally) +- **Two separate processes** (desktop apps) +- **Two separate browser profiles/containers** (web), if supported by the environment + +## What to Clarify + +- Platform (Web vs Windows/macOS vs Android/iOS vs Linux) +- Whether you need: + - audio/video in both meetings concurrently + - or just monitoring/viewing one while connected to another +- Any compliance constraints (recording, transcription, etc.) + diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/signature-playbook.md b/plugins/zoom-developers/skills/meeting-sdk/references/signature-playbook.md new file mode 100644 index 00000000..6bb13e4c --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/signature-playbook.md @@ -0,0 +1,45 @@ +--- +title: "Meeting SDK Signature Playbook" +--- + +# Meeting SDK Signature Playbook + +Most “join failed” issues reduce to signature generation or mismatched inputs. + +## Rules + +1. **Generate signatures server-side only**. Never ship the SDK Secret to the browser/app. +2. `meetingNumber` must be digits only. +3. `role` must match what you are doing: + - `0` = join as attendee + - `1` = start as host +4. Keep `iat/exp` reasonable and account for clock skew (server time matters). + +## Required Payload Fields (Common Pattern) + +You’ll typically see fields like: + +- `sdkKey` +- `mn` (meeting number) +- `role` +- `iat`, `exp`, `tokenExp` + +If the developer is mixing in REST API OAuth tokens or Marketplace JWT app-type tokens, stop and clarify: those are **not** Meeting SDK signatures. + +## Common Failure Modes + +- **Invalid signature**: + - wrong secret + - wrong `mn` format + - expired `exp/tokenExp` + - generating signature for role=1 but joining (or vice versa) +- **4003 Invalid Parameter** (common on Web “start” flows): + - role mismatch or missing host requirements (often needs ZAK for host start flows) +- **Works locally but not in prod**: + - different env vars/secret + - prod server clock skew + +## Web-Specific Gotcha: `passWord` + +On Web Client View (`ZoomMtg.join`) the key is `passWord` (capital W). If the meeting has a passcode and it’s missing/misnamed, join fails in a way that looks like auth trouble. + diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/triage-intake.md b/plugins/zoom-developers/skills/meeting-sdk/references/triage-intake.md new file mode 100644 index 00000000..258f31af --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/triage-intake.md @@ -0,0 +1,52 @@ +--- +title: "Meeting SDK Triage Intake (What to Ask First)" +--- + +# Meeting SDK Triage Intake (What to Ask First) + +This is the fastest way to turn a vague “Meeting SDK isn’t working” question into an answer. + +## 1) Which SDK + Which Platform? + +- Platform: `web` | `android` | `ios` | `react-native` | `electron` | `windows` | `macos` | `linux` +- Exact SDK package/version (and Zoom client version if relevant) + +## 2) Web: Client View vs Component View + +Ask explicitly which one they’re using: + +- **Client View (CDN)**: uses `ZoomMtg.*` (callbacks) +- **Component View (npm)**: uses `ZoomMtgEmbedded.createClient()` (promises) + +These have different APIs and different customization limitations. + +## 3) Join vs Start, and Role + +- Are you **joining** as an attendee (`role=0`) or **starting** as host (`role=1`)? +- If starting as host, do you have a **ZAK** (Zoom Access Key) when required by the platform/SDK flow? + +## 4) Inputs (Copy/Paste) + +Request the exact values and formats (redact secrets): + +- `meetingNumber` (digits only) +- `passcode` / `password` (and on Web Client View, confirm they used `passWord` key name) +- `userName` +- `sdkKey` (OK to share) +- signature generation code (server-side) and the payload fields used (`mn`, `role`, `iat`, `exp`, `tokenExp`) + +## 5) The Symptom + +Pick one bucket: + +- signature/auth: “Invalid signature”, “signature expired”, “invalid parameter” +- web environment: “Joining meeting timeout”, “organization disabled access”, “SharedArrayBuffer”, “gallery view” +- UI/customization: “hide meeting info”, “hide passcode/invite URL”, “remove buttons” +- media: “no audio/video”, “screen share problems”, “performance/cpu” + +## 6) Minimal Repro + Logs + +- Minimal steps to reproduce +- SDK logs (see `general/references/sdk-logs-troubleshooting.md`) +- On Web: browser + OS, console errors, and whether corporate proxy/adblock is present + diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/troubleshooting.md b/plugins/zoom-developers/skills/meeting-sdk/references/troubleshooting.md new file mode 100644 index 00000000..45a22c31 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/troubleshooting.md @@ -0,0 +1,132 @@ +# Meeting SDK - Troubleshooting + +Common issues and solutions for Meeting SDK. + +## Overview + +Troubleshooting guide for Meeting SDK across all platforms. + +## Common Issues + +### Join Meeting Failed + +| Error | Possible Cause | Solution | +|-------|----------------|----------| +| Invalid signature | JWT malformed or expired | Regenerate signature server-side | +| Meeting not found | Invalid meeting number | Verify meeting exists | +| Wrong password | Password mismatch | Check meeting password | +| Meeting locked | Host locked meeting | Contact host | + +**Note:** Error code 0 often means success - check the SDK enum values (e.g., `SDKERR_SUCCESS = 0`). + +### Authentication Issues + +| Issue | Possible Cause | Solution | +|-------|----------------|----------| +| Auth failed | Invalid credentials | Check SDK Key/Secret | +| Token expired | JWT too old | Generate fresh signature | +| Signature invalid | Wrong secret used | Verify SDK Secret | + +### No Video + +| Issue | Possible Cause | Solution | +|-------|----------------|----------| +| Black screen | Permission denied | Request camera permission | +| Video not starting | Camera in use | Close other camera apps | +| Poor quality | Low bandwidth | Check network | + +### No Audio + +| Issue | Possible Cause | Solution | +|-------|----------------|----------| +| Can't hear | Audio not connected | Join audio | +| Muted | User is muted | Check mute state | +| Echo | No echo cancellation | Use headphones | + +### Web-Specific Issues + +| Issue | Possible Cause | Solution | +|-------|----------------|----------| +| SharedArrayBuffer error | Missing headers | Add COOP/COEP headers | +| Component not rendering | Wrong container | Check `zoomAppRoot` element | +| Toolbar/controls missing | Global CSS resets | Don't use `* { margin: 0; }` - scope styles to your app | +| Toolbar cropped/off-screen | Zoom UI exceeds viewport | Use `transform: scale(0.95)` on `#zmmtg-root` | +| `ZoomMtgEmbedded is undefined` | Using CDN but Component View API | CDN provides `ZoomMtg`, use npm for `ZoomMtgEmbedded` | + +### Web: "Black Screen" After Join (UI Hidden Behind App) + +If the meeting "joins" but you only see your app shell or a blank/black area, the Zoom UI may be rendered but **covered by your SPA layout**, modals, or fixed headers. + +**Applies mostly to Client View**, and occasionally to Component View if your container is mis-sized/overlaid. + +Fix (Client View): + +```css +/* Ensure the root container occupies the viewport and sits above your app shell. */ +#zmmtg-root { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100vw !important; + height: 100vh !important; + z-index: 9999 !important; +} +``` + +Be critical: if you still see a "black screen", it can also be: + +- camera permission denied / no video started +- your container is `display:none` or has `height: 0` (Component View) +- global CSS resets breaking Zoom's layout + +### UI Customization Confusion (Web) + +If the question is about hiding passcode/invite URL or removing built-in controls: + +- Confirm **Client View vs Component View** +- Prefer supported customization knobs (e.g., `customize.meetingInfo` in Component View) +- Avoid brittle CSS hacks unless there's no supported alternative + +See: +- `web/references/component-view-ui-customization.md` + +### Client View CSS Fixes + +**Toolbar falling off screen:** +```css +#zmmtg-root { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100vw !important; + height: 100vh !important; + transform: scale(0.95) !important; + transform-origin: top center !important; +} +``` + +**Hide your app when meeting starts:** +```css +body.meeting-active .your-app { display: none !important; } +body.meeting-active { background: #000 !important; } +``` + +## Collecting Logs + +See [SDK Logs & Troubleshooting](../../general/references/sdk-logs-troubleshooting.md) for log collection. + +## Getting Support + +1. Collect SDK logs +2. Note SDK version and platform +3. Document steps to reproduce +4. Contact [Developer Support](https://devsupport.zoom.us/) + +## Resources + +- **Developer forum**: https://devforum.zoom.us/ +- **Support**: https://devsupport.zoom.us/ diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/unreal.md b/plugins/zoom-developers/skills/meeting-sdk/references/unreal.md new file mode 100644 index 00000000..6bc09a44 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/unreal.md @@ -0,0 +1,16 @@ +# Meeting SDK (Unreal) Pointers + +Stable pointer for Unreal Meeting SDK wrapper questions. + +Start here: +- [../unreal/SKILL.md](../unreal/SKILL.md) + +Common question patterns: +- Which methods are available in C++ wrapper vs Blueprint wrapper? +- Why does wrapper behavior differ from native platform SDK docs? +- How do I validate wrapper version compatibility with Unreal Engine version? + +Practical guidance: +- Treat wrapper docs as wrapper behavior source of truth first. +- Then cross-check base semantics against native Meeting SDK reference where wrapper notes indicate parity. +- Use [../unreal/references/versioning-and-compatibility.md](../unreal/references/versioning-and-compatibility.md) for known contradictions and lag risks. diff --git a/plugins/zoom-developers/skills/meeting-sdk/references/webinars.md b/plugins/zoom-developers/skills/meeting-sdk/references/webinars.md new file mode 100644 index 00000000..30913271 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/references/webinars.md @@ -0,0 +1,349 @@ +# Meeting SDK - Webinars + +Embed webinar experiences using Meeting SDK. + +## Overview + +Meeting SDK can join webinars, not just meetings. This guide covers webinar-specific features and considerations. + +## Webinar vs Meeting + +| Feature | Meeting | Webinar | +|---------|---------|---------| +| Max participants | 100-1000 | 500-50,000 | +| Participant roles | Host, co-host, participant | Host, panelist, attendee | +| Attendee video | Yes | No (view-only) | +| Attendee audio | Yes | Raise hand to unmute | +| Q&A | No | Yes | +| Registration | Optional | Common | +| Practice session | No | Yes | + +## Joining Webinars + +### Web SDK + +```javascript +const client = ZoomMtgEmbedded.createClient(); + +await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', +}); + +// Join webinar (same as meeting) +await client.join({ + sdkKey: SDK_KEY, + signature: signature, + meetingNumber: webinarId, // Webinar ID + userName: 'Attendee Name', + userEmail: 'attendee@example.com', // Required for registration + passWord: password, + tk: registrantToken, // If registration required +}); +``` + +### Registration Token + +If the webinar requires registration: + +```javascript +// 1. Register via API first +const response = await axios.post( + `https://api.zoom.us/v2/webinars/${webinarId}/registrants`, + { + email: 'attendee@example.com', + first_name: 'John', + last_name: 'Doe' + }, + { headers: { 'Authorization': `Bearer ${accessToken}` }} +); + +// 2. Get registrant token +const registrantToken = response.data.registrant_id; + +// 3. Use token when joining +await client.join({ + // ... other params + tk: registrantToken, +}); +``` + +### Native SDKs (iOS/Android/Windows) + +```swift +// iOS - Join webinar +let joinParam = ZoomSDKJoinWebinarParam() +joinParam.webinarNumber = webinarNumber +joinParam.userName = "Attendee" +joinParam.userEmail = "attendee@example.com" +joinParam.webinarPassword = password +joinParam.webinarToken = registrantToken // If registered + +meetingService.joinWebinar(with: joinParam) +``` + +```kotlin +// Android - Join webinar +val joinParams = JoinMeetingParams().apply { + meetingNo = webinarId + displayName = "Attendee" + password = webinarPassword +} + +// Webinar uses same joinMeeting API +meetingService.joinMeetingWithParams(context, joinParams, JoinMeetingOptions()) +``` + +## Attendee vs Panelist + +### Role Detection + +```javascript +// Web SDK +client.on('user-updated', (payload) => { + const { userId } = payload; + const user = client.getUser(userId); + + // Check role + if (user.isHost) { + console.log('User is host'); + } else if (user.userRole === 'panelist') { + console.log('User is panelist'); + } else { + console.log('User is attendee'); + } +}); +``` + +### Attendee Limitations + +Attendees in webinars have restricted capabilities: + +| Action | Attendee Can Do? | +|--------|------------------| +| View video | Yes | +| Send video | No | +| Listen to audio | Yes | +| Unmute self | No (must be promoted) | +| Send chat | To panelists/everyone (if enabled) | +| Ask Q&A | Yes | +| Raise hand | Yes | + +## Q&A Feature + +### Web SDK Q&A + +```javascript +// Check if Q&A is enabled +const qaClient = client.getQAClient(); +const isQAEnabled = qaClient.isQAEnabled(); + +// Ask a question +await qaClient.askQuestion('What is the pricing?', false); // false = public + +// Answer a question (panelist/host only) +await qaClient.answerQuestion(questionId, 'The pricing is...'); + +// Get all questions +const questions = qaClient.getAllQuestions(); +questions.forEach(q => { + console.log(`Q: ${q.text}`); + console.log(`A: ${q.answerText || 'Unanswered'}`); +}); + +// Q&A events +client.on('question-created', (payload) => { + console.log('New question:', payload.question.text); +}); + +client.on('answer-created', (payload) => { + console.log('Answer:', payload.answer.text); +}); +``` + +### Native SDK Q&A + +```swift +// iOS +let qaController = meetingService.getWebinarQAController() + +// Ask question +qaController?.askQuestion("What is the pricing?", isAnonymous: false) + +// Get questions (host/panelist) +let questions = qaController?.getAllQuestionList() +``` + +## Raise Hand + +### Web SDK + +```javascript +// Raise hand +client.raiseHand(); + +// Lower hand +client.lowerHand(); + +// Check status +const myself = client.getCurrentUser(); +console.log('Hand raised:', myself.bRaiseHand); + +// Event +client.on('user-updated', (payload) => { + if (payload.bRaiseHand !== undefined) { + console.log(`${payload.userId} ${payload.bRaiseHand ? 'raised' : 'lowered'} hand`); + } +}); +``` + +### Native SDK + +```swift +// iOS +let webinarController = meetingService.getWebinarController() +webinarController?.raiseHand() +webinarController?.lowerHand() +``` + +## Promote/Demote Attendees + +Hosts can promote attendees to panelists: + +```javascript +// Web SDK (host only) +const webinarClient = client.getWebinarClient(); + +// Promote to panelist (allows video/audio) +await webinarClient.promoteAttendee(userId); + +// Demote back to attendee +await webinarClient.demoteAttendee(userId); + +// Allow attendee to talk (temporary unmute) +await webinarClient.allowAttendeeTalk(userId); +await webinarClient.disallowAttendeeTalk(userId); +``` + +## Polling + +### Web SDK Polling + +```javascript +// Get polling client +const pollingClient = client.getPollingClient(); + +// Check if polling available +const isAvailable = pollingClient.isPollingEnabled(); + +// Submit poll answer (attendee) +await pollingClient.submitPollAnswer(pollId, [ + { questionId: 'q1', answerIds: ['a1'] } +]); + +// Poll events +client.on('poll-started', (payload) => { + console.log('Poll started:', payload.poll); + displayPoll(payload.poll); +}); + +client.on('poll-ended', (payload) => { + console.log('Poll ended'); +}); +``` + +## Practice Session + +Before a webinar starts, hosts can run a practice session: + +```javascript +// Detect practice session +client.on('meeting-status-changed', (payload) => { + if (payload.status === 'practice') { + console.log('In practice session - attendees cannot join yet'); + } else if (payload.status === 'webinar') { + console.log('Webinar is live'); + } +}); +``` + +## Chat in Webinars + +Chat permissions differ in webinars: + +```javascript +// Get chat privilege +const chatClient = client.getChatClient(); +const privilege = chatClient.getPrivilege(); + +// privilege values: +// 1 = No one +// 2 = Host and panelists only +// 3 = Everyone publicly +// 4 = Everyone publicly and privately + +// Send to panelists (attendee) +await chatClient.send('Question about pricing', /* to all panelists */); + +// Send to everyone (if allowed) +await chatClient.sendToAll('Hello everyone!'); +``` + +## Webinar-Specific Events + +```javascript +// Web SDK events +client.on('webinar-invite', (payload) => { + // Attendee invited to become panelist +}); + +client.on('webinar-depromote', (payload) => { + // Demoted from panelist to attendee +}); + +client.on('webinar-allow-attendee-chat', (payload) => { + // Chat permissions changed +}); + +client.on('webinar-attendee-status', (payload) => { + // Attendee count updated + console.log('Attendees:', payload.attendeeCount); +}); +``` + +## Best Practices + +### For Large Webinars + +1. **Disable attendee video** - Already default for webinars +2. **Use Q&A instead of chat** - More organized for large audiences +3. **Pre-register attendees** - Better tracking and control +4. **Test with practice session** - Verify setup before going live + +### UI Considerations + +```javascript +// Adjust UI based on role +function setupUI(role) { + if (role === 'attendee') { + // Hide video controls (can't send video) + hideElement('#videoControls'); + + // Show Q&A and raise hand + showElement('#qaPanel'); + showElement('#raiseHandButton'); + + // Disable unmute (until promoted) + disableElement('#unmuteButton'); + } else if (role === 'panelist' || role === 'host') { + // Full controls + showAllControls(); + } +} +``` + +## Resources + +- **Webinar API**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Webinars +- **Meeting SDK Webinar**: https://developers.zoom.us/docs/meeting-sdk/web/webinar/ +- **Developer Forum**: https://devforum.zoom.us/ diff --git a/plugins/zoom-developers/skills/meeting-sdk/unreal/RUNBOOK.md b/plugins/zoom-developers/skills/meeting-sdk/unreal/RUNBOOK.md new file mode 100644 index 00000000..6cdaf845 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/unreal/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK Unreal 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 Meeting SDK embed path for Unreal (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/unreal/ +- https://marketplacefront.zoom.us/sdk/meeting/unreal/MSDKUnrealSDKreferencedocs.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/unreal/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/unreal/` diff --git a/plugins/zoom-developers/skills/meeting-sdk/unreal/SKILL.md b/plugins/zoom-developers/skills/meeting-sdk/unreal/SKILL.md new file mode 100644 index 00000000..f5106311 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/unreal/SKILL.md @@ -0,0 +1,32 @@ +--- +name: zoom-meeting-sdk-unreal +description: | + Zoom Meeting SDK for Unreal Engine wrapper integrations. Use when building Unreal projects that + embed Zoom meetings with C++ and Blueprint wrappers, including wrapper-to-SDK mapping concerns. +--- + +# Zoom Meeting SDK (Unreal Engine) + +Use this skill when integrating Meeting SDK into Unreal Engine projects. + +## Start Here + +1. [unreal.md](unreal.md) +2. [concepts/lifecycle-workflow.md](concepts/lifecycle-workflow.md) +3. [concepts/architecture.md](concepts/architecture.md) +4. [examples/join-start-pattern.md](examples/join-start-pattern.md) +5. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +6. [references/unreal-reference-map.md](references/unreal-reference-map.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +9. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Key Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/unreal/ +- API reference: https://marketplacefront.zoom.us/sdk/meeting/unreal/MSDKUnrealSDKreferencedocs.html +- Broader guide: [../SKILL.md](../SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/meeting-sdk/unreal/concepts/architecture.md b/plugins/zoom-developers/skills/meeting-sdk/unreal/concepts/architecture.md new file mode 100644 index 00000000..7f331345 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/unreal/concepts/architecture.md @@ -0,0 +1,23 @@ +# Unreal Architecture + +## Layer Model + +- Unreal game/app layer (C++ and Blueprint graphs). +- Unreal wrapper layer (method adaptation for Blueprint/C++). +- Core Meeting SDK layer (native behavior baseline). +- Backend signature/token service. + +## Reference Flow + +```text +Unreal Gameplay/UI -> Unreal Wrapper -> Meeting SDK Core -> Zoom services + ^ | | | + | v v v + Player actions Wrapper events Native callbacks Meeting state +``` + +## Key Concept + +Always separate: +- wrapper behavior (Unreal-specific), and +- core SDK behavior (native reference semantics). diff --git a/plugins/zoom-developers/skills/meeting-sdk/unreal/concepts/lifecycle-workflow.md b/plugins/zoom-developers/skills/meeting-sdk/unreal/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..26018278 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/unreal/concepts/lifecycle-workflow.md @@ -0,0 +1,14 @@ +# Unreal Lifecycle Workflow + +## Core Sequence + +1. Plugin/wrapper initialization in Unreal project startup. +2. SDK init + auth sequence (JWT/signature path). +3. Join/start flow. +4. In-meeting event handling through wrapper event interfaces. +5. Cleanup and session/resource release. + +## Wrapper-Specific Risk + +- Method availability differs between C++ wrapper and Blueprint wrapper. +- Some wrapper methods are modified or newly introduced vs native SDK method behavior. diff --git a/plugins/zoom-developers/skills/meeting-sdk/unreal/examples/join-start-pattern.md b/plugins/zoom-developers/skills/meeting-sdk/unreal/examples/join-start-pattern.md new file mode 100644 index 00000000..6aeb2133 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/unreal/examples/join-start-pattern.md @@ -0,0 +1,14 @@ +# Unreal Join/Start Pattern + +## Join/Start Sequence + +1. Initialize wrapper SDK context. +2. Authenticate with backend-provided short-lived token/signature. +3. Trigger join/start through wrapper API. +4. Bind wrapper event callbacks before user-interactive meeting controls. + +## Blueprint/C++ Guardrails + +- Confirm node/function exists in selected wrapper mode. +- For modified wrapper methods, verify input/output differences from native docs. +- Keep a thin C++ adapter for shared validation logic when Blueprint nodes diverge. diff --git a/plugins/zoom-developers/skills/meeting-sdk/unreal/references/environment-variables.md b/plugins/zoom-developers/skills/meeting-sdk/unreal/references/environment-variables.md new file mode 100644 index 00000000..47e657f0 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/unreal/references/environment-variables.md @@ -0,0 +1,15 @@ +# Unreal Meeting SDK Environment Variables + +| Variable | Required | Purpose | Where to find | +| --- | --- | --- | --- | +| `ZOOM_SDK_KEY` | Yes | SDK signing identity | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_SDK_SECRET` | Yes | Server-side signing secret | Zoom Marketplace -> Meeting SDK app -> App Credentials | +| `ZOOM_MEETING_NUMBER` | Join/start | Meeting identifier | Zoom invite / web portal / Meetings API | +| `ZOOM_MEETING_PASSWORD` | Conditional | Meeting passcode | Zoom invite details / Meetings API | +| `ZOOM_ROLE` | Yes | Signature role (`0` attendee, `1` host) | App business logic | +| `ZOOM_ZAK` | Host start | Host authorization token | Zoom REST API token flow | + +## Notes + +- Keep signing logic outside Unreal client. +- Treat local config values as development-only and avoid committing secrets. diff --git a/plugins/zoom-developers/skills/meeting-sdk/unreal/references/unreal-reference-map.md b/plugins/zoom-developers/skills/meeting-sdk/unreal/references/unreal-reference-map.md new file mode 100644 index 00000000..824a8df6 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/unreal/references/unreal-reference-map.md @@ -0,0 +1,23 @@ +# Unreal Reference Map + +## Sources + +- Docs: https://developers.zoom.us/docs/meeting-sdk/unreal/ +- API Reference: https://marketplacefront.zoom.us/sdk/meeting/unreal/MSDKUnrealSDKreferencedocs.html + +## Crawl Coverage Snapshot + +- Docs pages captured: `6` +- API reference pages captured: `1` (single consolidated mapping page) + +## Reference Characteristics + +- Lists many wrapper interfaces and controllers. +- Explicitly documents method availability across C++ and Blueprint wrappers. +- Notes wrapper modifications/new methods relative to base Meeting SDK behavior. +- Directs developers to Windows SDK reference for unchanged base semantics. + +## Drift Signals to Watch + +- Wrapper version lag relative to latest native platform SDK versions. +- Changed Blueprint node names/signatures across Unreal wrapper releases. diff --git a/plugins/zoom-developers/skills/meeting-sdk/unreal/references/versioning-and-compatibility.md b/plugins/zoom-developers/skills/meeting-sdk/unreal/references/versioning-and-compatibility.md new file mode 100644 index 00000000..10f7714e --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/unreal/references/versioning-and-compatibility.md @@ -0,0 +1,19 @@ +# Unreal Versioning and Compatibility + +## Observed Versions + +- Local package version: `v6.1.5.43366` (`UE v5.4.3`) +- Local package naming: `zoom-meeting-sdk-unreal-engine-6.1.5-full` +- Docs baseline: current Unreal Meeting SDK docs tree captured on this crawl. + +## Compatibility Practices + +- Validate Unreal engine version compatibility before SDK integration. +- Confirm wrapper API availability in both C++ and Blueprint paths. +- Keep a version matrix (Unreal Engine version x wrapper version x Meeting SDK behavior). + +## Contradiction/Drift Notes + +- Unreal package `CHANGELOG.md` currently points to a Windows changelog URL (packaging inconsistency). +- Wrapper docs reference base behavior from Windows SDK for unchanged methods; verify assumptions per wrapper release. +- Unreal wrapper package version is behind current mobile/desktop package streams in this workspace snapshot. diff --git a/plugins/zoom-developers/skills/meeting-sdk/unreal/scenarios/high-level-scenarios.md b/plugins/zoom-developers/skills/meeting-sdk/unreal/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..86012780 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/unreal/scenarios/high-level-scenarios.md @@ -0,0 +1,19 @@ +# Unreal High-Level Scenarios + +## Scenario 1: Virtual event experience + +- Unreal scene renders branded environment. +- Meeting SDK feeds participant media into scene surfaces. +- Hosts control sessions from Unreal UI panel. + +## Scenario 2: Industrial remote collaboration + +- Engineers join a meeting from Unreal simulation app. +- Shared scene context accompanies live meeting discussion. +- Blueprint layer drives low-code UI interactions. + +## Scenario 3: Training/education simulation + +- Learners join via Unreal experience. +- Session controls and overlays are simplified in Blueprint. +- Fallback path exists for wrapper/API differences across versions. diff --git a/plugins/zoom-developers/skills/meeting-sdk/unreal/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/meeting-sdk/unreal/troubleshooting/common-issues.md new file mode 100644 index 00000000..7d0c9a77 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/unreal/troubleshooting/common-issues.md @@ -0,0 +1,20 @@ +# Unreal Common Issues + +## 1. Wrapper method mismatch + +- Check whether method is available in C++ wrapper, Blueprint wrapper, or both. +- Validate renamed/modified Blueprint node signatures. + +## 2. Join/start behavior not matching expectations + +- Compare wrapper docs and base SDK semantics. +- Verify token/signature validity and role alignment. + +## 3. Version mismatch issues + +- Confirm Unreal engine version compatibility with package. +- Verify wrapper package version and docs are from same release family. + +## 4. Packaging contradictions + +- If changelog/reference links look inconsistent, trust runtime behavior + validated wrapper docs + controlled test matrix. diff --git a/plugins/zoom-developers/skills/meeting-sdk/unreal/unreal.md b/plugins/zoom-developers/skills/meeting-sdk/unreal/unreal.md new file mode 100644 index 00000000..b62664bc --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/unreal/unreal.md @@ -0,0 +1,17 @@ +# Meeting SDK Unreal Guide + +## Scope + +Unreal Engine Meeting SDK wrapper integration with C++ and Blueprint exposure. + +## Validation Snapshot + +- Docs coverage includes: get-started, integrate, PKCE auth, error codes, and reference landing. +- API reference capture is a single consolidated wrapper reference page. +- Local package checked: `zoom-meeting-sdk-unreal-engine-6.1.5-full` (`UE v5.4.3`) with sample project. + +## Practical Guidance + +1. Validate wrapper version alignment before coding. +2. Confirm C++ wrapper method availability vs Blueprint wrapper availability. +3. Treat wrapper docs as mapping docs, then cross-check behavior against Meeting SDK Windows/native references for base semantics. diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/RUNBOOK.md b/plugins/zoom-developers/skills/meeting-sdk/web/RUNBOOK.md new file mode 100644 index 00000000..db5c6510 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/RUNBOOK.md @@ -0,0 +1,66 @@ +# Meeting SDK Web 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 Meeting SDK embed path for Web (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/web/ +- https://marketplacefront.zoom.us/sdk/meeting/web/index.html +- https://marketplacefront.zoom.us/sdk/meeting/web/components/index.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/web/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/web/client-view/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/web/component-view/` diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/SKILL.md b/plugins/zoom-developers/skills/meeting-sdk/web/SKILL.md new file mode 100644 index 00000000..fe3e9ab7 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/SKILL.md @@ -0,0 +1,1115 @@ +--- +name: zoom-meeting-sdk-web +description: | + Zoom Meeting SDK for Web - Embed Zoom meeting capabilities into web applications. Two integration + options: Client View (full-page, familiar Zoom UI) and Component View (embeddable, Promise-based API). + Includes SharedArrayBuffer setup for HD video, gallery view, and virtual backgrounds. +--- + +# Zoom Meeting SDK (Web) + +Embed Zoom meeting capabilities into web applications with two integration options: **Client View** (full-page) or **Component View** (embeddable). + +## How to Implement a Custom Video User Interface for a Zoom Meeting in a Web App + +Use **Meeting SDK Web Component View**. + +Do not use Video SDK for this question unless the user is explicitly building a non-meeting session +product. + +Minimal architecture: + +```text +Browser page + -> fetch Meeting SDK signature from backend + -> ZoomMtgEmbedded.createClient() + -> client.init({ zoomAppRoot }) + -> client.join({ signature, sdkKey, meetingNumber, userName, password }) + -> apply layout/style/customize options around the embedded meeting container +``` + +Minimal implementation: + +```ts +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +const client = ZoomMtgEmbedded.createClient(); + +export async function startEmbeddedMeeting(meetingNumber: string, userName: string, password: string) { + const sigRes = await fetch('/api/signature', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ meetingNumber, role: 0 }), + }); + + if (!sigRes.ok) throw new Error(`signature_fetch_failed:${sigRes.status}`); + + const { signature, sdkKey } = await sigRes.json(); + + await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement')!, + language: 'en-US', + patchJsMedia: true, + leaveOnPageUnload: true, + customize: { + video: { isResizable: true, popper: { disableDraggable: false } }, + }, + }); + + await client.join({ + signature, + sdkKey, + meetingNumber, + userName, + password, + }); +} +``` + +Common failure points: +- wrong route: Video SDK instead of Meeting SDK Component View +- missing backend signature endpoint +- wrong password field (`password` here, not `passWord`) +- missing OBF/ZAK requirements for meetings outside the app account +- missing SharedArrayBuffer headers when higher-end meeting features are expected + +## Hard Routing Rule + +If the user wants a **custom video user interface for a Zoom meeting in a web app**, route to +**Component View**, not Video SDK. + +- **Meeting SDK Component View** = custom UI for a real Zoom meeting +- **Video SDK Web** = custom UI for a non-meeting video session product + +For the direct custom-meeting-UI path, start with +[component-view/SKILL.md](component-view/SKILL.md). + +## New to Web SDK? Start Here! + +**The fastest way to master the SDK:** + +1. **Choose Your View** - [Client View vs Component View](#client-view-vs-component-view) - Understand the key architectural differences +2. **Quick Start** - [Client View](#quick-start-client-view) or [Component View](#quick-start-component-view) - Get a working meeting in minutes +3. **SharedArrayBuffer** - [concepts/sharedarraybuffer.md](concepts/sharedarraybuffer.md) - Required for HD video, gallery view, virtual backgrounds +4. **Optional preflight diagnostics** - [../../probe-sdk/SKILL.md](../../probe-sdk/SKILL.md) - Validate browser/device/network before join + +**Building a Custom Integration?** +- Component View gives you Promise-based API and embeddable UI +- Client View gives you the familiar full-page Zoom experience +- For a custom meeting UI, prefer **Component View** first +- Cross-product routing example: [../../general/use-cases/custom-meeting-ui-web.md](../../general/use-cases/custom-meeting-ui-web.md) +- [Browser Support](concepts/browser-support.md) - Feature matrix by browser +- Exact deep-dive path: [component-view/SKILL.md](component-view/SKILL.md) + +**Having issues?** +- Join errors → Check signature generation and password spelling (`passWord` vs `password`) +- HD video not working → Enable SharedArrayBuffer headers +- Complete navigation → [SKILL.md](SKILL.md) + +## Prerequisites + +- Zoom app with Meeting SDK credentials from [Marketplace](https://marketplace.zoom.us/) +- SDK Key (Client ID) and Secret +- Modern browser (Chrome, Firefox, Safari, Edge) +- Backend auth endpoint for signature generation + +> **Need help with authentication?** See the **[zoom-oauth](../../oauth/SKILL.md)** skill for JWT/signature generation. +> +> **Want pre-join diagnostics?** Chain **[probe-sdk](../../probe-sdk/SKILL.md)** before `init()`/`join()` to gate low-readiness environments. + +## Optional Preflight Gate (Probe SDK) + +For unstable first-join environments, run Probe SDK checks before calling `ZoomMtg.init()` or `client.join()`: + +1. Run Probe permissions/device/network diagnostics. +2. Apply readiness policy (`allow`, `warn`, `block`). +3. Continue to Meeting SDK join only for `allow`/approved `warn`. + +See [../../probe-sdk/SKILL.md](../../probe-sdk/SKILL.md) and [../../general/use-cases/probe-sdk-preflight-readiness-gate.md](../../general/use-cases/probe-sdk-preflight-readiness-gate.md). + +## Client View vs Component View + +**CRITICAL DIFFERENCE**: These are two completely different APIs with different patterns! + +| Aspect | Client View | Component View | +|--------|-------------|----------------| +| **Object** | `ZoomMtg` (global singleton) | `ZoomMtgEmbedded.createClient()` (instance) | +| **API Style** | Callbacks | Promises | +| **UI** | Full-page takeover | Embeddable in any container | +| **Password param** | `passWord` (capital W) | `password` (lowercase) | +| **Events** | `inMeetingServiceListener()` | `on()`/`off()` | +| **Import (npm)** | `import { ZoomMtg } from '@zoom/meetingsdk'` | `import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'` | +| **CDN** | `zoom-meeting-{VERSION}.min.js` | `zoom-meeting-embedded-{VERSION}.min.js` | +| **Best For** | Quick integration, standard Zoom UI | Custom layouts, React/Vue apps | + +### When to Use Which + +**Use Client View when:** +- You want the familiar Zoom meeting interface +- Quick integration is priority over customization +- Full-page meeting experience is acceptable + +**Use Component View when:** +- You need to embed meetings in a specific area of your page +- Building React/Vue/Angular applications +- You want Promise-based async/await syntax +- Custom positioning and resizing is required + +## Installation + +### NPM (Recommended) + +```bash +npm install @zoom/meetingsdk --save +``` + +### CDN + +```html + + + + + + + + + + + + +``` + +Replace `{VERSION}` with the [latest version](https://www.npmjs.com/package/@zoom/meetingsdk) (e.g., `3.11.0`). + +## Quick Start (Client View) + +```javascript +import { ZoomMtg } from '@zoom/meetingsdk'; + +// Step 1: Check browser compatibility +console.log('System requirements:', ZoomMtg.checkSystemRequirements()); + +// Step 2: Preload WebAssembly for faster initialization +ZoomMtg.preLoadWasm(); +ZoomMtg.prepareWebSDK(); + +// Step 3: Load language files (MUST complete before init) +ZoomMtg.i18n.load('en-US'); +ZoomMtg.i18n.onLoad(() => { + + // Step 4: Initialize SDK + ZoomMtg.init({ + leaveUrl: 'https://yoursite.com/meeting-ended', + disableCORP: !window.crossOriginIsolated, // Auto-detect SharedArrayBuffer + patchJsMedia: true, // Auto-apply media dependency fixes + leaveOnPageUnload: true, // Clean up when page unloads + externalLinkPage: './external.html', // Page for external links + success: () => { + + // Step 5: Join meeting (note: passWord with capital W!) + ZoomMtg.join({ + signature: signature, // From your auth endpoint + meetingNumber: '1234567890', + userName: 'User Name', + passWord: 'meeting-password', // Capital W! + success: (res) => { + console.log('Joined meeting:', res); + + // Post-join: Get meeting info + ZoomMtg.getAttendeeslist({}); + ZoomMtg.getCurrentUser({ + success: (res) => console.log('Current user:', res.result.currentUser) + }); + }, + error: (err) => { + console.error('Join error:', err); + } + }); + }, + error: (err) => { + console.error('Init error:', err); + } + }); +}); +``` + +## Quick Start (Component View) + +```javascript +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +// Create client instance (do this ONCE, not on every render!) +const client = ZoomMtgEmbedded.createClient(); + +async function startMeeting() { + try { + // Initialize with container element + await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', + debug: true, // Enable debug logging + patchJsMedia: true, // Auto-apply media fixes + leaveOnPageUnload: true, // Clean up on page unload + }); + + // Join meeting (note: password lowercase!) + await client.join({ + signature: signature, // From your auth endpoint + sdkKey: SDK_KEY, + meetingNumber: '1234567890', + userName: 'User Name', + password: 'meeting-password', // Lowercase! + }); + + console.log('Joined successfully!'); + } catch (error) { + console.error('Failed to join:', error); + } +} +``` + +## Authentication Endpoint (Required) + +Both views require a JWT signature from a backend server. **Never expose your SDK Secret in frontend code!** + +```bash +# Clone Zoom's official auth endpoint +git clone https://github.com/zoom/meetingsdk-auth-endpoint-sample --depth 1 +cd meetingsdk-auth-endpoint-sample +cp .env.example .env +# Edit .env with your SDK Key and Secret +npm install && npm run start +``` + +### Signature Generation + +The signature encodes: +- `sdkKey` (or `clientId` for newer apps) +- `meetingNumber` +- `role` (0 = participant, 1 = host) +- `iat` (issued at timestamp) +- `exp` (expiration timestamp) +- `tokenExp` (token expiration) + +> **IMPORTANT (March 2026)**: Apps joining meetings outside their account will require an App Privilege Token (OBF) or ZAK token. See [Authorization Requirements](#authorization-requirements-2026-update). + +## Core Workflow + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Get Signature │───►│ init() │───►│ join() │ +│ (from backend)│ │ (SDK setup) │ │ (enter mtg) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ + ▼ ▼ + success/error success/error + callback callback + (or Promise resolve) (or Promise resolve) +``` + +## Client View API Reference + +### ZoomMtg.init() - Key Options + +```javascript +ZoomMtg.init({ + // Required + leaveUrl: string, // URL to redirect after leaving + + // Display Options + showMeetingHeader: boolean, // Show meeting number/topic (default: true) + disableInvite: boolean, // Hide invite button (default: false) + disableRecord: boolean, // Hide record button (default: false) + disableJoinAudio: boolean, // Hide join audio option (default: false) + disablePreview: boolean, // Skip A/V preview (default: false) + + // HD Video (requires SharedArrayBuffer) + enableHD: boolean, // Enable 720p (default: true for >=2.8.0) + enableFullHD: boolean, // Enable 1080p for webinars (default: false) + + // View Options + defaultView: 'gallery' | 'speaker' | 'multiSpeaker', + + // Feature Toggles + isSupportChat: boolean, // Enable chat (default: true) + isSupportCC: boolean, // Enable closed captions (default: true) + isSupportBreakout: boolean, // Enable breakout rooms (default: true) + isSupportPolling: boolean, // Enable polling (default: true) + isSupportQA: boolean, // Enable Q&A for webinars (default: true) + + // Cross-Origin + disableCORP: boolean, // For dev without COOP/COEP headers + + // Callbacks + success: Function, + error: Function, +}); +``` + +### ZoomMtg.join() - Key Options + +```javascript +ZoomMtg.join({ + // Required + signature: string, // JWT signature from backend + meetingNumber: string | number, + userName: string, + + // Authentication + passWord: string, // Meeting password (capital W!) + zak: string, // Host's ZAK token (required to start) + tk: string, // Registration token (if required) + obfToken: string, // App Privilege Token (for 2026 requirement) + + // Optional + userEmail: string, // Required for webinars + customerKey: string, // Custom identifier (max 36 chars) + + // Callbacks + success: Function, + error: Function, +}); +``` + +### Event Listeners (Client View) + +```javascript +// User events +ZoomMtg.inMeetingServiceListener('onUserJoin', (data) => { + console.log('User joined:', data); +}); + +ZoomMtg.inMeetingServiceListener('onUserLeave', (data) => { + console.log('User left:', data); + // data.reasonCode values: + // 0: OTHER + // 1: HOST_ENDED_MEETING + // 2: SELF_LEAVE_FROM_IN_MEETING + // 3: SELF_LEAVE_FROM_WAITING_ROOM + // 4: SELF_LEAVE_FROM_WAITING_FOR_HOST_START + // 5: MEETING_TRANSFER + // 6: KICK_OUT_FROM_MEETING + // 7: KICK_OUT_FROM_WAITING_ROOM + // 8: LEAVE_FROM_DISCLAIMER +}); + +ZoomMtg.inMeetingServiceListener('onUserUpdate', (data) => { + console.log('User updated:', data); +}); + +// Meeting status +ZoomMtg.inMeetingServiceListener('onMeetingStatus', (data) => { + // status: 1=connecting, 2=connected, 3=disconnected, 4=reconnecting + console.log('Meeting status:', data.status); +}); + +// Waiting room +ZoomMtg.inMeetingServiceListener('onUserIsInWaitingRoom', (data) => { + console.log('User in waiting room:', data); +}); + +// Active speaker detection +ZoomMtg.inMeetingServiceListener('onActiveSpeaker', (data) => { + // [{userId: number, userName: string}] + console.log('Active speaker:', data); +}); + +// Network quality monitoring +ZoomMtg.inMeetingServiceListener('onNetworkQualityChange', (data) => { + // {level: 0-5, userId, type: 'uplink'} + // 0-1 = bad, 2 = normal, 3-5 = good + if (data.level <= 1) { + console.warn('Poor network quality'); + } +}); + +// Join performance metrics +ZoomMtg.inMeetingServiceListener('onJoinSpeed', (data) => { + console.log('Join speed metrics:', data); + // Useful for performance monitoring dashboards +}); + +// Chat +ZoomMtg.inMeetingServiceListener('onReceiveChatMsg', (data) => { + console.log('Chat message:', data); +}); + +// Recording +ZoomMtg.inMeetingServiceListener('onRecordingChange', (data) => { + console.log('Recording status:', data); +}); + +// Screen sharing +ZoomMtg.inMeetingServiceListener('onShareContentChange', (data) => { + console.log('Share content changed:', data); +}); + +// Transcription (requires "save closed captions" enabled) +ZoomMtg.inMeetingServiceListener('onReceiveTranscriptionMsg', (data) => { + console.log('Transcription:', data); +}); + +// Breakout room status +ZoomMtg.inMeetingServiceListener('onRoomStatusChange', (data) => { + // status: 2=InProgress, 3=Closing, 4=Closed + console.log('Breakout room status:', data); +}); +``` + +### Common Methods (Client View) + +```javascript +// Get current user info +ZoomMtg.getCurrentUser({ + success: (res) => console.log(res.result.currentUser) +}); + +// Get all attendees +ZoomMtg.getAttendeeslist({}); + +// Audio/Video control +ZoomMtg.mute({ userId, mute: true }); +ZoomMtg.muteAll({ muteAll: true }); + +// Chat +ZoomMtg.sendChat({ message: 'Hello!', userId: 0 }); // 0 = everyone + +// Leave/End +ZoomMtg.leaveMeeting({}); +ZoomMtg.endMeeting({}); + +// Host controls +ZoomMtg.makeHost({ userId }); +ZoomMtg.makeCoHost({ oderId }); +ZoomMtg.expel({ userId }); // Remove participant +ZoomMtg.putOnHold({ oderId, bHold: true }); + +// Breakout rooms +ZoomMtg.createBreakoutRoom({ rooms: [...] }); +ZoomMtg.openBreakoutRooms({}); +ZoomMtg.closeBreakoutRooms({}); + +// Virtual background +ZoomMtg.setVirtualBackground({ imageUrl: '...' }); +``` + +## Component View API Reference + +### client.init() - Key Options + +```javascript +await client.init({ + // Required + zoomAppRoot: HTMLElement, // Container element + + // Display + language: string, // e.g., 'en-US' + debug: boolean, // Enable debug logging (default: false) + + // Media + patchJsMedia: boolean, // Auto-apply media fixes (default: false) + leaveOnPageUnload: boolean, // Clean up on page unload (default: false) + + // Video + enableHD: boolean, // Enable 720p + enableFullHD: boolean, // Enable 1080p + + // Customization + customize: { + video: { + isResizable: boolean, + viewSizes: { default: { width, height } } + }, + meetingInfo: ['topic', 'host', 'mn', 'pwd', 'telPwd', 'invite', 'participant', 'dc', 'enctype'], + toolbar: { + buttons: [ + { + text: 'Custom Button', + className: 'custom-btn', + onClick: () => { + console.log('Custom button clicked'); + } + } + ] + } + }, + + // For ZFG + webEndpoint: string, + assetPath: string, // Custom path for AV libraries (self-hosting) +}); +``` + +### client.join() - Key Options + +```javascript +await client.join({ + // Required + signature: string, + sdkKey: string, + meetingNumber: string | number, + userName: string, + + // Authentication + password: string, // Lowercase! (different from Client View) + zak: string, // Host's ZAK token + tk: string, // Registration token + + // Optional + userEmail: string, +}); +``` + +### Event Listeners (Component View) + +```javascript +// Connection state +client.on('connection-change', (payload) => { + // payload.state: 'Connecting', 'Connected', 'Reconnecting', 'Closed' + console.log('Connection:', payload.state); +}); + +// User events +client.on('user-added', (payload) => { + console.log('Users added:', payload); +}); + +client.on('user-removed', (payload) => { + console.log('Users removed:', payload); +}); + +client.on('user-updated', (payload) => { + console.log('Users updated:', payload); +}); + +// Active speaker +client.on('active-speaker', (payload) => { + console.log('Active speaker:', payload); +}); + +// Video state +client.on('video-active-change', (payload) => { + console.log('Video active:', payload); +}); + +// Unsubscribe +client.off('connection-change', handler); +``` + +### Common Methods (Component View) + +```javascript +// Get current user +const currentUser = client.getCurrentUser(); + +// Get all participants +const participants = client.getParticipantsList(); + +// Audio control +await client.mute(true); +await client.muteAudio(userId, true); + +// Video control +await client.muteVideo(userId, true); + +// Leave +client.leaveMeeting(); + +// End (host only) +client.endMeeting(); +``` + +## SharedArrayBuffer (CRITICAL for HD) + +SharedArrayBuffer enables advanced features: +- 720p/1080p video +- Gallery view +- Virtual backgrounds +- Background noise suppression + +### Enable with HTTP Headers + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +### Verify in Browser + +```javascript +if (typeof SharedArrayBuffer === 'function') { + console.log('SharedArrayBuffer enabled!'); +} else { + console.warn('HD features will be limited'); +} + +// Or check cross-origin isolation +console.log('Cross-origin isolated:', window.crossOriginIsolated); +``` + +### Platform-Specific Setup + +See [concepts/sharedarraybuffer.md](concepts/sharedarraybuffer.md) for: +- Vercel, Netlify, AWS CloudFront configuration +- nginx/Apache configuration +- Service worker fallback for GitHub Pages + +### Development Setup (Two-Server Pattern) + +The official samples use a **two-server pattern** for development because COOP/COEP headers can break navigation: + +```javascript +// Server 1: Main app (port 9999) - NO isolation headers +// Serves index.html, navigation works normally + +// Server 2: Meeting page (port 9998) - WITH isolation headers +// Serves meeting.html with SharedArrayBuffer support + +// Main server proxies to meeting server +proxy: [{ + path: '/meeting.html', + target: 'http://YOUR_MEETING_SERVER_HOST:9998/' +}] +``` + +**Vite config with headers:** +```typescript +// vite.config.ts +export default defineConfig({ + server: { + headers: { + 'Cross-Origin-Embedder-Policy': 'require-corp', + 'Cross-Origin-Opener-Policy': 'same-origin', + } + } +}); +``` + +## Common Issues & Solutions + +| Issue | Solution | +|-------|----------| +| **Join fails with signature error** | Verify signature generation, check sdkKey format | +| **"passWord" typo** | Client View uses `passWord` (capital W), Component View uses `password` | +| **No HD video** | Enable SharedArrayBuffer headers, check browser support | +| **Callbacks not firing** | Ensure `inMeetingServiceListener` called after init success | +| **Virtual background not working** | Requires SharedArrayBuffer + Chrome/Edge | +| **Screen share fails on Safari** | Safari 17+ with macOS 14+ required for client view | + +**Complete troubleshooting**: [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Browser Support Matrix + +| Feature | Chrome | Firefox | Safari | Edge | iOS | Android | +|---------|--------|---------|--------|------|-----|---------| +| 720p (receive) | Yes | Yes | Yes | Yes | Yes | Yes | +| 720p (send) | Yes* | Yes* | Yes* | Yes* | Yes* | Yes* | +| Virtual background | Yes | Yes | No | Yes | No | No | +| Screen share (send) | Yes | Yes | Safari 17+ | Yes | No | No | +| Gallery view | Yes | Yes | Yes** | Yes | Yes | Yes | + +*Requires SharedArrayBuffer +**Safari 17+ with macOS Sonoma + +See [concepts/browser-support.md](concepts/browser-support.md) for complete matrix. + +## Authorization Requirements (2026 Update) + +> **IMPORTANT**: Beginning **March 2, 2026**, apps joining meetings outside their account must be authorized. + +### Options + +1. **App Privilege Token (OBF)** - Recommended for bots + ```javascript + ZoomMtg.join({ + ... + obfToken: 'your-app-privilege-token' + }); + ``` + +2. **ZAK Token** - For host operations + ```javascript + ZoomMtg.join({ + ... + zak: 'host-zak-token' + }); + ``` + +## Zoom for Government (ZFG) + +### Option 1: ZFG-specific NPM Package + +```json +{ + "dependencies": { + "@zoom/meetingsdk": "3.11.2-zfg" + } +} +``` + +### Option 2: Configure ZFG Endpoints + +**Client View:** +```javascript +ZoomMtg.setZoomJSLib('https://source.zoomgov.com/{VERSION}/lib', '/av'); +ZoomMtg.init({ + webEndpoint: 'www.zoomgov.com', + ... +}); +``` + +**Component View:** +```javascript +await client.init({ + webEndpoint: 'www.zoomgov.com', + assetPath: 'https://source.zoomgov.com/{VERSION}/lib/av', + ... +}); +``` + +## China CDN + +```javascript +// Set before preLoadWasm() +ZoomMtg.setZoomJSLib('https://jssdk.zoomus.cn/{VERSION}/lib', '/av'); +``` + +## React Integration + +### Official Pattern (from zoom/meetingsdk-react-sample) + +The official React sample uses **imperative initialization** rather than React hooks: + +```tsx +import { ZoomMtg } from '@zoom/meetingsdk'; + +// Preload at module level (outside component) +ZoomMtg.preLoadWasm(); +ZoomMtg.prepareWebSDK(); + +function App() { + const authEndpoint = import.meta.env.VITE_AUTH_ENDPOINT; + const meetingNumber = ''; + const passWord = ''; + const role = 0; + const userName = 'React User'; + + const getSignature = async () => { + const response = await fetch(authEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + meetingNumber, + role, + }), + }); + const data = await response.json(); + startMeeting(data.signature); + }; + + const startMeeting = (signature: string) => { + document.getElementById('zmmtg-root')!.style.display = 'block'; + + ZoomMtg.init({ + leaveUrl: window.location.origin, + patchJsMedia: true, + leaveOnPageUnload: true, + success: () => { + ZoomMtg.join({ + signature, + meetingNumber, + userName, + passWord, + success: (res) => console.log('Joined:', res), + error: (err) => console.error('Join error:', err), + }); + }, + error: (err) => console.error('Init error:', err), + }); + }; + + return ( + + ); +} +``` + +### React Gotchas (from official samples) + +| Issue | Problem | Solution | +|-------|---------|----------| +| **Client Recreation** | `createClient()` in component body runs every render | Use `useRef` to persist client | +| **No useEffect** | Official sample doesn't use React lifecycle hooks | SDK's `leaveOnPageUnload` handles cleanup | +| **Direct DOM** | Sample uses `getElementById` | Use `useRef` in production | +| **No Error State** | Silent failures | Add `useState` for error handling | +| **Module-Scope Side Effects** | `preLoadWasm()` at top level | May cause issues with SSR | + +### Production-Ready React Pattern + +```tsx +import { useEffect, useRef, useState, useCallback } from 'react'; +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +type ZoomClient = ReturnType; + +function ZoomMeeting({ meetingNumber, password, userName }: Props) { + const clientRef = useRef(null); + const containerRef = useRef(null); + const [isJoining, setIsJoining] = useState(false); + const [error, setError] = useState(null); + + // Create client once + useEffect(() => { + if (!clientRef.current) { + clientRef.current = ZoomMtgEmbedded.createClient(); + } + }, []); + + const joinMeeting = useCallback(async () => { + if (!clientRef.current || !containerRef.current) return; + + setIsJoining(true); + setError(null); + + try { + // Get signature from your backend + const response = await fetch('/api/signature', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ meetingNumber, role: 0 }), + }); + const { signature, sdkKey } = await response.json(); + + await clientRef.current.init({ + zoomAppRoot: containerRef.current, + language: 'en-US', + patchJsMedia: true, + leaveOnPageUnload: true, + }); + + await clientRef.current.join({ + signature, + sdkKey, + meetingNumber, + password, + userName, + }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to join'); + } finally { + setIsJoining(false); + } + }, [meetingNumber, password, userName]); + + return ( +
+
+ + {error &&
{error}
} +
+ ); +} +``` + +### Environment Variables (Vite) + +```bash +# .env.local +VITE_AUTH_ENDPOINT=http://YOUR_AUTH_SERVER_HOST:4000 +VITE_SDK_KEY=your_sdk_key +``` + +```tsx +const authEndpoint = import.meta.env.VITE_AUTH_ENDPOINT; +const sdkKey = import.meta.env.VITE_SDK_KEY; +``` + +## Detailed References + +### Core Documentation +- **[SKILL.md](SKILL.md)** - Complete navigation guide +- **[client-view/SKILL.md](client-view/SKILL.md)** - Full Client View reference +- **[component-view/SKILL.md](component-view/SKILL.md)** - Full Component View reference + +### Concepts +- **[concepts/sharedarraybuffer.md](concepts/sharedarraybuffer.md)** - HD video requirements +- **[concepts/browser-support.md](concepts/browser-support.md)** - Feature matrix by browser + +### Troubleshooting +- **[troubleshooting/error-codes.md](troubleshooting/error-codes.md)** - All SDK error codes +- **[troubleshooting/common-issues.md](troubleshooting/common-issues.md)** - Quick diagnostics + +### Examples +- **[client-view/SKILL.md](client-view/SKILL.md)** - Complete Client View guide +- **[component-view/SKILL.md](component-view/SKILL.md)** - Component View React integration + +## Helper Utilities + +### Extract Meeting Number from Invite Link + +```javascript +// Users can paste full Zoom invite links +document.getElementById('meeting_number').addEventListener('input', (e) => { + // Extract meeting number (9-11 digits) + let meetingNumber = e.target.value.replace(/([^0-9])+/i, ''); + if (meetingNumber.match(/([0-9]{9,11})/)) { + meetingNumber = meetingNumber.match(/([0-9]{9,11})/)[1]; + } + + // Auto-extract password from invite link + const pwdMatch = e.target.value.match(/pwd=([\d,\w]+)/); + if (pwdMatch) { + document.getElementById('password').value = pwdMatch[1]; + } +}); +``` + +### Dynamic Language Switching + +```javascript +// Change language at runtime +document.getElementById('language').addEventListener('change', (e) => { + const lang = e.target.value; + ZoomMtg.i18n.load(lang); + ZoomMtg.i18n.reload(lang); + ZoomMtg.reRender({ lang }); +}); +``` + +### Check System Requirements + +```javascript +// Check browser compatibility before initializing +const requirements = ZoomMtg.checkSystemRequirements(); +console.log('Browser info:', JSON.stringify(requirements)); + +if (!requirements.browserInfo.isChrome && !requirements.browserInfo.isFirefox) { + alert('For best experience, use Chrome or Firefox'); +} +``` + +## Sample Repositories + +| Repository | Description | +|------------|-------------| +| [meetingsdk-web-sample](https://github.com/zoom/meetingsdk-web-sample) | Official samples (Client View & Component View) | +| [meetingsdk-react-sample](https://github.com/zoom/meetingsdk-react-sample) | React integration with TypeScript + Vite | +| [meetingsdk-web](https://github.com/zoom/meetingsdk-web) | SDK source with helper.html | +| [meetingsdk-auth-endpoint-sample](https://github.com/zoom/meetingsdk-auth-endpoint-sample) | Signature generation backend | + +## Official Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/web/ +- **Client View API Reference**: https://marketplacefront.zoom.us/sdk/meeting/web/index.html +- **Component View API Reference**: https://marketplacefront.zoom.us/sdk/meeting/web/components/index.html +- **Developer forum**: https://devforum.zoom.us/ + +--- + +**Documentation Version**: Based on Zoom Web Meeting SDK v3.11+ + +**Need help?** Start with [SKILL.md](SKILL.md) for complete navigation. + + +## Merged from meeting-sdk/web/SKILL.md + +# Zoom Meeting SDK (Web) - Documentation Index + +Quick navigation guide for all Web SDK documentation. + +## Start Here + +| Document | Description | +|----------|-------------| +| **[SKILL.md](SKILL.md)** | Main entry point - Quick starts for both Client View and Component View | + +## By View Type + +### Client View (Full-Page) +| Document | Description | +|----------|-------------| +| **[client-view/SKILL.md](client-view/SKILL.md)** | Complete Client View reference | + +### Component View (Embeddable) +| Document | Description | +|----------|-------------| +| **[component-view/SKILL.md](component-view/SKILL.md)** | Complete Component View reference | + +## Concepts + +| Document | Description | +|----------|-------------| +| **[concepts/sharedarraybuffer.md](concepts/sharedarraybuffer.md)** | HD video requirements, COOP/COEP headers | +| **[concepts/browser-support.md](concepts/browser-support.md)** | Feature matrix by browser | + +## Examples + +| Document | Description | +|----------|-------------| +| [examples/client-view-basic.md](examples/client-view-basic.md) | Basic Client View integration | +| [examples/component-view-react.md](examples/component-view-react.md) | React integration with Component View | + +## Troubleshooting + +| Document | Description | +|----------|-------------| +| **[troubleshooting/error-codes.md](troubleshooting/error-codes.md)** | All SDK error codes (3000-10000 range) | +| **[troubleshooting/common-issues.md](troubleshooting/common-issues.md)** | Quick diagnostics and fixes | + +## By Topic + +### Authentication +- [SKILL.md#authentication-endpoint](SKILL.md#authentication-endpoint-required) - Signature generation +- [SKILL.md#authorization-requirements-2026-update](SKILL.md#authorization-requirements-2026-update) - OBF tokens + +### HD Video & Performance +- [concepts/sharedarraybuffer.md](concepts/sharedarraybuffer.md) - Enable 720p/1080p + +### Events & Callbacks +- [SKILL.md#event-listeners-client-view](SKILL.md#event-listeners-client-view) - Client View events +- [SKILL.md#event-listeners-component-view](SKILL.md#event-listeners-component-view) - Component View events + +### Government (ZFG) +- [SKILL.md#zoom-for-government-zfg](SKILL.md#zoom-for-government-zfg) - ZFG configuration + +### China CDN +- [SKILL.md#china-cdn](SKILL.md#china-cdn) - China-specific CDN + +## Quick Reference + +### Client View vs Component View + +| Aspect | Client View | Component View | +|--------|-------------|----------------| +| **Object** | `ZoomMtg` | `ZoomMtgEmbedded.createClient()` | +| **API Style** | Callbacks | Promises | +| **Password param** | `passWord` (capital W) | `password` (lowercase) | +| **Events** | `inMeetingServiceListener()` | `on()`/`off()` | + +### Key Gotchas + +1. **Password spelling differs between views!** + - Client View: `passWord` (capital W) + - Component View: `password` (lowercase) + +2. **SharedArrayBuffer required for HD features** + - 720p/1080p video + - Gallery view (25 videos) + - Virtual backgrounds + +3. **March 2026 Authorization Change** + - Apps joining external meetings need OBF or ZAK tokens + +## External Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/web/ +- **Client View API**: https://marketplacefront.zoom.us/sdk/meeting/web/index.html +- **Component View API**: https://marketplacefront.zoom.us/sdk/meeting/web/components/index.html +- **GitHub samples**: https://github.com/zoom/meetingsdk-web-sample + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/client-view/RUNBOOK.md b/plugins/zoom-developers/skills/meeting-sdk/web/client-view/RUNBOOK.md new file mode 100644 index 00000000..a10362fe --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/client-view/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK Web Client View 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 Meeting SDK embed path for Web Client View (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/web/ +- https://marketplacefront.zoom.us/sdk/meeting/web/index.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/web/client-view/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/web/client-view/` diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/client-view/SKILL.md b/plugins/zoom-developers/skills/meeting-sdk/web/client-view/SKILL.md new file mode 100644 index 00000000..813cc92c --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/client-view/SKILL.md @@ -0,0 +1,613 @@ +--- +name: zoom-meeting-sdk-web-client-view +description: | + Zoom Meeting SDK Web - Client View. Full-page Zoom meeting experience with the familiar Zoom interface. + Uses ZoomMtg global singleton with callback-based API. Ideal for quick integration with minimal + customization. Provides the same UI as Zoom Web Client. +--- + +# Zoom Meeting SDK Web - Client View + +Full-page Zoom meeting experience embedded in your web application. Client View provides the familiar Zoom interface with minimal customization needed. + +## Overview + +Client View uses the `ZoomMtg` global singleton to render a full-page meeting experience identical to the Zoom Web Client. + +| Aspect | Details | +|--------|---------| +| **API Object** | `ZoomMtg` (global singleton) | +| **API Style** | Callback-based | +| **UI** | Full-page takeover | +| **Password param** | `passWord` (capital W) | +| **Events** | `inMeetingServiceListener()` | +| **Best For** | Quick integration, standard Zoom UI | + +## Installation + +### NPM + +```bash +npm install @zoom/meetingsdk --save +``` + +```javascript +import { ZoomMtg } from '@zoom/meetingsdk'; +``` + +### CDN + +```html + + + + + + +``` + +## Complete Initialization Flow + +```javascript +// Step 1: Check browser compatibility +console.log('Requirements:', ZoomMtg.checkSystemRequirements()); + +// Step 2: Set CDN path (optional - for China or custom hosting) +// ZoomMtg.setZoomJSLib('https://source.zoom.us/{VERSION}/lib', '/av'); + +// Step 3: Preload WebAssembly modules +ZoomMtg.preLoadWasm(); +ZoomMtg.prepareWebSDK(); + +// Step 4: Load language resources +ZoomMtg.i18n.load('en-US'); +ZoomMtg.i18n.onLoad(() => { + + // Step 5: Initialize SDK + ZoomMtg.init({ + leaveUrl: '/meeting-ended', + patchJsMedia: true, + disableCORP: !window.crossOriginIsolated, + success: () => { + console.log('SDK initialized'); + + // Step 6: Join meeting + const joinPayload = { + signature: signature, + meetingNumber: meetingNumber, + userName: userName, + passWord: passWord, + success: (res) => { + console.log('Joined meeting'); + + // Step 7: Post-join operations + ZoomMtg.getAttendeeslist({}); + ZoomMtg.getCurrentUser({ + success: (res) => console.log('Current user:', res.result.currentUser) + }); + }, + error: (err) => console.error('Join failed:', err) + }; + + // IMPORTANT: only include optional fields when they have real values + // Passing undefined for some optional fields can cause runtime join errors + if (userEmail) joinPayload.userEmail = userEmail; + if (tk) joinPayload.tk = tk; + if (zak) joinPayload.zak = zak; + + ZoomMtg.join(joinPayload); + }, + error: (err) => console.error('Init failed:', err) + }); +}); +``` + +## ZoomMtg.init() - All Options + +### Required + +| Parameter | Type | Description | +|-----------|------|-------------| +| `leaveUrl` | `string` | URL to redirect after leaving meeting | + +### UI Customization + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `showMeetingHeader` | `boolean` | `true` | Show meeting number and topic | +| `disableInvite` | `boolean` | `false` | Hide invite button | +| `disableCallOut` | `boolean` | `false` | Hide call out option | +| `disableRecord` | `boolean` | `false` | Hide record button | +| `disableJoinAudio` | `boolean` | `false` | Hide join audio option | +| `disablePreview` | `boolean` | `false` | Skip audio/video preview | +| `audioPanelAlwaysOpen` | `boolean` | `false` | Keep audio panel open | +| `showPureSharingContent` | `boolean` | `false` | Prevent overlays on shared content | +| `videoHeader` | `boolean` | `true` | Show video tile header | +| `isLockBottom` | `boolean` | `true` | Show/hide footer | +| `videoDrag` | `boolean` | `true` | Enable drag video tiles | +| `sharingMode` | `string` | `'both'` | `'both'` or `'fit'` | +| `screenShare` | `boolean` | `true` | Enable browser URL sharing | +| `hideShareAudioOption` | `boolean` | `false` | Hide "Share tab audio" checkbox | +| `disablePictureInPicture` | `boolean` | `false` | Disable PiP mode | +| `disableZoomLogo` | `boolean` | `false` | Remove Zoom logo (deprecated) | +| `defaultView` | `string` | `'speaker'` | `'gallery'`, `'speaker'`, `'multiSpeaker'` | + +### Feature Toggles + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `isSupportAV` | `boolean` | `true` | Enable audio/video | +| `isSupportChat` | `boolean` | `true` | Enable in-meeting chat | +| `isSupportQA` | `boolean` | `true` | Enable webinar Q&A | +| `isSupportCC` | `boolean` | `true` | Enable closed captions | +| `isSupportPolling` | `boolean` | `true` | Enable polling | +| `isSupportBreakout` | `boolean` | `true` | Enable breakout rooms | +| `isSupportNonverbal` | `boolean` | `true` | Enable nonverbal feedback | +| `isSupportSimulive` | `boolean` | `false` | Enable Simulive | +| `disableVoIP` | `boolean` | `false` | Disable VoIP | +| `disableReport` | `boolean` | `false` | Disable report feature | + +### Video Quality + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `enableHD` | `boolean` | `true` (≥2.8.0) | Enable 720p video | +| `enableFullHD` | `boolean` | `false` | Enable 1080p for webinar attendees | + +### Advanced + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `debug` | `boolean` | `false` | Enable debug logging | +| `patchJsMedia` | `boolean` | `false` | Auto-apply media fixes | +| `disableCORP` | `boolean` | `false` | Disable web isolation | +| `helper` | `string` | `''` | Path to helper.html | +| `externalLinkPage` | `string` | - | Page for external links | +| `webEndpoint` | `string` | - | For ZFG environments | +| `leaveOnPageUnload` | `boolean` | `false` | Auto cleanup on page close | +| `isShowJoiningErrorDialog` | `boolean` | `true` | Show error dialog on join failure | +| `meetingInfo` | `Array` | `[...]` | Meeting info to display | +| `inviteUrlFormat` | `string` | `''` | Custom invite URL format | +| `loginWindow` | `object` | `{width: 400, height: 380}` | Login popup size | + +### Callbacks + +| Parameter | Type | Description | +|-----------|------|-------------| +| `success` | `Function` | Called on successful init | +| `error` | `Function` | Called on init failure | + +## ZoomMtg.join() - All Options + +### Required + +| Parameter | Type | Description | +|-----------|------|-------------| +| `signature` | `string` | SDK JWT from backend (v5.0+: must include appKey prefix) | +| `meetingNumber` | `string \| number` | Meeting or webinar number | +| `userName` | `string` | Display name | +| `passWord` | `string` | Meeting password (capital W!) | + +### Authentication + +| Parameter | Type | When Required | Description | +|-----------|------|---------------|-------------| +| `zak` | `string` | Starting as host | Host's Zoom Access Key | +| `tk` | `string` | Registration required | Registrant token | +| `userEmail` | `string` | Webinars | User email | +| `obfToken` | `string` | March 2026+ | App Privilege Token | + +### Optional + +| Parameter | Type | Description | +|-----------|------|-------------| +| `customerKey` | `string` | Custom ID (max 36 chars) | +| `recordingToken` | `string` | Local recording permission | + +### Callbacks + +| Parameter | Type | Description | +|-----------|------|-------------| +| `success` | `Function` | Called on successful join | +| `error` | `Function` | Called on join failure | + +## Event Listeners + +### User Events + +```javascript +ZoomMtg.inMeetingServiceListener('onUserJoin', (data) => { + console.log('User joined:', data); + // { userId, userName, ... } +}); + +ZoomMtg.inMeetingServiceListener('onUserLeave', (data) => { + console.log('User left:', data); + // Reason codes: + // 0: OTHER + // 1: HOST_ENDED_MEETING + // 2: SELF_LEAVE_FROM_IN_MEETING + // 3: SELF_LEAVE_FROM_WAITING_ROOM + // 4: SELF_LEAVE_FROM_WAITING_FOR_HOST_START + // 5: MEETING_TRANSFER + // 6: KICK_OUT_FROM_MEETING + // 7: KICK_OUT_FROM_WAITING_ROOM + // 8: LEAVE_FROM_DISCLAIMER +}); + +ZoomMtg.inMeetingServiceListener('onUserUpdate', (data) => { + console.log('User updated:', data); +}); + +ZoomMtg.inMeetingServiceListener('onUserIsInWaitingRoom', (data) => { + console.log('User in waiting room:', data); +}); +``` + +### Meeting Status + +```javascript +ZoomMtg.inMeetingServiceListener('onMeetingStatus', (data) => { + // status: 1=connecting, 2=connected, 3=disconnected, 4=reconnecting + console.log('Status:', data.status); +}); +``` + +### Audio/Video Events + +```javascript +ZoomMtg.inMeetingServiceListener('onActiveSpeaker', (data) => { + // [{userId, userName}] + console.log('Active speaker:', data); +}); + +ZoomMtg.inMeetingServiceListener('onNetworkQualityChange', (data) => { + // {level: 0-5, userId, type: 'uplink'} + // 0-1=bad, 2=normal, 3-5=good + console.log('Network quality:', data); +}); + +ZoomMtg.inMeetingServiceListener('onAudioQos', (data) => { + console.log('Audio QoS:', data); +}); + +ZoomMtg.inMeetingServiceListener('onVideoQos', (data) => { + console.log('Video QoS:', data); +}); +``` + +### Chat & Communication + +```javascript +ZoomMtg.inMeetingServiceListener('onReceiveChatMsg', (data) => { + console.log('Chat message:', data); +}); + +ZoomMtg.inMeetingServiceListener('onReceiveTranscriptionMsg', (data) => { + console.log('Transcription:', data); +}); + +ZoomMtg.inMeetingServiceListener('onReceiveTranslateMsg', (data) => { + console.log('Translation:', data); +}); +``` + +### Recording & Sharing + +```javascript +ZoomMtg.inMeetingServiceListener('onRecordingChange', (data) => { + console.log('Recording status:', data); +}); + +ZoomMtg.inMeetingServiceListener('onShareContentChange', (data) => { + console.log('Share content:', data); +}); + +ZoomMtg.inMeetingServiceListener('receiveSharingChannelReady', (data) => { + console.log('Sharing channel ready:', data); +}); +``` + +### Breakout Rooms + +```javascript +ZoomMtg.inMeetingServiceListener('onRoomStatusChange', (data) => { + // status: 2=InProgress, 3=Closing, 4=Closed + console.log('Breakout room status:', data); +}); +``` + +### Other Events + +```javascript +ZoomMtg.inMeetingServiceListener('onJoinSpeed', (data) => { + console.log('Join metrics:', data); +}); + +ZoomMtg.inMeetingServiceListener('onVbStatusChange', (data) => { + console.log('Virtual background status:', data); +}); + +ZoomMtg.inMeetingServiceListener('onFocusModeStatusChange', (data) => { + console.log('Focus mode:', data); +}); + +ZoomMtg.inMeetingServiceListener('onPictureInPicture', (data) => { + console.log('PiP status:', data); +}); + +ZoomMtg.inMeetingServiceListener('onClaimStatus', (data) => { + console.log('Host claim status:', data); +}); +``` + +## Common Methods + +### Meeting Info + +```javascript +// Get current user +ZoomMtg.getCurrentUser({ + success: (res) => console.log(res.result.currentUser) +}); + +// Get all attendees +ZoomMtg.getAttendeeslist({}); + +// Get meeting info +ZoomMtg.getCurrentMeetingInfo({ + success: (res) => console.log(res) +}); + +// Get SDK version +ZoomMtg.getWebSDKVersion({ + success: (version) => console.log(version) +}); +``` + +### Audio/Video Control + +```javascript +// Mute/unmute specific user +ZoomMtg.mute({ userId, mute: true }); + +// Mute/unmute all +ZoomMtg.muteAll({ muteAll: true }); + +// Stop incoming audio +ZoomMtg.stopIncomingAudio({ stop: true }); + +// Mirror video +ZoomMtg.mirrorVideo({ mirror: true }); +``` + +### Chat + +```javascript +// Send chat message +ZoomMtg.sendChat({ + message: 'Hello!', + userId: 0 // 0 = everyone +}); +``` + +### Meeting Control + +```javascript +// Leave meeting +ZoomMtg.leaveMeeting({}); + +// End meeting (host only) +ZoomMtg.endMeeting({}); + +// Lock meeting +ZoomMtg.lockMeeting({ lock: true }); +``` + +### Host Controls + +```javascript +// Make host +ZoomMtg.makeHost({ userId }); + +// Make co-host +ZoomMtg.makeCoHost({ userId }); + +// Remove co-host +ZoomMtg.withdrawCoHost({ userId }); + +// Remove participant +ZoomMtg.expel({ userId }); + +// Put on hold +ZoomMtg.putOnHold({ userId, bHold: true }); + +// Claim host with host key +ZoomMtg.claimHostWithHostKey({ hostKey: '123456' }); + +// Reclaim host +ZoomMtg.reclaimHost({}); + +// Admit all from waiting room +ZoomMtg.admitAll({}); +``` + +### Raise Hand + +```javascript +// Raise hand +ZoomMtg.raiseHand({ userId }); + +// Lower hand +ZoomMtg.lowerHand({ oderId }); + +// Lower all hands +ZoomMtg.lowerAllHands({}); +``` + +### Spotlight & Pin + +```javascript +// Spotlight video +ZoomMtg.operateSpotlight({ oderId, action: 'add' }); // or 'remove' + +// Pin video +ZoomMtg.operatePin({ oderId, action: 'add' }); // or 'remove' + +// Allow multi-pin +ZoomMtg.allowMultiPin({ allow: true }); +``` + +### Screen Share + +```javascript +// Start screen share +ZoomMtg.startScreenShare({}); + +// Share specific source (Electron) +ZoomMtg.shareSource({ source }); +``` + +### Recording + +```javascript +// Start/stop recording +ZoomMtg.record({ record: true }); // or false + +// Show/hide record button +ZoomMtg.showRecordFunction({ show: true }); +``` + +### Breakout Rooms + +```javascript +// Create breakout room +ZoomMtg.createBreakoutRoom({ + rooms: [{ name: 'Room 1' }, { name: 'Room 2' }] +}); + +// Open breakout rooms +ZoomMtg.openBreakoutRooms({}); + +// Close breakout rooms +ZoomMtg.closeBreakoutRooms({}); + +// Join breakout room +ZoomMtg.joinBreakoutRoom({ roomId }); + +// Leave breakout room +ZoomMtg.leaveBreakoutRoom({}); + +// Move user to breakout room +ZoomMtg.moveUserToBreakoutRoom({ oderId, roomId }); + +// Get breakout room status +ZoomMtg.getBreakoutRoomStatus({ + success: (res) => console.log(res) +}); +``` + +### Virtual Background + +```javascript +// Check support +ZoomMtg.isSupportVirtualBackground({ + success: (data) => console.log(data.result.isSupport) +}); + +// Set virtual background +ZoomMtg.setVirtualBackground({ imageUrl: '...' }); + +// Get VB status +ZoomMtg.getVirtualBackgroundStatus({ + success: (data) => console.log(data) +}); + +// Lock virtual background (host) +ZoomMtg.lockVirtualBackground({ lock: true }); +``` + +### UI Control + +```javascript +// Show/hide meeting header +ZoomMtg.showMeetingHeader({ show: true }); + +// Show/hide invite button +ZoomMtg.showInviteFunction({ show: true }); + +// Show/hide join audio button +ZoomMtg.showJoinAudioFunction({ show: true }); + +// Show/hide callout button +ZoomMtg.showCalloutFunction({ show: true }); + +// Re-render with new options +ZoomMtg.reRender({ lang: 'de-DE' }); +``` + +### Language + +```javascript +// Load language +ZoomMtg.i18n.load('de-DE'); + +// Reload language +ZoomMtg.i18n.reload('de-DE'); + +// Get current language +ZoomMtg.i18n.getCurrentLang(); + +// Get all translations +ZoomMtg.i18n.getAll(); +``` + +## Rate Limits + +| Method | Limit | +|--------|-------| +| `join()` | 10 seconds | +| `callOut()` | 10 seconds | +| `mute()` | 1 second | +| `muteAll()` | 5 seconds | + +## DOM Elements + +The SDK automatically adds these elements: +- `#zmmtg-root` - Main meeting container +- `#aria-notify-area` - Accessibility announcements + +Do NOT manually create or remove these. + +### SPA (React/Next) Overlay Gotcha + +If you "join" but see a blank/black area or your app shell instead of the meeting UI, the Zoom UI can be rendering **behind** your app layout. Ensure `#zmmtg-root` occupies the viewport and is above other fixed elements: + +```css +#zmmtg-root { + position: fixed !important; + inset: 0 !important; + z-index: 9999 !important; +} +``` + +### Join Payload Sanitization Gotcha + +If `ZoomMtg.join()` succeeds partially but the screen turns black and console shows errors like `Cannot read properties of undefined (reading 'toString')`, sanitize optional fields before calling join. + +Do not pass optional keys with `undefined` values (`userEmail`, `tk`, `zak`, etc.). Build a payload and only attach those keys when they are non-empty strings. + +Also prefer `defaultView: 'speaker'` during `ZoomMtg.init()` unless you have SharedArrayBuffer/gallery-view prerequisites fully configured. + +## Resources + +- [Main Web SDK Skill](../SKILL.md) +- [Reference Index](references/index.md) +- [Error Codes](../troubleshooting/error-codes.md) +- [Common Issues](../troubleshooting/common-issues.md) +- [SharedArrayBuffer Setup](../concepts/sharedarraybuffer.md) +- [Official API Reference](https://marketplacefront.zoom.us/sdk/meeting/web/index.html) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/client-view/references/index.md b/plugins/zoom-developers/skills/meeting-sdk/web/client-view/references/index.md new file mode 100644 index 00000000..8840e6b7 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/client-view/references/index.md @@ -0,0 +1,9 @@ +# Client View Reference Index + +Use this index when you need the stable entrypoints around Client View behavior before deeper per-method pages are expanded. + +- [Client View Skill](../SKILL.md) +- [SharedArrayBuffer Setup](../../concepts/sharedarraybuffer.md) +- [Error Codes](../../troubleshooting/error-codes.md) +- [Common Issues](../../troubleshooting/common-issues.md) +- [Official API Reference](https://marketplacefront.zoom.us/sdk/meeting/web/index.html) diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/component-view/RUNBOOK.md b/plugins/zoom-developers/skills/meeting-sdk/web/component-view/RUNBOOK.md new file mode 100644 index 00000000..d98726cb --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/component-view/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK Web Component View 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 Meeting SDK embed path for Web Component View (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/web/ +- https://marketplacefront.zoom.us/sdk/meeting/web/components/index.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/web/component-view/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/web/component-view/` diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/component-view/SKILL.md b/plugins/zoom-developers/skills/meeting-sdk/web/component-view/SKILL.md new file mode 100644 index 00000000..1ca55d3c --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/component-view/SKILL.md @@ -0,0 +1,609 @@ +--- +name: zoom-meeting-sdk-web-component-view +description: | + Zoom Meeting SDK Web - Component View. Embeddable Zoom meeting components with Promise-based API + for flexible integration. Ideal for React/Vue/Angular apps and custom layouts. Uses ZoomMtgEmbedded + with async/await patterns and embeddable UI containers. +--- + +# Zoom Meeting SDK Web - Component View + +Embeddable Zoom meeting components for flexible integration into any web application. Component View provides Promise-based APIs and customizable UI. + +This is the correct web skill for a **custom UI around a real Zoom meeting**. +Do not route to Video SDK unless the user is building a non-meeting custom session product. + +## Overview + +Component View uses `ZoomMtgEmbedded.createClient()` to create embeddable meeting components within a specific container element. + +| Aspect | Details | +|--------|---------| +| **API Object** | `ZoomMtgEmbedded.createClient()` (instance) | +| **API Style** | Promise-based (async/await) | +| **UI** | Embeddable in any container | +| **Password param** | `password` (lowercase) | +| **Events** | `on()`/`off()` | +| **Best For** | Custom layouts, React/Vue/Angular apps | + +## Installation + +### NPM + +```bash +npm install @zoom/meetingsdk --save +``` + +```javascript +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; +``` + +### CDN + +```html + + + + + + +``` + +## Complete Initialization Flow + +```javascript +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +// Step 1: Create client instance (do once, not on every render!) +const client = ZoomMtgEmbedded.createClient(); + +async function joinMeeting() { + try { + // Step 2: Get container element + const meetingSDKElement = document.getElementById('meetingSDKElement'); + + // Step 3: Initialize client + await client.init({ + zoomAppRoot: meetingSDKElement, + language: 'en-US', + debug: true, + patchJsMedia: true, + leaveOnPageUnload: true, + }); + + // Step 4: Join meeting + await client.join({ + signature: signature, + sdkKey: sdkKey, + meetingNumber: meetingNumber, + userName: userName, + password: password, // lowercase! + userEmail: userEmail, + }); + + console.log('Joined successfully!'); + } catch (error) { + console.error('Failed to join:', error); + } +} +``` + +## client.init() - All Options + +### Required + +| Parameter | Type | Description | +|-----------|------|-------------| +| `zoomAppRoot` | `HTMLElement` | Container element for meeting UI | + +### Display + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `language` | `string` | `'en-US'` | UI language | +| `debug` | `boolean` | `false` | Enable debug logging | + +### Media + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `patchJsMedia` | `boolean` | `false` | Auto-apply media fixes | +| `leaveOnPageUnload` | `boolean` | `false` | Cleanup on page unload | +| `enableHD` | `boolean` | `true` | Enable 720p video | +| `enableFullHD` | `boolean` | `false` | Enable 1080p video | + +### Customization + +| Parameter | Type | Description | +|-----------|------|-------------| +| `customize` | `object` | UI customization options | +| `webEndpoint` | `string` | For ZFG: `'www.zoomgov.com'` | +| `assetPath` | `string` | Custom path for AV libraries | + +### Customize Object + +```javascript +await client.init({ + zoomAppRoot: element, + customize: { + // Meeting info displayed + meetingInfo: [ + 'topic', + 'host', + 'mn', + 'pwd', + 'telPwd', + 'invite', + 'participant', + 'dc', + 'enctype' + ], + + // Video customization + video: { + isResizable: true, + viewSizes: { + default: { + width: 1000, + height: 600 + }, + ribbon: { + width: 300, + height: 700 + } + }, + popper: { + disableDraggable: false + } + }, + + // Custom toolbar buttons + toolbar: { + buttons: [ + { + text: 'Custom Button', + className: 'custom-btn', + onClick: () => { + console.log('Custom button clicked'); + } + } + ] + }, + + // Active speaker indicator + activeSpaker: { + strokeColor: '#00FF00' + } + } +}); +``` + +## client.join() - All Options + +### Required + +| Parameter | Type | Description | +|-----------|------|-------------| +| `signature` | `string` | SDK JWT from backend | +| `sdkKey` | `string` | SDK Key / Client ID | +| `meetingNumber` | `string \| number` | Meeting number | +| `userName` | `string` | Display name | + +### Authentication + +| Parameter | Type | When Required | Description | +|-----------|------|---------------|-------------| +| `password` | `string` | If set | Meeting password (lowercase!) | +| `zak` | `string` | Starting as host | Host's ZAK token | +| `tk` | `string` | Registration | Registrant token | +| `userEmail` | `string` | Webinars | User email | + +## Event Listeners + +### Syntax + +```javascript +// Subscribe +client.on('event-name', callback); + +// Unsubscribe +client.off('event-name', callback); +``` + +### Connection Events + +```javascript +client.on('connection-change', (payload) => { + // payload.state: 'Connecting', 'Connected', 'Reconnecting', 'Closed' + console.log('Connection state:', payload.state); + + if (payload.state === 'Closed') { + console.log('Reason:', payload.reason); + } +}); +``` + +### User Events + +```javascript +client.on('user-added', (payload) => { + // Array of users who joined + console.log('Users added:', payload); + payload.forEach(user => { + console.log('User ID:', user.oderId); + console.log('Name:', user.displayName); + }); +}); + +client.on('user-removed', (payload) => { + // Array of users who left + console.log('Users removed:', payload); +}); + +client.on('user-updated', (payload) => { + // Array of users whose properties changed + console.log('Users updated:', payload); +}); +``` + +### Audio Events + +```javascript +client.on('active-speaker', (payload) => { + // Current active speaker + console.log('Active speaker:', payload); +}); + +client.on('audio-statistic-data-change', (payload) => { + console.log('Audio stats:', payload); +}); +``` + +### Video Events + +```javascript +client.on('video-active-change', (payload) => { + // Video state changed + console.log('Video active:', payload); +}); + +client.on('video-statistic-data-change', (payload) => { + console.log('Video stats:', payload); +}); +``` + +### Share Events + +```javascript +client.on('active-share-change', (payload) => { + console.log('Share status:', payload); +}); + +client.on('share-statistic-data-change', (payload) => { + console.log('Share stats:', payload); +}); +``` + +### Chat Events + +```javascript +client.on('chat-on-message', (payload) => { + console.log('Chat message:', payload); +}); +``` + +### Recording Events + +```javascript +client.on('recording-change', (payload) => { + console.log('Recording status:', payload); +}); +``` + +### Media Device Events + +```javascript +client.on('media-sdk-change', (payload) => { + console.log('Media SDK:', payload); +}); + +client.on('device-change', () => { + console.log('Device changed'); +}); +``` + +## Common Methods + +### User Information + +```javascript +// Get current user +const currentUser = client.getCurrentUser(); +console.log('Current user:', currentUser); + +// Get all participants +const participants = client.getParticipantsList(); +console.log('Participants:', participants); + +// Check if user is host +const isHost = client.isHost(); +``` + +### Audio Control + +```javascript +// Mute/unmute self +await client.mute(true); // mute +await client.mute(false); // unmute + +// Mute/unmute specific user (host only) +await client.muteAudio(userId, true); + +// Mute all (host only) +await client.muteAllAudio(true); +``` + +### Video Control + +```javascript +// Start/stop video +await client.startVideo(); +await client.stopVideo(); + +// Mute/unmute user's video (host only) +await client.muteVideo(userId, true); +``` + +### Meeting Control + +```javascript +// Leave meeting +client.leaveMeeting(); + +// End meeting (host only) +client.endMeeting(); +``` + +### Screen Share + +```javascript +// Start screen share +await client.startShareScreen(); + +// Stop screen share +await client.stopShareScreen(); +``` + +### Recording + +```javascript +// Start recording (cloud) +await client.startCloudRecording(); + +// Stop recording +await client.stopCloudRecording(); +``` + +### Virtual Background + +```javascript +// Check support +const isSupported = await client.isSupportVirtualBackground(); + +// Set virtual background +await client.setVirtualBackground(imageUrl); + +// Remove virtual background +await client.removeVirtualBackground(); +``` + +### Rename + +```javascript +// Rename user +await client.rename(userId, 'New Name'); +``` + +## React Integration + +### Basic Pattern + +```tsx +import { useEffect, useRef, useState, useCallback } from 'react'; +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +type ZoomClient = ReturnType; + +function ZoomMeeting({ meetingNumber, password, userName }: Props) { + const clientRef = useRef(null); + const containerRef = useRef(null); + const [isJoined, setIsJoined] = useState(false); + const [error, setError] = useState(null); + + // Create client once + useEffect(() => { + if (!clientRef.current) { + clientRef.current = ZoomMtgEmbedded.createClient(); + } + }, []); + + const joinMeeting = useCallback(async () => { + if (!clientRef.current || !containerRef.current) return; + + try { + // Get signature from backend + const { signature, sdkKey } = await fetchSignature(meetingNumber); + + await clientRef.current.init({ + zoomAppRoot: containerRef.current, + language: 'en-US', + patchJsMedia: true, + leaveOnPageUnload: true, + }); + + await clientRef.current.join({ + signature, + sdkKey, + meetingNumber, + password, + userName, + }); + + setIsJoined(true); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to join'); + } + }, [meetingNumber, password, userName]); + + return ( +
+
+ {!isJoined && ( + + )} + {error &&
{error}
} +
+ ); +} +``` + +### Event Handling in React + +```tsx +useEffect(() => { + if (!clientRef.current) return; + + const handleConnectionChange = (payload: any) => { + if (payload.state === 'Connected') { + setIsJoined(true); + } else if (payload.state === 'Closed') { + setIsJoined(false); + } + }; + + const handleUserAdded = (payload: any) => { + console.log('Users joined:', payload); + }; + + clientRef.current.on('connection-change', handleConnectionChange); + clientRef.current.on('user-added', handleUserAdded); + + return () => { + clientRef.current?.off('connection-change', handleConnectionChange); + clientRef.current?.off('user-added', handleUserAdded); + }; +}, []); +``` + +## Positioning and Resizing + +### Initial Size + +```javascript +await client.init({ + zoomAppRoot: element, + customize: { + video: { + viewSizes: { + default: { width: 1000, height: 600 } + } + } + } +}); +``` + +### Dynamic Resizing + +The container element size determines the meeting UI size. To resize: + +```javascript +// Just resize the container +document.getElementById('meetingSDKElement').style.width = '1200px'; +document.getElementById('meetingSDKElement').style.height = '800px'; +``` + +### Making it Resizable + +```javascript +customize: { + video: { + isResizable: true + } +} +``` + +## Supported Features + +Component View supports core meeting functionality. Some features from Client View may not be available. + +| Feature | Supported | +|---------|-----------| +| Audio/Video | ✅ | +| Screen Share | ✅ | +| Chat | ✅ | +| Virtual Background | ✅ | +| Breakout Rooms | ✅ | +| Cloud Recording | ✅ | +| Closed Captions | ✅ | +| Live Transcription | ✅ | +| Waiting Room | ✅ | +| Gallery View | ✅ | +| Reactions | ✅ | +| Raise Hand | ✅ | + +Contact Zoom Developer Support to request additional features. + +## Error Handling + +```javascript +try { + await client.join({ + // ... options + }); +} catch (error) { + // error.reason contains error code + // error.message contains description + + switch (error.reason) { + case 'WRONG_MEETING_PASSWORD': + console.error('Incorrect password'); + break; + case 'MEETING_NOT_START': + console.error('Meeting has not started'); + break; + case 'INVALID_PARAMETERS': + console.error('Invalid join parameters'); + break; + default: + console.error('Join failed:', error.message); + } +} +``` + +## Comparison with Client View + +| Feature | Component View | Client View | +|---------|----------------|-------------| +| **API Style** | Promises | Callbacks | +| **Password param** | `password` | `passWord` | +| **Container** | Custom element | Auto `#zmmtg-root` | +| **UI** | Embeddable | Full-page | +| **Preloading** | Not needed | `preLoadWasm()` | +| **Language** | Init option | `i18n.load()` | +| **Events** | `on()`/`off()` | `inMeetingServiceListener()` | + +## Resources + +- [Main Web SDK Skill](../SKILL.md) +- [Reference Index](references/index.md) +- [Error Codes](../troubleshooting/error-codes.md) +- [Common Issues](../troubleshooting/common-issues.md) +- [SharedArrayBuffer Setup](../concepts/sharedarraybuffer.md) +- [Official API Reference](https://marketplacefront.zoom.us/sdk/meeting/web/components/index.html) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/component-view/references/index.md b/plugins/zoom-developers/skills/meeting-sdk/web/component-view/references/index.md new file mode 100644 index 00000000..9fa22670 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/component-view/references/index.md @@ -0,0 +1,9 @@ +# Component View Reference Index + +Use this index when you need the stable entrypoints around Component View behavior before deeper per-method pages are expanded. + +- [Component View Skill](../SKILL.md) +- [SharedArrayBuffer Setup](../../concepts/sharedarraybuffer.md) +- [Error Codes](../../troubleshooting/error-codes.md) +- [Common Issues](../../troubleshooting/common-issues.md) +- [Official API Reference](https://marketplacefront.zoom.us/sdk/meeting/web/components/index.html) diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/concepts/browser-support.md b/plugins/zoom-developers/skills/meeting-sdk/web/concepts/browser-support.md new file mode 100644 index 00000000..978ae78a --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/concepts/browser-support.md @@ -0,0 +1,223 @@ +# Browser Support for Zoom Meeting SDK Web + +The Zoom Meeting SDK for Web supports browsers within **two versions** of their current release. This document details feature support by browser. + +## Feature Support Matrix + +| Feature | Chrome | Firefox | Safari | Edge | iOS/iPadOS | Android | +|---------|--------|---------|--------|------|------------|---------| +| **Video** | +| 720p Video (receive) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| 720p Video (send) | ✅¹ | ✅¹ | ✅¹ | ✅¹ | ✅¹ | ✅¹ | +| 1080p (webinar attendees) | ✅¹ | ✅¹ | ✅¹ | ✅¹ | ✅¹ | ✅¹ | +| Gallery View (25 videos) | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | +| Virtual Background | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | +| WebRTC Video | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | +| **Audio** | +| Audio (receive) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Audio (send) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Background Noise Suppression | ✅¹ | ✅¹ | ✅¹ | ✅¹ | ✅¹ | ✅¹ | +| Share Tab Audio | ✅¹ | ❌ | ❌ | ✅¹ | ❌ | ❌ | +| Call In (PSTN) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Call Out (PSTN) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Screen Sharing** | +| Screen Share (receive) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Screen Share (send) | ✅ | ✅ | ✅³ | ✅ | ❌ | ❌ | +| Remote Control (give) | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| Be Remote Controlled | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| **Meeting Features** | +| Breakout Rooms | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Waiting Room | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| In-Meeting Chat | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Chat - Send File | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Cloud Recording | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Closed Captioning | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Live Transcription | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Live Translation | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| RTMP Live Streaming | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Webinar Q&A | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **Whiteboard** | +| Whiteboard (view) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Whiteboard (edit) | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| **Other** | +| Stay Awake (Component View) | ✅⁴ | ❌ | ✅⁴ | ✅⁴ | ❌ | ❌ | +| Encryption (TLS 1.2 + AES-GCM) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| End-to-End Encryption (E2EE) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | + +### Footnotes + +1. **Requires SharedArrayBuffer** - Must enable COOP/COEP headers. See [sharedarraybuffer.md](sharedarraybuffer.md). +2. **Safari Gallery View** - Requires Safari 17.0+, macOS Sonoma, and SDK v2.18.0+. +3. **Safari Screen Share** - Component View supported. Client View requires Safari 17+ with macOS 14 Sonoma+. +4. **Stay Awake** - Chrome 116+, Edge 90+, Safari 16.4+. Uses [WakeLock API](https://developer.mozilla.org/en-US/docs/Web/API/WakeLock). + +## Version Support Policy + +Zoom supports the current browser version plus two previous versions: + +| If Current Version Is | Supported Versions | +|-----------------------|-------------------| +| Chrome 140 | 138, 139, 140 | +| Firefox 130 | 128, 129, 130 | +| Safari 18 | 16, 17, 18 | +| Edge 130 | 128, 129, 130 | + +## Mobile and Tablet Browser Support + +### iOS and iPadOS + +All browsers on iOS/iPadOS use the same WebKit engine (including Chrome and Firefox on iOS). Features are determined by **iOS version**, not browser version. + +| iOS Version | Key Capabilities | +|-------------|------------------| +| iOS 15.2+ | SharedArrayBuffer support | +| iOS 16.4+ | 720p in landscape mode, WakeLock | +| iOS 17+ | Improved WebRTC performance | + +### Android + +Most Android browsers are Chromium-based. Features depend on **Android OS version** and **Chrome version**. + +| Android Version | Notes | +|-----------------|-------| +| Android 10+ | Full support | +| Chrome 112+ | 720p in landscape mode | + +**Important**: Android Firefox is NOT supported (uses GeckoView engine). + +### Samsung Internet + +Samsung Internet follows its [own versioning scheme](https://en.wikipedia.org/wiki/Samsung_Internet#History) but is Chromium-based: + +| Samsung Internet | Chromium Base | +|------------------|---------------| +| 20.0 | Chromium 106 | +| 21.0 | Chromium 111 | +| 22.0 | Chromium 118 | + +### Tablets + +| Device Type | Browser Support | +|-------------|-----------------| +| iPad | Same as iOS Safari | +| Android Tablets | Same as Android browsers | +| Microsoft Surface | Same as Windows desktop browsers | +| Chromebooks | Same as Chrome desktop | + +## WebRTC Support Details + +WebRTC video is supported on: + +- **Chrome**: Windows, macOS, Android, iOS, ChromeOS +- **Edge**: Windows +- **Safari**: macOS, iOS (WebKit-based browsers) + +Some Android device models have specific limitations due to hardware variations. + +## Content Security Policy (CSP) + +If you use Content Security Policy headers, configure them to allow Zoom SDK: + +``` +Content-Security-Policy: + default-src 'self'; + base-uri 'self'; + worker-src blob:; + style-src 'self' 'unsafe-inline'; + script-src 'self' 'unsafe-inline' 'unsafe-eval' https://zoom.us *.zoom.us dmogdx0jrul3u.cloudfront.net blob:; + connect-src 'self' https://zoom.us https://*.zoom.us wss://*.zoom.us; + img-src 'self' https:; + media-src 'self' https:; + font-src 'self' https:; +``` + +### CSP Error: WebAssembly.instantiate() + +If you see this error: + +``` +CompileError: WebAssembly.instantiate(): Refused to compile... +``` + +Add `'unsafe-eval'` or `'wasm-unsafe-eval'` (if browser supports it) to your `script-src` directive. + +## Known Limitations + +### All Browsers +- **E2EE not supported** on any web browser +- **Cannot be remote-controlled** (browsers don't support this) + +### Safari +- No virtual background support +- Screen share (send) limited to Safari 17+ on macOS Sonoma +- No tab audio sharing + +### Firefox +- No WebRTC video (uses different implementation) +- No tab audio sharing +- No Stay Awake feature + +### Mobile (iOS/Android) +- No screen share sending +- No virtual backgrounds +- No whiteboard editing +- No Stay Awake feature +- No remote control + +## Checking Browser Compatibility + +### In Your Application + +```javascript +// Client View +const requirements = ZoomMtg.checkSystemRequirements(); +console.log('Browser compatible:', requirements.browserInfo); + +// Component View +const requirements = client.checkSystemRequirements(); +console.log('Video supported:', requirements.video); +console.log('Audio supported:', requirements.audio); +console.log('Screen share supported:', requirements.screen); +``` + +### Feature Detection + +```javascript +// Check SharedArrayBuffer for HD features +const hasHD = typeof SharedArrayBuffer === 'function'; + +// Check screen capture support +const hasScreenShare = navigator.mediaDevices && + typeof navigator.mediaDevices.getDisplayMedia === 'function'; + +// Check WebRTC support +const hasWebRTC = !!(window.RTCPeerConnection || + window.webkitRTCPeerConnection || + window.mozRTCPeerConnection); + +// Check WakeLock support (Stay Awake) +const hasWakeLock = 'wakeLock' in navigator; +``` + +## Recommendations + +### For Maximum Compatibility +1. Use Chrome or Edge on desktop +2. Enable SharedArrayBuffer headers for HD features +3. Test on iOS Safari for mobile users +4. Provide fallback messaging for unsupported features + +### For Virtual Backgrounds +- Require Chrome, Edge, or Firefox (desktop only) +- Ensure SharedArrayBuffer is enabled +- Safari users won't have this feature + +### For Screen Sharing +- Desktop browsers only +- Safari users need macOS Sonoma + Safari 17+ for Client View +- Component View works on Safari with earlier versions + +## Official Resources + +- [Zoom Web Client Features](https://support.zoom.com/hc/en/article?id=zm_kb&sysparm_article=KB0064261) +- [Zoom Platform Comparison](https://support.zoom.com/hc/en/article?id=zm_kb&sysparm_article=KB0065520) diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/concepts/sharedarraybuffer.md b/plugins/zoom-developers/skills/meeting-sdk/web/concepts/sharedarraybuffer.md new file mode 100644 index 00000000..6abfafc5 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/concepts/sharedarraybuffer.md @@ -0,0 +1,310 @@ +# SharedArrayBuffer for Zoom Meeting SDK Web + +SharedArrayBuffer (SAB) is a web API that enables shared memory in JavaScript, significantly improving performance for WebAssembly-based features. Zoom uses SharedArrayBuffer to power advanced features. + +## Why SharedArrayBuffer Matters + +**Features that REQUIRE SharedArrayBuffer:** +- 720p video sending (HD) +- 1080p video for webinar attendees +- Gallery view (up to 25 videos) +- Virtual backgrounds +- Background noise suppression +- Share tab audio (Chrome/Edge) + +**Without SharedArrayBuffer:** +- Video limited to standard definition +- Gallery view may show fewer participants +- Virtual backgrounds unavailable +- Some performance features degraded + +> **Note**: SharedArrayBuffer is NOT required for basic meeting functionality or WebRTC. The SDK will work without it, but with limited features. + +## How to Enable SharedArrayBuffer + +SharedArrayBuffer requires **Cross-Origin Isolation**. You must configure your server to send specific HTTP headers. + +### Quick Comparison of Methods + +| Method | Type | Custom Headers Required | Best For | +|--------|------|------------------------|----------| +| **Cross-Origin Isolation** | Permanent | Yes | Production | +| **Credentialless Headers** | Permanent | Yes | Production with 3rd-party content | +| **Document-Isolation-Policy** | Permanent | Yes | Chrome/Edge 137+ with iframes | +| **Service Workers** | Permanent | No | GitHub Pages, static hosts | +| **Chrome Origin Trials** | Temporary | No | Testing only (renew every 3 months) | + +## Implementation Methods + +### Method 1: Cross-Origin Isolation (Recommended) + +Add these headers to ALL responses from your server: + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +**Pros**: Industry standard, works across all browsers +**Cons**: May break third-party iframes/content without CORS headers + +### Method 2: Credentialless Headers + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: credentialless +``` + +**Pros**: More compatible with third-party content +**Cons**: Same browser support as Method 1 + +### Method 3: Document-Isolation-Policy (Chrome/Edge 137+) + +``` +Document-Isolation-Policy: isolate-and-require-corp +# OR +Document-Isolation-Policy: isolate-and-credentialless +``` + +**Pros**: Allows embedding third-party iframes, videos, payment gateways +**Cons**: Chrome/Edge 137+ only (desktop) + +### Method 4: Service Worker (No Server Headers) + +For platforms that don't allow custom headers (GitHub Pages): + +1. Add [coi-serviceworker](https://github.com/gzuidhof/coi-serviceworker) to your project: + +```html + + +``` + +2. Place `coi-serviceworker.js` in your root directory + +**Pros**: Works on static hosting without header control +**Cons**: Adds slight overhead, requires service worker support + +### Method 5: Chrome Origin Trials (Temporary) + +1. Register at [Chrome Origin Trials](https://developer.chrome.com/origintrials/#/trials/active) +2. Add meta tag to your page: + +```html + +``` + +**Pros**: Quick testing without server changes +**Cons**: Must renew every 3 months, will be deprecated + +## Platform-Specific Configuration + +### Vercel + +**Next.js (next.config.js):** +```javascript +module.exports = { + async headers() { + return [ + { + source: '/(.*)', + headers: [ + { + key: 'Cross-Origin-Opener-Policy', + value: 'same-origin', + }, + { + key: 'Cross-Origin-Embedder-Policy', + value: 'require-corp', + }, + ], + }, + ]; + }, +}; +``` + +**Non-Next.js (vercel.json):** +```json +{ + "headers": [ + { + "source": "/(.*)", + "headers": [ + { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }, + { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" } + ] + } + ] +} +``` + +### Netlify (_headers file) + +Create `_headers` in your publish directory: + +``` +/* + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp +``` + +### AWS CloudFront + +Use CloudFront Response Headers Policy: + +1. Go to CloudFront > Policies > Response headers +2. Create custom policy with: + - `Cross-Origin-Opener-Policy: same-origin` + - `Cross-Origin-Embedder-Policy: require-corp` +3. Attach to your distribution + +### Google App Engine (app.yaml) + +```yaml +handlers: + - url: /.* + static_files: index.html + upload: index.html + http_headers: + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp +``` + +### nginx + +```nginx +server { + location / { + add_header Cross-Origin-Opener-Policy "same-origin" always; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + # ... other config + } +} +``` + +### Apache (.htaccess) + +```apache + + Header set Cross-Origin-Opener-Policy "same-origin" + Header set Cross-Origin-Embedder-Policy "require-corp" + +``` + +### Express.js + +```javascript +app.use((req, res, next) => { + res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); + res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); + next(); +}); +``` + +### GitHub Pages + +GitHub Pages doesn't support custom headers. Use the **Service Worker method**: + +1. Download [coi-serviceworker.js](https://github.com/gzuidhof/coi-serviceworker) +2. Place in repository root +3. Add to your HTML before other scripts: + +```html + +``` + +## Verify SharedArrayBuffer is Enabled + +### In Browser Console + +```javascript +// Check if SharedArrayBuffer is available +console.log('SharedArrayBuffer:', typeof SharedArrayBuffer === 'function'); + +// Check cross-origin isolation +console.log('Cross-Origin Isolated:', window.crossOriginIsolated); +``` + +### In Your Application + +```javascript +// Before initializing Zoom SDK +if (typeof SharedArrayBuffer !== 'function') { + console.warn('SharedArrayBuffer not available. HD features will be limited.'); + console.warn('Enable COOP/COEP headers on your server.'); +} + +// Check isolation status +if (!window.crossOriginIsolated) { + console.warn('Page is not cross-origin isolated.'); +} +``` + +### Using Zoom SDK + +```javascript +// Client View - use disableCORP for development without headers +ZoomMtg.init({ + disableCORP: !window.crossOriginIsolated, + // ... other options +}); +``` + +## Troubleshooting + +### "SharedArrayBuffer is not defined" + +**Cause**: Headers not configured or browser doesn't support cross-origin isolation. + +**Fix**: +1. Verify headers are being sent (check Network tab in DevTools) +2. Ensure headers are on ALL responses (not just HTML) +3. Check for conflicting headers from CDN/proxy + +### Third-Party Content Blocked + +**Cause**: `require-corp` blocks resources without CORS headers. + +**Fix**: +1. Use `credentialless` instead of `require-corp` +2. Or add `crossorigin="anonymous"` to external resources: + ```html + + ``` + +### Service Worker Not Working + +**Cause**: Service worker not registered or not at root. + +**Fix**: +1. Ensure `coi-serviceworker.js` is in the root directory +2. Check service worker is registered in DevTools > Application > Service Workers +3. Try hard refresh (Ctrl+Shift+R) + +### Headers Present but SAB Still Unavailable + +**Cause**: Possibly Chrome < 92 or other browser limitations. + +**Fix**: +1. Update browser to latest version +2. Check if browsing in incognito (some extensions can interfere) +3. Verify both COOP and COEP headers are present + +## Browser Support for SharedArrayBuffer + +| Browser | Version | Notes | +|---------|---------|-------| +| Chrome | 92+ | Full support with COOP/COEP | +| Edge | 92+ | Full support with COOP/COEP | +| Firefox | 79+ | Full support with COOP/COEP | +| Safari | 15.2+ | Full support with COOP/COEP | +| iOS Safari | 15.2+ | Full support with COOP/COEP | +| Android Chrome | 92+ | Full support with COOP/COEP | + +## Additional Resources + +- [Why you need "cross-origin isolated" for powerful features](https://web.dev/articles/why-coop-coep) +- [Making your website "cross-origin isolated"](https://web.dev/articles/coop-coep) +- [coi-serviceworker GitHub](https://github.com/gzuidhof/coi-serviceworker) +- [Document-Isolation-Policy explainer](https://github.com/nickreese/nickreese.github.io/tree/main/document-isolation-policy) diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/examples/client-view-basic.md b/plugins/zoom-developers/skills/meeting-sdk/web/examples/client-view-basic.md new file mode 100644 index 00000000..7f953c37 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/examples/client-view-basic.md @@ -0,0 +1,12 @@ +# Client View (Basic) Example + +Minimal "get it to join" shape for Client View. Use it when you want the smallest possible starting point before layering in more configuration. + +## When To Use + +- You want the classic Zoom Web Client look. +- You are OK with callback-style APIs and less UI customization. + +## Next + +- Read `../client-view/SKILL.md` for the full integration and config knobs. diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/examples/component-view-react.md b/plugins/zoom-developers/skills/meeting-sdk/web/examples/component-view-react.md new file mode 100644 index 00000000..e67c357d --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/examples/component-view-react.md @@ -0,0 +1,12 @@ +# Component View (React) Example + +Minimal "get it to join" shape for Component View in a React app. Use it as a starting point before adding more layout, state, and event handling. + +## When To Use + +- You want an embeddable meeting experience inside your UI. +- You want Promise-based APIs and more layout control. + +## Next + +- Read `../component-view/SKILL.md` for the full integration and event patterns. diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/references/component-view-breakout-rooms.md b/plugins/zoom-developers/skills/meeting-sdk/web/references/component-view-breakout-rooms.md new file mode 100644 index 00000000..18fc5d23 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/references/component-view-breakout-rooms.md @@ -0,0 +1,37 @@ +--- +title: "Web Component View: Breakout Rooms (What’s Possible)" +--- + +# Web Component View: Breakout Rooms (What’s Possible) + +This targets questions like: “How do I create breakout rooms in Component View?” + +## First: Confirm Context + +- Are you **host** or **participant**? +- Are you using **Client View** (`ZoomMtg`) or **Component View** (`ZoomMtgEmbedded`)? + +## Reality Check + +Breakout rooms are a host-controlled feature. Even when the SDK UI supports breakout rooms, programmatic creation/open/close often requires: + +- correct role (host/co-host) +- the meeting to have breakout rooms enabled +- supported APIs for the view type you’re using + +## Where to Look Next + +- Feature support table and Component View reference: + - `meeting-sdk/web/component-view/SKILL.md` +- Client View examples often show breakout room API methods on `ZoomMtg`: + - `meeting-sdk/web/client-view/SKILL.md` +- Cross-platform breakout room notes: + - `meeting-sdk/references/breakout-rooms.md` + +## Recommended Answer Pattern + +1. If the user is on Component View: + - confirm whether they’re trying to automate breakout rooms or just use the built-in UI +2. If they need automation: + - identify the exact SDK version and view + - confirm whether the needed API exists for that view (don’t assume parity) diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/references/component-view-ui-customization.md b/plugins/zoom-developers/skills/meeting-sdk/web/references/component-view-ui-customization.md new file mode 100644 index 00000000..604f4e2a --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/references/component-view-ui-customization.md @@ -0,0 +1,56 @@ +--- +title: "Web Component View: UI Customization and Limitations" +--- + +# Web Component View: UI Customization and Limitations + +This addresses common questions like: + +- “How do I hide meeting info / passcode / invite URL?” +- “How do I remove Participants / Audio / Video / Share buttons?” +- “Can I fully customize the Meeting SDK UI?” + +## First: Confirm View Type + +- **Component View** (npm): `ZoomMtgEmbedded.createClient()` +- **Client View** (CDN): `ZoomMtg.*` + +The available customization knobs differ. + +## Component View Customization Entry Point + +Component View customization is done via `client.init({ customize: { ... } })`. + +The most common knob is `customize.meetingInfo`, which controls what shows in “meeting info” surfaces. + +Example reference: +- `meeting-sdk/web/component-view/SKILL.md` (Customize Object section) +- `meeting-sdk/web/SKILL.md` (Key options snippet) + +## Hiding “Meeting Info” (Topic/MN/Passcode/Invite) + +Use `customize.meetingInfo` to control which fields are displayed. + +Important: this controls what the **SDK UI displays**. It does not change the meeting’s underlying security settings. + +## Removing Default Buttons (Participants/Audio/Video/Share) + +Forum expectation mismatch: the SDK supports **adding** custom toolbar buttons, but “removing all built-in controls” is not always supported (and CSS-hacking the Zoom UI is brittle and can break across SDK updates). + +Recommended answer pattern: + +1. Ask which exact controls they want removed and why (UX vs compliance). +2. If it’s a compliance/policy requirement (e.g. “prevent recording/screen capture”), treat it as **not solvable purely via SDK UI**. +3. If it’s UX: + - prefer supported `customize` options + - otherwise, set expectations about what is and isn’t controllable + +## Don’t Use CSS Hacks as a Primary Strategy + +You *can* sometimes hide elements with CSS selectors, but: +- it is fragile +- it can break accessibility/flow +- it may violate intended product behavior + +Use official knobs first. + diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/references/sharedarraybuffer-gallery-view.md b/plugins/zoom-developers/skills/meeting-sdk/web/references/sharedarraybuffer-gallery-view.md new file mode 100644 index 00000000..5063ba4d --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/references/sharedarraybuffer-gallery-view.md @@ -0,0 +1,32 @@ +--- +title: "Web: SharedArrayBuffer and Gallery View" +--- + +# Web: SharedArrayBuffer and Gallery View + +Many Meeting SDK Web issues that mention gallery view or performance are caused by missing cross-origin isolation. + +## What to Ask + +- Are they using **Client View** (CDN) or **Component View** (npm)? +- Is the app served over **HTTPS**? +- Does the browser report `crossOriginIsolated === true`? + +## Symptoms + +- “SharedArrayBuffer is not defined” +- “Your browser doesn’t support gallery view” +- performance degradation on certain machines/browsers + +## What to Do + +1. Serve the app with the required COOP/COEP headers so `crossOriginIsolated` can become true. +2. Avoid embedding the app in contexts that break isolation (some iframes/proxies). +3. If the environment cannot be isolated (enterprise proxy, incompatible embeds), treat gallery view / HD features as best-effort and fall back gracefully. + +## Debug Checklist + +- In DevTools console: + - `window.crossOriginIsolated` should be `true` for SAB-dependent features. +- Verify required headers are present on **all** relevant responses (HTML + JS + WASM). + diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/references/web-performance-cpu.md b/plugins/zoom-developers/skills/meeting-sdk/web/references/web-performance-cpu.md new file mode 100644 index 00000000..276fec9c --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/references/web-performance-cpu.md @@ -0,0 +1,26 @@ +--- +title: "Web: Performance and CPU (Meeting SDK)" +--- + +# Web: Performance and CPU (Meeting SDK) + +Common “meeting web” questions boil down to resource usage and rendering constraints. + +## Clarify the Integration + +- Client View vs Component View +- Expected participant count and whether gallery view is required +- Target devices (low-end laptops, thin clients, mobile browsers) + +## General Levers + +- Prefer realistic expectations: browsers + multi-video rendering are CPU-heavy. +- Avoid extra DOM/layout work around the meeting container. +- Ensure SharedArrayBuffer / cross-origin isolation is configured when required (see `sharedarraybuffer-gallery-view.md`). + +## Practical Checks + +- Confirm the meeting container is not constantly re-rendering (React state loops around the Zoom root). +- Check for global CSS resets that impact layout and cause expensive reflows. +- On constrained machines, reduce unnecessary UI overlays and avoid heavy background effects. + diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/references/web-timeout-browser-restriction.md b/plugins/zoom-developers/skills/meeting-sdk/web/references/web-timeout-browser-restriction.md new file mode 100644 index 00000000..ac530491 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/references/web-timeout-browser-restriction.md @@ -0,0 +1,33 @@ +--- +title: "Web: Joining Meeting Timeout / Browser Restriction" +--- + +# Web: Joining Meeting Timeout / Browser Restriction + +Forum threads often report errors like: + +- “Joining meeting timeout” +- “Your network connection has timed out” +- “Your organization has disabled access to Zoom from the browser” + +## What to Ask (Quick) + +- Browser + OS +- Are they behind a corporate proxy/VPN/firewall? +- Does the issue reproduce on a clean network (mobile hotspot)? +- Are any ad blockers/privacy tools enabled? +- Client View (CDN `ZoomMtg`) vs Component View (npm `ZoomMtgEmbedded`) + +## Common Root Causes + +- Network blocks: WebSocket / media / Zoom domains blocked by org policy +- CSP/proxy rewriting that breaks SDK resources +- Mixed content / non-HTTPS in dev environments + +## Practical Debug Steps + +1. Try from a different network (hotspot) to isolate policy blocks quickly. +2. Disable ad blockers/privacy extensions for the site. +3. Verify the page and SDK assets load without 4xx/5xx in DevTools Network tab. +4. If in enterprise environment: ask for the organization’s allowlist policy for Zoom web traffic. + diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/references/web-tracking-id.md b/plugins/zoom-developers/skills/meeting-sdk/web/references/web-tracking-id.md new file mode 100644 index 00000000..66ece08f --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/references/web-tracking-id.md @@ -0,0 +1,57 @@ +# Meeting SDK Web - Tracking ID + +Use tracking IDs to identify users across sessions. + +## Overview + +Tracking IDs allow you to identify users joining meetings from your application for analytics and tracking purposes. + +## Setting Tracking ID + +### Component View + +```javascript +await client.join({ + sdkKey: SDK_KEY, + signature: signature, + meetingNumber: meetingNumber, + userName: 'User Name', + trackingId: 'your-tracking-id-here' +}); +``` + +### Client View + +```javascript +ZoomMtg.join({ + sdkKey: SDK_KEY, + signature: signature, + meetingNumber: meetingNumber, + userName: 'User Name', + trackingId: 'your-tracking-id-here' +}); +``` + +## Use Cases + +- Analytics and reporting +- User identification across meetings +- Integration with your user database +- Conversion tracking + +## Tracking ID in Reports + +Tracking IDs appear in: +- Meeting participant reports +- Dashboard analytics +- Webhook payloads + +## Best Practices + +1. Use consistent IDs across sessions +2. Don't include sensitive data in tracking IDs +3. Document your tracking ID schema + +## Resources + +- **Meeting SDK Web docs**: https://developers.zoom.us/docs/meeting-sdk/web/ diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/references/web.md b/plugins/zoom-developers/skills/meeting-sdk/web/references/web.md new file mode 100644 index 00000000..c5751836 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/references/web.md @@ -0,0 +1,309 @@ +# Meeting SDK - Web + +Embed Zoom meetings in web applications. + +## Overview + +The Zoom Meeting SDK for Web embeds the full Zoom meeting experience into your web application with two view options: Component View and Client View. + +## Forum-Derived Hotspots + +- SharedArrayBuffer / gallery view: [sharedarraybuffer-gallery-view.md](sharedarraybuffer-gallery-view.md) +- Joining meeting timeout / browser restriction: [web-timeout-browser-restriction.md](web-timeout-browser-restriction.md) +- Performance/CPU guidance: [web-performance-cpu.md](web-performance-cpu.md) +- Component View UI customization: [component-view-ui-customization.md](component-view-ui-customization.md) +- Component View breakout rooms: [component-view-breakout-rooms.md](component-view-breakout-rooms.md) + +## Prerequisites + +- Meeting SDK credentials from [Marketplace](https://marketplace.zoom.us/) (sign-in required) +- SDK Key and Secret +- Modern browser (Chrome, Firefox, Safari, Edge) + +## View Options + +| View | Description | Use Case | +|------|-------------|----------| +| **Component View** | Flexible, embeddable UI components | Custom layouts, React/Vue integration | +| **Client View** | Full-page Zoom meeting experience | Quick integration, standard Zoom UI | + +## Implementation Approaches + +| Approach | Technology | Port | View | Best For | +|----------|-----------|------|------|----------| +| **Components** | React + TypeScript + Vite | 3000 | Component | Modern, flexible integration | +| **Local** | React + Webpack + NPM | 9999 | Client | Traditional npm-based setup | +| **CDN** | Vanilla JS + CDN | 9999 | Client | Simple, no build tools | + +## Installation + +### Component View (Recommended) + +```bash +npm install @zoom/meetingsdk +``` + +### Client View (CDN) + +```html + + + + + + +``` + +> **Note:** CDN provides `ZoomMtg` (Client View). For `ZoomMtgEmbedded` (Component View), use npm. + +### Auth Endpoint (Required) + +The Meeting SDK requires a signature from an authentication backend: + +```bash +# Clone Zoom's auth endpoint sample +git clone https://github.com/zoom/meetingsdk-auth-endpoint-sample --depth 1 +cd meetingsdk-auth-endpoint-sample +cp .env.example .env +# Edit .env with your SDK credentials +npm install && npm run start +``` + +## Quick Start (Component View) + +```javascript +import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded'; + +const client = ZoomMtgEmbedded.createClient(); + +await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', +}); + +await client.join({ + sdkKey: SDK_KEY, + signature: signature, + meetingNumber: meetingNumber, + userName: 'User', + password: password, +}); +``` + +## WebRTC Optimizations + +Meeting SDK Web uses WebRTC for real-time communication with optimizations for: +- HD video (720p/1080p) +- Low latency audio +- Adaptive bitrate + +### Codec Support +- H.264 for video +- VP8 as fallback +- Opus for audio + +## HD Video + +### Check System Requirements + +```javascript +// Check browser compatibility +const compatibility = client.checkSystemRequirements(); +console.log('Video supported:', compatibility.video); +console.log('Audio supported:', compatibility.audio); +console.log('Screen share supported:', compatibility.screen); +``` + +### Check SharedArrayBuffer (Required for HD) + +```javascript +// SharedArrayBuffer is REQUIRED for 720p, gallery view, virtual background +const sabAvailable = typeof SharedArrayBuffer === 'function'; + +if (!sabAvailable) { + console.warn('HD features require SharedArrayBuffer'); + console.warn('Enable COOP/COEP headers on your server'); +} +``` + +### Enable HD in Init + +```javascript +await client.init({ + zoomAppRoot: document.getElementById('meetingSDKElement'), + language: 'en-US', + + // Enable 720p (default true for SDK >= 2.8.0) + enableHD: true, + + // Enable 1080p for webinar attendees (SDK >= 2.9.0) + enableFullHD: true, +}); +``` + +### HD Requirements + +To enable 720p: +1. Contact Zoom Support to enable on your account +2. Enable "Group HD" in Zoom profile settings +3. SharedArrayBuffer must be available (COOP/COEP headers) +4. Solid internet connection and low CPU usage + +**Resolution tiers:** +- 1:1 calls: Up to 1080p +- Small groups (2-4): Up to 720p +- Larger meetings: Adaptive + +**Key limitation:** If a 3rd participant turns video on, quality reverts to standard definition. + +## SharedArrayBuffer + +For optimal performance, configure these headers: + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +## Event Handling + +### Component View Events + +```javascript +client.on('connection-change', (payload) => { + console.log('Connection state:', payload.state); +}); + +client.on('user-added', (payload) => { + console.log('User joined:', payload); +}); + +client.on('user-removed', (payload) => { + console.log('User left:', payload); +}); +``` + +### Client View In-Meeting Listeners + +```javascript +// User events +ZoomMtg.inMeetingServiceListener('onUserJoin', (data) => { + console.log('User joined:', data); +}); + +ZoomMtg.inMeetingServiceListener('onUserLeave', (data) => { + console.log('User left:', data); +}); + +// Waiting room +ZoomMtg.inMeetingServiceListener('onUserIsInWaitingRoom', (data) => { + console.log('User in waiting room:', data); +}); + +// Meeting status changes +ZoomMtg.inMeetingServiceListener('onMeetingStatus', (data) => { + console.log('Meeting status:', data); +}); +``` + +## Common Tasks + +### Join Meeting +```javascript +await client.join({ + sdkKey: SDK_KEY, + signature: signature, + meetingNumber: meetingNumber, + userName: 'User', +}); +``` + +### Leave Meeting +```javascript +client.leaveMeeting(); +``` + +## Client View Full Example + +```javascript +import { ZoomMtg } from '@zoom/meetingsdk'; + +// Check system requirements +console.log(ZoomMtg.checkSystemRequirements()); + +// Preload WebAssembly for faster init +ZoomMtg.preLoadWasm(); +ZoomMtg.prepareWebSDK(); + +// Initialize and join +ZoomMtg.init({ + leaveUrl: '/meeting-ended', + disableCORP: !window.crossOriginIsolated, // Disable if no COOP/COEP + success: () => { + ZoomMtg.join({ + meetingNumber: '123456789', + userName: 'User Name', + signature: signature, // From auth endpoint + userEmail: 'user@example.com', + passWord: 'meeting-password', + success: (res) => { + console.log('Joined meeting'); + ZoomMtg.getAttendeeslist({}); + ZoomMtg.getCurrentUser({ + success: (res) => console.log('Current user:', res.result.currentUser) + }); + }, + error: (err) => console.error('Join error:', err) + }); + }, + error: (err) => console.error('Init error:', err) +}); +``` + +## China CDN + +For users in China, use the China-specific CDN: + +```javascript +// Set before preLoadWasm() +ZoomMtg.setZoomJSLib('https://jssdk.zoomus.cn/{VERSION}/lib', '/av'); +``` + +## Zoom for Government (ZFG) + +For government applications, apply for SDK credentials at [ZFG Marketplace](https://marketplace.zoomgov.com/). + +### Option 1: ZFG-specific NPM Package + +```json +{ + "dependencies": { + "@zoom/meetingsdk": "3.11.2-zfg" + } +} +``` + +### Option 2: Configure ZFG Endpoints + +**Client View:** +```javascript +ZoomMtg.setZoomJSLib('https://source.zoomgov.com/{VERSION}/lib', '/av'); +ZoomMtg.init({ + webEndpoint: 'www.zoomgov.com', +}); +``` + +**Component View:** +```javascript +const client = ZoomMtgEmbedded.createClient(); +client.init({ + assetPath: 'https://source.zoomgov.com/{VERSION}/lib/av', + webEndpoint: 'www.zoomgov.com' +}); +``` + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/web/ +- **Component View sample**: https://github.com/zoom/meetingsdk-web-sample +- **Client View sample**: https://github.com/zoom/meetingsdk-sample-signature-node.js diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/meeting-sdk/web/troubleshooting/common-issues.md new file mode 100644 index 00000000..222f3eec --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/troubleshooting/common-issues.md @@ -0,0 +1,471 @@ +# Common Issues - Zoom Meeting SDK Web + +Quick diagnostics and solutions for the most common issues. + +## Quick Diagnostic Workflow + +``` +1. Check browser console for errors +2. Verify signature is valid and not expired +3. Check COOP/COEP headers for HD features +4. Verify SDK version is supported +5. Test in Chrome/Edge first (most compatible) +``` + +## Initialization Issues + +### "Meeting not initialized" (Error 2) + +**Symptom**: SDK throws error when trying to join. + +**Cause**: `join()` called before `init()` completed. + +**Solution**: +```javascript +// WRONG +ZoomMtg.init({ leaveUrl: '...' }); +ZoomMtg.join({ ... }); // Too early! + +// CORRECT +ZoomMtg.init({ + leaveUrl: '...', + success: () => { + ZoomMtg.join({ ... }); // Wait for success callback + } +}); +``` + +### SDK Not Loading (CDN) + +**Symptom**: `ZoomMtg is not defined` + +**Cause**: Scripts not loaded in correct order. + +**Solution**: +```html + + + + + + + +``` + +### Language Loading Timeout + +**Symptom**: SDK hangs or UI shows wrong language. + +**Cause**: `init()` called before language loaded. + +**Solution**: +```javascript +ZoomMtg.i18n.load('en-US'); +ZoomMtg.i18n.onLoad(() => { + // ONLY init after language is loaded + ZoomMtg.init({ ... }); +}); +``` + +## Authentication Issues + +### "Signature is invalid" (Error 3712) + +**Symptom**: Join fails with signature error. + +**Causes & Solutions**: + +1. **Wrong SDK Secret** + ```bash + # Verify in Zoom Marketplace > App > App Credentials + ``` + +2. **Signature expired** + ```javascript + // Check signature expiration (default 2 hours) + // Regenerate signature if needed + ``` + +3. **Missing appKey prefix (v5.0.0+)** + ```javascript + // WRONG (pre-5.0 format) + signature: "eyJhbGc..." + + // CORRECT (5.0+ format) + signature: "appKey:sdkKey.eyJhbGc..." + ``` + +4. **Wrong algorithm** + ```javascript + // MUST use HS256 + jwt.sign(payload, secret, { algorithm: 'HS256' }); + ``` + +### "API Key is invalid" (Error 3704) + +**Symptom**: SDK Key rejected. + +**Causes**: +1. Typo in SDK Key +2. SDK Key from different app type +3. SDK Key not activated + +**Solution**: Verify SDK Key in Zoom Marketplace matches exactly. + +### "SDK Key is disabled" (Error 3710) + +**Symptom**: Previously working key now fails. + +**Cause**: App deactivated in Marketplace. + +**Solution**: +1. Go to Zoom Marketplace > Manage > Your Apps +2. Re-enable or create new app + +## Join Issues + +### "passWord" vs "password" Typo + +**Symptom**: Join fails with password error even with correct password. + +**Cause**: Different spelling between views! + +**Solution**: +```javascript +// Client View - capital W +ZoomMtg.join({ + passWord: 'meeting123', // Capital W! +}); + +// Component View - lowercase +client.join({ + password: 'meeting123', // lowercase! +}); +``` + +### "Meeting does not exist" (Error 3001/3610) + +**Symptom**: Valid meeting number rejected. + +**Causes & Solutions**: + +1. **Wrong meeting number** + - Check for typos + - Use the 9-11 digit number, not Meeting ID from API + +2. **Meeting deleted or expired** + - Create new meeting + +3. **Meeting not started yet** + - Wait for host or enable "join before host" + +### "Wrong meeting password" (Error 3004) + +**Symptom**: Correct password rejected. + +**Causes**: +1. Space/encoding issues in password +2. Password changed after you got it +3. Using URL-encoded password directly + +**Solution**: +```javascript +// Extract password correctly from invite link +const url = new URL(inviteLink); +const password = url.searchParams.get('pwd'); +``` + +### "Another meeting running" (Error 3005) + +**Symptom**: Can't join new meeting. + +**Cause**: User already in another SDK meeting instance. + +**Solution**: +```javascript +// Leave current meeting first +ZoomMtg.leaveMeeting({}); +// Then join new meeting +``` + +## HD Video Issues + +### No HD Video / Low Quality + +**Symptom**: Video stuck at low resolution. + +**Cause**: SharedArrayBuffer not available. + +**Diagnostic**: +```javascript +console.log('Cross-origin isolated:', window.crossOriginIsolated); +console.log('SharedArrayBuffer:', typeof SharedArrayBuffer === 'function'); +``` + +**Solution**: Add COOP/COEP headers: +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +See [concepts/sharedarraybuffer.md](../concepts/sharedarraybuffer.md) for details. + +### Virtual Background Not Working + +**Symptom**: Virtual background option missing or grayed out. + +**Causes**: +1. SharedArrayBuffer not available +2. Browser not supported (Safari, iOS, Android) +3. Hardware limitations + +**Solution**: +```javascript +// Check support first +ZoomMtg.isSupportVirtualBackground({ + success: (data) => { + if (data.result.isSupport) { + // VB supported + } else { + console.log('VB not supported:', data.result.reason); + } + } +}); +``` + +## Event Listener Issues + +### Callbacks Not Firing + +**Symptom**: `inMeetingServiceListener` events never trigger. + +**Causes & Solutions**: + +1. **Registered too late** + ```javascript + // Register BEFORE or AFTER init, but make sure SDK is ready + ZoomMtg.inMeetingServiceListener('onUserJoin', callback); + ``` + +2. **Wrong event name** + ```javascript + // Event names are case-sensitive + 'onUserJoin' // Correct + 'OnUserJoin' // Wrong + 'on-user-join' // Wrong + ``` + +3. **Meeting not fully joined** + ```javascript + // Wait for join success before expecting events + ZoomMtg.join({ + success: () => { + // Now events will fire + } + }); + ``` + +### Component View Events Not Firing + +**Symptom**: `client.on()` callbacks never trigger. + +**Solution**: +```javascript +// Component View uses different event names +client.on('connection-change', callback); // Not 'onMeetingStatus' +client.on('user-added', callback); // Not 'onUserJoin' +client.on('user-removed', callback); // Not 'onUserLeave' +``` + +## Browser-Specific Issues + +### Safari Screen Share Not Working + +**Symptom**: Screen share option missing on Safari. + +**Cause**: Requires Safari 17+ with macOS Sonoma for Client View. + +**Solution**: +- Use Component View (works with earlier Safari) +- Or instruct users to use Chrome/Edge + +### Firefox WebRTC Issues + +**Symptom**: Video issues on Firefox. + +**Cause**: Firefox uses different WebRTC implementation. + +**Solution**: Test in Chrome first, then adapt for Firefox. + +### Mobile Browser Limitations + +**Symptom**: Features missing on mobile. + +**Reality**: These features are NOT supported on mobile browsers: +- Screen share (send) +- Virtual backgrounds +- Whiteboard editing +- Remote control + +**Solution**: Detect mobile and adjust UI accordingly: +```javascript +const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); +if (isMobile) { + // Hide unsupported feature buttons +} +``` + +## CORS Issues + +### "Blocked by CORS policy" + +**Symptom**: SDK resources blocked. + +**Solution 1**: Use helper.html +```javascript +ZoomMtg.init({ + helper: './helper.html', + // ... +}); +``` + +**Solution 2**: Configure CSP headers +``` +Content-Security-Policy: + script-src 'self' 'unsafe-inline' 'unsafe-eval' https://zoom.us *.zoom.us blob:; + connect-src 'self' https://zoom.us https://*.zoom.us wss://*.zoom.us; +``` + +### WebAssembly CORS Error + +**Symptom**: "Failed to load WebAssembly module" + +**Cause**: WASM files blocked by CSP. + +**Solution**: Add `wasm-unsafe-eval` or `unsafe-eval` to script-src: +``` +script-src 'self' 'wasm-unsafe-eval' ... +``` + +## React Integration Issues + +### Client Recreated on Every Render + +**Symptom**: Multiple SDK instances, memory leaks. + +**Cause**: `createClient()` in component body. + +**Solution**: +```javascript +// WRONG +function App() { + const client = ZoomMtgEmbedded.createClient(); // Created every render! +} + +// CORRECT +function App() { + const clientRef = useRef(null); + + useEffect(() => { + if (!clientRef.current) { + clientRef.current = ZoomMtgEmbedded.createClient(); + } + }, []); +} +``` + +### "Cannot read property of null" on Container + +**Symptom**: Error when initializing Component View. + +**Cause**: Container element not ready. + +**Solution**: +```javascript +// WRONG +await client.init({ + zoomAppRoot: document.getElementById('meeting'), // Might be null +}); + +// CORRECT (React) +const containerRef = useRef(null); + +useEffect(() => { + if (containerRef.current) { + client.init({ zoomAppRoot: containerRef.current }); + } +}, []); + +return
; +``` + +## Performance Issues + +### Slow Join Time + +**Causes & Solutions**: + +1. **Not preloading WASM** + ```javascript + // Call early, before user clicks join + ZoomMtg.preLoadWasm(); + ZoomMtg.prepareWebSDK(); + ``` + +2. **Network latency** + - Use China CDN for China users + - Self-host assets with `assetPath` + +3. **Large bundle** + - Use code splitting + - Lazy load SDK + +### Memory Leaks + +**Symptom**: Browser memory grows over time. + +**Causes**: +1. Multiple SDK instances +2. Not cleaning up on unmount +3. Event listeners not removed + +**Solution**: +```javascript +ZoomMtg.init({ + leaveOnPageUnload: true, // Auto cleanup +}); +``` + +## Debug Mode + +### Enable Debug Logging + +**Client View**: +```javascript +ZoomMtg.init({ + debug: true, // Logs to console +}); +``` + +**Component View**: +```javascript +client.init({ + debug: true, +}); +``` + +### Mobile Debugging + +```javascript +// Use vConsole for mobile debugging +if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) { + const vConsole = new VConsole(); +} +``` + +## Getting Help + +1. **Check error codes**: [troubleshooting/error-codes.md](error-codes.md) +2. **Official docs**: https://developers.zoom.us/docs/meeting-sdk/web/ +3. **Developer forum**: https://devforum.zoom.us/ +4. **GitHub issues**: https://github.com/zoom/meetingsdk-web-sample/issues diff --git a/plugins/zoom-developers/skills/meeting-sdk/web/troubleshooting/error-codes.md b/plugins/zoom-developers/skills/meeting-sdk/web/troubleshooting/error-codes.md new file mode 100644 index 00000000..7837eb6c --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/web/troubleshooting/error-codes.md @@ -0,0 +1,275 @@ +# Zoom Meeting SDK Web - Error Codes + +Comprehensive reference for all error codes returned by the Zoom Meeting SDK for Web. + +## Quick Lookup + +| Code Range | Category | +|------------|----------| +| 0-2 | General/Success | +| 3000-3999 | Meeting Validation | +| 4000-4999 | Connection Status | +| 6000+ | System/Service | +| 10000+ | SDK Version | +| 13000+ | Simulive | + +## General Errors (0-2) + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `0` | SUCCESS | Function invoked successfully | N/A | +| `1` | FAIL | General function error | Check parameters and SDK state | +| `2` | MEETING_NOT_INIT | Meeting not initialized | Call `init()` before `join()` | + +## Meeting Validation Errors (3000-3999) + +### Authentication Errors + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `3704` | API_KEY_INVALID | SDK Key/Client ID is invalid | Verify SDK credentials in Marketplace | +| `3705` | SIGNATURE_EXPIRED | JWT signature has expired | Generate new signature with valid `exp` | +| `3708` | ROLE_ERROR | Incorrect role in signature | Use role 0 (participant) or 1 (host) | +| `3710` | API_KEY_DISABLED | SDK Key is deactivated | Re-enable in Marketplace or create new app | +| `3712` | SIGNATURE_INVALID | Signature verification failed | Check SDK secret, verify signature generation | +| `3265` | TOKEN_ERROR | Token validation failed | Check ZAK/OBF token format and expiry | +| `3623` | TOKEN_ERROR_ALT | Token error (alternate) | Same as 3265 | +| `3713` | NO_PERMISSION | Insufficient permissions | Verify account permissions and scopes | + +### Meeting Errors + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `3001` | ERROR_NOT_EXIST | Meeting does not exist | Verify meeting number | +| `3003` | ERROR_NOT_HOST | Not meeting host | Use host's ZAK token to start | +| `3004` | WRONG_MEETING_PASSWORD | Incorrect password | Verify `passWord` (Client View) or `password` (Component View) | +| `3005` | ANOTHER_MEETING_RUNNING | Already in another meeting | Leave current meeting first | +| `3008` | MEETING_NOT_START | Meeting hasn't started | Wait for host or use "join before host" | +| `3009` | BE_REMOVED | User was removed from meeting | Cannot rejoin; contact host | +| `3610` | MEETING_NOT_EXIST_ALT | Meeting does not exist (alt) | Same as 3001 | + +### Registration & Login + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `3000` | EMAIL_REQUIRED | Email required for webinar | Provide `userEmail` in join params | +| `3099` | REGISTRATION_REQUIRED | Meeting requires registration | Get `tk` token from registration API | +| `3100` | LOGIN_REQUIRED | Zoom login required | Provide ZAK token for authenticated join | +| `3624` | HOST_EMAIL_REQUIRED | Host/alt host needed for webinar | Use host credentials to start | + +### Host Errors + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `3625` | HOST_INACTIVE | Meeting host is inactive | Contact host to activate account | +| `3702` | HOST_NOT_FOUND | Host does not exist | Verify host account | +| `3709` | HOST_NOT_FOUND | Host not found (alt) | Same as 3702 | +| `3711` | CANT_HOST_CONCURRENT | Can't host multiple meetings | End other meeting first | + +### Platform Restrictions + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `3603` | NOT_SUPPORT_WEBCLIENT | Web join not allowed | Admin must enable web client | +| `3608` | TSP_NOT_SUPPORT | TSP audio not supported on web | Use computer audio or phone | +| `3611` | USE_DESKTOP_OR_MOBILE | Browser join disabled | Use Zoom desktop/mobile app | +| `3620` | EMAIL_BLOCKED | Email blocked by admin | Contact account administrator | +| `3621` | NO_RESPONSE_FROM_WEB | Server timeout | Retry request | + +## Connection Errors (4000-4999) + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `4000` | RE_CONNECTING | Reconnecting to meeting | Wait for reconnection | +| `4001` | DISCONNECT | Disconnected from meeting | Check network, try rejoining | +| `4003` | INVALID_PARAMETER | Invalid join parameter | Check all required fields | +| `4004` | MEETING_ENDED | Meeting has ended | Cannot join ended meeting | +| `4005` | MEETING_CAPACITY_REACHED | Meeting is full | Host needs to increase capacity | +| `4006` | MEETING_LOCKED | Meeting is locked | Host must unlock to allow joins | +| `4007` | REJECT_BARRIERS | Information barriers rejection | Contact admin about policies | +| `4008` | PARTICIPANT_EXIST | Already a participant | Already in meeting or leave first | +| `4009` | SERVER_ERROR | Internal server error | Retry request | +| `4011` | NOT_ALLOW_CROSS_JOIN | Cross-account join blocked | Publish app on Marketplace (see 6.1 ToS) | + +## OBF/Anonymous Join Errors (March 2026+) + +> **IMPORTANT**: Starting **March 2, 2026**, anonymous joins to external meetings are blocked. You must provide a valid OBF or ZAK token. + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `4012` | NOT_ALLOW_ANONYMOUS_JOIN | Anonymous join not allowed | Provide valid OBF or ZAK token | +| `4013` | USER_LEVEL_TOKEN_NOT_HAVE_HOST_ZAK_OBF | OBF/ZAK token invalid or missing | Verify token is not expired or malformed | + +### 4012 Error Response + +```json +{ + "meetingStatus": 3, + "errorCode": 4012, + "errorMessage": "Anonymous joins are not allowed for this SDK app. Authenticate the Zoom user and provide a ZAK or OBF token." +} +``` + +**Solutions for 4012:** +1. Generate OBF token via Zoom API +2. Or get user's ZAK token via `/users/me/zak` +3. Pass token in `obfToken` or `zak` join parameter + +### 4013 Error Response + +```json +{ + "meetingStatus": 3, + "errorCode": 4013, + "errorMessage": "The OBF or ZAK token is not provided or invalid. Make sure it's not expired or malformed." +} +``` + +**Solutions for 4013:** +1. Check token expiration (OBF tokens expire) +2. Verify token format is correct +3. Regenerate token if expired + +## System Errors (6000+) + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `6603` | BLOCKED_BY_HOST_ADMIN | SDK Key blocked by host's admin | Contact host's admin to whitelist | + +## SDK Version Errors (10000+) + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `10000` | SDK_VERSION_UNSUPPORTED | SDK version no longer supported | Upgrade to latest SDK version | + +## Simulive Errors (13000+) + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| `13208` | UNABLE_JOIN_ENDED_SIMULIVE | Simulive webinar has ended | Cannot join ended simulive | + +## Error Handling Patterns + +### Client View + +```javascript +ZoomMtg.join({ + // ... options + success: (res) => { + console.log('Joined successfully'); + }, + error: (err) => { + console.error('Join failed:', err); + + switch (err.errorCode) { + case 3004: + alert('Incorrect meeting password'); + break; + case 3712: + console.error('Signature invalid - check SDK secret'); + break; + case 4012: + console.error('OBF token required for external meetings'); + break; + default: + console.error(`Error ${err.errorCode}: ${err.errorMessage}`); + } + } +}); +``` + +### Component View + +```javascript +try { + await client.join({ + // ... options + }); +} catch (error) { + console.error('Join failed:', error); + + // error.reason contains error code + // error.message contains description + + if (error.reason === 'WRONG_MEETING_PASSWORD') { + alert('Incorrect password'); + } +} +``` + +### Listen for Connection Changes + +```javascript +// Client View +ZoomMtg.inMeetingServiceListener('onMeetingStatus', (data) => { + // status: 1=connecting, 2=connected, 3=disconnected, 4=reconnecting + if (data.status === 3) { + console.error('Disconnected:', data.errorCode); + } +}); + +// Component View +client.on('connection-change', (payload) => { + if (payload.state === 'Closed') { + console.error('Connection closed:', payload.reason); + } +}); +``` + +## Common Error Scenarios + +### "Signature is invalid" (3712) + +**Causes:** +1. SDK Secret doesn't match SDK Key +2. Signature generated with wrong algorithm (must be HS256) +3. Clock skew between server and Zoom +4. Missing or incorrect `appKey` field in signature + +**Debug Steps:** +1. Verify SDK Key and Secret in Marketplace +2. Check signature generation code +3. Ensure server time is accurate (NTP sync) +4. Test with official [auth-endpoint-sample](https://github.com/zoom/meetingsdk-auth-endpoint-sample) + +### "Meeting does not exist" (3001/3610) + +**Causes:** +1. Typo in meeting number +2. Meeting was deleted +3. Meeting ID vs Meeting Number confusion + +**Note:** Meeting ID (from API) and Meeting Number (shown in client) are the same. + +### "Anonymous join not allowed" (4012) + +**Causes:** +1. Joining meeting outside your account without authorization +2. No OBF or ZAK token provided + +**Solutions:** +1. For bots: Use App Privilege Token (OBF) +2. For users: Get their ZAK token +3. For same-account meetings: No token needed + +### "Cross-account join blocked" (4011) + +**Cause:** Your app isn't published on Marketplace and is trying to join meetings outside your account. + +**Solutions:** +1. Publish your app on Zoom Marketplace +2. Or only join meetings within your account +3. Or use OBF token for authorization + +## OBF Token Timeline + +| Date | Enforcement | +|------|-------------| +| **February 7, 2026** | If OBF provided, must be valid (not expired/malformed) | +| **March 2, 2026** | Cannot join external meetings without valid OBF/ZAK | + +## Additional Resources + +- [OBF FAQ](https://developers.zoom.us/docs/meeting-sdk/obf-faq/) +- [Signature Generation](https://developers.zoom.us/docs/meeting-sdk/auth/) +- [ZAK Token API](https://developers.zoom.us/docs/api/users/#tag/users/get/users/me/zak) diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/RUNBOOK.md b/plugins/zoom-developers/skills/meeting-sdk/windows/RUNBOOK.md new file mode 100644 index 00000000..af265e53 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/RUNBOOK.md @@ -0,0 +1,64 @@ +# Meeting SDK Windows 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 Meeting SDK embed path for Windows (not REST `join_url` only). +- Choose default/full UI first, then move to custom UI after stable join/start. +- Wrapper platforms (Web/React Native/Electron) require extra runtime and bridge checks. + +## 2) Confirm Required Credentials + +- Meeting SDK app credentials (Client ID/Secret). +- Backend-generated Meeting SDK signature/JWT. +- Meeting identifiers (`meetingNumber`, password) and ZAK for host start flows when needed. + +## 3) Confirm Lifecycle Order + +1. Initialize SDK and register event handlers. +2. Authenticate SDK session/token. +3. Join or start meeting/webinar with role-appropriate credentials. +4. Handle in-meeting events and network/media state updates. + +## 4) Confirm Event/State Handling + +- Correlate meeting/session state changes with participant identity and role. +- Handle reconnect/waiting-room transitions explicitly. +- Keep callback/promise/event handlers idempotent to avoid duplicate actions. + +## 5) Confirm Cleanup + Upgrade Posture + +- Leave meeting and release SDK resources cleanly. +- Remove listeners/subscriptions during component/app teardown. +- Re-check quarterly version enforcement windows before release updates. + +## 6) Quick Probes + +- Init/auth succeeds before join/start attempt. +- Join/start flow completes once on target platform without stale state. +- Core media controls (audio/video/share) respond to expected events. + +## 7) Fast Decision Tree + +- 401/signature errors -> backend signature claims/time skew/app credentials mismatch. +- UI loads but cannot join -> wrong role/ZAK/password field or invalid meeting data. +- Random event behavior -> listeners attached multiple times or detached too early. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/meeting-sdk/windows/ +- https://marketplacefront.zoom.us/sdk/meeting/windows/annotated.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/meeting-sdk/windows/` +- `raw-docs/marketplacefront.zoom.us/sdk/meeting-sdk/windows/` diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/SKILL.md b/plugins/zoom-developers/skills/meeting-sdk/windows/SKILL.md new file mode 100644 index 00000000..32a2ed24 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/SKILL.md @@ -0,0 +1,1186 @@ +--- +name: zoom-meeting-sdk-windows +description: | + Zoom Meeting SDK for Windows - Native C++ SDK for embedding Zoom meetings into Windows desktop + applications. Supports custom UI architecture with raw video/audio data, headless bots, and deep + integration with meeting features. Includes SDK architecture patterns and Windows message loop handling. +--- + +# Zoom Meeting SDK (Windows) + +Embed Zoom meeting capabilities into Windows desktop applications for native C++ integrations and headless bots. + +## New to Zoom SDK? Start Here! + +**The fastest way to master the SDK:** + +1. **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - Learn the universal pattern that works for ALL 35+ features +2. **[Authentication Pattern](examples/authentication-pattern.md)** - Get a working bot joining meetings +3. **[Windows Message Loop](troubleshooting/windows-message-loop.md)** - Fix the #1 reason callbacks don't fire + +**Building a Custom UI?** +- [Custom UI Architecture](concepts/custom-ui-architecture.md) - How SDK rendering actually works (child HWNDs, D3D, etc.) +- [Custom UI Video Rendering Example](examples/custom-ui-video-rendering.md) - Complete working code +- [SDK-Rendered vs Self-Rendered](concepts/custom-ui-vs-raw-data.md) - Choose the right approach +- [Custom UI Interface Methods](references/interface-methods.md) - All 13 required virtual methods + +**Having issues?** +- Build errors → [Build Errors Guide](troubleshooting/build-errors.md) +- Callbacks not firing → [Windows Message Loop](troubleshooting/windows-message-loop.md) +- Quick diagnostics → [Common Issues](troubleshooting/common-issues.md) +- Performance / service quality → [service-quality.md](examples/service-quality.md) +- Deployment notes → [deployment.md](references/deployment.md) +- MSBuild from git bash → [Build Errors Guide](troubleshooting/build-errors.md#msbuild-command-pattern) +- Complete navigation → [SKILL.md](SKILL.md) + +## Prerequisites + +- Zoom app with Meeting SDK credentials (Client ID & Secret) +- Visual Studio 2019/2022 or later +- Windows 10 or later +- C++ development environment +- vcpkg for dependency management + +> **Need help with authentication?** See the **[zoom-oauth](../../oauth/SKILL.md)** skill for JWT token generation. + +## Project Preferences & Learnings + +> **IMPORTANT**: These are hard-won preferences from real project experience. Follow these when creating new projects. + +### Do NOT use CMake — Use native Visual Studio `.vcxproj` + +**Always create a native Visual Studio `.sln` + `.vcxproj` project**, not a CMake project. Reasons: +- More standard and familiar for Windows C++ developers +- Developers can double-click the `.sln` to open in Visual Studio immediately +- Project settings (include dirs, lib dirs, preprocessor defines) are easier to see and edit in the VS Property Pages UI +- No extra CMake tooling or configuration step required +- Friendlier and easier for developers to understand and maintain + +### `config.json` must be visible in Solution Explorer + +The `config.json` file (containing `sdk_jwt`, `meeting_number`, `passcode`) must be: +1. **Included in the `.vcxproj`** as a `` item with `PreserveNewest` +2. **Placed in a "Config" filter** in the `.vcxproj.filters` file so it appears under a "Config" folder in Solution Explorer +3. **Easily editable** by developers directly from Solution Explorer — they should never have to hunt for it in File Explorer + +Example `.vcxproj` entry: +```xml + + + PreserveNewest + + +``` + +Example `.vcxproj.filters` entry: +```xml + + + {GUID-HERE} + + + + + Config + + +``` + +## Overview + +The Windows SDK is a **C++ native SDK** designed for: +- **Desktop applications** - Native Windows apps with full UI control +- **Headless bots** - Join meetings without UI +- **Raw media access** - Capture/send audio/video streams +- **Local recording** - Record meetings locally or to cloud + +### Key Architectural Insight + +The SDK follows a **universal 3-step pattern** for every feature: +1. **Get controller** (singleton): `meetingService->Get[Feature]Controller()` +2. **Implement event listener**: `class MyListener : public I[Feature]Event { ... }` +3. **Register and use**: `controller->SetEvent(listener)` then call methods + +**This works for ALL features**: audio, video, chat, recording, participants, screen sharing, breakout rooms, webinars, Q&A, polling, whiteboard, and 20+ more! + +Learn more: **[SDK Architecture Pattern Guide](concepts/sdk-architecture-pattern.md)** + +## Quick Start + +### 1. Download Windows SDK + +Download from [Zoom Marketplace](https://marketplace.zoom.us/): +- Extract `zoom-meeting-sdk-windows_x86_64-{version}.zip` + +### 2. Setup Project Structure + +``` +your-project/ + YourApp/ + SDK/ + x64/ + bin/ # DLL files and dependencies + h/ # Header files + lib/ # sdk.lib + x86/ + bin/ + h/ + lib/ + YourApp.cpp + YourApp.vcxproj + config.json +``` + +Copy SDK files: +```cmd +xcopy /E /I sdk-package\x64 your-project\YourApp\SDK\x64\ +xcopy /E /I sdk-package\x86 your-project\YourApp\SDK\x86\ +``` + +### 3. Install Dependencies (vcpkg) + +```powershell +# Install vcpkg +git clone https://github.com/Microsoft/vcpkg.git C:\vcpkg +cd C:\vcpkg +.\bootstrap-vcpkg.bat +.\vcpkg integrate install + +# Install dependencies +.\vcpkg install jsoncpp:x64-windows +.\vcpkg install curl:x64-windows +``` + +### 4. Configure Visual Studio Project + +**Project Properties → C/C++ → General → Additional Include Directories:** +``` +$(SolutionDir)SDK\$(PlatformTarget)\h +C:\vcpkg\packages\jsoncpp_x64-windows\include +C:\vcpkg\packages\curl_x64-windows\include +``` + +**Project Properties → Linker → General → Additional Library Directories:** +``` +$(SolutionDir)SDK\$(PlatformTarget)\lib +``` + +**Project Properties → Linker → Input → Additional Dependencies:** +``` +sdk.lib +``` + +**Post-Build Event** (Copy DLLs to output): +```cmd +xcopy /Y /D "$(SolutionDir)SDK\$(PlatformTarget)\bin\*.*" "$(OutDir)" +``` + +### 5. Configure Credentials + +Create `config.json`: +```json +{ + "sdk_jwt": "YOUR_JWT_TOKEN", + "meeting_number": "1234567890", + "passcode": "password123", + "zak": "" +} +``` + +### 6. Build & Run + +- Open solution in Visual Studio +- Select x64 or x86 configuration +- Press F5 to build and run + +## Core Workflow + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ InitSDK │───►│ AuthSDK │───►│ JoinMeeting │───►│ Raw Data │ +│ │ │ (JWT) │ │ │ │ Subscribe │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ + │ │ + ▼ ▼ + OnAuthComplete onInMeeting + callback callback +``` + +**⚠️ CRITICAL**: Add Windows message loop or callbacks won't fire! +```cpp +while (!done) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} +``` +See: [Windows Message Loop Guide](troubleshooting/windows-message-loop.md) + +## Code Examples + +> **💡 Pro Tip**: These are minimal examples. For complete, tested code see: +> - [Authentication Pattern](examples/authentication-pattern.md) - Full auth workflow +> - [Raw Video Capture](examples/raw-video-capture.md) - Complete video capture +> - [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Implement any feature + +### Important: Include Order + +**CRITICAL**: Include headers in this exact order or you'll get build errors: + +```cpp +#include // MUST be first +#include // MUST be second (SDK headers use uint32_t) +// ... other standard headers ... +#include +#include // BEFORE participants! +#include +``` + +See: [Build Errors Guide](troubleshooting/build-errors.md) for all dependency fixes. + +### 1. Initialize SDK + +```cpp +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +bool InitMeetingSDK() { + InitParam initParam; + initParam.strWebDomain = L"https://zoom.us"; + initParam.strSupportUrl = L"https://zoom.us"; + initParam.emLanguageID = LANGUAGE_English; + initParam.enableLogByDefault = true; + initParam.enableGenerateDump = true; + + SDKError err = InitSDK(initParam); + if (err != SDKERR_SUCCESS) { + std::wcout << L"InitSDK failed: " << err << std::endl; + return false; + } + return true; +} +``` + +### 2. Authenticate with JWT + +```cpp +#include +#include +#include +#include "AuthServiceEventListener.h" + +IAuthService* authService = nullptr; + +void OnAuthenticationComplete() { + std::cout << "Authentication successful!" << std::endl; + JoinMeeting(); // Proceed to join meeting +} + +bool AuthenticateSDK(const std::wstring& jwtToken) { + CreateAuthService(&authService); + if (!authService) return false; + + // Set event listener BEFORE calling SDKAuth + authService->SetEvent(new AuthServiceEventListener(&OnAuthenticationComplete)); + + // Authenticate with JWT + AuthContext authContext; + authContext.jwt_token = jwtToken.c_str(); + + SDKError err = authService->SDKAuth(authContext); + if (err != SDKERR_SUCCESS) { + std::wcout << L"SDKAuth failed: " << err << std::endl; + return false; + } + + // CRITICAL: Add message loop or callback won't fire! + // See complete example: examples/authentication-pattern.md + + return true; +} +``` + +**AuthServiceEventListener.h:** +```cpp +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class AuthServiceEventListener : public IAuthServiceEvent { +public: + AuthServiceEventListener(void (*onComplete)()) + : onAuthComplete(onComplete) {} + + void onAuthenticationReturn(AuthResult ret) override { + if (ret == AUTHRET_SUCCESS && onAuthComplete) { + onAuthComplete(); + } else { + std::cout << "Auth failed: " << ret << std::endl; + } + } + + // Must implement ALL pure virtual methods (6 total) + void onLoginReturnWithReason(LOGINSTATUS ret, IAccountInfo* info, LoginFailReason reason) override {} + void onLogout() override {} + void onZoomIdentityExpired() override {} + void onZoomAuthIdentityExpired() override {} +#if defined(WIN32) + void onNotificationServiceStatus(SDKNotificationServiceStatus status, SDKNotificationServiceError error) override {} +#endif + +private: + void (*onAuthComplete)(); +}; +``` + +**See complete working code**: [Authentication Pattern Guide](examples/authentication-pattern.md) + +### 3. Join Meeting + +```cpp +#include +#include "MeetingServiceEventListener.h" + +IMeetingService* meetingService = nullptr; + +void OnMeetingJoined() { + std::cout << "Joining meeting..." << std::endl; +} + +void OnInMeeting() { + std::cout << "In meeting now!" << std::endl; + // Start raw data capture here +} + +void OnMeetingEnds() { + std::cout << "Meeting ended" << std::endl; +} + +bool JoinMeeting(UINT64 meetingNumber, const std::wstring& password) { + CreateMeetingService(&meetingService); + if (!meetingService) return false; + + // Set event listener + meetingService->SetEvent( + new MeetingServiceEventListener(&OnMeetingJoined, &OnMeetingEnds, &OnInMeeting) + ); + + // Prepare join parameters + JoinParam joinParam; + joinParam.userType = SDK_UT_WITHOUT_LOGIN; + + JoinParam4WithoutLogin& params = joinParam.param.withoutloginuserJoin; + params.meetingNumber = meetingNumber; + params.userName = L"Bot User"; + params.psw = password.c_str(); + params.isVideoOff = false; + params.isAudioOff = false; + + SDKError err = meetingService->Join(joinParam); + if (err != SDKERR_SUCCESS) { + std::wcout << L"Join failed: " << err << std::endl; + return false; + } + return true; +} +``` + +**MeetingServiceEventListener.h:** +```cpp +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class MeetingServiceEventListener : public IMeetingServiceEvent { +public: + MeetingServiceEventListener( + void (*onJoined)(), + void (*onEnded)(), + void (*onInMeeting)() + ) : onMeetingJoined(onJoined), + onMeetingEnded(onEnded), + onInMeetingCallback(onInMeeting) {} + + void onMeetingStatusChanged(MeetingStatus status, int iResult) override { + if (status == MEETING_STATUS_CONNECTING) { + if (onMeetingJoined) onMeetingJoined(); + } + else if (status == MEETING_STATUS_INMEETING) { + if (onInMeetingCallback) onInMeetingCallback(); + } + else if (status == MEETING_STATUS_ENDED) { + if (onMeetingEnded) onMeetingEnded(); + } + } + + // Must implement ALL pure virtual methods (9 total) + void onMeetingStatisticsWarningNotification(StatisticsWarningType type) override {} + void onMeetingParameterNotification(const MeetingParameter* param) override {} + void onSuspendParticipantsActivities() override {} + void onAICompanionActiveChangeNotice(bool isActive) override {} + void onMeetingTopicChanged(const zchar_t* sTopic) override {} + void onMeetingFullToWatchLiveStream(const zchar_t* sLiveStreamUrl) override {} + void onUserNetworkStatusChanged(MeetingComponentType type, ConnectionQuality level, unsigned int userId, bool uplink) override {} +#if defined(WIN32) + void onAppSignalPanelUpdated(IMeetingAppSignalHandler* pHandler) override {} +#endif + +private: + void (*onMeetingJoined)(); + void (*onMeetingEnded)(); + void (*onInMeetingCallback)(); +}; +``` + +**See all required methods**: [Interface Methods Guide](references/interface-methods.md) + +### 4. Subscribe to Raw Video + +```cpp +#include +#include +#include +#include +#include // REQUIRED for YUVRawDataI420 +#include "ZoomSDKRendererDelegate.h" + +IZoomSDKRenderer* videoHelper = nullptr; +ZoomSDKRendererDelegate* videoSource = new ZoomSDKRendererDelegate(); + +bool StartVideoCapture(uint32_t userId) { + // STEP 1: Start raw recording FIRST (required!) + IMeetingRecordingController* recordCtrl = + meetingService->GetMeetingRecordingController(); + + SDKError canStart = recordCtrl->CanStartRawRecording(); + if (canStart != SDKERR_SUCCESS) { + std::cout << "Cannot start recording: " << canStart << std::endl; + return false; + } + + recordCtrl->StartRawRecording(); + + // Wait for recording to initialize + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + // STEP 2: Create renderer + SDKError err = createRenderer(&videoHelper, videoSource); + if (err != SDKERR_SUCCESS || !videoHelper) { + std::cout << "createRenderer failed: " << err << std::endl; + return false; + } + + // STEP 3: Set resolution and subscribe + videoHelper->setRawDataResolution(ZoomSDKResolution_720P); + err = videoHelper->subscribe(userId, RAW_DATA_TYPE_VIDEO); + if (err != SDKERR_SUCCESS) { + std::cout << "Subscribe failed: " << err << std::endl; + return false; + } + + std::cout << "Video capture started! Frames arrive in onRawDataFrameReceived()" << std::endl; + return true; +} +``` + +**ZoomSDKRendererDelegate.h:** +```cpp +#include +#include +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class ZoomSDKRendererDelegate : public IZoomSDKRendererDelegate { +public: + void onRawDataFrameReceived(YUVRawDataI420* data) override { + if (!data) return; + + // YUV420 (I420) format: Y plane + U plane + V plane + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Calculate buffer sizes + // Y = full resolution, U/V = quarter resolution each + size_t ySize = width * height; + size_t uvSize = ySize / 4; // (width/2) * (height/2) + + // Total size: width * height * 1.5 bytes + + // Save to file (playback: ffplay -f rawvideo -pixel_format yuv420p -video_size 1280x720 output.yuv) + std::ofstream outputFile("output.yuv", std::ios::binary | std::ios::app); + outputFile.write(data->GetYBuffer(), ySize); // Brightness + outputFile.write(data->GetUBuffer(), uvSize); // Blue-difference + outputFile.write(data->GetVBuffer(), uvSize); // Red-difference + outputFile.close(); + } + + void onRawDataStatusChanged(RawDataStatus status) override { + std::cout << "Raw data status: " << status << std::endl; + } + + void onRendererBeDestroyed() override { + std::cout << "Renderer destroyed" << std::endl; + } +}; +``` + +**Complete video capture guide**: [Raw Video Capture Guide](examples/raw-video-capture.md) + +### 5. Subscribe to Raw Audio + +```cpp +#include + +class ZoomSDKAudioRawDataDelegate : public IZoomSDKAudioRawDataDelegate { +public: + void onMixedAudioRawDataReceived(AudioRawData* data) override { + // Process PCM audio (mixed from all participants) + std::ofstream pcmFile("audio.pcm", std::ios::binary | std::ios::app); + pcmFile.write((char*)data->GetBuffer(), data->GetBufferLen()); + pcmFile.close(); + } + + void onOneWayAudioRawDataReceived(AudioRawData* data, uint32_t node_id) override { + // Process audio from specific participant + } +}; + +// Subscribe to audio +IZoomSDKAudioRawDataHelper* audioHelper = GetAudioRawdataHelper(); +audioHelper->subscribe(new ZoomSDKAudioRawDataDelegate()); +``` + +### 6. Main Message Loop (CRITICAL!) + +**⚠️ WITHOUT THIS, CALLBACKS WON'T FIRE!** + +```cpp +#include +#include +#include + +int main() { + // Initialize COM + CoInitialize(NULL); + + // Load config and initialize + LoadConfig(); + InitMeetingSDK(); + AuthenticateSDK(sdk_jwt); + + // CRITICAL: Windows message loop for SDK callbacks + // SDK uses Windows message pump to dispatch callbacks + // Without this, callbacks are queued but NEVER fire! + MSG msg; + while (!g_exit) { + // Process all pending Windows messages + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + g_exit = true; + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Small sleep to avoid busy-waiting + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // Cleanup + CleanSDK(); + CoUninitialize(); + + return 0; +} +``` + +**Why message loop is critical**: The SDK uses Windows COM/messaging for async callbacks. Without `PeekMessage()`, the SDK queues messages but they're never retrieved/dispatched, so callbacks never execute. + +**Symptoms without message loop**: +- Authentication timeout (even with valid JWT) +- Meeting join timeout +- No callback events fire +- Appears like network/auth issue but it's a message loop issue + +**See detailed explanation**: [Windows Message Loop Guide](troubleshooting/windows-message-loop.md) + +## Common Issues & Solutions + +| Issue | Solution | +|-------|----------| +| **Callbacks don't fire / Auth timeout** | Add Windows message loop → [Guide](troubleshooting/windows-message-loop.md) | +| **`uint32_t` / `AudioType` errors** | Fix include order → [Guide](troubleshooting/build-errors.md) | +| **Abstract class error** | Implement all virtual methods → [Guide](references/interface-methods.md) | +| **How to implement [feature]?** | Follow universal pattern → [Guide](concepts/sdk-architecture-pattern.md) | +| **Authentication fails** | Check JWT token & error codes → [Guide](troubleshooting/common-issues.md) | +| **No video frames received** | Call StartRawRecording() first → [Guide](examples/raw-video-capture.md) | + +**Complete troubleshooting**: [Common Issues Guide](troubleshooting/common-issues.md) + +## How to Implement Any Feature + +The SDK has **35+ feature controllers** (audio, video, chat, recording, participants, screen sharing, breakout rooms, webinars, Q&A, polling, whiteboard, captions, AI companion, etc.). + +**Universal pattern that works for ALL features:** + +1. **Get the controller** (singleton): + ```cpp + IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); + IMeetingChatController* chatCtrl = meetingService->GetMeetingChatController(); + // ... 33 more controllers available + ``` + +2. **Implement event listener** (observer pattern): + ```cpp + class MyAudioListener : public IMeetingAudioCtrlEvent { + void onUserAudioStatusChange(IList* lst) override { + // React to audio events + } + // ... implement all required methods + }; + ``` + +3. **Register and use**: + ```cpp + audioCtrl->SetEvent(new MyAudioListener()); + audioCtrl->MuteAudio(userId, true); // Use feature + ``` + +**Complete guide with examples**: [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +## Available Examples + +| Example | Description | +|---------|-------------| +| **SkeletonDemo** | Minimal join meeting - start here | +| **GetVideoRawData** | Subscribe to raw video streams | +| **GetAudioRawData** | Subscribe to raw audio streams | +| **SendVideoRawData** | Send custom video as virtual camera | +| **SendAudioRawData** | Send custom audio as virtual mic | +| **GetShareRawData** | Capture screen share content | +| **LocalRecording** | Local MP4 recording | +| **ChatDemo** | In-meeting chat functionality | +| **CaptionDemo** | Closed caption/live transcription | +| **BreakoutDemo** | Breakout room management | + +## Detailed References + +### 🎯 Core Concepts (START HERE!) +- **[concepts/sdk-architecture-pattern.md](concepts/sdk-architecture-pattern.md)** - **Universal pattern for implementing ANY feature** - Understanding this unlocks the entire SDK! + +### 📚 Complete Examples +- **[examples/authentication-pattern.md](examples/authentication-pattern.md)** - Complete working authentication with JWT tokens +- **[examples/raw-video-capture.md](examples/raw-video-capture.md)** - YUV420 video capture with detailed format explanation + +### 🔧 Troubleshooting Guides +- **[troubleshooting/windows-message-loop.md](troubleshooting/windows-message-loop.md)** - **Why callbacks don't fire** (MOST CRITICAL!) +- **[troubleshooting/build-errors.md](troubleshooting/build-errors.md)** - SDK header dependency issues and fixes +- **[troubleshooting/common-issues.md](troubleshooting/common-issues.md)** - Quick diagnostic workflow and error code tables + +### 📖 References +- **[references/interface-methods.md](references/interface-methods.md)** - How to implement ALL required virtual methods +- **[references/windows-reference.md](references/windows-reference.md)** - Dependencies, Visual Studio setup +- **[../references/authorization.md](../references/authorization.md)** - SDK JWT generation +- **[../references/bot-authentication.md](../references/bot-authentication.md)** - Bot token types (ZAK, OBF, JWT) + +### 🎨 Feature-Specific Guides +- **[../references/breakout-rooms.md](../references/breakout-rooms.md)** - Programmatic breakout room management +- **[../references/ai-companion.md](../references/ai-companion.md)** - AI Companion controls + +## Sample Repositories + +| Repository | Description | +|------------|-------------| +| [meetingsdk-windows-raw-recording-sample](https://github.com/zoom/meetingsdk-windows-raw-recording-sample) | Official raw data capture samples | +| [meetingsdk-windows-local-recording-sample](https://github.com/zoom/meetingsdk-windows-local-recording-container-sample) | Local recording with Docker | + +## Playing Raw Video/Audio Files + +Raw YUV/PCM files have no headers - you must specify format explicitly. + +### Play Raw YUV Video +```cmd +ffplay -video_size 1280x720 -pixel_format yuv420p -f rawvideo output.yuv +``` + +### Convert YUV to MP4 +```cmd +ffmpeg -video_size 1280x720 -pixel_format yuv420p -f rawvideo -i output.yuv -c:v libx264 output.mp4 +``` + +### Play Raw PCM Audio +```cmd +ffplay -f s16le -ar 32000 -ac 1 audio.pcm +``` + +### Convert PCM to WAV +```cmd +ffmpeg -f s16le -ar 32000 -ac 1 -i audio.pcm output.wav +``` + +### Combine Video + Audio +```cmd +ffmpeg -video_size 1280x720 -pixel_format yuv420p -f rawvideo -i output.yuv ^ + -f s16le -ar 32000 -ac 1 -i audio.pcm ^ + -c:v libx264 -c:a aac -shortest output.mp4 +``` + +**Key flags:** +| Flag | Description | +|------|-------------| +| `-video_size WxH` | Frame dimensions (e.g., 1280x720) | +| `-pixel_format yuv420p` | I420/YUV420 planar format | +| `-f rawvideo` | Raw video input (no container) | +| `-f s16le` | Signed 16-bit little-endian PCM | +| `-ar 32000` | Sample rate (Zoom uses 32kHz) | +| `-ac 1` | Mono (use `-ac 2` for stereo) | + +## Authentication Requirements (2026 Update) + +> **Important**: Beginning **March 2, 2026**, apps joining meetings outside their account must be authorized. + +Use one of: +- **App Privilege Token (OBF)** - Recommended for bots (`app_privilege_token` in JoinParam) +- **ZAK Token** - Zoom Access Key (`userZAK` in JoinParam) +- **On Behalf Token** - For specific use cases (`onBehalfToken` in JoinParam) + +## 📖 Complete Documentation Library + +This skill includes comprehensive guides created from real-world debugging: + +### 🎯 Start Here +- **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - Master document: Universal pattern for ANY feature +- **[SKILL.md](SKILL.md)** - Complete navigation guide + +### 📚 By Category + +**Core Concepts:** +- [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - How every feature works (singleton + observer pattern) + +**Complete Examples:** +- [Authentication Pattern](examples/authentication-pattern.md) - Working JWT auth with all code +- [Raw Video Capture](examples/raw-video-capture.md) - YUV420 video capture explained + +**Troubleshooting:** +- [Windows Message Loop](troubleshooting/windows-message-loop.md) - **CRITICAL**: Why callbacks don't fire +- [Build Errors](troubleshooting/build-errors.md) - SDK header dependency fixes +- [Common Issues](troubleshooting/common-issues.md) - Quick diagnostics & error codes + +**References:** +- [Interface Methods](references/interface-methods.md) - All required virtual methods (6 auth + 9 meeting) +- [Windows Reference](references/windows-reference.md) - Platform setup +- [Authorization](../references/authorization.md) - JWT generation +- [Bot Authentication](../references/bot-authentication.md) - Bot token types +- [Breakout Rooms](../references/breakout-rooms.md) - Breakout room API +- [AI Companion](../references/ai-companion.md) - AI features + +### 🚨 Most Critical Issues (From Real Debugging) + +1. **Callbacks not firing** → Missing Windows message loop (99% of issues) + - See: [Windows Message Loop Guide](troubleshooting/windows-message-loop.md) + +2. **Build errors** → SDK header dependencies (`uint32_t`, `AudioType`, etc.) + - See: [Build Errors Guide](troubleshooting/build-errors.md) + +3. **Abstract class errors** → Missing virtual method implementations + - See: [Interface Methods Guide](references/interface-methods.md) + +### 💡 Key Insight + +**Once you learn the 3-step pattern, you can implement ANY of the 35+ features:** +1. Get controller → 2. Implement event listener → 3. Register and use + +See: [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +## Official Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/windows/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/meeting/windows/annotated.html +- **Developer forum**: https://devforum.zoom.us/ +- **SDK download**: https://marketplace.zoom.us/ + +--- + +**Documentation Version**: Based on Zoom Windows Meeting SDK v6.7.2.26830 + +**Need help?** Start with [SKILL.md](SKILL.md) for complete navigation. + + +## Merged from meeting-sdk/windows/SKILL.md + +# Zoom Windows Meeting SDK - Complete Documentation Index + +## 🚀 Quick Start Path + +**If you're new to the SDK, follow this order:** + +1. **Read the architecture pattern** → [concepts/sdk-architecture-pattern.md](concepts/sdk-architecture-pattern.md) + - This teaches you the universal formula that applies to ALL features + - Once you understand this, you can implement any feature by reading the `.h` files + +2. **Fix build errors** → [troubleshooting/build-errors.md](troubleshooting/build-errors.md) + - SDK header dependencies issues + - Required include order + +3. **Implement authentication** → [examples/authentication-pattern.md](examples/authentication-pattern.md) + - Complete working JWT authentication code + +4. **Fix callback issues** → [troubleshooting/windows-message-loop.md](troubleshooting/windows-message-loop.md) + - **CRITICAL**: Why callbacks don't fire without Windows message loop + - This was the hardest issue to diagnose! + +5. **Implement virtual methods** → [references/interface-methods.md](references/interface-methods.md) + - Complete lists of all required methods + - How to avoid abstract class errors + +6. **Capture video (optional)** → [examples/raw-video-capture.md](examples/raw-video-capture.md) + - YUV420 format explained + - Complete raw data capture workflow + +7. **Troubleshoot any issues** → [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + - Quick diagnostic checklist + - Error code tables + - "If you see X, do Y" reference + +--- + +## 📂 Documentation Structure + +``` +meeting-sdk/windows/ +├── SKILL.md # Main skill overview +├── SKILL.md # This file - navigation guide +│ +├── concepts/ # Core architectural patterns +│ ├── sdk-architecture-pattern.md # THE MOST IMPORTANT DOC +│ │ # Universal formula for ANY feature +│ ├── singleton-hierarchy.md # Navigation guide for SDK services +│ │ # 4-level deep service tree, when/how +│ ├── custom-ui-architecture.md # How Custom UI rendering works +│ │ # Child HWNDs, D3D, layout, events +│ └── custom-ui-vs-raw-data.md # SDK-rendered vs self-rendered +│ # Decision guide for Custom UI approach +│ +├── examples/ # Complete working code +│ ├── authentication-pattern.md # JWT auth with full code +│ ├── raw-video-capture.md # Video capture with YUV420 details +│ │ # Recording vs Streaming, permissions +│ ├── custom-ui-video-rendering.md # Custom UI with video container +│ │ # Active speaker + gallery layout +│ ├── breakout-rooms.md # Complete breakout room guide +│ │ # 5 roles, create/manage/join +│ ├── chat.md # Send/receive chat messages +│ │ # Rich text, threading, file transfer +│ ├── captions-transcription.md # Live transcription & closed captions +│ │ # Multi-language translation +│ ├── local-recording.md # Local MP4 recording +│ │ # Permission flow, encoder monitoring +│ ├── share-raw-data-capture.md # Screen share raw data capture +│ │ # YUV420 frames from shared content +│ └── send-raw-data.md # Virtual camera/mic/share +│ # Send custom video/audio/share +│ +├── troubleshooting/ # Problem solving guides +│ ├── windows-message-loop.md # CRITICAL - Why callbacks fail +│ ├── build-errors.md # Header dependency fixes + MSBuild +│ └── common-issues.md # Quick diagnostic workflow +│ +└── references/ # Reference documentation + ├── interface-methods.md # Required virtual methods + │ # Auth(6) + Meeting(9) + CustomUI(13) + ├── windows-reference.md # Platform setup + ├── authorization.md # JWT generation + ├── bot-authentication.md # Bot token types + ├── breakout-rooms.md # Breakout room features + └── ai-companion.md # AI Companion features +``` + +--- + +## 🎯 By Use Case + +### I want to build a meeting bot +1. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Understand the pattern +2. [Authentication Pattern](examples/authentication-pattern.md) - Join meetings +3. [Windows Message Loop](troubleshooting/windows-message-loop.md) - Fix callback issues +4. [Interface Methods](references/interface-methods.md) - Implement callbacks + +### I'm getting build errors +1. [Build Errors Guide](troubleshooting/build-errors.md) - SDK header dependencies +2. [Interface Methods](references/interface-methods.md) - Abstract class errors +3. [Common Issues](troubleshooting/common-issues.md) - Linker errors + +### I'm getting runtime errors +1. [Windows Message Loop](troubleshooting/windows-message-loop.md) - Callbacks not firing +2. [Authentication Pattern](examples/authentication-pattern.md) - Auth timeout +3. [Common Issues](troubleshooting/common-issues.md) - Error code tables + +### I want to build a Custom UI meeting app +1. [Custom UI Architecture](concepts/custom-ui-architecture.md) - How SDK rendering works +2. [SDK-Rendered vs Self-Rendered](concepts/custom-ui-vs-raw-data.md) - Choose your approach +3. [Custom UI Video Rendering](examples/custom-ui-video-rendering.md) - Complete working code +4. [Interface Methods](references/interface-methods.md) - 13 Custom UI virtual methods +5. [Build Errors Guide](troubleshooting/build-errors.md) - MSBuild from git bash + +### I want to capture video/audio +1. [Raw Video Capture](examples/raw-video-capture.md) - Complete video workflow + - Recording vs Streaming approaches + - Permission requirements (host, OAuth tokens) + - Audio PCM capture +2. [Share Raw Data Capture](examples/share-raw-data-capture.md) - Screen share capture + - Subscribe to RAW_DATA_TYPE_SHARE + - Handle dynamic resolution +3. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Controller pattern +4. [Common Issues](troubleshooting/common-issues.md) - No frames received + +### I want to use breakout rooms +1. [Breakout Rooms Guide](examples/breakout-rooms.md) - Complete breakout room workflow + - 5 roles: Creator, Admin, Data, Assistant, Attendee + - Create, configure, manage, join/leave rooms +2. [Common Issues](troubleshooting/common-issues.md) - Breakout room error codes + +### I want to implement chat +1. [Chat Guide](examples/chat.md) - Send/receive messages + - Rich text formatting (bold, italic, links) + - Private messages and threading + - File transfer events + +### I want to use live transcription +1. [Captions & Transcription Guide](examples/captions-transcription.md) - Live transcription + - Automatic speech-to-text + - Multi-language translation + - Manual closed captions (host feature) + +### I want to record meetings +1. [Local Recording Guide](examples/local-recording.md) - Local MP4 recording + - Permission request workflow + - zTscoder.exe encoder monitoring + - Gallery view vs active speaker + +### I want to implement a specific feature +1. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - **START HERE!** +2. Find the controller in `SDK/x64/h/meeting_service_interface.h` +3. Find the header in `SDK/x64/h/meeting_service_components/` +4. Follow the universal pattern: Get controller → Implement listener → Use methods + +### I want to understand the SDK architecture +1. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Complete architecture overview +2. [Singleton Hierarchy](concepts/singleton-hierarchy.md) - Navigate the service tree (4 levels) +3. [Interface Methods](references/interface-methods.md) - Event listener pattern +4. [Authentication Pattern](examples/authentication-pattern.md) - Service pattern + +--- + +## 🔥 Most Critical Documents + +### 1. SDK Architecture Pattern (⭐ MASTER DOCUMENT) +**[concepts/sdk-architecture-pattern.md](concepts/sdk-architecture-pattern.md)** + +This is THE most important document. It teaches the universal 3-step pattern: +1. Get controller (singleton pattern) +2. Implement event listener (observer pattern) +3. Register and use + +Once you understand this pattern, you can implement **any of the 35+ features** by just reading the SDK headers. + +**Key insight**: The Zoom SDK follows a perfectly consistent architecture. Every feature works the same way. + +--- + +### 2. Windows Message Loop (⚠️ MOST COMMON ISSUE) +**[troubleshooting/windows-message-loop.md](troubleshooting/windows-message-loop.md)** + +99% of "callbacks not firing" issues are caused by missing Windows message loop. This document explains: +- Why SDK requires `PeekMessage()` loop +- How to implement it correctly +- How to diagnose callback issues + +**This was the hardest bug to find during development** (took ~2 hours). + +--- + +### 3. Build Errors Guide +**[troubleshooting/build-errors.md](troubleshooting/build-errors.md)** + +SDK headers have dependency bugs that cause build errors. This document provides: +- Required include order +- Missing `` fix +- Missing `AudioType` fix +- Missing `YUVRawDataI420` fix + +--- + +## 📊 By Document Type + +### Concepts (Why and How) +- [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Universal implementation pattern +- [Singleton Hierarchy](concepts/singleton-hierarchy.md) - Navigation guide for SDK services (4 levels deep) + +### Examples (Complete Working Code) +- [Authentication Pattern](examples/authentication-pattern.md) - JWT authentication +- [Raw Video Capture](examples/raw-video-capture.md) - Video capture with YUV420, recording vs streaming +- [Custom UI Video Rendering](examples/custom-ui-video-rendering.md) - SDK-rendered video containers +- [Breakout Rooms](examples/breakout-rooms.md) - Create, manage, join breakout rooms +- [Chat](examples/chat.md) - Send/receive messages with rich formatting +- [Captions & Transcription](examples/captions-transcription.md) - Live transcription and closed captions +- [Local Recording](examples/local-recording.md) - Local MP4 recording with permission flow +- [Share Raw Data Capture](examples/share-raw-data-capture.md) - Screen share raw data capture +- [Send Raw Data](examples/send-raw-data.md) - Virtual camera, microphone, and share + +### Troubleshooting (Problem Solving) +- [Windows Message Loop](troubleshooting/windows-message-loop.md) - Callback issues +- [Build Errors](troubleshooting/build-errors.md) - Compilation issues +- [Common Issues](troubleshooting/common-issues.md) - Quick diagnostics + +### References (Lookup Information) +- [Interface Methods](references/interface-methods.md) - Required virtual methods +- [Windows Reference](references/windows-reference.md) - Platform setup +- [Authorization](../references/authorization.md) - JWT generation +- [Bot Authentication](../references/bot-authentication.md) - Bot tokens +- [Breakout Rooms](../references/breakout-rooms.md) - Breakout room API +- [AI Companion](../references/ai-companion.md) - AI features + +--- + +## 💡 Key Learnings from Real Debugging + +These documents were created from actual debugging of a non-functional Zoom SDK sample. Here are the key insights: + +### Critical Discoveries: + +1. **Windows Message Loop is MANDATORY** (not optional) + - SDK uses Windows message pump for callbacks + - Without it, callbacks are queued but never fire + - Manifests as "authentication timeout" even with valid JWT + - See: [Windows Message Loop Guide](troubleshooting/windows-message-loop.md) + +2. **SDK Headers Have Dependency Bugs** + - Missing `#include ` in SDK headers + - `meeting_participants_ctrl_interface.h` doesn't include `meeting_audio_interface.h` + - `rawdata_renderer_interface.h` only forward-declares `YUVRawDataI420` + - See: [Build Errors Guide](troubleshooting/build-errors.md) + +3. **Include Order is CRITICAL** + - `` must be FIRST + - `` must be SECOND + - Then SDK headers in specific order + - See: [Build Errors Guide](troubleshooting/build-errors.md) + +4. **ALL Virtual Methods Must Be Implemented** + - Including WIN32-conditional methods + - SDK v6.7.2 requires 6 auth methods + 9 meeting methods + - Different versions have different requirements + - See: [Interface Methods Guide](references/interface-methods.md) + +5. **The Architecture is Beautifully Consistent** + - Every feature follows the same 3-step pattern + - Controllers are singletons + - Event listeners use observer pattern + - Once you learn the pattern, you can implement any feature + - See: [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +--- + +## 🎓 Learning Path by Skill Level + +### Beginner (Never used Zoom SDK) +1. Read [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) to understand the overall design +2. Follow [Authentication Pattern](examples/authentication-pattern.md) to join your first meeting +3. Reference [Common Issues](troubleshooting/common-issues.md) when you hit problems + +### Intermediate (Familiar with SDK basics) +1. Deep dive into [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - implement multiple features +2. Learn [Raw Video Capture](examples/raw-video-capture.md) for media processing +3. Use [Interface Methods](references/interface-methods.md) as reference + +### Advanced (Building production bots) +1. Study [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - learn to implement ANY feature +2. Master [Windows Message Loop](troubleshooting/windows-message-loop.md) - understand async callback flow +3. Reference SDK headers directly using the universal pattern + +--- + +## 🔍 How to Find What You Need + +### "My code won't compile" +→ [Build Errors Guide](troubleshooting/build-errors.md) + +### "Authentication times out" +→ [Windows Message Loop](troubleshooting/windows-message-loop.md) + +### "Callbacks never fire" +→ [Windows Message Loop](troubleshooting/windows-message-loop.md) + +### "Abstract class error" +→ [Interface Methods](references/interface-methods.md) + +### "How do I implement [feature]?" +→ [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +### "How do I join a meeting?" +→ [Authentication Pattern](examples/authentication-pattern.md) + +### "How do I capture video?" +→ [Raw Video Capture](examples/raw-video-capture.md) + +### "What error code means what?" +→ [Common Issues](troubleshooting/common-issues.md) - Comprehensive error code tables (SDKERR, AUTHRET, Login, BO, Phone, OBF) + +### "How do I use breakout rooms?" +→ [Breakout Rooms Guide](examples/breakout-rooms.md) + +### "How does the SDK work?" +→ [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +### "How do I navigate to a specific controller/feature?" +→ [Singleton Hierarchy](concepts/singleton-hierarchy.md) + +### "How do I send/receive chat messages?" +→ [Chat Guide](examples/chat.md) + +### "How do I use live transcription?" +→ [Captions & Transcription Guide](examples/captions-transcription.md) + +### "How do I record locally?" +→ [Local Recording Guide](examples/local-recording.md) + +### "How do I capture screen share?" +→ [Share Raw Data Capture](examples/share-raw-data-capture.md) + +--- + +## 📝 Document Version + +All documents are based on **Zoom Windows Meeting SDK v6.7.2.26830**. + +Different SDK versions may have: +- Different required callback methods +- Different error codes +- Different API behavior + +If using a different version, use `grep "= 0" SDK/x64/h/*.h` to verify required methods. + +--- + +Remember: The [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) is the fastest way to understand how the Windows Meeting SDK fits together. Read it first if you are debugging custom UI or event flow issues. + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/concepts/custom-ui-architecture.md b/plugins/zoom-developers/skills/meeting-sdk/windows/concepts/custom-ui-architecture.md new file mode 100644 index 00000000..50c58e95 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/concepts/custom-ui-architecture.md @@ -0,0 +1,224 @@ +# Custom UI Architecture — How It Actually Works + +> **Skill**: Zoom Meeting SDK (Windows) +> **Category**: Concepts +> **Prerequisite**: [SDK Architecture Pattern](sdk-architecture-pattern.md) + +## Overview + +Custom UI mode lets you create your OWN meeting window instead of the SDK's default meeting UI. The SDK renders video into your window using Direct3D, but you control all layout, window management, and UI elements. + +**This is NOT "HWND hijacking."** The SDK creates child windows inside your parent window and renders into those using its own D3D pipeline. Your window and WndProc remain untouched. + +## Enabling Custom UI Mode + +Set `ENABLE_CUSTOMIZED_UI_FLAG` during SDK initialization: + +```cpp +InitParam initParam; +initParam.strWebDomain = L"https://zoom.us"; +initParam.emLanguageID = LANGUAGE_English; + +// CRITICAL: Enable Custom UI mode +initParam.obConfigOpts.optionalFeatures = ENABLE_CUSTOMIZED_UI_FLAG; + +SDKError err = InitSDK(initParam); +``` + +Without this flag, the SDK creates its own default meeting window. With it, the SDK creates NO UI — you must provide everything. + +## Internal Architecture + +``` +Your Window (WS_OVERLAPPEDWINDOW) — you own this, your WndProc + | + +-- [SDK Child HWND] — created internally by CreateVideoContainer() + | - SDK owns the WndProc + | - D3D11 swap chain bound to this child HWND + | - Composites all video onto one surface + | + +-- VideoElement: Active (logical RECT region, NOT a window) + +-- VideoElement: Normal0 (logical RECT region, NOT a window) + +-- VideoElement: Normal1 (logical RECT region, NOT a window) + +-- ... + | + +-- [SDK Child HWND for Share] — created by CreateShareRender() + | - Separate D3D surface for screen share content + | - Requires HandleWindowsMoveMsg() for DWM resync +``` + +### Key architectural facts + +1. **`CreateVideoContainer(hParentWnd, rc)`** — SDK creates a **child HWND** inside your parent. You never see or manage this child HWND directly. + +2. **Video elements are NOT separate windows** — they are **logical render regions** within a single D3D surface. `SetPos(RECT)` tells the SDK's compositor where to place each video texture within the container. + +3. **Your app does ZERO rendering** — no `WM_PAINT`, no GDI calls, no `BitBlt`. The SDK handles 100% of video drawing internally. + +## Rendering Technology + +The SDK supports multiple rendering backends, configurable via `InitParam.renderOpts.videoRenderMode`: + +```cpp +enum ZoomSDKVideoRenderMode { + ZoomSDKVideoRenderMode_None = 0, // Auto (default) + ZoomSDKVideoRenderMode_Auto, + ZoomSDKVideoRenderMode_D3D11EnableFLIP, // D3D11 with DXGI flip model (best) + ZoomSDKVideoRenderMode_D3D11, // D3D11 standard + ZoomSDKVideoRenderMode_D3D9, // D3D9 fallback + ZoomSDKVideoRenderMode_GDI, // GDI software fallback (VMs) +}; +``` + +**Hierarchy**: D3D11 FLIP > D3D11 > D3D9 > GDI + +The D3D11 FLIP model uses `DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL` — this requires a dedicated child HWND (further confirming the child window architecture). + +## Why onWindowMsgNotification Exists + +The SDK's child HWND has its own WndProc that **intercepts input messages**. Your parent window's WndProc never sees mouse/keyboard events that land on the video area. The SDK forwards them back to you through callbacks: + +``` +Forwarded messages: + WM_MOUSEMOVE, WM_MOUSEENTER, WM_MOUSELEAVE, + WM_LBUTTONDOWN, WM_LBUTTONUP, WM_RBUTTONUP, + WM_LBUTTONDBLCLK, WM_KEYDOWN +``` + +If you need to handle clicks on the video (e.g., click a participant to select them), you must handle them in `onWindowMsgNotification`, not in your parent WndProc. + +## Why HandleWindowsMoveMsg() Exists (Share Render Only) + +```cpp +// On ICustomizedShareRender only +virtual SDKError HandleWindowsMoveMsg() = 0; +``` + +When using Direct3D, the swap chain's presentation is tied to the window's screen position via DWM (Desktop Window Manager) composition. When the parent window moves: + +1. Child HWND moves with it automatically +2. BUT the D3D swap chain may not update its DWM surface coordinates immediately +3. This causes "ghost frame" / "stale composition" artifacts + +`HandleWindowsMoveMsg()` tells the SDK to force re-present at the new coordinates. + +**This only exists on `ICustomizedShareRender`**, not on `ICustomizedVideoContainer` — the video container likely handles this internally or uses the FLIP model which doesn't have this issue. + +## HasLicense() Check + +The `ICustomizedUIMgr` interface has a `HasLicense()` method. The official SDK demo checks it as a hard gate: + +```cpp +SDKError err = m_pCustomUIMgr->HasLicense(); +if (err != SDKERR_SUCCESS) { + // Demo aborts here +} +``` + +In practice, modern SDK licenses may include Custom UI by default. It's safe to log a warning but continue if it fails — the SDK will return errors on actual API calls if the license is truly missing. + +## Custom UI Object Lifecycle + +### Creation order (during meeting connect): +1. `CreateCustomizedUIMgr(&pMgr)` — global, creates the manager +2. `pMgr->SetEvent(&listener)` — register for destroy notifications +3. `pMgr->CreateVideoContainer(&pContainer, hParentWnd, rc)` — creates the rendering surface +4. `pContainer->SetEvent(&containerListener)` — register for layout/render events +5. `pContainer->Show()` / `SetBkColor()` — configure appearance +6. `pContainer->CreateVideoElement(&pElement, type)` — create render slots + +### Destruction order (when meeting ends): +1. `pContainer->DestroyAllVideoElement()` — remove all render slots +2. `pMgr->DestroyVideoContainer(pContainer)` — destroy the rendering surface +3. `pMgr->DestroyShareRender(pShareRender)` — destroy share render if created +4. `DestroyCustomizedUIMgr(pMgr)` — global cleanup + +### Important: The SDK may also destroy containers on its own (e.g., meeting ends). That's why `ICustomizedUIMgrEvent` has `onVideoContainerDestroyed` and `onShareRenderDestroyed` callbacks — so you can null out your pointers. + +## Video Element Types + +### Active Speaker Element (`VideoRenderElement_ACTIVE`) +- Auto-follows whoever is currently speaking +- No need to subscribe to a specific user +- Call `Start()` to begin, `Stop()` to pause + +```cpp +IVideoRenderElement* pElement = nullptr; +pContainer->CreateVideoElement(&pElement, VideoRenderElement_ACTIVE); +IActiveVideoRenderElement* pActive = dynamic_cast(pElement); +pActive->SetPos(rect); +pActive->Show(); +pActive->Start(); +``` + +### Normal Element (`VideoRenderElement_NORMAL`) +- Shows a specific participant's video +- Must call `Subscribe(userId)` to bind it to a user +- Set resolution with `SetResolution()` + +```cpp +IVideoRenderElement* pElement = nullptr; +pContainer->CreateVideoElement(&pElement, VideoRenderElement_NORMAL); +INormalVideoRenderElement* pNormal = dynamic_cast(pElement); +pNormal->Subscribe(userId); +pNormal->SetResolution(VideoRenderResolution_360p); +pNormal->SetPos(rect); +pNormal->Show(); +``` + +### Preview Element (`VideoRenderElement_PREVIEW`) +- Shows the local camera preview before joining +- Used for pre-meeting camera setup + +## Layout Management + +Video element positions are **RECTs relative to the container's client area**, not screen coordinates. + +When the container receives `onLayoutNotification(RECT wnd_client_rect)`, you should recalculate and re-apply all element positions: + +```cpp +void OnLayoutNotification(RECT clientRect) { + int width = clientRect.right - clientRect.left; + int height = clientRect.bottom - clientRect.top; + + // Active speaker: top 70% + RECT activeRect = { 0, 0, width, (int)(height * 0.7) }; + pActiveElement->SetPos(activeRect); + + // Gallery: bottom 30%, evenly split horizontally + int galleryTop = (int)(height * 0.7); + int elemWidth = width / galleryCount; + for (int i = 0; i < galleryCount; i++) { + RECT r = { i * elemWidth, galleryTop, (i+1) * elemWidth, height }; + normalElements[i]->SetPos(r); + } +} +``` + +## Share Render + +The share render is a **separate SDK child window** for displaying screen shares: + +```cpp +pMgr->CreateShareRender(&pShareRender, hParentWnd, rc); +pShareRender->SetEvent(&shareListener); +pShareRender->Hide(); // Hidden until someone shares + +// When sharing starts (via onSharingSourceNotification): +pShareRender->SetShareSourceID(shareSourceID); +pShareRender->Show(); +pShareRender->SetViewMode(CSM_FULLFILL); // or CSM_LETTER_BOX +``` + +## Relevant SDK DLLs + +- `zVideoUI.dll`, `zVideoApp.dll`, `zVideoAppFrame.dll` — core video rendering +- `avcodec_zm-59.dll`, `avutil_zm-57.dll`, `swscale_zm-6.dll` — FFmpeg decoders +- `clDNN64.dll`, `mkldnn.dll` — Intel DNN for AI features (background blur, etc.) + +--- + +**See also:** +- [Custom UI Working Code Example](../examples/custom-ui-video-rendering.md) +- [Two Approaches: SDK-Rendered vs Self-Rendered](custom-ui-vs-raw-data.md) +- [Custom UI Interface Methods](../references/interface-methods.md) diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/concepts/custom-ui-vs-raw-data.md b/plugins/zoom-developers/skills/meeting-sdk/windows/concepts/custom-ui-vs-raw-data.md new file mode 100644 index 00000000..96915419 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/concepts/custom-ui-vs-raw-data.md @@ -0,0 +1,180 @@ +# Two Approaches to Custom UI: SDK-Rendered vs Self-Rendered + +> **Skill**: Zoom Meeting SDK (Windows) +> **Category**: Concepts +> **Prerequisite**: [Custom UI Architecture](custom-ui-architecture.md), [Raw Video Capture](../examples/raw-video-capture.md) + +## Overview + +There are two fundamentally different ways to build a custom meeting UI with the Zoom SDK. Both require Custom UI mode (`ENABLE_CUSTOMIZED_UI_FLAG`), but they differ in who renders the video. + +## Approach 1: SDK-Rendered (ICustomizedVideoContainer) + +The SDK renders video into your window using Direct3D. You control layout, the SDK controls pixels. + +### How it works +``` +Your Win32 Window + -> SDK creates child HWND inside it + -> SDK renders video via D3D11 into child HWND + -> You call SetPos(RECT) to position video elements +``` + +### You control +- Window creation, sizing, positioning +- Which participants are visible and where (`SetPos`) +- Active speaker vs gallery layout +- Your own UI controls (buttons, toolbar) +- Background color (`SetBkColor`) + +### SDK controls +- Actual video decoding and rendering +- D3D pipeline +- Frame timing +- Video quality / resolution scaling + +### Key APIs +- `CreateCustomizedUIMgr()` / `DestroyCustomizedUIMgr()` +- `ICustomizedUIMgr::CreateVideoContainer()` / `DestroyVideoContainer()` +- `ICustomizedVideoContainer::CreateVideoElement()` +- `IActiveVideoRenderElement::Start()` / `Stop()` +- `INormalVideoRenderElement::Subscribe(userId)` +- `IVideoRenderElement::SetPos(RECT)` / `Show()` / `Hide()` + +### Best for +- Standard meeting applications +- Apps that need a meeting window with video +- Rapid development (less code) +- When you don't need pixel-level access + +### Limitations +- No access to raw video frames +- Cannot apply custom filters/effects +- Cannot render video in your own graphics engine +- Cannot pop individual videos into separate windows (elements are regions within one container) +- Single rendering surface per container + +--- + +## Approach 2: Self-Rendered (IZoomSDKRenderer + Raw Data) + +You get raw YUV420 frames per participant and render them yourself. Maximum control. + +### How it works +``` +SDK decodes video internally + -> IZoomSDKRendererDelegate::onRawDataFrameReceived(YUVRawDataI420*) + -> You get raw pixel data (Y, U, V planes) + -> You render with your own engine (D3D11, OpenGL, GDI, etc.) +``` + +### You control +- Everything from Approach 1, PLUS: +- Raw pixel access (YUV420 frames) +- Your own rendering pipeline (D3D11, OpenGL, Vulkan, GDI, etc.) +- Custom video processing (filters, watermarks, overlays, effects) +- Multiple separate windows per participant +- Picture-in-picture +- Recording/streaming with effects applied +- Any layout imaginable + +### SDK provides +- Decoded video frames as `YUVRawDataI420*` +- Frame metadata (width, height, rotation) +- Per-participant subscription + +### Key APIs +- `createRenderer()` — creates a renderer for one participant +- `IZoomSDKRenderer::setRawDataResolution()` — set quality +- `IZoomSDKRenderer::subscribe(userId)` — bind to participant +- `IZoomSDKRendererDelegate::onRawDataFrameReceived(YUVRawDataI420*)` — frame callback +- `IZoomSDKRendererDelegate::onRawDataStatusChanged(RawDataStatus)` — status changes +- Raw recording: `IMeetingRecordingController::StartRawRecording()` + +### Best for +- Custom rendering engines +- Video processing / effects +- Multi-window layouts +- Recording with overlays +- Streaming applications +- Research / computer vision + +### Limitations +- More code to write (you handle all rendering) +- Must convert YUV420 to RGB/BGRA for display +- Must manage frame buffers and timing +- Higher CPU usage if not GPU-accelerated +- Must call `StartRawRecording()` before frames flow + +--- + +## Approach 3: Hybrid (Both Together) + +You can combine both approaches in the same application: + +``` +Custom UI mode enabled + | + +-- ICustomizedVideoContainer for main meeting view + | (SDK renders, you layout) + | + +-- IZoomSDKRenderer for specific participants + (raw frames for recording, processing, pop-out windows) +``` + +### Use cases +- Main meeting window uses SDK rendering (efficient, easy) +- Simultaneously capture raw frames for recording with watermarks +- Pop out a specific participant into a custom-rendered window +- Apply computer vision to one participant's feed while showing others normally + +### How to combine +1. Initialize with `ENABLE_CUSTOMIZED_UI_FLAG` +2. Create `ICustomizedVideoContainer` for the main view +3. Also call `createRenderer()` + `subscribe()` for raw data on specific users +4. Both can run simultaneously + +--- + +## Comparison Table + +| Feature | SDK-Rendered | Self-Rendered | Hybrid | +|---------|-------------|---------------|--------| +| Raw pixel access | No | Yes | Yes (selected users) | +| Custom filters/effects | No | Yes | Yes (selected users) | +| Multiple windows | No (regions in 1 container) | Yes | Yes | +| Rendering effort | Minimal | High | Medium | +| CPU usage | Low (SDK uses D3D) | Higher (unless GPU) | Medium | +| Code complexity | Low | High | Medium | +| Layout flexibility | RECT regions | Unlimited | Mix | +| Active speaker tracking | Built-in (`VideoRenderElement_ACTIVE`) | Manual | Mix | +| Screen share display | Built-in (`ICustomizedShareRender`) | Manual from raw data | Mix | +| Time to implement | Hours | Days | Hours + targeted raw data | + +--- + +## Decision Guide + +**Use SDK-Rendered when:** +- You just need a meeting window with custom layout +- You want to add your own toolbar/controls around the video +- Development speed matters +- You don't need to touch the video pixels + +**Use Self-Rendered when:** +- You need to apply effects/filters to video +- You're building a custom rendering engine +- You need participants in separate windows +- You're doing computer vision / ML on the video +- You need full pixel control + +**Use Hybrid when:** +- You want the easy SDK rendering for the main view +- But also need raw frames for specific purposes (recording, one pop-out window, etc.) + +--- + +**See also:** +- [Custom UI Architecture](custom-ui-architecture.md) — How SDK rendering works internally +- [Custom UI Video Rendering Example](../examples/custom-ui-video-rendering.md) — SDK-rendered approach +- [Raw Video Capture](../examples/raw-video-capture.md) — Self-rendered approach diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/concepts/sdk-architecture-pattern.md b/plugins/zoom-developers/skills/meeting-sdk/windows/concepts/sdk-architecture-pattern.md new file mode 100644 index 00000000..847dc761 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/concepts/sdk-architecture-pattern.md @@ -0,0 +1,654 @@ +# SDK Architecture Pattern: The Universal Implementation Formula + +## Core Concept + +The Zoom Windows Meeting SDK follows a **consistent architectural pattern** that applies to EVERY feature. Once you understand this pattern, you can implement ANY feature just by reading the `.h` header files. + +--- + +## The Universal Pattern + +Every SDK feature follows this 3-step pattern: + +### 1. Get the Controller/Helper (Singleton Pattern) + +```cpp +// Pattern: meetingService->Get[Feature]Controller() +IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); +IMeetingChatController* chatCtrl = meetingService->GetMeetingChatController(); +IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController(); +``` + +### 2. Implement the Event Listener Interface (Observer Pattern) + +```cpp +// Pattern: I[Feature]Event or I[Feature]CtrlEvent +class MyAudioListener : public IMeetingAudioCtrlEvent { + void onUserAudioStatusChange(IList* lstAudioStatusChange) override { + // React to audio events + } + // ... implement all pure virtual methods +}; +``` + +### 3. Register Listener and Use Controller Methods + +```cpp +// Pattern: controller->SetEvent(listener), then call methods +audioCtrl->SetEvent(new MyAudioListener()); +audioCtrl->MuteAudio(userId, true); // Use feature +``` + +--- + +## Complete Architecture Overview + +### The Singleton Services + +**Top-level services** (created via global functions): + +```cpp +// Authentication Service (JWT, user login) +IAuthService* authService; +CreateAuthService(&authService); + +// Meeting Service (join, leave, access features) +IMeetingService* meetingService; +CreateMeetingService(&meetingService); +``` + +### Feature Controllers (35+ Available) + +**All controllers** are accessed through `IMeetingService->Get[Feature]Controller()`: + +```cpp +// Audio features +IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); + +// Video features +IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController(); + +// Chat features +IMeetingChatController* chatCtrl = meetingService->GetMeetingChatController(); + +// Participants management +IMeetingParticipantsController* participantsCtrl = meetingService->GetMeetingParticipantsController(); + +// Recording features +IMeetingRecordingController* recordingCtrl = meetingService->GetMeetingRecordingController(); + +// Screen sharing +IMeetingShareController* shareCtrl = meetingService->GetMeetingShareController(); + +// Waiting room +IMeetingWaitingRoomController* waitingRoomCtrl = meetingService->GetMeetingWaitingRoomController(); + +// Webinar features +IMeetingWebinarController* webinarCtrl = meetingService->GetMeetingWebinarController(); + +// Breakout rooms +IMeetingBOController* boCtrl = meetingService->GetMeetingBOController(); + +// AI Companion +IMeetingAICompanionController* aiCtrl = meetingService->GetMeetingAICompanionController(); + +// Q&A +IMeetingQAController* qaCtrl = meetingService->GetMeetingQAController(); + +// Polling +IMeetingPollingController* pollingCtrl = meetingService->GetMeetingPollingController(); + +// Whiteboard +IMeetingWhiteboardController* whiteboardCtrl = meetingService->GetMeetingWhiteboardController(); + +// Closed captions +IClosedCaptionController* captionCtrl = meetingService->GetMeetingClosedCaptionController(); + +// Live streaming +IMeetingLiveStreamController* liveStreamCtrl = meetingService->GetMeetingLiveStreamController(); + +// Emoji reactions +IEmojiReactionController* emojiCtrl = meetingService->GetMeetingEmojiReactionController(); + +// Interpretation +IMeetingInterpretationController* interpretCtrl = meetingService->GetMeetingInterpretationController(); + +// Remote support +IMeetingRemoteSupportController* remoteSupportCtrl = meetingService->GetMeetingRemoteSupportController(); + +// Encryption +IMeetingEncryptionController* encryptionCtrl = meetingService->GetInMeetingEncryptionController(); + +// ... and 15+ more controllers! +``` + +**Complete list** (35 controllers as of SDK v6.7.2): +1. `GetMeetingAudioController()` - Audio mute/unmute +2. `GetMeetingVideoController()` - Video start/stop +3. `GetMeetingShareController()` - Screen sharing +4. `GetMeetingChatController()` - Meeting chat +5. `GetMeetingParticipantsController()` - Participant management +6. `GetMeetingRecordingController()` - Recording/raw data +7. `GetMeetingWaitingRoomController()` - Waiting room management +8. `GetMeetingWebinarController()` - Webinar features +9. `GetMeetingBOController()` - Breakout rooms +10. `GetMeetingAICompanionController()` - AI features +11. `GetMeetingQAController()` - Q&A management +12. `GetMeetingPollingController()` - Polls/surveys +13. `GetMeetingWhiteboardController()` - Whiteboard +14. `GetMeetingClosedCaptionController()` - Captions +15. `GetMeetingLiveStreamController()` - Live streaming +16. `GetMeetingEmojiReactionController()` - Emoji reactions +17. `GetMeetingInterpretationController()` - Language interpretation +18. `GetMeetingSignInterpretationController()` - Sign language +19. `GetMeetingRemoteSupportController()` - Remote support +20. `GetMeetingEncryptionController()` - E2E encryption +21. `GetMeetingRawArchivingController()` - Raw archiving +22. `GetMeetingReminderController()` - Meeting reminders +23. `GetMeetingSmartSummaryController()` - Smart summary (deprecated) +24. `GetUIController()` - UI controls +25. `GetAnnotationController()` - Annotations +26. `GetMeetingRemoteController()` - Remote control +27. `GetH323Helper()` - H.323 support +28. `GetMeetingPhoneHelper()` - Phone dial-in +29. `GetMeetingRealNameAuthController()` - Real name auth +30. `GetMeetingAANController()` - Auto Accept Notification +31. `GetMeetingImmersiveController()` - Immersive view +32. `GetMeetingDocsController()` - Documents +33. `GetMeetingIndicatorController()` - Meeting indicators +34. `GetMeetingProductionStudioController()` - Production studio +35. And more in future versions... + +--- + +## Deep Dive: Audio Feature Example + +Let's implement audio mute/unmute to demonstrate the pattern: + +### Step 1: Read the Header File + +Open `SDK/x64/h/meeting_service_components/meeting_audio_interface.h`: + +```cpp +// The event listener interface +class IMeetingAudioCtrlEvent { +public: + virtual void onUserAudioStatusChange(IList* lstAudioStatusChange) = 0; + virtual void onUserActiveAudioChange(IList* plstActiveAudioUser) = 0; + // ... more callback methods +}; + +// The controller interface +class IMeetingAudioController { +public: + virtual SDKError SetEvent(IMeetingAudioCtrlEvent* pEvent) = 0; + virtual SDKError JoinVoip() = 0; + virtual SDKError MuteAudio(unsigned int userid, bool allowUnmuteBySelf = true) = 0; + virtual SDKError UnMuteAudio(unsigned int userid) = 0; + // ... more control methods +}; +``` + +### Step 2: Implement the Event Listener + +**AudioEventListener.h**: +```cpp +#pragma once +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class AudioEventListener : public IMeetingAudioCtrlEvent { +public: + // Implement ALL pure virtual methods from IMeetingAudioCtrlEvent + void onUserAudioStatusChange(IList* lstAudioStatusChange) override; + void onUserActiveAudioChange(IList* plstActiveAudioUser) override; + void onHostRequestStartAudio(IRequestStartAudioHandler* handler) override; + void onMuteOnEntryStatusChange(bool bEnabled) override; + // ... implement all other required methods +}; +``` + +**AudioEventListener.cpp**: +```cpp +#include "AudioEventListener.h" + +void AudioEventListener::onUserAudioStatusChange(IList* lstAudioStatusChange) { + if (!lstAudioStatusChange) return; + + int count = lstAudioStatusChange->GetCount(); + for (int i = 0; i < count; i++) { + IUserAudioStatus* audioStatus = lstAudioStatusChange->GetItem(i); + unsigned int userId = audioStatus->GetUserId(); + AudioStatus status = audioStatus->GetStatus(); + + std::cout << "[AUDIO] User " << userId << " status changed: " << status << std::endl; + } +} + +void AudioEventListener::onUserActiveAudioChange(IList* plstActiveAudioUser) { + if (!plstActiveAudioUser) return; + + int count = plstActiveAudioUser->GetCount(); + std::cout << "[AUDIO] Active speakers: " << count << std::endl; +} + +void AudioEventListener::onHostRequestStartAudio(IRequestStartAudioHandler* handler) { + std::cout << "[AUDIO] Host requests unmute" << std::endl; + // Auto-accept or ignore based on your logic + if (handler) { + handler->Accept(); // or handler->Ignore() + } +} + +void AudioEventListener::onMuteOnEntryStatusChange(bool bEnabled) { + std::cout << "[AUDIO] Mute on entry: " << (bEnabled ? "enabled" : "disabled") << std::endl; +} +``` + +### Step 3: Use the Feature + +**main.cpp**: +```cpp +void SetupAudioFeatures() { + // 1. Get the controller (singleton) + IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); + if (!audioCtrl) { + std::cerr << "Failed to get audio controller" << std::endl; + return; + } + + // 2. Set event listener + audioCtrl->SetEvent(new AudioEventListener()); + + // 3. Use controller methods + + // Join audio (connect to VoIP) + SDKError err = audioCtrl->JoinVoip(); + if (err == SDKERR_SUCCESS) { + std::cout << "[AUDIO] Joined VoIP successfully" << std::endl; + } + + // Mute yourself (userId = 0 or your own user ID) + audioCtrl->MuteAudio(0, true); + std::cout << "[AUDIO] Muted self" << std::endl; + + // Unmute yourself + audioCtrl->UnMuteAudio(0); + std::cout << "[AUDIO] Unmuted self" << std::endl; + + // Mute all participants (host only, userId = 0 for all) + audioCtrl->MuteAudio(0, false); // false = don't allow self-unmute + std::cout << "[AUDIO] Muted all participants" << std::endl; +} +``` + +--- + +## The Pattern Applied to ANY Feature + +### Example: Implementing Chat + +**Step 1: Read header** (`meeting_chat_interface.h`) + +```cpp +class IMeetingChatCtrlEvent { + virtual void onChatMsgNotification(IChatMsgInfo* chatMsg) = 0; + // ... more methods +}; + +class IMeetingChatController { + virtual SDKError SetEvent(IMeetingChatCtrlEvent* pEvent) = 0; + virtual SDKError SendChatTo(const wchar_t* receiver, const wchar_t* content) = 0; + // ... more methods +}; +``` + +**Step 2: Implement listener** + +```cpp +class ChatEventListener : public IMeetingChatCtrlEvent { + void onChatMsgNotification(IChatMsgInfo* chatMsg) override { + std::wcout << L"[CHAT] From: " << chatMsg->GetSenderDisplayName() + << L", Message: " << chatMsg->GetContent() << std::endl; + } +}; +``` + +**Step 3: Use it** + +```cpp +IMeetingChatController* chatCtrl = meetingService->GetMeetingChatController(); +chatCtrl->SetEvent(new ChatEventListener()); +chatCtrl->SendChatTo(L"everyone", L"Hello from bot!"); +``` + +### Example: Implementing Screen Share Detection + +**Step 1: Read header** (`meeting_sharing_interface.h`) + +```cpp +class IMeetingShareCtrlEvent { + virtual void onSharingStatus(SharingStatus status, unsigned int userId) = 0; +}; +``` + +**Step 2: Implement listener** + +```cpp +class ShareEventListener : public IMeetingShareCtrlEvent { + void onSharingStatus(SharingStatus status, unsigned int userId) override { + if (status == Sharing_Self_Send_Begin) { + std::cout << "[SHARE] User " << userId << " started sharing" << std::endl; + } else if (status == Sharing_Self_Send_End) { + std::cout << "[SHARE] User " << userId << " stopped sharing" << std::endl; + } + } +}; +``` + +**Step 3: Use it** + +```cpp +IMeetingShareController* shareCtrl = meetingService->GetMeetingShareController(); +shareCtrl->SetEvent(new ShareEventListener()); +``` + +--- + +## Key Architectural Insights + +### 1. Controllers are Singletons + +Each controller is a singleton accessed through `meetingService`: + +```cpp +// Always returns the SAME instance +IMeetingAudioController* ctrl1 = meetingService->GetMeetingAudioController(); +IMeetingAudioController* ctrl2 = meetingService->GetMeetingAudioController(); +// ctrl1 == ctrl2 (same pointer) +``` + +**Why this matters**: You can get the controller anywhere in your code without passing pointers around. + +### 2. Event Listeners Use Observer Pattern + +```cpp +// SDK maintains the listener internally +audioCtrl->SetEvent(new AudioEventListener()); + +// SDK calls listener methods when events occur +// You don't call these methods yourself! +``` + +**Why this matters**: The SDK manages listener lifecycle. Use `new` and let SDK handle cleanup. + +### 3. Controllers Have Both Actions and Queries + +**Actions** (change state): +```cpp +audioCtrl->MuteAudio(userId, true); // DO something +chatCtrl->SendChatTo(L"user", L"hello"); // DO something +``` + +**Queries** (get information): +```cpp +bool isMuted = audioCtrl->IsAudioMuted(userId); // GET information +bool isHost = participantsCtrl->IsHost(userId); // GET information +``` + +### 4. Feature Availability Varies by SDK App Type + +Some features require specific permissions: + +```cpp +// Recording requires special SDK app type +IMeetingRecordingController* recordingCtrl = meetingService->GetMeetingRecordingController(); +if (recordingCtrl) { + SDKError canRecord = recordingCtrl->CanStartRawRecording(); + if (canRecord == SDKERR_SUCCESS) { + recordingCtrl->StartRawRecording(); + } else { + // Not available (wrong app type, insufficient permissions) + } +} +``` + +--- + +## How to Implement Any Feature (Universal Recipe) + +### Recipe for Success: + +1. **Find the header file**: + - Look in `SDK/x64/h/meeting_service_components/` + - File naming: `meeting_[feature]_interface.h` + - Examples: `meeting_audio_interface.h`, `meeting_chat_interface.h` + +2. **Identify the interfaces**: + - Find `I[Feature]Event` or `I[Feature]CtrlEvent` (for callbacks) + - Find `I[Feature]Controller` (for actions/queries) + +3. **Implement the event listener**: + - Create class inheriting from `I[Feature]Event` + - Implement ALL pure virtual methods (use [Interface Methods Guide](../references/interface-methods.md)) + - Add logging to see when events fire + +4. **Get the controller**: + - Call `meetingService->Get[Feature]Controller()` + - Check for `nullptr` (might not be available) + +5. **Register listener and use**: + - Call `controller->SetEvent(new YourListener())` + - Call controller methods to use features + +6. **Test incrementally**: + - Start with event logging only + - Verify events fire correctly + - Add action methods one at a time + +--- + +## Complete Working Example: Mute Bot + +This bot auto-mutes when joining and responds to host unmute requests: + +```cpp +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class MuteBotAudioListener : public IMeetingAudioCtrlEvent { +private: + IMeetingAudioController* audioCtrl; + +public: + MuteBotAudioListener(IMeetingAudioController* ctrl) : audioCtrl(ctrl) {} + + void onUserAudioStatusChange(IList* lstAudioStatusChange) override { + // Just log for debugging + std::cout << "[AUDIO] Audio status changed" << std::endl; + } + + void onUserActiveAudioChange(IList* plstActiveAudioUser) override { + // Not needed for this bot + } + + void onHostRequestStartAudio(IRequestStartAudioHandler* handler) override { + std::cout << "[AUDIO] Host requested unmute - auto accepting" << std::endl; + if (handler) { + handler->Accept(); // Automatically accept host request + } + } + + void onMuteOnEntryStatusChange(bool bEnabled) override { + std::cout << "[AUDIO] Mute on entry: " << bEnabled << std::endl; + } +}; + +void SetupMuteBot() { + // Get audio controller + IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); + if (!audioCtrl) return; + + // Register event listener + audioCtrl->SetEvent(new MuteBotAudioListener(audioCtrl)); + + // Join VoIP + audioCtrl->JoinVoip(); + + // Mute self immediately + audioCtrl->MuteAudio(0, true); + + std::cout << "[BOT] Mute bot active - will auto-accept host unmute requests" << std::endl; +} +``` + +--- + +## Advanced Pattern: Multiple Feature Integration + +Real bots typically use multiple controllers: + +```cpp +void SetupAdvancedBot() { + // Audio: Auto-mute on join + IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); + audioCtrl->SetEvent(new AudioEventListener()); + audioCtrl->JoinVoip(); + audioCtrl->MuteAudio(0, true); + + // Video: Keep video off + IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController(); + videoCtrl->SetEvent(new VideoEventListener()); + // Video off by default when joining with isVideoOff = true + + // Chat: Respond to commands + IMeetingChatController* chatCtrl = meetingService->GetMeetingChatController(); + chatCtrl->SetEvent(new ChatCommandListener()); + chatCtrl->SendChatTo(L"everyone", L"Bot is ready!"); + + // Participants: Track who joins/leaves + IMeetingParticipantsController* participantsCtrl = meetingService->GetMeetingParticipantsController(); + participantsCtrl->SetEvent(new ParticipantsEventListener()); + + // Recording: Capture meeting + IMeetingRecordingController* recordingCtrl = meetingService->GetMeetingRecordingController(); + if (recordingCtrl->CanStartRawRecording() == SDKERR_SUCCESS) { + recordingCtrl->StartRawRecording(); + } +} +``` + +--- + +## Common Patterns Across All Controllers + +### Pattern 1: Check Availability Before Use + +```cpp +IMeetingRecordingController* ctrl = meetingService->GetMeetingRecordingController(); +if (ctrl) { // Controller exists + SDKError canUse = ctrl->CanStartRawRecording(); + if (canUse == SDKERR_SUCCESS) { // Feature available + ctrl->StartRawRecording(); + } +} +``` + +### Pattern 2: User ID = 0 Often Means "Self" + +```cpp +audioCtrl->MuteAudio(0, true); // Mute self +audioCtrl->MuteAudio(userId, true); // Mute specific user +``` + +### Pattern 3: Event Listeners Are Optional + +```cpp +// Some features work without event listener +IMeetingChatController* chatCtrl = meetingService->GetMeetingChatController(); +// Don't need SetEvent() if you only send messages, don't receive +chatCtrl->SendChatTo(L"everyone", L"Hello"); +``` + +### Pattern 4: IList Collections + +```cpp +IList* participantList = participantsCtrl->GetParticipantsList(); +int count = participantList->GetCount(); +for (int i = 0; i < count; i++) { + unsigned int userId = participantList->GetItem(i); + // Use userId... +} +``` + +--- + +## Troubleshooting + +### Controller is nullptr + +**Cause**: Feature not available in this SDK app type or meeting state + +**Solution**: +- Check if you're in a meeting (`MEETING_STATUS_INMEETING`) +- Verify SDK app permissions in Zoom Marketplace +- Some controllers only available to hosts/co-hosts + +### SetEvent() Does Nothing + +**Cause**: Forgot Windows message loop + +**Solution**: See [Windows Message Loop Guide](../troubleshooting/windows-message-loop.md) + +### Methods Return SDKERR_WRONG_USAGE + +**Cause**: Called at wrong time or insufficient permissions + +**Solution**: +- Check method documentation for prerequisites +- Verify you're in correct meeting state +- Check if you're host/co-host (if required) + +--- + +## Summary: The Universal Formula + +``` +1. Read header file → Find I[Feature]Controller and I[Feature]Event +2. Implement event listener → Inherit I[Feature]Event, implement all methods +3. Get controller → meetingService->Get[Feature]Controller() +4. Register listener → controller->SetEvent(new YourListener()) +5. Use features → Call controller methods +``` + +**This pattern works for ALL 35+ controllers!** + +--- + +## Next Steps + +- Browse all available controllers: `SDK/x64/h/meeting_service_interface.h` (lines 1103-1314) +- Pick a feature you want to implement +- Find its header in `SDK/x64/h/meeting_service_components/` +- Follow the universal pattern above + +--- + +## Related Documentation + +- [Interface Methods Guide](../references/interface-methods.md) - How to implement event listeners +- [Authentication Pattern](../examples/authentication-pattern.md) - How to get meetingService +- [Raw Video Capture](../examples/raw-video-capture.md) - Example using recording controller +- [Windows Message Loop](../troubleshooting/windows-message-loop.md) - Required for callbacks + +--- + +**Last Updated**: Based on Zoom Windows Meeting SDK v6.7.2.26830 diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/concepts/singleton-hierarchy.md b/plugins/zoom-developers/skills/meeting-sdk/windows/concepts/singleton-hierarchy.md new file mode 100644 index 00000000..b65b71e3 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/concepts/singleton-hierarchy.md @@ -0,0 +1,404 @@ +# Singleton Hierarchy: Navigation Guide + +## Overview + +The Zoom Windows Meeting SDK uses a **service locator pattern** - a tree of singletons where you navigate from root services down to specific features. You don't construct objects; you traverse to them. + +``` +You want to... You navigate to... +───────────────────────────────────────────────────── +Mute audio IMeetingService → IMeetingAudioController +Create breakout rooms IMeetingService → IMeetingBOController → IBOCreator +Control remote camera IMeetingService → IMeetingVideoController → IMeetingCameraHelper +Start live stream IMeetingService → IMeetingLiveStreamController +Add Q&A questions IMeetingService → IMeetingQAController +Enable interpretation IMeetingService → IMeetingInterpretationController +Batch invite contacts IAuthService → INotificationServiceHelper → IPresenceHelper → IBatchRequestContactHelper +``` + +--- + +## Complete Hierarchy (4 Levels Deep) + +``` +Level 0: Global Factory Functions (zoom_sdk.h) +│ +├─► Level 1: IAuthService +│ ├─► Level 2: IDirectShareServiceHelper [LEAF] +│ └─► Level 2: INotificationServiceHelper +│ └─► Level 3: IPresenceHelper +│ └─► Level 4: IBatchRequestContactHelper [LEAF - MAX DEPTH] +│ +├─► Level 1: IMeetingService +│ │ +│ │ ══════════════════════════════════════════════════════════════ +│ │ CROSS-PLATFORM CONTROLLERS (All platforms) +│ │ ══════════════════════════════════════════════════════════════ +│ │ +│ ├─► Level 2: IMeetingVideoController +│ │ ├─► Level 3: IMeetingCameraHelper [LEAF] +│ │ ├─► Level 3: ISetVideoOrderHelper [LEAF - Windows] +│ │ └─► Level 3: ICameraController [LEAF - Windows] +│ │ +│ ├─► Level 2: IMeetingAudioController [LEAF] +│ ├─► Level 2: IMeetingShareController [LEAF] +│ ├─► Level 2: IMeetingChatController [LEAF] +│ ├─► Level 2: IMeetingRecordingController [LEAF] +│ ├─► Level 2: IMeetingParticipantsController [LEAF] +│ ├─► Level 2: IMeetingWaitingRoomController [LEAF] +│ ├─► Level 2: IMeetingWebinarController [LEAF] +│ ├─► Level 2: IMeetingRawArchivingController [LEAF] +│ ├─► Level 2: IMeetingReminderController [LEAF] +│ ├─► Level 2: IMeetingEncryptionController [LEAF] +│ ├─► Level 2: IMeetingConfiguration [LEAF] +│ ├─► Level 2: IListFactory [LEAF - utility] +│ │ +│ ├─► Level 2: IMeetingBOController (Breakout Rooms) +│ │ ├─► Level 3: IBOCreator +│ │ │ └─► Level 4: IBatchCreateBOHelper [LEAF - MAX DEPTH] +│ │ ├─► Level 3: IBOAdmin [LEAF] +│ │ ├─► Level 3: IBOAssistant [LEAF] +│ │ ├─► Level 3: IBOAttendee [LEAF] +│ │ └─► Level 3: IBOData [LEAF] +│ │ +│ ├─► Level 2: IMeetingAICompanionController +│ │ ├─► Level 3: IMeetingSmartSummaryHelper [LEAF - DEPRECATED] +│ │ ├─► Level 3: IMeetingAICompanionSmartSummaryHelper [LEAF] +│ │ └─► Level 3: IMeetingAICompanionQueryHelper [LEAF] +│ │ +│ │ ══════════════════════════════════════════════════════════════ +│ │ WINDOWS-ONLY CONTROLLERS (#if defined(WIN32)) +│ │ ══════════════════════════════════════════════════════════════ +│ │ +│ ├─► Level 2: IMeetingUIController [LEAF - Windows] +│ │ +│ ├─► Level 2: IAnnotationController +│ │ └─► Level 3: ICustomizedAnnotationController [LEAF - Custom UI] +│ │ +│ ├─► Level 2: IMeetingRemoteController [LEAF - Windows] +│ ├─► Level 2: IMeetingH323Helper [LEAF - Windows] +│ ├─► Level 2: IMeetingPhoneHelper [LEAF - Windows] +│ ├─► Level 2: IMeetingLiveStreamController [LEAF - Windows] +│ ├─► Level 2: IClosedCaptionController [LEAF - Windows] +│ ├─► Level 2: IZoomRealNameAuthMeetingHelper [LEAF - Windows] +│ ├─► Level 2: IMeetingQAController [LEAF - Windows] +│ ├─► Level 2: IMeetingInterpretationController [LEAF - Windows] +│ ├─► Level 2: IMeetingSignInterpretationController [LEAF - Windows] +│ ├─► Level 2: IEmojiReactionController [LEAF - Windows] +│ ├─► Level 2: IMeetingAANController [LEAF - Windows] +│ ├─► Level 2: IMeetingWhiteboardController [LEAF - Windows] +│ ├─► Level 2: IMeetingDocsController [LEAF - Windows] +│ ├─► Level 2: IMeetingPollingController [LEAF - Windows] +│ ├─► Level 2: IMeetingRemoteSupportController [LEAF - Windows] +│ ├─► Level 2: IMeetingIndicatorController [LEAF - Windows] +│ ├─► Level 2: IMeetingProductionStudioController [LEAF - Windows] +│ │ +│ └─► Level 2: ICustomImmersiveController +│ └─► Level 3: ICustomImmersivePreLayoutHelper [LEAF] +│ +├─► Level 1: ISettingService +│ ├─► Level 2: IGeneralSettingContext [LEAF] +│ ├─► Level 2: IAudioSettingContext [LEAF] +│ ├─► Level 2: IVideoSettingContext [LEAF] +│ ├─► Level 2: IRecordingSettingContext [LEAF] +│ ├─► Level 2: IShareSettingContext [LEAF] +│ ├─► Level 2: IStatisticSettingContext [LEAF] +│ └─► Level 2: IWallpaperSettingContext [LEAF] +│ +├─► Level 1: INetworkConnectionHelper [LEAF] +│ +└─► Level 1: ICustomizedUIMgr (Custom UI Mode) + ├─► Level 2: ICustomizedVideoContainer (factory-created) + ├─► Level 2: ICustomizedShareRender (factory-created) + └─► Level 2: ICustomizedImmersiveContainer (factory-created) +``` + +--- + +## Controller Reference by Feature Domain + +### Cross-Platform Controllers + +| Controller | Getter Method | Purpose | +|------------|---------------|---------| +| `IMeetingVideoController` | `GetMeetingVideoController()` | Video on/off, spotlight, pin, virtual background | +| `IMeetingAudioController` | `GetMeetingAudioController()` | Mute/unmute, VoIP, audio device selection | +| `IMeetingShareController` | `GetMeetingShareController()` | Screen/app sharing, share settings | +| `IMeetingChatController` | `GetMeetingChatController()` | In-meeting chat, file transfer | +| `IMeetingRecordingController` | `GetMeetingRecordingController()` | Local/cloud recording control | +| `IMeetingParticipantsController` | `GetMeetingParticipantsController()` | User list, rename, remove, roles | +| `IMeetingWaitingRoomController` | `GetMeetingWaitingRoomController()` | Admit/deny users, waiting room settings | +| `IMeetingWebinarController` | `GetMeetingWebinarController()` | Webinar-specific controls, panelists | +| `IMeetingRawArchivingController` | `GetMeetingRawArchivingController()` | Raw archiving for compliance | +| `IMeetingReminderController` | `GetMeetingReminderController()` | Meeting reminders and notifications | +| `IMeetingEncryptionController` | `GetInMeetingEncryptionController()` | E2E encryption status | +| `IMeetingConfiguration` | `GetMeetingConfiguration()` | Meeting behavior configuration | +| `IMeetingBOController` | `GetMeetingBOController()` | Breakout rooms (has Level 3 helpers) | +| `IMeetingAICompanionController` | `GetMeetingAICompanionController()` | AI Companion features (has Level 3 helpers) | +| `IListFactory` | `GetListFactory()` | Factory for creating SDK list objects | + +### Windows-Only Controllers + +| Controller | Getter Method | Purpose | +|------------|---------------|---------| +| `IMeetingUIController` | `GetUIController()` | SDK UI window control, toolbar customization | +| `IAnnotationController` | `GetAnnotationController()` | Drawing/annotation on shared content | +| `IMeetingRemoteController` | `GetMeetingRemoteController()` | Remote control of shared content | +| `IMeetingH323Helper` | `GetH323Helper()` | H.323/SIP room system integration | +| `IMeetingPhoneHelper` | `GetMeetingPhoneHelper()` | PSTN dial-in/dial-out | +| `IMeetingLiveStreamController` | `GetMeetingLiveStreamController()` | YouTube/Facebook/custom RTMP streaming | +| `IClosedCaptionController` | `GetMeetingClosedCaptionController()` | Closed captions, live transcription | +| `IZoomRealNameAuthMeetingHelper` | `GetMeetingRealNameAuthController()` | China real-name authentication | +| `IMeetingQAController` | `GetMeetingQAController()` | Webinar Q&A feature | +| `IMeetingInterpretationController` | `GetMeetingInterpretationController()` | Language interpretation channels | +| `IMeetingSignInterpretationController` | `GetMeetingSignInterpretationController()` | Sign language interpretation | +| `IEmojiReactionController` | `GetMeetingEmojiReactionController()` | Emoji reactions (👍 🎉 etc.) | +| `IMeetingAANController` | `GetMeetingAANController()` | Advanced Audio Networking | +| `ICustomImmersiveController` | `GetMeetingImmersiveController()` | Immersive view/scenes (has Level 3 helper) | +| `IMeetingWhiteboardController` | `GetMeetingWhiteboardController()` | Collaborative whiteboard | +| `IMeetingDocsController` | `GetMeetingDocsController()` | In-meeting document sharing | +| `IMeetingPollingController` | `GetMeetingPollingController()` | Polls and quizzes | +| `IMeetingRemoteSupportController` | `GetMeetingRemoteSupportController()` | Remote support features | +| `IMeetingIndicatorController` | `GetMeetingIndicatorController()` | UI indicators and status | +| `IMeetingProductionStudioController` | `GetMeetingProductionStudioController()` | Production studio/broadcast features | + +--- + +## When to Use Each Level + +| Level | When | Example | +|-------|------|---------| +| **Level 1** | App startup, before joining | `CreateMeetingService()`, `CreateAuthService()` | +| **Level 2** | After joining meeting, for features | `meetingService->GetMeetingAudioController()` | +| **Level 3** | For specialized sub-features | `boController->GetBOCreatorHelper()` | +| **Level 4** | For batch/bulk operations | `boCreator->GetBatchCreateBOHelper()` | + +--- + +## How to Use (Universal Pattern) + +Every feature follows the **same 3-step pattern**: + +```cpp +// Step 1: Navigate to the controller (singleton) +IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); + +// Step 2: Register event listener (observer pattern) +audioCtrl->SetEvent(new MyAudioEventListener()); + +// Step 3: Call methods +audioCtrl->MuteAudio(userId, true); +``` + +--- + +## Examples by Depth + +### Level 2 - Basic Feature (Audio) + +```cpp +// Get controller +IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); + +// Use it +audioCtrl->JoinVoip(); +audioCtrl->MuteAudio(0, true); // 0 = self +``` + +### Level 3 - Sub-Feature (Breakout Room Creation) + +```cpp +// Navigate: Level 1 → Level 2 → Level 3 +IMeetingBOController* boCtrl = meetingService->GetMeetingBOController(); +IBOCreator* creator = boCtrl->GetBOCreatorHelper(); + +// Use it +creator->CreateBreakoutRoom(L"Room 1"); +creator->AssignUserToBO(strUserID, strBOID); +``` + +### Level 4 - Batch Operations (Bulk Room Creation) + +```cpp +// Navigate: Level 1 → Level 2 → Level 3 → Level 4 +IMeetingBOController* boCtrl = meetingService->GetMeetingBOController(); +IBOCreator* creator = boCtrl->GetBOCreatorHelper(); +IBatchCreateBOHelper* batch = creator->GetBatchCreateBOHelper(); + +// Use it (transaction pattern) +batch->CreateBOTransactionBegin(); +batch->AddNewBoToList(L"Room 1"); +batch->AddNewBoToList(L"Room 2"); +batch->AddNewBoToList(L"Room 3"); +batch->CreateBoTransactionCommit(); // Creates all 3 at once +``` + +--- + +## Why the Hierarchy Exists + +| Depth | Design Purpose | +|-------|----------------| +| **Level 1** (Services) | Lifecycle management - created once, destroyed at cleanup | +| **Level 2** (Controllers) | Feature grouping - one controller per domain | +| **Level 3** (Helpers) | Role-based access - different helpers for host vs attendee | +| **Level 4** (Batch) | Performance optimization - bulk ops instead of N individual calls | + +--- + +## Practical Rules + +### 1. Don't Cache Too Early + +Controllers return `nullptr` if not in meeting: + +```cpp +// WRONG - cached before meeting joined +IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); +meetingService->Join(joinParam); +audioCtrl->MuteAudio(0, true); // audioCtrl might be nullptr! + +// RIGHT - get after joining +meetingService->Join(joinParam); +// ... wait for MEETING_STATUS_INMEETING callback ... +IMeetingAudioController* audioCtrl = meetingService->GetMeetingAudioController(); +if (audioCtrl) { + audioCtrl->MuteAudio(0, true); +} +``` + +### 2. Re-get After State Changes + +After joining/leaving meeting, get controllers again - previous pointers may be invalid. + +### 3. Check for nullptr + +Some helpers only available for hosts: + +```cpp +IBOCreator* creator = boCtrl->GetBOCreatorHelper(); +if (creator) { + // Only hosts get a valid creator + creator->CreateBreakoutRoom(L"Room 1"); +} +``` + +### 4. Batch When Possible + +Level 4 helpers exist specifically for performance: + +```cpp +// SLOW - 10 individual calls +for (int i = 0; i < 10; i++) { + creator->CreateBreakoutRoom(roomNames[i]); +} + +// FAST - 1 batch call +IBatchCreateBOHelper* batch = creator->GetBatchCreateBOHelper(); +batch->CreateBOTransactionBegin(); +for (int i = 0; i < 10; i++) { + batch->AddNewBoToList(roomNames[i]); +} +batch->CreateBoTransactionCommit(); +``` + +--- + +## Deepest Paths (Maximum Depth = 4) + +| Path | Use Case | +|------|----------| +| `IMeetingService` → `IMeetingBOController` → `IBOCreator` → `IBatchCreateBOHelper` | Bulk breakout room creation | +| `IAuthService` → `INotificationServiceHelper` → `IPresenceHelper` → `IBatchRequestContactHelper` | Bulk contact operations | + +--- + +## Quick Reference: Common Navigation Paths + +### Core Meeting Features + +| Feature | Navigation Path | +|---------|-----------------| +| Audio control | `IMeetingService` → `GetMeetingAudioController()` | +| Video control | `IMeetingService` → `GetMeetingVideoController()` | +| Screen sharing | `IMeetingService` → `GetMeetingShareController()` | +| Chat | `IMeetingService` → `GetMeetingChatController()` | +| Recording | `IMeetingService` → `GetMeetingRecordingController()` | +| Participants | `IMeetingService` → `GetMeetingParticipantsController()` | +| Waiting room | `IMeetingService` → `GetMeetingWaitingRoomController()` | +| Breakout rooms | `IMeetingService` → `GetMeetingBOController()` → `GetBO*Helper()` | +| AI Companion | `IMeetingService` → `GetMeetingAICompanionController()` | +| AI Smart Summary | `IMeetingService` → `GetMeetingAICompanionController()` → `GetMeetingAICompanionSmartSummaryHelper()` | +| AI Query | `IMeetingService` → `GetMeetingAICompanionController()` → `GetMeetingAICompanionQueryHelper()` | +| Remote camera | `IMeetingService` → `GetMeetingVideoController()` → `GetMeetingCameraHelper()` | +| Video order (gallery) | `IMeetingService` → `GetMeetingVideoController()` → `GetSetVideoOrderHelper()` | +| Local camera device | `IMeetingService` → `GetMeetingVideoController()` → `GetMyCameraController()` | + +### Windows-Only Features + +| Feature | Navigation Path | +|---------|-----------------| +| Live streaming | `IMeetingService` → `GetMeetingLiveStreamController()` | +| Q&A (webinars) | `IMeetingService` → `GetMeetingQAController()` | +| Interpretation | `IMeetingService` → `GetMeetingInterpretationController()` | +| Sign language | `IMeetingService` → `GetMeetingSignInterpretationController()` | +| Closed captions | `IMeetingService` → `GetMeetingClosedCaptionController()` | +| Annotations | `IMeetingService` → `GetAnnotationController()` | +| Annotations (Custom UI) | `IMeetingService` → `GetAnnotationController()` → `GetCustomizedAnnotationController()` | +| Emoji reactions | `IMeetingService` → `GetMeetingEmojiReactionController()` | +| Polling | `IMeetingService` → `GetMeetingPollingController()` | +| Whiteboard | `IMeetingService` → `GetMeetingWhiteboardController()` | +| Docs | `IMeetingService` → `GetMeetingDocsController()` | +| H.323/SIP | `IMeetingService` → `GetH323Helper()` | +| Phone dial-in/out | `IMeetingService` → `GetMeetingPhoneHelper()` | +| Remote control | `IMeetingService` → `GetMeetingRemoteController()` | +| Immersive view | `IMeetingService` → `GetMeetingImmersiveController()` | +| UI control | `IMeetingService` → `GetUIController()` | + +### Settings & Pre-Meeting + +| Feature | Navigation Path | +|---------|-----------------| +| Audio settings | `ISettingService` → `GetAudioSettings()` | +| Video settings | `ISettingService` → `GetVideoSettings()` | +| Recording settings | `ISettingService` → `GetRecordingSettings()` | +| Share settings | `ISettingService` → `GetShareSettings()` | +| Presence/contacts | `IAuthService` → `GetNotificationServiceHelper()` → `GetPresenceHelper()` | + +--- + +## Deprecated Controllers & Helpers + +| Deprecated | Replacement | +|------------|-------------| +| `IMeetingSmartSummaryController` | Use `IMeetingAICompanionController` | +| `IMeetingSmartSummaryHelper` | Use `IMeetingAICompanionSmartSummaryHelper` via `GetMeetingAICompanionSmartSummaryHelper()` | + +--- + +## Platform Availability Summary + +| Category | Count | Platform | +|----------|-------|----------| +| Cross-platform controllers | 15 | Windows, macOS, Linux | +| Windows-only controllers | 20 | Windows only (`#if defined(WIN32)`) | +| **Total** | **35** | — | + +> **Note**: When developing cross-platform apps, use `#if defined(WIN32)` guards around Windows-only controller access. + +--- + +## Related Documentation + +- [SDK Architecture Pattern](sdk-architecture-pattern.md) - The universal 3-step pattern +- [Custom UI Architecture](custom-ui-architecture.md) - Custom UI specific hierarchy +- [Breakout Rooms Example](../examples/breakout-rooms.md) - Level 3 helpers in action +- [Chat Example](../examples/chat.md) - IMeetingChatController usage +- [Captions/Transcription Example](../examples/captions-transcription.md) - IClosedCaptionController usage +- [Local Recording Example](../examples/local-recording.md) - IMeetingRecordingController usage +- [Video Advanced Example](../examples/video-advanced.md) - Camera control, video order (Level 3 helpers) +- [AI Companion Example](../examples/ai-companion.md) - Smart Summary, AI Query (Level 3 helpers) + +--- + +**TL;DR**: The hierarchy is your navigation map. Start at a service, drill down to the feature you need, then call methods. Deeper levels = more specialized operations. Windows has 20 additional controllers not available on other platforms. diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/examples/ai-companion.md b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/ai-companion.md new file mode 100644 index 00000000..e398859a --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/ai-companion.md @@ -0,0 +1,539 @@ +# AI Companion Features: Smart Summary & Query + +## Overview + +The `IMeetingAICompanionController` provides Level 3 sub-helpers for AI-powered meeting features: + +| Helper | Getter | Status | Purpose | +|--------|--------|--------|---------| +| `IMeetingSmartSummaryHelper` | `GetMeetingSmartSummaryHelper()` | **DEPRECATED** | Legacy smart summary API | +| `IMeetingAICompanionSmartSummaryHelper` | `GetMeetingAICompanionSmartSummaryHelper()` | Current | AI-powered meeting summaries | +| `IMeetingAICompanionQueryHelper` | `GetMeetingAICompanionQueryHelper()` | Current | AI Q&A about meeting content | + +--- + +## Navigation Path + +``` +IMeetingService + └─► GetMeetingAICompanionController() + ├─► GetMeetingSmartSummaryHelper() // DEPRECATED + ├─► GetMeetingAICompanionSmartSummaryHelper() // Smart Summary + └─► GetMeetingAICompanionQueryHelper() // AI Query/Q&A +``` + +--- + +## Prerequisites + +AI Companion features require: +1. Feature enabled in Zoom account settings +2. Host privileges (for starting features) +3. Meeting in progress (`MEETING_STATUS_INMEETING`) + +--- + +## 1. Smart Summary (IMeetingAICompanionSmartSummaryHelper) + +AI-generated meeting summaries that capture key points, action items, and decisions. + +### Get the Helper + +```cpp +IMeetingAICompanionController* aiCtrl = meetingService->GetMeetingAICompanionController(); +if (!aiCtrl) return; + +IMeetingAICompanionSmartSummaryHelper* summaryHelper = + aiCtrl->GetMeetingAICompanionSmartSummaryHelper(); +if (!summaryHelper) return; +``` + +### Set Up Event Handler + +```cpp +class SmartSummaryEventHandler : public IMeetingAICompanionSmartSummaryHelperEvent { +public: + void onSmartSummaryStateNotSupported() override { + // Meeting doesn't support smart summary + // Account may not have the feature enabled + } + + void onSmartSummaryStateSupportedButDisabled( + IMeetingEnableSmartSummaryHandler* handler) override + { + // Feature supported but needs to be enabled + if (handler) { + if (handler->IsForRequest()) { + // This is a request from another user + // Host should decide whether to enable + } else { + // Current user can enable directly + handler->EnableSmartSummary(); + } + } + } + + void onSmartSummaryStateEnabledButNotStarted( + IMeetingStartSmartSummaryHandler* handler) override + { + // Feature enabled, ready to start + if (handler) { + if (handler->IsForRequest()) { + // Request from another user to start + } else { + // Can start directly + handler->StartSmartSummary(); + } + } + } + + void onSmartSummaryStateStarted( + IMeetingStopSmartSummaryHandler* handler) override + { + // Smart summary is now active + // handler is nullptr if current user can't stop it + if (handler) { + // Can stop if needed + // handler->StopSmartSummary(); + } + } + + void onFailedToStartSmartSummary(bool bTimeout) override { + if (bTimeout) { + // Request timed out + } else { + // Host/cohost declined the request + } + } + + void onSmartSummaryEnableRequestReceived( + IMeetingApproveEnableSmartSummaryHandler* handler) override + { + // Another user requested to enable smart summary + // Host/cohost receives this callback + if (handler) { + unsigned int requesterId = handler->GetSenderUserID(); + // Approve or handle the request + handler->ContinueApprove(); + } + } + + void onSmartSummaryStartRequestReceived( + IMeetingApproveStartSmartSummaryHandler* handler) override + { + // Another user requested to start smart summary + if (handler) { + unsigned int requesterId = handler->GetSenderUserID(); + handler->Approve(); // or handler->Decline(); + } + } + + void onSmartSummaryEnableActionCallback( + IMeetingEnableSmartSummaryActionHandler* handler) override + { + // Confirmation dialog for enabling smart summary + if (handler) { + const zchar_t* title = handler->GetTipTitle(); + const zchar_t* tip = handler->GetTipString(); + // Show UI with tip, then: + handler->Confirm(); // or handler->Cancel(); + } + } +}; + +// Register the handler +SmartSummaryEventHandler summaryHandler; +summaryHelper->SetEvent(&summaryHandler); +``` + +--- + +## 2. AI Query (IMeetingAICompanionQueryHelper) + +Ask questions about meeting content and get AI-generated answers. + +### Get the Helper + +```cpp +IMeetingAICompanionController* aiCtrl = meetingService->GetMeetingAICompanionController(); +if (!aiCtrl) return; + +IMeetingAICompanionQueryHelper* queryHelper = + aiCtrl->GetMeetingAICompanionQueryHelper(); +if (!queryHelper) return; +``` + +### Set Up Event Handler + +```cpp +class AIQueryEventHandler : public IMeetingAICompanionQueryHelperEvent { +public: + void onQueryStateNotSupported() override { + // Meeting doesn't support AI query + } + + void onQueryStateSupportedButDisabled( + IMeetingEnableQueryHandler* pHandler) override + { + // Query supported but disabled + if (pHandler && !pHandler->IsForRequest()) { + pHandler->EnableQuery(); + } + } + + void onQueryStateEnabledButNotStarted( + IMeetingStartQueryHandler* pHandler) override + { + // Query enabled, ready to start + if (pHandler && !pHandler->IsForRequest()) { + pHandler->StartMeetingQuery(); + } + } + + void onQueryStateStarted(IMeetingSendQueryHandler* pHandler) override { + // Query is active - can now send questions + if (pHandler) { + // Get suggested questions + IList* defaultQuestions = + pHandler->GetDefaultQueryQuestions(); + if (defaultQuestions) { + for (int i = 0; i < defaultQuestions->GetCount(); i++) { + const zchar_t* question = defaultQuestions->GetItem(i); + // Display in UI + } + } + + // Check if we can send queries + if (pHandler->CanSendQuery()) { + // Send a question + pHandler->SendQueryQuestion(L"What were the main action items?"); + } else { + // Need to request privilege + pHandler->RequestSendQueryPrivilege(); + } + } + } + + void onReceiveQueryAnswer(IMeetingAICompanionQueryItem* pQueryItem) override { + // Received an answer to our question + if (pQueryItem) { + const zchar_t* queryId = pQueryItem->GetQueryID(); + const zchar_t* question = pQueryItem->GetQustionContent(); + const zchar_t* answer = pQueryItem->GetAnswerContent(); + time_t timestamp = pQueryItem->GetTimeStamp(); + + MeetingAICompanionQueryRequestError errorCode = pQueryItem->GetErrorCode(); + if (errorCode == MeetingAICompanionQueryRequestError_OK) { + // Display answer to user + + // Send feedback if user indicates quality + // pQueryItem->Feedback(MeetingAICompanionQueryFeedbackType_Good); + // or + // pQueryItem->Feedback(MeetingAICompanionQueryFeedbackType_Bad); + } else { + // Handle error + const zchar_t* errorMsg = pQueryItem->GetErrorMsg(); + } + } + } + + void onQuerySettingChanged(MeetingAICompanionQuerySettingOptions eSetting) override { + // Query settings changed + switch (eSetting) { + case MeetingAICompanionQuerySettingOptions_WhenQueryStarted: + // All can ask about discussions since AI started + break; + case MeetingAICompanionQuerySettingOptions_WhenParticipantsJoin: + // Can ask about discussions since they joined + break; + case MeetingAICompanionQuerySettingOptions_OnlyHost: + // Only host can ask questions + break; + // ... handle other settings + } + } + + void onFailedToStartQuery(bool bTimeout) override { + if (bTimeout) { + // Request timed out + } else { + // Request declined + } + } + + void onSendQueryPrivilegeChanged(bool canSendQuery) override { + // Privilege to send queries changed + if (canSendQuery) { + // Can now send questions + } else { + // Lost ability to send questions + } + } + + void onFailedToRequestSendQuery(bool bTimeout) override { + // Failed to get send query privilege + } + + // ... other callbacks for request handling + void onReceiveRequestToEnableQuery( + IMeetingApproveEnableQueryHandler* pHandler) override {} + void onReceiveRequestToStartQuery( + IMeetingApproveStartQueryHandler* pHandler) override {} + void onQueryEnableActionCallback( + IMeetingEnableQueryActionHandler* pHandler) override {} + void onReceiveRequestToSendQuery( + IMeetingApproveSendQueryHandler* pHandler) override {} +}; + +// Register the handler +AIQueryEventHandler queryHandler; +queryHelper->SetEvent(&queryHandler); +``` + +### Change Query Settings (Host Only) + +```cpp +bool canChange = false; +SDKError err = queryHelper->CanChangeQuerySetting(canChange); +if (err == SDKERR_SUCCESS && canChange) { + // Set who can ask questions + queryHelper->ChangeQuerySettings( + MeetingAICompanionQuerySettingOptions_WhenParticipantsJoin); + + // Get current setting + MeetingAICompanionQuerySettingOptions currentSetting = + queryHelper->GetSelectedQuerySetting(); +} +``` + +### Legal Notices + +```cpp +bool isAvailable = false; +queryHelper->IsAICompanionQueryLegalNoticeAvailable(isAvailable); +if (isAvailable) { + const zchar_t* prompt = queryHelper->GetAICompanionQueryLegalNoticesPrompt(); + const zchar_t* explained = queryHelper->GetAICompanionQueryLegalNoticesExplained(); + // Display legal notice to user +} +``` + +--- + +## 3. Controller-Level Operations + +The main controller provides global AI Companion controls. + +### Turn All AI Features On/Off + +```cpp +IMeetingAICompanionController* aiCtrl = meetingService->GetMeetingAICompanionController(); + +// Check support +if (aiCtrl->IsTurnoffAllAICompanionsSupported()) { + // Can turn off all AI features + if (aiCtrl->CanTurnOffAllAICompanions()) { + bool deleteAssets = false; // Keep or delete meeting assets + aiCtrl->TurnOffAllAICompanions(deleteAssets); + } +} + +if (aiCtrl->IsTurnOnAllAICompanionsSupported()) { + // Can turn on all AI features + if (aiCtrl->CanTurnOnAllAICompanions()) { + aiCtrl->TurnOnAllAICompanions(); + } +} +``` + +### Request Host to Toggle AI Features + +```cpp +// For non-host users +if (aiCtrl->CanRequestTurnoffAllAICompanions()) { + aiCtrl->RequestTurnoffAllAICompanions(); +} + +if (aiCtrl->CanRequestTurnOnAllAICompanions()) { + aiCtrl->RequestTurnOnAllAICompanions(); +} +``` + +### Controller Event Handler + +```cpp +class AICompanionCtrlEventHandler : public IMeetingAICompanionCtrlEvent { +public: + void onAICompanionFeatureTurnOffByParticipant( + IAICompanionFeatureTurnOnAgainHandler* handler) override + { + // Participant turned off AI feature before host joined + if (handler) { + IList* features = handler->GetFeatureList(); + IList* deletedAssets = + handler->GetAssetsDeletedFeatureList(); + + // Host can turn features back on + handler->TurnOnAgain(); + // Or agree to keep them off + // handler->AgreeTurnOff(); + } + } + + void onAICompanionFeatureSwitchRequested( + IAICompanionFeatureSwitchHandler* handler) override + { + // User requested to toggle AI features + if (handler) { + unsigned int requesterId = handler->GetRequestUserID(); + bool isTurningOn = handler->IsTurnOn(); + + bool deleteAssets = false; + handler->Agree(deleteAssets); + // or handler->Decline(); + } + } + + void onAICompanionFeatureSwitchRequestResponse( + bool bTimeout, bool bAgree, bool bTurnOn) override + { + // Response to our request + if (!bTimeout && bAgree) { + // Request approved + } + } + + void onAICompanionFeatureCanNotBeTurnedOff( + IList* features) override + { + // These features cannot be turned off + if (features) { + for (int i = 0; i < features->GetCount(); i++) { + AICompanionFeature feature = features->GetItem(i); + // Handle each feature + } + } + } + + void onHostUnsupportedStopNotesRequest() override { + // Host's client doesn't support stopping Notes + } +}; + +// Register controller event handler +AICompanionCtrlEventHandler ctrlHandler; +aiCtrl->SetEvent(&ctrlHandler); +``` + +--- + +## Complete Example: Smart Summary Flow + +```cpp +class SmartSummaryManager { +private: + IMeetingAICompanionController* m_aiCtrl = nullptr; + IMeetingAICompanionSmartSummaryHelper* m_summaryHelper = nullptr; + + class SummaryHandler : public IMeetingAICompanionSmartSummaryHelperEvent { + public: + SmartSummaryManager* m_manager = nullptr; + + void onSmartSummaryStateNotSupported() override { + m_manager->OnFeatureNotSupported(); + } + + void onSmartSummaryStateSupportedButDisabled( + IMeetingEnableSmartSummaryHandler* handler) override { + if (handler && !handler->IsForRequest()) { + handler->EnableSmartSummary(); + } + } + + void onSmartSummaryStateEnabledButNotStarted( + IMeetingStartSmartSummaryHandler* handler) override { + if (handler && !handler->IsForRequest()) { + handler->StartSmartSummary(); + } + } + + void onSmartSummaryStateStarted( + IMeetingStopSmartSummaryHandler* handler) override { + m_manager->OnSummaryStarted(handler); + } + + void onFailedToStartSmartSummary(bool bTimeout) override { + m_manager->OnStartFailed(bTimeout); + } + + void onSmartSummaryEnableRequestReceived( + IMeetingApproveEnableSmartSummaryHandler* handler) override { + if (handler) handler->ContinueApprove(); + } + + void onSmartSummaryStartRequestReceived( + IMeetingApproveStartSmartSummaryHandler* handler) override { + if (handler) handler->Approve(); + } + + void onSmartSummaryEnableActionCallback( + IMeetingEnableSmartSummaryActionHandler* handler) override { + if (handler) handler->Confirm(); + } + }; + + SummaryHandler m_handler; + IMeetingStopSmartSummaryHandler* m_stopHandler = nullptr; + +public: + bool Initialize(IMeetingService* meetingService) { + m_aiCtrl = meetingService->GetMeetingAICompanionController(); + if (!m_aiCtrl) return false; + + m_summaryHelper = m_aiCtrl->GetMeetingAICompanionSmartSummaryHelper(); + if (!m_summaryHelper) return false; + + m_handler.m_manager = this; + m_summaryHelper->SetEvent(&m_handler); + return true; + } + + void OnFeatureNotSupported() { + // Update UI - feature not available + } + + void OnSummaryStarted(IMeetingStopSmartSummaryHandler* handler) { + m_stopHandler = handler; + // Update UI - summary is recording + } + + void OnStartFailed(bool timeout) { + // Show error message + } + + void StopSummary() { + if (m_stopHandler) { + m_stopHandler->StopSmartSummary(); + m_stopHandler = nullptr; + } + } +}; +``` + +--- + +## AI Companion Features Summary + +| Feature | Description | Assets Generated | +|---------|-------------|------------------| +| `SMART_SUMMARY` | AI meeting summary | Summary document | +| `QUERY` | AI Q&A about meeting | Transcript | +| `SMART_RECORDING` | AI-enhanced recording | Recording with AI insights | + +--- + +## Related Documentation + +- [Singleton Hierarchy](../concepts/singleton-hierarchy.md) - Complete navigation map +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Universal 3-step pattern +- [Captions & Transcription](captions-transcription.md) - Related live transcription features diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/examples/authentication-pattern.md b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/authentication-pattern.md new file mode 100644 index 00000000..d0268b30 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/authentication-pattern.md @@ -0,0 +1,702 @@ +# Authentication Flow Pattern + +Complete guide to authenticating with the Zoom Windows SDK using JWT tokens. + +--- + +## Overview + +Authentication is the first required step before joining meetings. The SDK uses JWT (JSON Web Token) authentication for Meeting SDK apps. + +### Flow Sequence + +``` +1. Initialize SDK (InitSDK) +2. [OPTIONAL] Register Network Connection Handler for proxy detection + 2a. Wait for onProxyDetectComplete() callback +3. Create Auth Service (CreateAuthService) +4. Set Event Listener (SetEvent) +5. Call SDKAuth with JWT token +6. Process Windows messages (CRITICAL!) +7. Wait for onAuthenticationReturn callback +8. Create Meeting Service (CreateMeetingService) +9. Join/Start meeting +10. Wait for MEETING_STATUS_INMEETING callback +11. NOW safe to use controllers (GetMeetingAudioController, etc.) +``` + +### State Machine + +``` +┌──────────────┐ InitSDK() ┌──────────────┐ +│ UNINITIALIZED │ ─────────────► │ INITIALIZED │ +└──────────────┘ └───────┬──────┘ + │ + │ CreateAuthService() + SDKAuth() + ▼ +┌──────────────┐ onAuthenticationReturn ┌──────────────┐ +│ MEETING │ ◄─────────────────── │ AUTHENTICATING │ +│ READY │ (AUTHRET_SUCCESS) └──────────────┘ +└───────┬──────┘ + │ + │ Join() or Start() + ▼ +┌──────────────┐ onMeetingStatusChanged ┌──────────────┐ +│ IN MEETING │ ◄─────────────────── │ JOINING │ +│ (Controllers OK)│ (MEETING_STATUS_ └──────────────┘ +└──────────────┘ INMEETING) +``` + +--- + +## Complete Working Example + +```cpp +#include +#include +#include +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +// Global state +bool g_authenticated = false; +bool g_exit = false; +IAuthService* g_authService = nullptr; + +// Step 1: Create Event Listener +class MyAuthListener : public IAuthServiceEvent { +public: + void onAuthenticationReturn(AuthResult ret) override { + std::cout << "[AUTH] Callback received! Result: " << ret << std::endl; + + switch (ret) { + case AUTHRET_SUCCESS: + std::cout << "[AUTH] Authentication successful!" << std::endl; + g_authenticated = true; + break; + case AUTHRET_KEYORSECRETEMPTY: + std::cerr << "[AUTH] ERROR: SDK Key or Secret is empty" << std::endl; + break; + case AUTHRET_JWTTOKENWRONG: + std::cerr << "[AUTH] ERROR: JWT token is invalid" << std::endl; + break; + case AUTHRET_OVERTIME: + std::cerr << "[AUTH] ERROR: Authentication timeout" << std::endl; + break; + case AUTHRET_NETWORKISSUE: + std::cerr << "[AUTH] ERROR: Network connection issue" << std::endl; + break; + default: + std::cerr << "[AUTH] ERROR: Unknown error: " << ret << std::endl; + } + } + + void onLoginReturnWithReason(LOGINSTATUS ret, IAccountInfo* info, LoginFailReason reason) override { + // Not used for JWT auth + } + + void onLogout() override { + std::cout << "[AUTH] Logged out" << std::endl; + } + + void onZoomIdentityExpired() override { + std::cout << "[AUTH] Zoom identity expired" << std::endl; + } + + void onZoomAuthIdentityExpired() override { + std::cout << "[AUTH] Zoom auth identity expired" << std::endl; + } + + #if defined(WIN32) + void onNotificationServiceStatus(SDKNotificationServiceStatus status, + SDKNotificationServiceError error) override { + std::cout << "[AUTH] Notification service status: " << status << std::endl; + } + #endif +}; + +// Step 2: Initialize SDK +bool InitializeSDK() { + std::cout << "[1/3] Initializing SDK..." << std::endl; + + InitParam initParam; + initParam.strWebDomain = L"https://zoom.us"; + initParam.strSupportUrl = L"https://zoom.us"; + initParam.emLanguageID = LANGUAGE_English; + initParam.enableLogByDefault = true; + initParam.enableGenerateDump = true; + + SDKError err = InitSDK(initParam); + if (err != SDKERR_SUCCESS) { + std::cerr << "ERROR: InitSDK failed: " << err << std::endl; + return false; + } + + std::cout << "SDK initialized successfully" << std::endl; + return true; +} + +// Step 3: Authenticate +bool AuthenticateSDK(const std::wstring& jwt_token) { + std::cout << "[2/3] Authenticating..." << std::endl; + + // Create auth service + SDKError err = CreateAuthService(&g_authService); + if (err != SDKERR_SUCCESS || !g_authService) { + std::cerr << "ERROR: CreateAuthService failed: " << err << std::endl; + return false; + } + std::cout << "Auth service created" << std::endl; + + // Set event listener + err = g_authService->SetEvent(new MyAuthListener()); + if (err != SDKERR_SUCCESS) { + std::cerr << "ERROR: SetEvent failed: " << err << std::endl; + return false; + } + std::cout << "Event listener set" << std::endl; + + // Validate JWT token + if (jwt_token.empty()) { + std::cerr << "ERROR: JWT token is empty!" << std::endl; + return false; + } + std::cout << "JWT token length: " << jwt_token.length() << " characters" << std::endl; + + // Create auth context + AuthContext authContext; + authContext.jwt_token = jwt_token.c_str(); + + // Call SDKAuth + err = g_authService->SDKAuth(authContext); + if (err != SDKERR_SUCCESS) { + std::cerr << "ERROR: SDKAuth failed: " << err << std::endl; + return false; + } + + std::cout << "SDKAuth called successfully, waiting for callback..." << std::endl; + return true; +} + +// Step 4: Wait for Authentication (WITH MESSAGE LOOP!) +bool WaitForAuthentication(int timeoutSeconds = 30) { + std::cout << "[3/3] Waiting for authentication..." << std::endl; + + auto startTime = std::chrono::steady_clock::now(); + + while (!g_authenticated && !g_exit) { + // CRITICAL: Process Windows messages for SDK callbacks! + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Check timeout + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - startTime).count(); + + if (elapsed >= timeoutSeconds) { + std::cerr << "ERROR: Authentication timeout after " << timeoutSeconds << " seconds" << std::endl; + return false; + } + + // Small sleep to avoid CPU spinning + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + return g_authenticated; +} + +// Main function +int main() { + // Your JWT token here + std::wstring jwt_token = L"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; + + // Step 1: Initialize SDK + if (!InitializeSDK()) { + return 1; + } + + // Step 2: Authenticate + if (!AuthenticateSDK(jwt_token)) { + return 1; + } + + // Step 3: Wait for callback + if (!WaitForAuthentication()) { + return 1; + } + + std::cout << "\n✓ Authentication complete! Ready to join meeting." << std::endl; + + // Now you can join meetings... + + // Cleanup + if (g_authService) { + DestroyAuthService(g_authService); + } + CleanUPSDK(); + + return 0; +} +``` + +--- + +## Optional: Network/Proxy Detection + +If your app needs to work behind corporate proxies, register the network connection handler **after InitSDK** but **before authentication**: + +```cpp +#include + +class MyNetworkHandler : public INetworkConnectionHandler { +public: + void onProxyDetectComplete() override { + std::cout << "[NETWORK] Proxy detection complete" << std::endl; + // NOW safe to proceed with authentication + g_proxyDetected = true; + } + + void onProxySettingNotification(IProxySettingHandler* handler) override { + // Handle proxy settings if needed + std::cout << "[NETWORK] Proxy settings notification" << std::endl; + } + + void onSSLCertVerifyNotification(ISSLCertVerificationHandler* handler) override { + // Handle SSL cert verification if needed + std::cout << "[NETWORK] SSL cert verification" << std::endl; + } +}; + +bool WaitForProxyDetection() { + // Create network helper + INetworkConnectionHelper* networkHelper = nullptr; + CreateNetworkConnectionHelper(&networkHelper); + + if (networkHelper) { + networkHelper->RegisterNetworkConnectionHandler(new MyNetworkHandler()); + + // Wait for onProxyDetectComplete callback + while (!g_proxyDetected) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + return true; + } + return false; +} +``` + +> **When to use**: Only needed if you're behind a corporate proxy or need SSL certificate handling. Most apps can skip this step. + +--- + +## JWT Token Setup + +### Generating JWT Token + +1. **Go to Zoom Marketplace**: https://marketplace.zoom.us/ +2. **Create/Select App**: Go to "Develop" → "Build App" → "Meeting SDK" +3. **Get Credentials**: Find "App Credentials" tab + - SDK Key (Client ID) + - SDK Secret (Client Secret) +4. **Generate JWT**: Use the built-in JWT generator or create manually + +### JWT Token Format + +Valid JWT token: +- **Length**: 200-500 characters +- **Starts with**: `eyJ` +- **Contains**: Two periods (`.`) separating three parts +- **Example**: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBLZXk...` + +### Loading from Configuration File + +```cpp +#include +#include + +bool LoadJWTFromConfig(std::wstring& jwt_token) { + std::ifstream f("config.json"); + if (!f.is_open()) { + std::cerr << "ERROR: config.json not found" << std::endl; + return false; + } + + Json::Value config; + try { + f >> config; + } catch (const std::exception& e) { + std::cerr << "ERROR: Failed to parse config.json: " << e.what() << std::endl; + return false; + } + + if (config["sdk_jwt"].empty()) { + std::cerr << "ERROR: sdk_jwt not found in config.json" << std::endl; + return false; + } + + std::string jwt_str = config["sdk_jwt"].asString(); + jwt_token = std::wstring(jwt_str.begin(), jwt_str.end()); + + return true; +} +``` + +**config.json**: +```json +{ + "sdk_jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "meeting_number": "1234567890", + "passcode": "meeting_password" +} +``` + +--- + +## Common Authentication Errors + +### AuthResult Error Codes + +| Code | Enum | Meaning | Solution | +|------|------|---------|----------| +| 0 | `AUTHRET_SUCCESS` | ✓ Success | Continue to join meeting | +| 1 | `AUTHRET_KEYORSECRETEMPTY` | SDK Key/Secret empty | Check JWT token | +| 3 | `AUTHRET_JWTTOKENWRONG` | Invalid JWT | Regenerate token | +| 4 | `AUTHRET_OVERTIME` | Request timeout | Check network | +| 5 | `AUTHRET_NETWORKISSUE` | Network problem | Check firewall/internet | +| 7 | `AUTHRET_CLIENT_INCOMPATIBLE` | SDK version mismatch | Update SDK | +| 10 | `AUTHRET_JWTTOKENEXPIRED` | JWT expired | Generate fresh token | + +### Timeout (No Callback) + +**Symptom**: Waiting forever, no callback received + +**Causes**: +1. **Missing Windows message loop** (most common!) +2. Network/firewall blocking +3. Invalid JWT format + +**Debug**: +```cpp +void onAuthenticationReturn(AuthResult ret) override { + std::cout << "CALLBACK FIRED! Result: " << ret << std::endl; // Does this print? +} +``` + +If you never see "CALLBACK FIRED!", you're not processing Windows messages! + +### Invalid JWT Token + +**Symptom**: `AUTHRET_JWTTOKENWRONG` (code 3) + +**Causes**: +1. Token expired (typically 24-48 hours) +2. Wrong SDK Key/Secret used to generate token +3. Token generated for different app +4. Malformed token + +**Solution**: +- Generate fresh JWT token from Zoom Marketplace +- Verify SDK credentials match +- Check token format (starts with "eyJ", has two dots) + +### Network Issues + +**Symptom**: `AUTHRET_NETWORKISSUE` (code 5) or timeout + +**Solutions**: +- Check internet connection +- Verify can access zoom.us from browser +- Check Windows Firewall settings +- If behind corporate proxy, may need proxy configuration +- Temporarily disable antivirus to test + +--- + +## Best Practices + +### 1. Validate JWT Before Using + +```cpp +bool ValidateJWT(const std::wstring& jwt_token) { + // Check length + if (jwt_token.length() < 100) { + std::cerr << "JWT token too short: " << jwt_token.length() << std::endl; + return false; + } + + // Check starts with "eyJ" + if (jwt_token.substr(0, 3) != L"eyJ") { + std::cerr << "JWT token doesn't start with 'eyJ'" << std::endl; + return false; + } + + // Check has two dots (three parts) + int dotCount = 0; + for (wchar_t c : jwt_token) { + if (c == L'.') dotCount++; + } + if (dotCount != 2) { + std::cerr << "JWT token should have 2 dots, found: " << dotCount << std::endl; + return false; + } + + return true; +} +``` + +### 2. Add Timeout with Progress Updates + +```cpp +bool WaitForAuthenticationWithProgress(int timeoutSeconds = 30) { + auto startTime = std::chrono::steady_clock::now(); + int lastProgressSeconds = 0; + + while (!g_authenticated && !g_exit) { + // Process messages + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Show progress every 5 seconds + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - startTime).count(); + + if (elapsed > lastProgressSeconds && elapsed % 5 == 0) { + std::cout << "Still waiting... (" << elapsed << "s)" << std::endl; + lastProgressSeconds = elapsed; + } + + if (elapsed >= timeoutSeconds) { + return false; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + return g_authenticated; +} +``` + +### 3. Cleanup on Failure + +```cpp +void Cleanup() { + if (g_authService) { + DestroyAuthService(g_authService); + g_authService = nullptr; + } + CleanUPSDK(); +} + +int main() { + if (!InitializeSDK()) { + return 1; + } + + if (!AuthenticateSDK(jwt_token)) { + Cleanup(); // Always cleanup on failure + return 1; + } + + if (!WaitForAuthentication()) { + Cleanup(); + return 1; + } + + // ... use SDK ... + + Cleanup(); // Cleanup on success too + return 0; +} +``` + +--- + +## After Authentication: Join/Start Meeting + +Once authenticated, create the meeting service and join: + +```cpp +#include + +IMeetingService* g_meetingService = nullptr; +bool g_inMeeting = false; + +class MyMeetingListener : public IMeetingServiceEvent { +public: + void onMeetingStatusChanged(MeetingStatus status, int iResult) override { + std::cout << "[MEETING] Status changed: " << status << std::endl; + + switch (status) { + case MEETING_STATUS_IDLE: + std::cout << "[MEETING] Idle" << std::endl; + break; + case MEETING_STATUS_CONNECTING: + std::cout << "[MEETING] Connecting..." << std::endl; + break; + case MEETING_STATUS_WAITINGFORHOST: + std::cout << "[MEETING] Waiting for host..." << std::endl; + break; + case MEETING_STATUS_INMEETING: + std::cout << "[MEETING] IN MEETING - Controllers now available!" << std::endl; + g_inMeeting = true; + SetupControllers(); // NOW safe to get controllers + break; + case MEETING_STATUS_DISCONNECTING: + std::cout << "[MEETING] Disconnecting..." << std::endl; + break; + case MEETING_STATUS_ENDED: + std::cout << "[MEETING] Ended" << std::endl; + g_inMeeting = false; + break; + case MEETING_STATUS_FAILED: + std::cerr << "[MEETING] FAILED - Error: " << iResult << std::endl; + break; + } + } + + void onMeetingStatisticsWarningNotification(StatisticsWarningType type) override {} + void onMeetingParameterNotification(const MeetingParameter* param) override {} + void onSuspendParticipantsActivities() override {} + void onAICompanionActiveChangeNotice(bool active) override {} + void onMeetingTopicChanged(const zchar_t* topic) override {} + void onMeetingFullToWatchLiveStream(const zchar_t* url) override {} + void onUserNetworkStatusChanged(MeetingComponentType type, ConnectionQuality quality, + unsigned int userId, bool uplink) override {} +#if defined(WIN32) + void onAppSignalPanelUpdated(IMeetingAppSignalHandler* handler) override {} +#endif +}; + +bool CreateAndJoinMeeting(UINT64 meetingNumber, const wchar_t* passcode, const wchar_t* userName) { + // Step 1: Create meeting service + SDKError err = CreateMeetingService(&g_meetingService); + if (err != SDKERR_SUCCESS || !g_meetingService) { + std::cerr << "ERROR: CreateMeetingService failed: " << err << std::endl; + return false; + } + + // Step 2: Set event listener + g_meetingService->SetEvent(new MyMeetingListener()); + + // Step 3: Prepare join parameters + JoinParam joinParam; + joinParam.userType = SDK_UT_WITHOUT_LOGIN; + + JoinParam4WithoutLogin& param = joinParam.param.withoutloginuserJoin; + param.meetingNumber = meetingNumber; + param.userName = userName; + param.psw = passcode; + param.isVideoOff = true; // Bot typically joins with video off + param.isAudioOff = false; // But audio on to hear + + // Step 4: Join! + err = g_meetingService->Join(joinParam); + if (err != SDKERR_SUCCESS) { + std::cerr << "ERROR: Join failed: " << err << std::endl; + return false; + } + + std::cout << "Join() called, waiting for MEETING_STATUS_INMEETING..." << std::endl; + return true; +} + +// Call this ONLY after MEETING_STATUS_INMEETING +void SetupControllers() { + // Audio + IMeetingAudioController* audioCtrl = g_meetingService->GetMeetingAudioController(); + if (audioCtrl) { + audioCtrl->JoinVoip(); + audioCtrl->MuteAudio(0, true); // Mute self + } + + // Video + IMeetingVideoController* videoCtrl = g_meetingService->GetMeetingVideoController(); + // ... + + // Chat + IMeetingChatController* chatCtrl = g_meetingService->GetMeetingChatController(); + // ... +} +``` + +### CRITICAL: Controller Availability + +| When | Controllers Available? | +|------|----------------------| +| Before `MEETING_STATUS_INMEETING` | **NO** - Returns `nullptr` | +| After `MEETING_STATUS_INMEETING` | **YES** - Safe to use | +| After `MEETING_STATUS_ENDED` | **NO** - Pointers invalid | + +**Common mistake**: Getting controllers before joining. ALWAYS wait for the `MEETING_STATUS_INMEETING` callback! + +--- + +## Troubleshooting Steps + +### Step 1: Check JWT Token + +```cpp +std::cout << "JWT length: " << jwt_token.length() << std::endl; +std::cout << "JWT preview: " << jwt_token.substr(0, 30) << "..." << std::endl; +``` + +Expected output: +``` +JWT length: 358 +JWT preview: eyJhbGciOiJIUzI1NiIsInR5cCI... +``` + +### Step 2: Verify SDKAuth Return + +```cpp +SDKError err = g_authService->SDKAuth(authContext); +std::cout << "SDKAuth returned: " << err << std::endl; +``` + +- `0` (SDKERR_SUCCESS): Good, wait for callback +- Non-zero: Immediate error, check JWT format + +### Step 3: Confirm Callback Fires + +Add logging in `onAuthenticationReturn`: +```cpp +void onAuthenticationReturn(AuthResult ret) override { + std::cout << "*** CALLBACK RECEIVED ***" << std::endl; // First line! + // ... rest of code ... +} +``` + +If you never see "CALLBACK RECEIVED", you need a message loop! + +### Step 4: Test Network + +```cpp +// Before SDKAuth +std::cout << "Testing network..." << std::endl; +// Try to access zoom.us or check internet connection +``` + +--- + +## See Also + +- [Windows Message Loop](../troubleshooting/windows-message-loop.md) - Why callbacks don't fire +- [Build Errors](../troubleshooting/build-errors.md) - Compilation issues +- [JWT Token Generation](../../../oauth/SKILL.md) - Creating JWT tokens +- [Joining Meetings](../SKILL.md) - Next steps after authentication diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/examples/breakout-rooms.md b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/breakout-rooms.md new file mode 100644 index 00000000..848d04e6 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/breakout-rooms.md @@ -0,0 +1,552 @@ +# Breakout Rooms Example + +## Overview + +This guide shows how to manage breakout rooms in the Zoom Windows Meeting SDK. Breakout rooms functionality works with **both Default UI and Custom UI**. + +--- + +## Understanding Breakout Room Roles + +Breakout room functionality is controlled by **five distinct roles**. A user can have multiple roles simultaneously. + +| Role | Interface | Capabilities | +|------|-----------|--------------| +| **Data** | `IBOData` | Read breakout room info, user assignments, names | +| **Admin** | `IBOAdmin` | Manage running BOs, receive help requests, assign users, broadcast | +| **Creator** | `IBOCreator` | Create/modify BOs, configure settings, preassign users | +| **Assistant** | `IBOAssistant` | Join any BO without assignment (minor role) | +| **Attendee** | `IBOAttendee` | Join assigned BO, request help from admin | + +**Typical role combinations**: +- **Host**: Creator + Admin + Data +- **Co-host**: Admin + Data +- **Regular participant**: Attendee + Data + +--- + +## Step 1: Set Up Role Listener + +First, implement `IMeetingBOControllerEvent` to receive role assignment callbacks: + +**BOControllerEventListener.h**: +```cpp +#pragma once +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class BOControllerEventListener : public IMeetingBOControllerEvent { +public: + // Role assignment callbacks - store interface pointers when received + void onHasCreatorRightsNotification(IBOCreator* pCreatorObj) override { + std::cout << "[BO] Received CREATOR role" << std::endl; + m_boCreator = pCreatorObj; + } + + void onHasAdminRightsNotification(IBOAdmin* pAdminObj) override { + std::cout << "[BO] Received ADMIN role" << std::endl; + m_boAdmin = pAdminObj; + } + + void onHasAttendeeRightsNotification(IBOAttendee* pAttendeeObj) override { + std::cout << "[BO] Received ATTENDEE role" << std::endl; + m_boAttendee = pAttendeeObj; + } + + void onHasDataHelperRightsNotification(IBOData* pDataHelperObj) override { + std::cout << "[BO] Received DATA role" << std::endl; + m_boData = pDataHelperObj; + } + + void onHasAssistantRightsNotification(IBOAssistant* pAssistantObj) override { + std::cout << "[BO] Received ASSISTANT role" << std::endl; + m_boAssistant = pAssistantObj; + } + + // Role removal callbacks - clear interface pointers + void onLostCreatorRightsNotification() override { + std::cout << "[BO] Lost CREATOR role" << std::endl; + m_boCreator = nullptr; + } + + void onLostAdminRightsNotification() override { + std::cout << "[BO] Lost ADMIN role" << std::endl; + m_boAdmin = nullptr; + } + + void onLostAttendeeRightsNotification() override { + std::cout << "[BO] Lost ATTENDEE role" << std::endl; + m_boAttendee = nullptr; + } + + void onLostDataHelperRightsNotification() override { + std::cout << "[BO] Lost DATA role" << std::endl; + m_boData = nullptr; + } + + void onLostAssistantRightsNotification() override { + std::cout << "[BO] Lost ASSISTANT role" << std::endl; + m_boAssistant = nullptr; + } + + // BO status changes + void onNewBroadcastMessageReceived(const zchar_t* strMsg, unsigned int nSenderID) override { + std::wcout << L"[BO] Broadcast message: " << strMsg << std::endl; + } + + void onBOStopCountDown(unsigned int nSeconds) override { + std::cout << "[BO] Rooms closing in " << nSeconds << " seconds" << std::endl; + } + + void onHostInviteReturnToMainSession(const zchar_t* strName, IReturnToMainSessionHandler* pHandler) override { + std::wcout << L"[BO] Host inviting back to main session from: " << strName << std::endl; + } + + void onBOStatusChanged(BO_STATUS eStatus) override { + std::cout << "[BO] Status changed: " << eStatus << std::endl; + } + + // Accessors for stored interfaces + IBOCreator* GetCreator() { return m_boCreator; } + IBOAdmin* GetAdmin() { return m_boAdmin; } + IBOAttendee* GetAttendee() { return m_boAttendee; } + IBOData* GetData() { return m_boData; } + IBOAssistant* GetAssistant() { return m_boAssistant; } + +private: + IBOCreator* m_boCreator = nullptr; + IBOAdmin* m_boAdmin = nullptr; + IBOAttendee* m_boAttendee = nullptr; + IBOData* m_boData = nullptr; + IBOAssistant* m_boAssistant = nullptr; +}; +``` + +**Register the listener**: +```cpp +void SetupBreakoutRoomListener() { + IMeetingBOController* boController = meetingService->GetMeetingBOController(); + if (!boController) { + std::cerr << "[BO] ERROR: Failed to get BO controller" << std::endl; + return; + } + + BOControllerEventListener* boListener = new BOControllerEventListener(); + boController->SetEvent(boListener); + + std::cout << "[BO] Breakout room listener registered" << std::endl; +} +``` + +--- + +## Step 2: Creator Role - Create & Configure Breakout Rooms + +If you're the host, you'll receive the Creator role and can create rooms: + +```cpp +void CreateBreakoutRooms(IBOCreator* creator) { + if (!creator) { + std::cerr << "[BO] ERROR: No creator role" << std::endl; + return; + } + + // Option A: Create single room + bool success = creator->CreateBO(L"Discussion Room 1"); + std::cout << "[BO] Create room 1: " << (success ? "OK" : "FAILED") << std::endl; + + success = creator->CreateBO(L"Discussion Room 2"); + std::cout << "[BO] Create room 2: " << (success ? "OK" : "FAILED") << std::endl; + + // Option B: Batch create rooms + IList* roomNames = /* your list of names */; + success = creator->CreateGroupBO(roomNames); + std::cout << "[BO] Batch create: " << (success ? "OK" : "FAILED") << std::endl; +} +``` + +**Configure breakout room options**: +```cpp +void ConfigureBreakoutRooms(IBOCreator* creator) { + if (!creator) return; + + BOOption option; + + // Timer settings + option.IsBOTimerEnabled = true; + option.timerDuration = 15; // 15 minutes + option.IsTimerAutoStopBOEnabled = true; // Auto-close after timer + + // Countdown before closing + option.countdown = BOStopCountdown_Seconds_60; // 60 second warning + + // Participant permissions + option.IsParticipantCanChooseBO = true; // Participants can self-select room + option.IsParticipantCanReturnToMainSessionAtAnyTime = true; + option.IsAutoMoveAllAssignedParticipantsEnabled = true; + + // For webinars + option.IsPanelistCanChooseBO = true; + option.IsAttendeeCanChooseBO = true; + option.IsAttendeeContained = true; + + // User limits per room + option.IsUserConfigMaxRoomUserLimitsEnabled = true; + option.nUserConfigMaxRoomUserLimits = 10; + + bool success = creator->SetBOOption(option); + std::cout << "[BO] Configure options: " << (success ? "OK" : "FAILED") << std::endl; +} +``` + +**Preassign users to rooms** (before rooms start): +```cpp +void PreassignUser(IBOCreator* creator, IBOData* data, const zchar_t* userName, const zchar_t* roomName) { + if (!creator || !data) return; + + // Find user ID + IList* unassignedUsers = data->GetUnassignedUserList(); + const zchar_t* userId = nullptr; + + for (int i = 0; i < unassignedUsers->GetCount(); i++) { + const zchar_t* uid = unassignedUsers->GetItem(i); + if (wcscmp(data->GetBOUserName(uid), userName) == 0) { + userId = uid; + break; + } + } + + if (!userId) { + std::wcerr << L"[BO] User not found: " << userName << std::endl; + return; + } + + // Find room ID + IList* roomIds = data->GetBOMeetingIDList(); + const zchar_t* roomId = nullptr; + + for (int i = 0; i < roomIds->GetCount(); i++) { + const zchar_t* rid = roomIds->GetItem(i); + IBOMeeting* room = data->GetBOMeetingByID(rid); + if (room && wcscmp(room->GetBOName(), roomName) == 0) { + roomId = rid; + break; + } + } + + if (!roomId) { + std::wcerr << L"[BO] Room not found: " << roomName << std::endl; + return; + } + + // Assign user to room + bool success = creator->AssignUserToBO(userId, roomId); + std::wcout << L"[BO] Assign " << userName << L" to " << roomName << L": " + << (success ? L"OK" : L"FAILED") << std::endl; +} +``` + +--- + +## Step 3: Admin Role - Manage Running Breakout Rooms + +Once rooms are created, the Admin role manages them: + +```cpp +void StartBreakoutRooms(IBOAdmin* admin) { + if (!admin) { + std::cerr << "[BO] ERROR: No admin role" << std::endl; + return; + } + + if (!admin->CanStartBO()) { + std::cerr << "[BO] Cannot start BOs - check if rooms are created and users assigned" << std::endl; + return; + } + + bool success = admin->StartBO(); + std::cout << "[BO] Start rooms: " << (success ? "OK" : "FAILED") << std::endl; +} + +void StopBreakoutRooms(IBOAdmin* admin) { + if (!admin) return; + + bool success = admin->StopBO(); + std::cout << "[BO] Stop rooms: " << (success ? "OK" : "FAILED") << std::endl; +} +``` + +**Assign users to RUNNING rooms**: +```cpp +void AssignUserToRunningRoom(IBOAdmin* admin, const zchar_t* userId, const zchar_t* roomId) { + if (!admin) return; + + bool success = admin->AssignNewUserToRunningBO(userId, roomId); + std::cout << "[BO] Assign to running room: " << (success ? "OK" : "FAILED") << std::endl; +} + +void SwitchUserRoom(IBOAdmin* admin, const zchar_t* userId, const zchar_t* newRoomId) { + if (!admin) return; + + bool success = admin->SwitchAssignedUserToRunningBO(userId, newRoomId); + std::cout << "[BO] Switch user room: " << (success ? "OK" : "FAILED") << std::endl; +} +``` + +**Broadcast message to all rooms**: +```cpp +void BroadcastMessage(IBOAdmin* admin, const wchar_t* message) { + if (!admin) return; + + bool success = admin->BroadcastMessage(message); + std::wcout << L"[BO] Broadcast: " << (success ? L"OK" : L"FAILED") << std::endl; +} +``` + +**Handle help requests**: +```cpp +class BOAdminEventListener : public IBOAdminEvent { +public: + void onHelpRequestReceived(const zchar_t* strUserID) override { + std::cout << "[BO] Help request from user: " << strUserID << std::endl; + + // Option 1: Join their room to help + m_admin->JoinBOByUserRequest(strUserID); + + // Option 2: Ignore the request + // m_admin->IgnoreUserHelpRequest(strUserID); + } + + void onStartBOError(BOControllerError errCode) override { + std::cerr << "[BO] Start error: " << errCode << std::endl; + } + + void onBOEndTimerUpdated(int remaining, bool isTimesUpNotice) override { + std::cout << "[BO] Timer: " << remaining << " seconds remaining" << std::endl; + } + + void SetAdmin(IBOAdmin* admin) { m_admin = admin; } + +private: + IBOAdmin* m_admin = nullptr; +}; +``` + +--- + +## Step 4: Data Role - Read Breakout Room Information + +The Data role lets you read BO information: + +```cpp +void ListBreakoutRooms(IBOData* data) { + if (!data) return; + + IList* roomIds = data->GetBOMeetingIDList(); + if (!roomIds) { + std::cout << "[BO] No breakout rooms" << std::endl; + return; + } + + std::cout << "[BO] Found " << roomIds->GetCount() << " breakout rooms:" << std::endl; + + for (int i = 0; i < roomIds->GetCount(); i++) { + const zchar_t* roomId = roomIds->GetItem(i); + IBOMeeting* room = data->GetBOMeetingByID(roomId); + + if (room) { + std::wcout << L" Room: " << room->GetBOName() << std::endl; + + // List users in this room + IList* users = room->GetBOUserList(); + if (users) { + for (int j = 0; j < users->GetCount(); j++) { + const zchar_t* userId = users->GetItem(j); + BO_USER_STATUS status = room->GetBOUserStatus(userId); + std::wcout << L" - " << data->GetBOUserName(userId) + << L" (status: " << status << L")" << std::endl; + } + } + } + } +} + +void ListUnassignedUsers(IBOData* data) { + if (!data) return; + + IList* unassigned = data->GetUnassignedUserList(); + if (!unassigned || unassigned->GetCount() == 0) { + std::cout << "[BO] No unassigned users" << std::endl; + return; + } + + std::cout << "[BO] Unassigned users:" << std::endl; + for (int i = 0; i < unassigned->GetCount(); i++) { + const zchar_t* userId = unassigned->GetItem(i); + std::wcout << L" - " << data->GetBOUserName(userId); + if (data->IsBOUserMyself(userId)) { + std::wcout << L" (me)"; + } + std::wcout << std::endl; + } +} +``` + +**Listen for data updates**: +```cpp +class BODataEventListener : public IBODataEvent { +public: + void onBOInfoUpdated(const zchar_t* strBOID) override { + std::cout << "[BO] Room info updated: " << strBOID << std::endl; + } + + void OnBOListInfoUpdated() override { + std::cout << "[BO] Room list updated" << std::endl; + } + + void onUnAssignedUserUpdated() override { + std::cout << "[BO] Unassigned user list updated" << std::endl; + } +}; + +// Register: data->SetEvent(new BODataEventListener()); +``` + +--- + +## Step 5: Attendee Role - Join & Leave Breakout Rooms + +As a regular participant: + +```cpp +void JoinMyBreakoutRoom(IBOAttendee* attendee) { + if (!attendee) { + std::cerr << "[BO] ERROR: No attendee role" << std::endl; + return; + } + + // Get your assigned room name + const zchar_t* roomName = attendee->GetBoName(); + std::wcout << L"[BO] Your assigned room: " << roomName << std::endl; + + // Join the room + bool success = attendee->JoinBo(); + std::cout << "[BO] Join room: " << (success ? "OK" : "FAILED") << std::endl; +} + +void LeaveBreakoutRoom(IBOAttendee* attendee) { + if (!attendee) return; + + if (!attendee->IsCanReturnMainSession()) { + std::cerr << "[BO] Cannot return to main session (disabled by host)" << std::endl; + return; + } + + bool success = attendee->LeaveBo(); + std::cout << "[BO] Leave room: " << (success ? "OK" : "FAILED") << std::endl; +} + +void RequestHelpFromHost(IBOAttendee* attendee) { + if (!attendee) return; + + // Only request help if host is NOT already in your room + if (attendee->IsHostInThisBO()) { + std::cout << "[BO] Host is already in this room" << std::endl; + return; + } + + bool success = attendee->RequestForHelp(); + std::cout << "[BO] Help request: " << (success ? "sent" : "FAILED") << std::endl; +} +``` + +--- + +## Step 6: Assistant Role - Join Any Room + +Co-hosts typically get the Assistant role: + +```cpp +void JoinAnyRoom(IBOAssistant* assistant, const zchar_t* roomId) { + if (!assistant) return; + + bool success = assistant->JoinBO(roomId); + std::cout << "[BO] Join room: " << (success ? "OK" : "FAILED") << std::endl; +} + +void LeaveCurrentRoom(IBOAssistant* assistant) { + if (!assistant) return; + + bool success = assistant->LeaveBO(); + std::cout << "[BO] Leave room: " << (success ? "OK" : "FAILED") << std::endl; +} +``` + +--- + +## Error Codes + +| Code | Name | Description | +|------|------|-------------| +| 0 | `BOControllerError_NULL_POINTER` | BO controller is null - SDK not initialized | +| 1 | `BOControllerError_WRONG_CURRENT_STATUS` | Incorrect current status | +| 2 | `BOControllerError_TOKEN_NOT_READY` | Token is not ready | +| 3 | `BOControllerError_NO_PRIVILEGE` | No privilege to perform action | +| 4 | `BOControllerError_BO_LIST_IS_UPLOADING` | BO list is being uploaded | +| 5 | `BOControllerError_UPLOAD_FAIL` | BO list upload failed | +| 6 | `BOControllerError_NO_ONE_HAS_BEEN_ASSIGNED` | Cannot start - no users assigned | +| 100 | `BOControllerError_UNKNOWN` | Unknown error | + +--- + +## Complete Workflow Example + +```cpp +void OnInMeeting() { + // Set up listener first + SetupBreakoutRoomListener(); + + // Wait for role callbacks... +} + +void OnCreatorRoleReceived(IBOCreator* creator, IBOData* data) { + // Step 1: Configure options + ConfigureBreakoutRooms(creator); + + // Step 2: Create rooms + creator->CreateBO(L"Team Alpha"); + creator->CreateBO(L"Team Beta"); + + // Step 3: Wait for onCreateBOResponse callback... +} + +void OnRoomsCreated(IBOCreator* creator, IBOData* data, IBOAdmin* admin) { + // Step 4: Assign users + PreassignUser(creator, data, L"John", L"Team Alpha"); + PreassignUser(creator, data, L"Jane", L"Team Beta"); + + // Step 5: Start rooms + StartBreakoutRooms(admin); +} + +void OnAttendeeRoleReceived(IBOAttendee* attendee) { + // As a participant, join your assigned room + JoinMyBreakoutRoom(attendee); +} +``` + +--- + +## Related Documentation + +- [Common Issues](../troubleshooting/common-issues.md) - Error code reference +- [Interface Methods](../references/interface-methods.md) - Complete interface reference +- [Custom UI Architecture](../concepts/custom-ui-architecture.md) - How UI works with breakout rooms + +--- + +**Last Updated**: Based on Zoom Windows Meeting SDK v6.7.2.26830 diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/examples/captions-transcription.md b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/captions-transcription.md new file mode 100644 index 00000000..159e6ff7 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/captions-transcription.md @@ -0,0 +1,551 @@ +# Closed Captions and Live Transcription + +## Overview + +The Meeting SDK provides two related caption features through `IClosedCaptionController`: + +1. **Manual Closed Captions** - Host assigns someone to type captions manually +2. **Live Transcription** - Automatic speech-to-text with optional translation + +Both features support multi-language translation when enabled. + +## Architecture + +``` +IMeetingService + └── GetMeetingClosedCaptionController() → IClosedCaptionController + ├── SetEvent(IClosedCaptionControllerEvent*) + ├── EnableCaptions(bool) + ├── StartLiveTranscription() + ├── SetMeetingSpokenLanguage(int) + ├── SetTranslationLanguage(int) + └── SendClosedCaption(const zchar_t*) +``` + +## Required Headers + +```cpp +#include +#include +``` + +## Step 1: Implement the Caption Event Listener + +```cpp +// ClosedCaptionControllerEventListener.h +#pragma once +#include +#include + +class ClosedCaptionControllerEventListener + : public ZOOMSDK::IClosedCaptionControllerEvent { +public: + using TranscriptionCallback = std::function; + using CaptionCallback = std::function; + + ClosedCaptionControllerEventListener( + TranscriptionCallback onTranscription = nullptr, + CaptionCallback onCaption = nullptr + ); + virtual ~ClosedCaptionControllerEventListener() = default; + + // Called when assigned to send closed captions + virtual void onAssignedToSendCC(bool bAssigned) override; + + // Called when manual closed caption is received + virtual void onClosedCaptionMsgReceived( + const zchar_t* ccMsg, + unsigned int sender_id, + time_t time + ) override; + + // Called when live transcription status changes + virtual void onLiveTranscriptionStatus( + ZOOMSDK::SDKLiveTranscriptionStatus status + ) override; + + // Called when original (untranslated) message is received + virtual void onOriginalLanguageMsgReceived( + ZOOMSDK::ILiveTranscriptionMessageInfo* messageInfo + ) override; + + // Called when translated transcription message is received + virtual void onLiveTranscriptionMsgInfoReceived( + ZOOMSDK::ILiveTranscriptionMessageInfo* messageInfo + ) override; + + // Called when translation error occurs + virtual void onLiveTranscriptionMsgError( + ZOOMSDK::ILiveTranscriptionLanguage* spokenLanguage, + ZOOMSDK::ILiveTranscriptionLanguage* transcriptLanguage + ) override; + + // Host receives this when someone requests transcription + virtual void onRequestForLiveTranscriptReceived( + unsigned int requester_id, + bool bAnonymous + ) override; + + // Called when request status changes + virtual void onRequestLiveTranscriptionStatusChange(bool bEnabled) override; + + // Called when caption enable status changes + virtual void onCaptionStatusChanged(bool bEnabled) override; + + // Called when someone requests to start captions + virtual void onStartCaptionsRequestReceived( + ZOOMSDK::ICCRequestHandler* handler + ) override; + + // Called when caption request is approved + virtual void onStartCaptionsRequestApproved() override; + + // Called when manual caption status changes + virtual void onManualCaptionStatusChanged(bool bEnabled) override; + + // Called when spoken language changes + virtual void onSpokenLanguageChanged( + ZOOMSDK::ILiveTranscriptionLanguage* spokenLanguage + ) override; + +private: + TranscriptionCallback m_onTranscription; + CaptionCallback m_onCaption; + std::string wstringToUtf8(const std::wstring& wstr); +}; +``` + +```cpp +// ClosedCaptionControllerEventListener.cpp +#include "ClosedCaptionControllerEventListener.h" +#include + +using namespace ZOOMSDK; + +ClosedCaptionControllerEventListener::ClosedCaptionControllerEventListener( + TranscriptionCallback onTranscription, + CaptionCallback onCaption +) : m_onTranscription(onTranscription), m_onCaption(onCaption) {} + +void ClosedCaptionControllerEventListener::onAssignedToSendCC(bool bAssigned) { + std::cout << "Assigned to send CC: " << (bAssigned ? "yes" : "no") << std::endl; +} + +void ClosedCaptionControllerEventListener::onClosedCaptionMsgReceived( + const zchar_t* ccMsg, + unsigned int sender_id, + time_t time +) { + if (ccMsg) { + std::wstring wstr(ccMsg); + std::string utf8 = wstringToUtf8(wstr); + std::cout << "[CC] " << utf8 << std::endl; + + if (m_onCaption) { + m_onCaption(wstr, sender_id); + } + } +} + +void ClosedCaptionControllerEventListener::onLiveTranscriptionStatus( + SDKLiveTranscriptionStatus status +) { + switch (status) { + case SDK_LiveTranscription_Status_Stop: + std::cout << "Live transcription: STOPPED" << std::endl; + break; + case SDK_LiveTranscription_Status_Start: + std::cout << "Live transcription: STARTED" << std::endl; + break; + case SDK_LiveTranscription_Status_User_Sub: + std::cout << "Live transcription: USER SUBSCRIBED" << std::endl; + break; + case SDK_LiveTranscription_Status_Connecting: + std::cout << "Live transcription: CONNECTING" << std::endl; + break; + } +} + +void ClosedCaptionControllerEventListener::onOriginalLanguageMsgReceived( + ILiveTranscriptionMessageInfo* messageInfo +) { + if (!messageInfo) return; + + // Only process complete messages (not partial/streaming) + if (messageInfo->GetMessageOperationType() == SDK_LiveTranscription_OperationType_Complete) { + const zchar_t* content = messageInfo->GetMessageContent(); + if (content) { + std::wstring wstr(content); + std::string utf8 = wstringToUtf8(wstr); + std::cout << "[Original] " << utf8 << std::endl; + } + } +} + +void ClosedCaptionControllerEventListener::onLiveTranscriptionMsgInfoReceived( + ILiveTranscriptionMessageInfo* messageInfo +) { + if (!messageInfo) return; + + // Only process complete messages + SDKLiveTranscriptionOperationType opType = messageInfo->GetMessageOperationType(); + if (opType == SDK_LiveTranscription_OperationType_Complete) { + const zchar_t* content = messageInfo->GetMessageContent(); + unsigned int speakerId = messageInfo->GetSpeakerID(); + + if (content) { + std::wstring wstr(content); + std::string utf8 = wstringToUtf8(wstr); + std::cout << "[Transcription] " << utf8 << std::endl; + + if (m_onTranscription) { + m_onTranscription(wstr, speakerId); + } + } + } +} + +void ClosedCaptionControllerEventListener::onLiveTranscriptionMsgError( + ILiveTranscriptionLanguage* spokenLanguage, + ILiveTranscriptionLanguage* transcriptLanguage +) { + std::cout << "Transcription error occurred" << std::endl; + if (spokenLanguage) { + std::wcout << L" Spoken: " << spokenLanguage->GetLanguageName() << std::endl; + } + if (transcriptLanguage) { + std::wcout << L" Target: " << transcriptLanguage->GetLanguageName() << std::endl; + } +} + +void ClosedCaptionControllerEventListener::onRequestForLiveTranscriptReceived( + unsigned int requester_id, + bool bAnonymous +) { + std::cout << "Live transcript requested by user " + << (bAnonymous ? "(anonymous)" : std::to_string(requester_id)) + << std::endl; +} + +void ClosedCaptionControllerEventListener::onRequestLiveTranscriptionStatusChange(bool bEnabled) { + std::cout << "Request transcription status: " << (bEnabled ? "enabled" : "disabled") << std::endl; +} + +void ClosedCaptionControllerEventListener::onCaptionStatusChanged(bool bEnabled) { + std::cout << "Caption status: " << (bEnabled ? "enabled" : "disabled") << std::endl; +} + +void ClosedCaptionControllerEventListener::onStartCaptionsRequestReceived( + ICCRequestHandler* handler +) { + std::cout << "Caption start request received" << std::endl; + // Host can approve or deny here +} + +void ClosedCaptionControllerEventListener::onStartCaptionsRequestApproved() { + std::cout << "Caption start request approved" << std::endl; +} + +void ClosedCaptionControllerEventListener::onManualCaptionStatusChanged(bool bEnabled) { + std::cout << "Manual caption: " << (bEnabled ? "enabled" : "disabled") << std::endl; +} + +void ClosedCaptionControllerEventListener::onSpokenLanguageChanged( + ILiveTranscriptionLanguage* spokenLanguage +) { + if (spokenLanguage) { + std::wcout << L"Spoken language changed to: " + << spokenLanguage->GetLanguageName() << std::endl; + } +} + +std::string ClosedCaptionControllerEventListener::wstringToUtf8(const std::wstring& wstr) { + std::string utf8Str; + for (wchar_t wchar : wstr) { + if (wchar < 0x80) { + utf8Str += static_cast(wchar); + } else if (wchar < 0x800) { + utf8Str += static_cast(0xC0 | ((wchar >> 6) & 0x1F)); + utf8Str += static_cast(0x80 | (wchar & 0x3F)); + } else { + utf8Str += static_cast(0xE0 | ((wchar >> 12) & 0x0F)); + utf8Str += static_cast(0x80 | ((wchar >> 6) & 0x3F)); + utf8Str += static_cast(0x80 | (wchar & 0x3F)); + } + } + return utf8Str; +} +``` + +## Step 2: Initialize Caption Controller + +```cpp +// Global variables +IClosedCaptionController* g_captionController = nullptr; +ClosedCaptionControllerEventListener* g_captionListener = nullptr; + +void initializeCaptions(IMeetingService* meetingService) { + // Get the caption controller + g_captionController = meetingService->GetMeetingClosedCaptionController(); + if (!g_captionController) { + std::cerr << "Failed to get caption controller" << std::endl; + return; + } + + // Create and set event listener + g_captionListener = new ClosedCaptionControllerEventListener( + // Transcription callback + [](const std::wstring& text, unsigned int speakerId) { + // Process transcription text + std::wcout << L"Speaker " << speakerId << L": " << text << std::endl; + }, + // Caption callback + [](const std::wstring& text, unsigned int senderId) { + // Process manual caption + std::wcout << L"Caption: " << text << std::endl; + } + ); + + SDKError err = g_captionController->SetEvent(g_captionListener); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to set caption listener: " << err << std::endl; + return; + } + + std::cout << "Caption controller initialized" << std::endl; +} +``` + +## Step 3: Enable Live Transcription + +### Check Feature Availability + +```cpp +void checkTranscriptionFeatures() { + if (!g_captionController) return; + + std::cout << "=== Transcription Features ===" << std::endl; + std::cout << "Captions enabled: " + << g_captionController->IsCaptionsEnabled() << std::endl; + std::cout << "Live transcription feature: " + << g_captionController->IsLiveTranscriptionFeatureEnabled() << std::endl; + std::cout << "Manual caption enabled: " + << g_captionController->IsMeetingManualCaptionEnabled() << std::endl; + std::cout << "Multi-language transcription: " + << g_captionController->IsMultiLanguageTranscriptionEnabled() << std::endl; + std::cout << "Receive spoken language: " + << g_captionController->IsReceiveSpokenLanguageContentEnabled() << std::endl; + std::cout << "Request to start enabled: " + << g_captionController->IsRequestToStartLiveTranscriptionEnabled() << std::endl; + std::cout << "Text translation enabled: " + << g_captionController->IsTextLiveTranslationEnabled() << std::endl; +} +``` + +### Start Live Transcription (with Translation) + +```cpp +void startLiveTranscription() { + if (!g_captionController) return; + + SDKError err; + + // Start live transcription service + err = g_captionController->StartLiveTranscription(); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to start transcription: " << err << std::endl; + // May need host permission + } + + // Set spoken language (0 = English) + // Use GetAvailableSpokenLanguageList() to get all options + err = g_captionController->SetMeetingSpokenLanguage(0); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to set spoken language: " << err << std::endl; + } + + // Set translation language (1 = Chinese, -1 = disable translation) + err = g_captionController->SetTranslationLanguage(1); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to set translation language: " << err << std::endl; + } + + // Enable receiving original spoken language content + err = g_captionController->EnableReceiveSpokenLanguageContent(true); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to enable spoken language content: " << err << std::endl; + } + + std::cout << "Live transcription started" << std::endl; +} +``` + +### Stop Live Transcription + +```cpp +void stopLiveTranscription() { + if (!g_captionController) return; + + SDKError err = g_captionController->StopLiveTranscription(); + if (err == SDKERR_SUCCESS) { + std::cout << "Live transcription stopped" << std::endl; + } +} +``` + +## Step 4: Manual Closed Captions (Host Only) + +### Enable Manual Captions + +```cpp +void enableManualCaptions() { + if (!g_captionController) return; + + SDKError err; + + // Enable captions feature + err = g_captionController->EnableCaptions(true); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to enable captions: " << err << std::endl; + } + + // Enable manual caption mode + err = g_captionController->EnableMeetingManualCaption(true); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to enable manual captions: " << err << std::endl; + } + + std::cout << "Manual captions enabled" << std::endl; +} +``` + +### Assign Caption Privilege + +```cpp +void assignCaptionPrivilege(unsigned int userId, bool assign) { + if (!g_captionController) return; + + // Host assigns someone to type captions + // userId = 0 means current user + SDKError err = g_captionController->AssignCCPrivilege(userId, assign); + if (err == SDKERR_SUCCESS) { + std::cout << "Caption privilege " + << (assign ? "assigned to" : "removed from") + << " user " << userId << std::endl; + } else { + std::cerr << "Failed to assign caption privilege: " << err << std::endl; + } +} +``` + +### Send Manual Caption + +```cpp +void sendManualCaption(const wchar_t* captionText) { + if (!g_captionController) return; + + // Only works if you have been assigned CC privilege + SDKError err = g_captionController->SendClosedCaption(captionText); + if (err == SDKERR_SUCCESS) { + std::cout << "Caption sent" << std::endl; + } else if (err == SDKERR_NO_PERMISSION) { + std::cerr << "No permission to send captions" << std::endl; + } +} +``` + +## Complete Integration Example + +```cpp +void onInMeeting(IMeetingService* meetingService) { + initializeCaptions(meetingService); + + // Check available features + checkTranscriptionFeatures(); + + // Start live transcription with translation + startLiveTranscription(); +} + +// For host: enable manual captions +void onIsHost() { + enableManualCaptions(); + assignCaptionPrivilege(0, true); // Assign to self +} +``` + +## Language IDs + +Common language IDs for `SetMeetingSpokenLanguage()` and `SetTranslationLanguage()`: + +| ID | Language | +|----|----------| +| 0 | English | +| 1 | Chinese (Simplified) | +| 2 | Japanese | +| 3 | German | +| 4 | French | +| 5 | Russian | +| 6 | Portuguese | +| 7 | Spanish | +| 8 | Korean | +| -1 | Disable translation | + +Use `GetAvailableSpokenLanguageList()` and `GetAvailableTranslationLanguageList()` to get the full list of supported languages for the meeting. + +## Transcription Operation Types + +```cpp +enum SDKLiveTranscriptionOperationType { + SDK_LiveTranscription_OperationType_None = 0, + SDK_LiveTranscription_OperationType_Add, // New text added + SDK_LiveTranscription_OperationType_Update, // Text updated + SDK_LiveTranscription_OperationType_Delete, // Text deleted + SDK_LiveTranscription_OperationType_Complete, // Sentence complete + SDK_LiveTranscription_OperationType_NotSupported +}; +``` + +**Important**: Only process `SDK_LiveTranscription_OperationType_Complete` for final transcription text. Other types represent partial/streaming results. + +## Transcription Status + +```cpp +enum SDKLiveTranscriptionStatus { + SDK_LiveTranscription_Status_Stop = 0, // Not running + SDK_LiveTranscription_Status_Start = 1, // Running + SDK_LiveTranscription_Status_User_Sub = 2, // User subscribed + SDK_LiveTranscription_Status_Connecting = 10 // Connecting +}; +``` + +## Error Handling + +```cpp +SDKError err = g_captionController->StartLiveTranscription(); +switch (err) { + case SDKERR_SUCCESS: + std::cout << "Transcription started" << std::endl; + break; + case SDKERR_NO_PERMISSION: + std::cerr << "No permission (need host or feature disabled)" << std::endl; + break; + case SDKERR_WRONG_USAGE: + std::cerr << "Feature not available in this meeting" << std::endl; + break; + case SDKERR_SERVICE_FAILED: + std::cerr << "Service failed to start" << std::endl; + break; + default: + std::cerr << "Error: " << err << std::endl; +} +``` + +## Common Pitfalls + +1. **Multi-language transcription**: Must be enabled in meeting settings for translation to work +2. **Manual captions vs transcription**: These are separate features - enable the one you need +3. **Partial results**: Filter by `GetMessageOperationType() == Complete` to avoid streaming updates +4. **Language ID -1**: Setting translation language to -1 receives closed captions without translation +5. **Host permission**: Most enable/disable operations require host privileges +6. **Unicode handling**: Caption text is `zchar_t*` (wide string) - convert to UTF-8 for display diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/examples/chat.md b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/chat.md new file mode 100644 index 00000000..0648cf45 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/chat.md @@ -0,0 +1,413 @@ +# Meeting Chat - Send and Receive Messages + +## Overview + +The Meeting SDK provides a rich chat API through `IMeetingChatController`. You can: +- Send messages to all participants or specific users +- Receive incoming chat messages +- Apply rich text formatting (bold, italic, links, colors) +- Handle file transfers +- Support threaded conversations + +## Architecture + +``` +IMeetingService + └── GetMeetingChatController() → IMeetingChatController + ├── SetEvent(IMeetingChatCtrlEvent*) + ├── GetChatMessageBuilder() → IChatMsgInfoBuilder + ├── SendChatMsgTo(IChatMsgInfo*) + └── GetChatStatus() +``` + +## Required Headers + +```cpp +#include +#include +``` + +## Step 1: Implement the Chat Event Listener + +```cpp +// MeetingChatEventListener.h +#pragma once +#include + +class MeetingChatEventListener : public ZOOMSDK::IMeetingChatCtrlEvent { +public: + MeetingChatEventListener() = default; + virtual ~MeetingChatEventListener() = default; + + // Called when a new chat message arrives + virtual void onChatMsgNotification( + ZOOMSDK::IChatMsgInfo* chatMsg, + const zchar_t* content + ) override; + + // Called when chat privileges change + virtual void onChatStatusChangedNotification( + ZOOMSDK::ChatStatus* status + ) override; + + // Called when a message is deleted + virtual void onChatMsgDeleteNotification( + const zchar_t* msgID, + ZOOMSDK::SDKChatMessageDeleteType deleteBy + ) override; + + // Called when a message is edited + virtual void onChatMessageEditNotification( + ZOOMSDK::IChatMsgInfo* chatMsg + ) override; + + // Called when meeting chat sharing status changes + virtual void onShareMeetingChatStatusChanged(bool isStart) override; + + // File transfer callbacks + virtual void onFileSendStart(ZOOMSDK::ISDKFileSender* sender) override; + virtual void onFileReceived(ZOOMSDK::ISDKFileReceiver* receiver) override; + virtual void onFileTransferProgress(ZOOMSDK::SDKFileTransferInfo* info) override; +}; +``` + +```cpp +// MeetingChatEventListener.cpp +#include "MeetingChatEventListener.h" +#include + +using namespace ZOOMSDK; + +void MeetingChatEventListener::onChatMsgNotification( + IChatMsgInfo* chatMsg, + const zchar_t* content +) { + if (chatMsg && content) { + // Get sender information + unsigned int senderId = chatMsg->GetSenderUserId(); + const zchar_t* senderName = chatMsg->GetSenderDisplayName(); + + // Get message details + const zchar_t* msgContent = chatMsg->GetContent(); + SDKChatMessageType msgType = chatMsg->GetChatMessageType(); + + std::wcout << L"[Chat] " << senderName << L": " << msgContent << std::endl; + + // Check message type + switch (msgType) { + case SDKChatMessageType_To_All: + std::cout << " (sent to everyone)" << std::endl; + break; + case SDKChatMessageType_To_Individual: + std::cout << " (private message)" << std::endl; + break; + case SDKChatMessageType_To_WaitingRoomUsers: + std::cout << " (to waiting room)" << std::endl; + break; + } + + // Check if this is a threaded reply + const zchar_t* threadId = chatMsg->GetThreadID(); + if (threadId && wcslen(threadId) > 0) { + std::wcout << L" Thread ID: " << threadId << std::endl; + } + } +} + +void MeetingChatEventListener::onChatStatusChangedNotification(ChatStatus* status) { + if (status) { + std::cout << "Chat status changed" << std::endl; + // Check what chat privileges are available + } +} + +void MeetingChatEventListener::onChatMsgDeleteNotification( + const zchar_t* msgID, + SDKChatMessageDeleteType deleteBy +) { + std::wcout << L"Message deleted: " << msgID << std::endl; + switch (deleteBy) { + case SDKChatMessageDeleteType_By_Self: + std::cout << " (deleted by sender)" << std::endl; + break; + case SDKChatMessageDeleteType_By_Host: + std::cout << " (deleted by host)" << std::endl; + break; + case SDKChatMessageDeleteType_By_DLP: + std::cout << " (deleted by DLP policy)" << std::endl; + break; + } +} + +void MeetingChatEventListener::onChatMessageEditNotification(IChatMsgInfo* chatMsg) { + if (chatMsg) { + std::wcout << L"Message edited: " << chatMsg->GetContent() << std::endl; + } +} + +void MeetingChatEventListener::onShareMeetingChatStatusChanged(bool isStart) { + std::cout << "Share meeting chat: " << (isStart ? "started" : "stopped") << std::endl; +} + +void MeetingChatEventListener::onFileSendStart(ISDKFileSender* sender) { + std::cout << "File send started" << std::endl; +} + +void MeetingChatEventListener::onFileReceived(ISDKFileReceiver* receiver) { + std::cout << "File received" << std::endl; +} + +void MeetingChatEventListener::onFileTransferProgress(SDKFileTransferInfo* info) { + // Handle file transfer progress +} +``` + +## Step 2: Initialize Chat Controller + +```cpp +// Global variables +IMeetingChatController* g_chatController = nullptr; +MeetingChatEventListener* g_chatListener = nullptr; + +void initializeChat(IMeetingService* meetingService) { + // Get the chat controller + g_chatController = meetingService->GetMeetingChatController(); + if (!g_chatController) { + std::cerr << "Failed to get chat controller" << std::endl; + return; + } + + // Create and set event listener + g_chatListener = new MeetingChatEventListener(); + SDKError err = g_chatController->SetEvent(g_chatListener); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to set chat event listener: " << err << std::endl; + return; + } + + std::cout << "Chat controller initialized" << std::endl; +} +``` + +## Step 3: Send Chat Messages + +### Simple Text Message (To Everyone) + +```cpp +void sendMessageToAll(const wchar_t* message) { + if (!g_chatController) return; + + // Get the message builder + IChatMsgInfoBuilder* builder = g_chatController->GetChatMessageBuilder(); + if (!builder) { + std::cerr << "Failed to get message builder" << std::endl; + return; + } + + // Build the message + builder->SetContent(message); + builder->SetReceiver(0); // 0 = everyone + builder->SetMessageType(SDKChatMessageType_To_All); + + // Build and send + IChatMsgInfo* msgInfo = builder->Build(); + if (msgInfo) { + SDKError err = g_chatController->SendChatMsgTo(msgInfo); + if (err == SDKERR_SUCCESS) { + std::cout << "Message sent successfully" << std::endl; + } else { + std::cerr << "Failed to send message: " << err << std::endl; + } + } +} +``` + +### Private Message (To Specific User) + +```cpp +void sendPrivateMessage(unsigned int userId, const wchar_t* message) { + if (!g_chatController) return; + + IChatMsgInfoBuilder* builder = g_chatController->GetChatMessageBuilder(); + if (!builder) return; + + builder->SetContent(message); + builder->SetReceiver(userId); // Specific user ID + builder->SetMessageType(SDKChatMessageType_To_Individual); + + IChatMsgInfo* msgInfo = builder->Build(); + if (msgInfo) { + SDKError err = g_chatController->SendChatMsgTo(msgInfo); + if (err == SDKERR_SUCCESS) { + std::cout << "Private message sent" << std::endl; + } + } +} +``` + +### Rich Text Message with Formatting + +```cpp +void sendFormattedMessage() { + if (!g_chatController) return; + + IChatMsgInfoBuilder* builder = g_chatController->GetChatMessageBuilder(); + if (!builder) return; + + // Set content: "Hello, this is BOLD and this is italic" + const wchar_t* content = L"Hello, this is BOLD and this is italic"; + builder->SetContent(content); + builder->SetReceiver(0); + builder->SetMessageType(SDKChatMessageType_To_All); + + // Apply bold to "BOLD" (positions 14-17, 0-indexed) + builder->SetBold(14, 18); + + // Apply italic to "italic" (positions 32-37) + builder->SetItalic(32, 38); + + // Build and send + IChatMsgInfo* msgInfo = builder->Build(); + if (msgInfo) { + g_chatController->SendChatMsgTo(msgInfo); + } +} +``` + +### Message with Link + +```cpp +void sendMessageWithLink() { + if (!g_chatController) return; + + IChatMsgInfoBuilder* builder = g_chatController->GetChatMessageBuilder(); + if (!builder) return; + + // Set content with link text + const wchar_t* content = L"Check out this website for more info"; + builder->SetContent(content); + builder->SetReceiver(0); + builder->SetMessageType(SDKChatMessageType_To_All); + + // Create link attributes + InsertLinkAttrs linkAttrs; + linkAttrs.insertLinkUrl = L"https://zoom.us"; + + // Apply link to "this website" (positions 10-21) + builder->SetInsertLink(linkAttrs, 10, 22); + + IChatMsgInfo* msgInfo = builder->Build(); + if (msgInfo) { + g_chatController->SendChatMsgTo(msgInfo); + } +} +``` + +### Threaded Reply + +```cpp +void sendThreadedReply(const wchar_t* threadId, const wchar_t* message) { + if (!g_chatController) return; + + IChatMsgInfoBuilder* builder = g_chatController->GetChatMessageBuilder(); + if (!builder) return; + + builder->SetContent(message); + builder->SetReceiver(0); + builder->SetMessageType(SDKChatMessageType_To_All); + builder->SetThreadId(threadId); // Reply to existing thread + + IChatMsgInfo* msgInfo = builder->Build(); + if (msgInfo) { + g_chatController->SendChatMsgTo(msgInfo); + } +} +``` + +## Complete Integration Example + +```cpp +void onInMeeting(IMeetingService* meetingService) { + // Initialize chat when in meeting + initializeChat(meetingService); + + // Send a greeting + sendMessageToAll(L"Hello everyone! Bot has joined the meeting."); +} + +// Call from your meeting status callback +void MeetingServiceEventListener::onMeetingStatusChanged( + MeetingStatus status, + int iResult +) { + if (status == MEETING_STATUS_INMEETING) { + onInMeeting(g_meetingService); + } +} +``` + +## IChatMsgInfoBuilder Methods Reference + +| Method | Description | +|--------|-------------| +| `SetContent(const zchar_t*)` | Set message text content | +| `SetReceiver(unsigned int)` | Set recipient (0 = everyone) | +| `SetMessageType(SDKChatMessageType)` | Set message type (all, individual, waiting room) | +| `SetThreadId(const zchar_t*)` | Set thread ID for replies | +| `SetBold(start, end)` | Apply bold style | +| `SetItalic(start, end)` | Apply italic style | +| `SetUnderline(start, end)` | Apply underline style | +| `SetStrikethrough(start, end)` | Apply strikethrough style | +| `SetFontColor(FontColorAttrs, start, end)` | Set font color | +| `SetBackgroundColor(BackgroundColorAttrs, start, end)` | Set background color | +| `SetFontSize(FontSizeAttrs, start, end)` | Set font size | +| `SetInsertLink(InsertLinkAttrs, start, end)` | Insert hyperlink | +| `SetBulletedList(start, end)` | Apply bulleted list style | +| `SetNumberedList(start, end)` | Apply numbered list style | +| `SetQuotePosition(start, end)` | Apply quote style | +| `SetParagraph(ParagraphAttrs, start, end)` | Set paragraph style (H1, H2, H3) | +| `ClearStyles()` | Clear all styles | +| `Clear()` | Clear all properties | +| `Build()` | Build the IChatMsgInfo object | + +## Chat Message Types + +```cpp +enum SDKChatMessageType { + SDKChatMessageType_To_None, // Invalid + SDKChatMessageType_To_All, // To everyone + SDKChatMessageType_To_Individual, // Private message + SDKChatMessageType_To_Individual_Panelist, // Webinar panelist + SDKChatMessageType_To_WaitingRoomUsers // To waiting room +}; +``` + +## Error Handling + +```cpp +SDKError err = g_chatController->SendChatMsgTo(msgInfo); +switch (err) { + case SDKERR_SUCCESS: + std::cout << "Message sent" << std::endl; + break; + case SDKERR_INVALID_PARAMETER: + std::cerr << "Invalid message parameters" << std::endl; + break; + case SDKERR_NO_PERMISSION: + std::cerr << "No permission to send chat" << std::endl; + break; + case SDKERR_WRONG_USAGE: + std::cerr << "Chat not available (not in meeting?)" << std::endl; + break; + default: + std::cerr << "Failed to send: " << err << std::endl; +} +``` + +## Common Pitfalls + +1. **Chat controller unavailable**: Must be in a meeting (`MEETING_STATUS_INMEETING`) +2. **Position indexing**: Style positions are 0-indexed and use character positions, not byte positions +3. **Unicode support**: Use `wchar_t*` and `std::wstring` for proper Unicode support +4. **Builder reuse**: The builder can be reused, but call `Clear()` between messages +5. **Thread ID**: When replying to a thread, get the thread ID from `IChatMsgInfo::GetThreadID()` diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/examples/custom-ui-video-rendering.md b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/custom-ui-video-rendering.md new file mode 100644 index 00000000..39c7e2fb --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/custom-ui-video-rendering.md @@ -0,0 +1,237 @@ +# Custom UI Video Rendering — Complete Working Example + +> **Skill**: Zoom Meeting SDK (Windows) +> **Category**: Examples +> **Prerequisite**: [Custom UI Architecture](../concepts/custom-ui-architecture.md), [Authentication Pattern](authentication-pattern.md) + +## Overview + +This example shows how to create a Custom UI meeting app that: +1. Creates its own Win32 window +2. Uses `ICustomizedVideoContainer` for SDK-rendered video +3. Shows an active speaker element (auto-follows who's talking) +4. Shows gallery elements for each participant +5. Handles screen sharing via `ICustomizedShareRender` + +## Flow + +``` +InitSDK (with ENABLE_CUSTOMIZED_UI_FLAG) + -> AuthSDK (JWT) + -> JoinMeeting + -> OnConnecting: Create window + CustomUIMgr + VideoContainer + -> OnInMeeting: Create video elements + subscribe to participants + -> Message loop (window events + SDK callbacks) + -> OnEnded: Destroy everything +``` + +## Step 1: Enable Custom UI in InitParam + +```cpp +InitParam initParam; +initParam.strWebDomain = L"https://zoom.us"; +initParam.emLanguageID = LANGUAGE_English; +initParam.enableLogByDefault = true; + +// CRITICAL: This is what makes it Custom UI mode +initParam.obConfigOpts.optionalFeatures = ENABLE_CUSTOMIZED_UI_FLAG; + +SDKError err = InitSDK(initParam); +``` + +## Step 2: Create the Custom UI Manager (on CONNECTING) + +When `onMeetingStatusChanged` fires with `MEETING_STATUS_CONNECTING`, create your window and the Custom UI manager: + +```cpp +#include +#include +#include +#include + +ICustomizedUIMgr* pCustomUIMgr = nullptr; +ICustomizedVideoContainer* pVideoContainer = nullptr; + +// Create the manager (global SDK function) +SDKError err = CreateCustomizedUIMgr(&pCustomUIMgr); + +// Optional: check license (log warning, don't abort) +err = pCustomUIMgr->HasLicense(); +if (err != SDKERR_SUCCESS) { + std::cout << "WARNING: HasLicense returned " << err << std::endl; +} + +// Register for destroy notifications +pCustomUIMgr->SetEvent(&myUIMgrEventListener); + +// Create video container inside your Win32 window +RECT rc; +::GetClientRect(hMyWindow, &rc); +err = pCustomUIMgr->CreateVideoContainer(&pVideoContainer, hMyWindow, rc); + +pVideoContainer->SetEvent(&myVideoContainerEventListener); +pVideoContainer->Show(); +pVideoContainer->SetBkColor(RGB(30, 30, 30)); // Dark background +``` + +## Step 3: Create Video Elements (on IN_MEETING) + +When `onMeetingStatusChanged` fires with `MEETING_STATUS_INMEETING`: + +### Active Speaker Element (auto-follows current speaker) + +```cpp +IVideoRenderElement* pElement = nullptr; +err = pVideoContainer->CreateVideoElement(&pElement, VideoRenderElement_ACTIVE); + +IActiveVideoRenderElement* pActive = dynamic_cast(pElement); +RECT activeRect = { 0, 0, windowWidth, (int)(windowHeight * 0.7) }; +pActive->SetPos(activeRect); +pActive->Show(); +pActive->Start(); // Begin auto-tracking active speaker +``` + +### Normal Elements (specific participants) + +```cpp +IMeetingParticipantsController* pParticipants = + pMeetingService->GetMeetingParticipantsController(); +IList* pUserList = pParticipants->GetParticipantsList(); + +for (int i = 0; i < pUserList->GetCount() && i < MAX_GALLERY; i++) { + unsigned int userId = pUserList->GetItem(i); + + IVideoRenderElement* pNormElement = nullptr; + err = pVideoContainer->CreateVideoElement(&pNormElement, VideoRenderElement_NORMAL); + + INormalVideoRenderElement* pNormal = + dynamic_cast(pNormElement); + + pNormal->Subscribe(userId); + pNormal->SetResolution(VideoRenderResolution_360p); + pNormal->Show(); + + // Position in gallery strip + int elemWidth = windowWidth / galleryCount; + RECT r = { i * elemWidth, galleryTop, (i+1) * elemWidth, windowHeight }; + pNormal->SetPos(r); +} +``` + +## Step 4: Handle Layout + +Respond to `onLayoutNotification` and `WM_SIZE` to re-layout elements: + +```cpp +void LayoutVideoElements() { + RECT clientRect; + ::GetClientRect(hMyWindow, &clientRect); + int totalWidth = clientRect.right - clientRect.left; + int totalHeight = clientRect.bottom - clientRect.top; + + // Resize container to fill window + pVideoContainer->Resize(clientRect); + + if (galleryElements.empty()) { + // Active speaker only — full window + RECT activeRect = { 0, 0, totalWidth, totalHeight }; + pActiveElement->SetPos(activeRect); + } else { + // Active speaker: top 70%, gallery: bottom 30% + int activeHeight = (int)(totalHeight * 0.7); + RECT activeRect = { 0, 0, totalWidth, activeHeight }; + pActiveElement->SetPos(activeRect); + + int elemWidth = totalWidth / (int)galleryElements.size(); + for (int i = 0; i < galleryElements.size(); i++) { + RECT r = { i * elemWidth, activeHeight, (i+1) * elemWidth, totalHeight }; + galleryElements[i]->SetPos(r); + } + } +} +``` + +## Step 5: Handle Screen Sharing + +```cpp +// Create share render (hidden until someone shares) +ICustomizedShareRender* pShareRender = nullptr; +RECT rc; +::GetClientRect(hMyWindow, &rc); +pCustomUIMgr->CreateShareRender(&pShareRender, hMyWindow, rc); +pShareRender->SetEvent(&myShareEventListener); +pShareRender->Hide(); + +// In ShareRenderEventListener: +void onSharingSourceNotification(unsigned int nShareSourceID) { + if (nShareSourceID > 0) { + pShareRender->SetShareSourceID(nShareSourceID); + pShareRender->SetViewMode(CSM_FULLFILL); + pShareRender->Show(); + } else { + pShareRender->Hide(); + } +} +``` + +## Step 6: Cleanup (on meeting end) + +```cpp +void Cleanup() { + if (pVideoContainer) { + pVideoContainer->DestroyAllVideoElement(); + pCustomUIMgr->DestroyVideoContainer(pVideoContainer); + pVideoContainer = nullptr; + } + if (pShareRender) { + pCustomUIMgr->DestroyShareRender(pShareRender); + pShareRender = nullptr; + } + if (pCustomUIMgr) { + DestroyCustomizedUIMgr(pCustomUIMgr); + pCustomUIMgr = nullptr; + } + if (hMyWindow) { + DestroyWindow(hMyWindow); + hMyWindow = nullptr; + } +} +``` + +## Complete Event Listener Implementations + +See [Interface Methods Reference](../references/interface-methods.md) for the full list of required virtual methods for: +- `ICustomizedUIMgrEvent` (3 methods) +- `ICustomizedVideoContainerEvent` (6 methods) +- `ICustomizedShareRenderEvent` (3 methods) + +## Required SDK Headers + +```cpp +#include +#include +#include +#include // CreateCustomizedUIMgr() +#include // ICustomizedUIMgr, ICustomizedUIMgrEvent +#include // ICustomizedVideoContainer, elements +#include // ICustomizedShareRender +#include +#include // Before participants! +#include +``` + +## Key Gotchas + +1. **Create Custom UI on CONNECTING, not IN_MEETING** — the SDK needs the video container ready before it starts rendering +2. **Active element needs `Start()`** — `Show()` alone is not enough, you must call `Start()` to begin active speaker tracking +3. **Normal elements need `Subscribe(userId)`** — without this they show nothing +4. **`SetPos()` coordinates are relative to container**, not screen or parent window +5. **`Resize()` the container when window resizes** — or the D3D surface won't match the window +6. **Destroy order matters** — elements first, then container, then manager + +--- + +**See also:** +- [Custom UI Architecture](../concepts/custom-ui-architecture.md) — How rendering works internally +- [Two Approaches: SDK-Rendered vs Self-Rendered](../concepts/custom-ui-vs-raw-data.md) +- [Raw Video Capture](raw-video-capture.md) — For self-rendered approach diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/examples/local-recording.md b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/local-recording.md new file mode 100644 index 00000000..45ebc133 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/local-recording.md @@ -0,0 +1,580 @@ +# Local Recording + +## Overview + +The Meeting SDK provides local recording capabilities through `IMeetingRecordingController`. Local recording saves meeting video/audio to the local disk as MP4 files. This is different from: + +- **Cloud Recording** - Saved to Zoom's cloud storage +- **Raw Recording** - Direct access to raw frames (see [raw-video-capture.md](raw-video-capture.md)) + +## Architecture + +``` +IMeetingService + └── GetMeetingRecordingController() → IMeetingRecordingController + ├── SetEvent(IMeetingRecordingCtrlEvent*) + ├── CanStartRecording(bool, unsigned int) + ├── StartRecording(time_t&) + ├── StopRecording() + ├── RequestLocalRecordingPrivilege() + └── IsSupportLocalRecording() +``` + +## Required Headers + +```cpp +#include +#include +#include +``` + +## Step 1: Implement the Recording Event Listener + +```cpp +// MeetingRecordingCtrlEventListener.h +#pragma once +#include +#include + +class MeetingRecordingCtrlEventListener + : public ZOOMSDK::IMeetingRecordingCtrlEvent { +public: + using PermissionCallback = std::function; + + MeetingRecordingCtrlEventListener(PermissionCallback onPermissionGranted = nullptr); + virtual ~MeetingRecordingCtrlEventListener() = default; + + // Local recording status changes + virtual void onRecordingStatus(ZOOMSDK::RecordingStatus status) override; + + // Cloud recording status changes + virtual void onCloudRecordingStatus(ZOOMSDK::RecordingStatus status) override; + + // Recording privilege changed (can now record or lost privilege) + virtual void onRecordPrivilegeChanged(bool bCanRec) override; + + // Result of RequestLocalRecordingPrivilege() + virtual void onLocalRecordingPrivilegeRequestStatus( + ZOOMSDK::RequestLocalRecordingStatus status + ) override; + + // Cloud recording permission request response + virtual void onRequestCloudRecordingResponse( + ZOOMSDK::RequestStartCloudRecordingStatus status + ) override; + + // Host received a recording privilege request + virtual void onLocalRecordingPrivilegeRequested( + ZOOMSDK::IRequestLocalRecordingPrivilegeHandler* handler + ) override; + + // Host received a cloud recording request + virtual void onStartCloudRecordingRequested( + ZOOMSDK::IRequestStartCloudRecordingHandler* handler + ) override; + + // MP4 conversion completed + virtual void onRecording2MP4Done( + bool bsuccess, + int iResult, + const zchar_t* szPath + ) override; + + // MP4 conversion progress + virtual void onRecording2MP4Processing(int iPercentage) override; + + // Custom UI recording layout callback + virtual void onCustomizedLocalRecordingSourceNotification( + ZOOMSDK::ICustomizedLocalRecordingLayoutHelper* layout_helper + ) override; + + // Cloud storage full warning + virtual void onCloudRecordingStorageFull(time_t gracePeriodDate) override; + + // Smart recording request + virtual void onEnableAndStartSmartRecordingRequested( + ZOOMSDK::IRequestEnableAndStartSmartRecordingHandler* handler + ) override; + + // Smart recording enable action + virtual void onSmartRecordingEnableActionCallback( + ZOOMSDK::ISmartRecordingEnableActionHandler* handler + ) override; + +private: + PermissionCallback m_onPermissionGranted; +}; +``` + +```cpp +// MeetingRecordingCtrlEventListener.cpp +#include "MeetingRecordingCtrlEventListener.h" +#include + +using namespace ZOOMSDK; + +MeetingRecordingCtrlEventListener::MeetingRecordingCtrlEventListener( + PermissionCallback onPermissionGranted +) : m_onPermissionGranted(onPermissionGranted) {} + +void MeetingRecordingCtrlEventListener::onRecordingStatus(RecordingStatus status) { + switch (status) { + case Recording_Start: + std::cout << "Recording started" << std::endl; + break; + case Recording_Stop: + std::cout << "Recording stopped" << std::endl; + break; + case Recording_Pause: + std::cout << "Recording paused" << std::endl; + break; + case Recording_Connecting: + std::cout << "Recording connecting..." << std::endl; + break; + case Recording_DiskFull: + std::cerr << "Recording stopped - disk full!" << std::endl; + break; + default: + std::cout << "Recording status: " << status << std::endl; + } +} + +void MeetingRecordingCtrlEventListener::onCloudRecordingStatus(RecordingStatus status) { + std::cout << "Cloud recording status: " << status << std::endl; +} + +void MeetingRecordingCtrlEventListener::onRecordPrivilegeChanged(bool bCanRec) { + std::cout << "Recording privilege: " << (bCanRec ? "GRANTED" : "REVOKED") << std::endl; + if (bCanRec && m_onPermissionGranted) { + m_onPermissionGranted(); + } +} + +void MeetingRecordingCtrlEventListener::onLocalRecordingPrivilegeRequestStatus( + RequestLocalRecordingStatus status +) { + switch (status) { + case LocalRecordingRequestStatus_Granted: + std::cout << "Recording privilege request: GRANTED" << std::endl; + break; + case LocalRecordingRequestStatus_Denied: + std::cout << "Recording privilege request: DENIED" << std::endl; + break; + case LocalRecordingRequestStatus_Timeout: + std::cout << "Recording privilege request: TIMEOUT" << std::endl; + break; + default: + std::cout << "Recording privilege request status: " << status << std::endl; + } +} + +void MeetingRecordingCtrlEventListener::onRequestCloudRecordingResponse( + RequestStartCloudRecordingStatus status +) { + std::cout << "Cloud recording request response: " << status << std::endl; +} + +void MeetingRecordingCtrlEventListener::onLocalRecordingPrivilegeRequested( + IRequestLocalRecordingPrivilegeHandler* handler +) { + if (handler) { + std::cout << "Recording privilege requested by user: " + << handler->GetRequesterId() << std::endl; + + // Auto-approve (or implement your logic) + // handler->GrantLocalRecordingPrivilege(); + // handler->DenyLocalRecordingPrivilege(); + } +} + +void MeetingRecordingCtrlEventListener::onStartCloudRecordingRequested( + IRequestStartCloudRecordingHandler* handler +) { + std::cout << "Cloud recording start requested" << std::endl; +} + +void MeetingRecordingCtrlEventListener::onRecording2MP4Done( + bool bsuccess, + int iResult, + const zchar_t* szPath +) { + if (bsuccess) { + std::wcout << L"Recording saved to: " << szPath << std::endl; + } else { + std::cerr << "Recording conversion failed with error: " << iResult << std::endl; + } +} + +void MeetingRecordingCtrlEventListener::onRecording2MP4Processing(int iPercentage) { + std::cout << "Converting to MP4: " << iPercentage << "%" << std::endl; +} + +void MeetingRecordingCtrlEventListener::onCustomizedLocalRecordingSourceNotification( + ICustomizedLocalRecordingLayoutHelper* layout_helper +) { + // Used for custom UI recording layout +} + +void MeetingRecordingCtrlEventListener::onCloudRecordingStorageFull(time_t gracePeriodDate) { + std::cerr << "Cloud recording storage full!" << std::endl; +} + +void MeetingRecordingCtrlEventListener::onEnableAndStartSmartRecordingRequested( + IRequestEnableAndStartSmartRecordingHandler* handler +) { + // Smart recording feature request +} + +void MeetingRecordingCtrlEventListener::onSmartRecordingEnableActionCallback( + ISmartRecordingEnableActionHandler* handler +) { + // Smart recording enable action +} +``` + +## Step 2: Initialize Recording Controller + +```cpp +// Global variables +IMeetingRecordingController* g_recordController = nullptr; +MeetingRecordingCtrlEventListener* g_recordListener = nullptr; +IMeetingParticipantsController* g_participantsController = nullptr; + +void initializeRecording(IMeetingService* meetingService) { + // Get recording controller + g_recordController = meetingService->GetMeetingRecordingController(); + if (!g_recordController) { + std::cerr << "Failed to get recording controller" << std::endl; + return; + } + + // Get participants controller (needed for permission checks) + g_participantsController = meetingService->GetMeetingParticipantsController(); + + // Create and set event listener + g_recordListener = new MeetingRecordingCtrlEventListener( + []() { + // Called when recording privilege is granted + attemptToStartRecording(); + } + ); + + SDKError err = g_recordController->SetEvent(g_recordListener); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to set recording listener: " << err << std::endl; + } + + std::cout << "Recording controller initialized" << std::endl; +} +``` + +## Step 3: Check Recording Permission + +```cpp +bool canStartRecording() { + if (!g_recordController) return false; + + // Check if local recording is supported + if (!g_recordController->IsSupportLocalRecording()) { + std::cout << "Local recording not supported" << std::endl; + return false; + } + + // Check if we can start recording + // false = local recording, 0 = current user + SDKError err = g_recordController->CanStartRecording(false, 0); + + if (err == SDKERR_SUCCESS) { + std::cout << "Can start recording" << std::endl; + return true; + } else if (err == SDKERR_NO_PERMISSION) { + std::cout << "No recording permission - requesting..." << std::endl; + + // Request permission from host + g_recordController->RequestLocalRecordingPrivilege(); + return false; + } else { + std::cerr << "Cannot start recording: " << err << std::endl; + return false; + } +} +``` + +## Step 4: Start/Stop Recording + +### Start Local Recording + +```cpp +void attemptToStartRecording() { + if (!g_recordController) return; + + if (!canStartRecording()) { + return; // Permission request will trigger callback when granted + } + + time_t startTime; + SDKError err = g_recordController->StartRecording(startTime); + + if (err == SDKERR_SUCCESS) { + std::cout << "Recording started at: " << startTime << std::endl; + } else { + std::cerr << "Failed to start recording: " << err << std::endl; + } +} +``` + +### Stop Recording + +```cpp +void stopRecording() { + if (!g_recordController) return; + + SDKError err = g_recordController->StopRecording(); + + if (err == SDKERR_SUCCESS) { + std::cout << "Recording stopped" << std::endl; + // Wait for onRecording2MP4Done callback for final file path + } else { + std::cerr << "Failed to stop recording: " << err << std::endl; + } +} +``` + +### Pause/Resume Recording + +```cpp +void pauseRecording() { + if (!g_recordController) return; + + SDKError err = g_recordController->PauseRecording(); + if (err == SDKERR_SUCCESS) { + std::cout << "Recording paused" << std::endl; + } +} + +void resumeRecording() { + if (!g_recordController) return; + + SDKError err = g_recordController->ResumeRecording(); + if (err == SDKERR_SUCCESS) { + std::cout << "Recording resumed" << std::endl; + } +} +``` + +## Step 5: Handle Permission Flow + +When you're not the host, you need to request recording permission: + +```cpp +void onInMeeting(IMeetingService* meetingService) { + initializeRecording(meetingService); + + // Check if we're the host + IUserInfo* myInfo = g_participantsController->GetMySelfUser(); + if (myInfo && myInfo->IsHost()) { + std::cout << "We are host - can record directly" << std::endl; + attemptToStartRecording(); + } else { + std::cout << "Not host - need to request permission" << std::endl; + if (!canStartRecording()) { + // Wait for onRecordPrivilegeChanged callback + } + } +} + +void onIsHost() { + std::cout << "Now host - can start recording" << std::endl; + attemptToStartRecording(); +} + +void onIsCoHost() { + std::cout << "Now co-host - checking recording permission" << std::endl; + attemptToStartRecording(); +} +``` + +## Step 6: Monitor Encoder Process (zTscoder.exe) + +After `StopRecording()`, Zoom uses `zTscoder.exe` to convert the recording to MP4. You can monitor this process: + +```cpp +#include +#include + +bool encoderHasStarted = false; +bool encoderFinished = false; + +bool IsProcessRunning(const std::wstring& processName) { + PROCESSENTRY32 entry; + entry.dwSize = sizeof(entry); + + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) return false; + + if (!Process32First(snapshot, &entry)) { + CloseHandle(snapshot); + return false; + } + + do { + if (std::wstring(entry.szExeFile) == processName) { + CloseHandle(snapshot); + return true; + } + } while (Process32Next(snapshot, &entry)); + + CloseHandle(snapshot); + return false; +} + +void monitorEncoder() { + const std::wstring encoderProcess = L"zTscoder.exe"; + + // Check if encoder started + if (IsProcessRunning(encoderProcess)) { + encoderHasStarted = true; + std::cout << "Encoder is running..." << std::endl; + } + + // Check if encoder finished + if (encoderHasStarted && !IsProcessRunning(encoderProcess)) { + encoderFinished = true; + std::cout << "Encoder finished - recording ready" << std::endl; + + // Now you can upload/move the recording file + uploadRecording(); + } +} +``` + +## Complete Integration Example + +```cpp +// Message loop with encoder monitoring +int main() { + LoadConfig(); + InitSDK(); + + MSG msg; + const std::wstring encoderProcess = L"zTscoder.exe"; + + while (!g_exit && GetMessage(&msg, nullptr, 0, 0) != 0) { + TranslateMessage(&msg); + DispatchMessage(&msg); + + // Monitor encoder after recording stops + if (IsProcessRunning(encoderProcess)) { + encoderHasStarted = true; + } + } + + // Wait for encoder to finish before cleanup + while (encoderHasStarted && !encoderFinished) { + if (!IsProcessRunning(encoderProcess)) { + encoderFinished = true; + std::cout << "Recording conversion complete" << std::endl; + + // Upload or process the recording file + processRecordingFile(); + } + Sleep(1000); // Check every second + } + + // Cleanup + if (meetingService) DestroyMeetingService(meetingService); + if (authService) DestroyAuthService(authService); + CleanUPSDK(); + + return 0; +} +``` + +## Recording Status Values + +```cpp +enum RecordingStatus { + Recording_Start, // Recording started + Recording_Stop, // Recording stopped + Recording_DiskFull, // Disk full error + Recording_Pause, // Recording paused + Recording_Connecting // Connecting to recording service +}; +``` + +## Permission Request Status Values + +```cpp +enum RequestLocalRecordingStatus { + LocalRecordingRequestStatus_Granted, // Request approved + LocalRecordingRequestStatus_Denied, // Request denied + LocalRecordingRequestStatus_Timeout, // Request timed out + LocalRecordingRequestStatus_None // No status +}; +``` + +## Recording File Location + +Local recordings are saved to the user's Zoom recordings folder: +- Default: `%USERPROFILE%\Documents\Zoom\` +- Meeting subfolder: `Meeting ID - Date/` + +The exact path is provided in `onRecording2MP4Done()` callback. + +## Error Handling + +```cpp +SDKError err = g_recordController->StartRecording(startTime); +switch (err) { + case SDKERR_SUCCESS: + std::cout << "Recording started" << std::endl; + break; + case SDKERR_NO_PERMISSION: + std::cerr << "No permission to record" << std::endl; + g_recordController->RequestLocalRecordingPrivilege(); + break; + case SDKERR_WRONG_USAGE: + std::cerr << "Cannot record now (meeting not started?)" << std::endl; + break; + case SDKERR_SERVICE_FAILED: + std::cerr << "Recording service failed" << std::endl; + break; + case SDKERR_NO_RECORDING_IN_PROGRESS: + std::cerr << "No recording in progress" << std::endl; + break; + default: + std::cerr << "Error: " << err << std::endl; +} +``` + +## Common Pitfalls + +1. **Permission timing**: Must be in meeting (`MEETING_STATUS_INMEETING`) before checking permission +2. **Host vs participant**: Non-hosts need to request permission first +3. **Encoder wait**: After `StopRecording()`, must wait for `zTscoder.exe` to finish +4. **MP4 callback**: `onRecording2MP4Done()` requires `EnableLocalRecordingConvertProgressBarDialog(false)` before meeting starts +5. **Disk space**: Recording will stop if disk is full (monitor `Recording_DiskFull`) +6. **View mode**: For gallery view recording, call `SwitchToVideoWall()` before starting + +## Gallery View Recording + +To record gallery view instead of active speaker: + +```cpp +void switchToGalleryView() { + IMeetingUIController* uiController = g_meetingService->GetUIController(); + if (uiController) { + SDKError err = uiController->SwitchToVideoWall(); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to switch to gallery view" << std::endl; + } + } +} + +void switchToActiveSpeaker() { + IMeetingUIController* uiController = g_meetingService->GetUIController(); + if (uiController) { + uiController->SwitchToActiveSpeaker(); + } +} +``` diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/examples/raw-video-capture.md b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/raw-video-capture.md new file mode 100644 index 00000000..456f0cab --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/raw-video-capture.md @@ -0,0 +1,814 @@ +# Raw Video Capture Example + +## Overview + +This guide shows how to capture raw video data from Zoom meetings using the Windows Meeting SDK. Raw video data is provided in **YUV420 (I420) format** for video and **PCM format** for audio. + +--- + +## Raw Recording vs Raw Streaming + +There are **two ways** to access raw data in the Zoom SDK: + +| Method | Permission Source | How to Get Permission | Use Case | +|--------|-------------------|----------------------|----------| +| **Raw Recording** | Local recording permission | Host/co-host OR request from host | Capture data with "recording" consent dialog | +| **Raw Streaming** | Live streaming permission | Must be licensed Pro/Business/Education/Enterprise | Capture data with "streaming" consent dialog | + +**Key differences**: +- **Raw Recording**: Disables local recording (`.mp4`) for the SDK user. Host can still cloud record. +- **Raw Streaming**: Other participants see "live streaming" notification instead of "recording". +- Both give you the same raw data access (YUV420 video, PCM audio). + +**SDK Version Requirements**: +- Raw recording: SDK 5.9.0+ +- Raw streaming by host: SDK 5.11.0+ +- Raw streaming by non-host: SDK 5.12.8+ +- Request local recording permission: SDK 5.13.5+ + +--- + +## Permission Requirements + +### For Raw Recording + +The meeting must have **local recording enabled**, AND you must meet **one** of: +- You are the meeting host or co-host +- You have been granted local recording permission by the host +- You joined with an OAuth app privilege token (see below) + +### For Raw Streaming + +You must meet **all** of: +- Current user has raw live streaming permission +- Meeting host has a Pro, Business, Education, or Enterprise account +- Meeting host is licensed for live streaming + +### OAuth App Privilege Token (Advanced) + +You can skip the "request permission from host" step by using OAuth tokens: + +**For recording**: +1. OAuth app requests `Meeting_token:read:admin:local_recording` (admin) or `Meeting_token:read:local_recording` (user) +2. Call REST API: `GET /meetings/{meetingId}/jointoken/local_recording` +3. Pass token to SDK via `app_privilege_token` join parameter + +**For streaming**: +1. OAuth app requests `Meeting_token:read:admin:live_streaming` (admin) or `Meeting_token:read:live_streaming` (user) +2. Call REST API: `GET /meetings/{meetingId}/jointoken/live_streaming` +3. Pass token to SDK via `app_privilege_token` join parameter + +--- + +## Complete Workflow + +### Step 1: Join Meeting Successfully + +Before capturing video, you must: +1. ✅ Initialize SDK +2. ✅ Authenticate with JWT token +3. ✅ Join meeting successfully (`MEETING_STATUS_INMEETING`) + +See [Authentication Pattern](authentication-pattern.md) for this setup. + +--- + +### Step 2: Start Raw Recording (or Streaming) + +**CRITICAL**: You MUST call `StartRawRecording()` before you can capture video data. + +```cpp +void OnInMeeting() { + std::cout << "[VIDEO] In meeting! Starting video capture..." << std::endl; + + // Get recording controller + IMeetingRecordingController* recordingCtrl = + meetingService->GetMeetingRecordingController(); + + if (!recordingCtrl) { + std::cerr << "[VIDEO] ERROR: Failed to get recording controller" << std::endl; + return; + } + + // Check if we can start recording + SDKError canStart = recordingCtrl->CanStartRecording(false, 0); + if (canStart != SDKERR_SUCCESS) { + std::cerr << "[VIDEO] Cannot start recording: " << canStart << std::endl; + return; + } + + // Start raw recording (this enables raw data capture) + SDKError err = recordingCtrl->StartRawRecording(); + if (err != SDKERR_SUCCESS) { + std::cerr << "[VIDEO] StartRawRecording failed: " << err << std::endl; + return; + } + + std::cout << "[VIDEO] Raw recording started!" << std::endl; + + // Wait a moment for recording to initialize + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + // Continue to Step 3... +} +``` + +**Important notes**: +- `StartRawRecording()` does NOT create a recording file on disk +- It only enables raw data capture via SDK callbacks +- You need host/co-host permissions OR special SDK app privileges +- If you get `SDKERR_WRONG_USAGE`, you may lack permissions + +**Alternative: Use Raw Streaming instead**: +```cpp +// If you have streaming permission instead of recording permission +IMeetingLiveStreamController* streamCtrl = meetingService->GetMeetingLiveStreamController(); + +SDKError err = streamCtrl->StartRawLiveStream(); +if (err != SDKERR_SUCCESS) { + std::cerr << "[VIDEO] StartRawLiveStream failed: " << err << std::endl; + return; +} + +std::cout << "[VIDEO] Raw streaming started!" << std::endl; +// Same subscription process applies after this +``` + +**Requesting permission at runtime**: +```cpp +// If you're not the host, request permission +IMeetingLiveStreamController* streamCtrl = meetingService->GetMeetingLiveStreamController(); +streamCtrl->RequestRawLiveStream(L"My AI Bot", L"https://example.com/description"); + +// Listen for onRawLiveStreamPrivilegeChanged callback to know when granted +``` + +--- + +### Step 3: Get Participant IDs + +You need to know which user's video you want to capture: + +```cpp +// Get participants controller +IMeetingParticipantsController* participantsCtrl = + meetingService->GetMeetingParticipantsController(); + +if (!participantsCtrl) { + std::cerr << "[VIDEO] Failed to get participants controller" << std::endl; + return; +} + +// Get list of all participants +IList* participantList = participantsCtrl->GetParticipantsList(); + +if (!participantList || participantList->GetCount() == 0) { + std::cerr << "[VIDEO] No participants in meeting" << std::endl; + return; +} + +// Get first participant's user ID +uint32_t userId = participantList->GetItem(0); +std::cout << "[VIDEO] Found participant: " << userId << std::endl; + +// You can also get the user's name: +IUserInfo* userInfo = participantsCtrl->GetUserByUserID(userId); +if (userInfo) { + const wchar_t* userName = userInfo->GetUserName(); + std::wcout << L"[VIDEO] User name: " << userName << std::endl; +} +``` + +**How to capture specific users**: +- **All participants**: Loop through `participantList` and subscribe to each +- **Active speaker**: Use `IMeetingVideoController()->GetActiveVideoUserID()` +- **Yourself**: Use `IMeetingParticipantsController()->GetMySelfUser()->GetUserID()` +- **Specific name**: Loop and match `GetUserName()` + +--- + +### Step 4: Implement Renderer Delegate + +This class receives the video frames: + +**ZoomSDKRendererDelegate.h**: +```cpp +#pragma once +#include +#include +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class ZoomSDKRendererDelegate : public IZoomSDKRendererDelegate { +public: + ZoomSDKRendererDelegate(); + ~ZoomSDKRendererDelegate(); + + // Called when a new video frame arrives + void onRawDataFrameReceived(YUVRawDataI420* data) override; + + // Called when raw data status changes + void onRawDataStatusChanged(RawDataStatus status) override; + + // Called before renderer is destroyed + void onRendererBeDestroyed() override; + +private: + void SaveToRawYUVFile(YUVRawDataI420* data); + int frameCount; +}; +``` + +**ZoomSDKRendererDelegate.cpp**: +```cpp +#include "ZoomSDKRendererDelegate.h" + +ZoomSDKRendererDelegate::ZoomSDKRendererDelegate() : frameCount(0) { + std::cout << "[VIDEO] Renderer delegate created" << std::endl; +} + +ZoomSDKRendererDelegate::~ZoomSDKRendererDelegate() { + std::cout << "[VIDEO] Renderer destroyed. Total frames: " << frameCount << std::endl; +} + +void ZoomSDKRendererDelegate::onRawDataFrameReceived(YUVRawDataI420* data) { + if (!data) return; + + // Get frame dimensions + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Get rotation (0, 90, 180, 270 degrees) + int rotation = data->GetRotation(); + + // Log every 30 frames (every ~1 second at 30fps) + if (frameCount % 30 == 0) { + std::cout << "[VIDEO] Frame " << frameCount + << " - " << width << "x" << height + << " (rotation: " << rotation << "°)" << std::endl; + } + + // Process the frame (save to file, encode, analyze, etc.) + SaveToRawYUVFile(data); + + frameCount++; +} + +void ZoomSDKRendererDelegate::onRawDataStatusChanged(RawDataStatus status) { + switch (status) { + case RawData_On: + std::cout << "[VIDEO] Raw data ON" << std::endl; + break; + case RawData_Off: + std::cout << "[VIDEO] Raw data OFF" << std::endl; + break; + default: + std::cout << "[VIDEO] Status: " << status << std::endl; + break; + } +} + +void ZoomSDKRendererDelegate::onRendererBeDestroyed() { + std::cout << "[VIDEO] Renderer being destroyed" << std::endl; +} + +void ZoomSDKRendererDelegate::SaveToRawYUVFile(YUVRawDataI420* data) { + // Open output file in append binary mode + std::ofstream outputFile("output.yuv", std::ios::out | std::ios::binary | std::ios::app); + if (!outputFile.is_open()) { + std::cerr << "[VIDEO] Error opening output.yuv" << std::endl; + return; + } + + // YUV420 format: Y plane + U plane + V plane + size_t ySize = data->GetStreamWidth() * data->GetStreamHeight(); + size_t uvSize = ySize / 4; // U and V are quarter size each + + // Write planes in order: Y, U, V + outputFile.write(data->GetYBuffer(), ySize); + outputFile.write(data->GetUBuffer(), uvSize); + outputFile.write(data->GetVBuffer(), uvSize); + + outputFile.close(); +} +``` + +--- + +### Step 5: Create Renderer and Subscribe + +```cpp +// Create renderer helper and delegate +IZoomSDKVideoSource* videoHelper = nullptr; +ZoomSDKRendererDelegate* videoDelegate = new ZoomSDKRendererDelegate(); + +SDKError err = createRenderer(&videoHelper, videoDelegate); +if (err != SDKERR_SUCCESS || !videoHelper) { + std::cerr << "[VIDEO] Failed to create renderer: " << err << std::endl; + delete videoDelegate; + return; +} + +// Set desired resolution (affects CPU/bandwidth usage) +videoHelper->setRawDataResolution(ZoomSDKResolution_720P); +// Other options: ZoomSDKResolution_90P, 180P, 360P, 720P, 1080P + +// Subscribe to user's video stream +err = videoHelper->subscribe(userId, RAW_DATA_TYPE_VIDEO); +if (err != SDKERR_SUCCESS) { + std::cerr << "[VIDEO] Subscribe failed: " << err << std::endl; + return; +} + +std::cout << "[VIDEO] Successfully subscribed! Frames will arrive in onRawDataFrameReceived()" << std::endl; +``` + +**Important notes**: +- Use `createRenderer()` (global function), not `CreateRenderer()` or `new` +- Set resolution BEFORE subscribing +- Higher resolutions = more CPU/bandwidth but better quality +- Frames arrive asynchronously in `onRawDataFrameReceived()` + +--- + +## YUV420 (I420) Format Explained + +### What is YUV420? + +YUV420 separates image into: +- **Y (luma)**: Brightness information (full resolution) +- **U (Cb, blue-difference)**: Color information (quarter resolution) +- **V (Cr, red-difference)**: Color information (quarter resolution) + +### Memory Layout + +For a 1920x1080 frame: + +``` +Total bytes: 1920 * 1080 * 1.5 = 3,110,400 bytes + +Y plane: [0 to 2,073,599] (1920 * 1080 = 2,073,600 bytes) +U plane: [2,073,600 to 2,592,639] (960 * 540 = 518,400 bytes) +V plane: [2,592,640 to 3,110,399] (960 * 540 = 518,400 bytes) +``` + +**Formula**: +```cpp +size_t ySize = width * height; +size_t uSize = (width / 2) * (height / 2) = width * height / 4; +size_t vSize = (width / 2) * (height / 2) = width * height / 4; +size_t totalSize = ySize + uSize + vSize = width * height * 1.5; +``` + +### Accessing Frame Data + +```cpp +void ProcessFrame(YUVRawDataI420* data) { + int width = data->GetStreamWidth(); // e.g., 1920 + int height = data->GetStreamHeight(); // e.g., 1080 + + // Get pointers to each plane + const char* yBuffer = data->GetYBuffer(); // Brightness + const char* uBuffer = data->GetUBuffer(); // Blue-difference + const char* vBuffer = data->GetVBuffer(); // Red-difference + + // Calculate sizes + size_t ySize = width * height; + size_t uvSize = (width / 2) * (height / 2); + + // Example: Access pixel at (x, y) + int x = 100, y = 50; + + // Y value (full resolution) + unsigned char yValue = yBuffer[y * width + x]; + + // U and V values (subsampled 2x2) + unsigned char uValue = uBuffer[(y/2) * (width/2) + (x/2)]; + unsigned char vValue = vBuffer[(y/2) * (width/2) + (x/2)]; + + std::cout << "Pixel (" << x << "," << y << "): " + << "Y=" << (int)yValue << " " + << "U=" << (int)uValue << " " + << "V=" << (int)vValue << std::endl; +} +``` + +### Converting YUV420 to RGB + +```cpp +void YUVtoRGB(unsigned char y, unsigned char u, unsigned char v, + unsigned char& r, unsigned char& g, unsigned char& b) { + int c = y - 16; + int d = u - 128; + int e = v - 128; + + int red = (298 * c + 409 * e + 128) >> 8; + int green = (298 * c - 100 * d - 208 * e + 128) >> 8; + int blue = (298 * c + 516 * d + 128) >> 8; + + // Clamp to [0, 255] + r = (red < 0) ? 0 : (red > 255) ? 255 : red; + g = (green < 0) ? 0 : (green > 255) ? 255 : green; + b = (blue < 0) ? 0 : (blue > 255) ? 255 : blue; +} +``` + +--- + +## Complete Example: Save to File + +### Save Raw YUV File + +```cpp +void SaveToRawYUVFile(YUVRawDataI420* data) { + std::ofstream file("output.yuv", std::ios::binary | std::ios::app); + + size_t ySize = data->GetStreamWidth() * data->GetStreamHeight(); + size_t uvSize = ySize / 4; + + file.write(data->GetYBuffer(), ySize); + file.write(data->GetUBuffer(), uvSize); + file.write(data->GetVBuffer(), uvSize); + + file.close(); +} +``` + +**Play back with ffplay**: +```bash +ffplay -f rawvideo -pixel_format yuv420p -video_size 1920x1080 -framerate 30 output.yuv +``` + +### Save as PNG Images (Using OpenCV) + +```cpp +#include + +void SaveAsPNG(YUVRawDataI420* data, int frameNumber) { + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Create YUV Mat + cv::Mat yuvImage(height + height/2, width, CV_8UC1); + + // Copy Y plane + memcpy(yuvImage.data, data->GetYBuffer(), width * height); + + // Copy U plane + memcpy(yuvImage.data + width * height, data->GetUBuffer(), width * height / 4); + + // Copy V plane + memcpy(yuvImage.data + width * height * 5/4, data->GetVBuffer(), width * height / 4); + + // Convert to BGR + cv::Mat bgrImage; + cv::cvtColor(yuvImage, bgrImage, cv::COLOR_YUV2BGR_I420); + + // Save as PNG + std::string filename = "frame_" + std::to_string(frameNumber) + ".png"; + cv::imwrite(filename, bgrImage); +} +``` + +--- + +## Handling Video Rotation + +Video streams may be rotated (0°, 90°, 180°, 270°): + +```cpp +void ProcessFrame(YUVRawDataI420* data) { + int rotation = data->GetRotation(); // 0, 90, 180, or 270 + + if (rotation == 0) { + // Normal orientation, no rotation needed + SaveFrame(data); + } else { + // Need to rotate frame before displaying/processing + RotateFrame(data, rotation); + SaveFrame(data); + } +} +``` + +**OpenCV rotation**: +```cpp +cv::Mat RotateImage(cv::Mat& image, int rotation) { + cv::Mat rotated; + switch (rotation) { + case 90: + cv::rotate(image, rotated, cv::ROTATE_90_CLOCKWISE); + break; + case 180: + cv::rotate(image, rotated, cv::ROTATE_180); + break; + case 270: + cv::rotate(image, rotated, cv::ROTATE_90_COUNTERCLOCKWISE); + break; + default: + rotated = image; + break; + } + return rotated; +} +``` + +--- + +## Performance Considerations + +### Frame Rate + +Video typically arrives at: +- **720p**: ~30 fps +- **1080p**: ~30 fps +- **Lower resolutions**: ~15-30 fps + +Process frames quickly to avoid dropping: +```cpp +void onRawDataFrameReceived(YUVRawDataI420* data) { + // Fast: Just save to buffer/queue + frameQueue.push(data); // Process in separate thread + + // Slow: Heavy processing here + ConvertToRGB(data); // May drop frames! + EncodeToH264(data); + SaveToDisk(data); +} +``` + +### Memory Usage + +**Per frame**: +- 720p (1280x720): 1.38 MB +- 1080p (1920x1080): 3.11 MB + +**At 30 fps**: +- 720p: ~41 MB/sec +- 1080p: ~93 MB/sec + +Use buffering and separate processing threads for high performance. + +### Resolution Selection + +```cpp +// Lower resolution = less CPU/bandwidth +videoHelper->setRawDataResolution(ZoomSDKResolution_360P); // Good for analysis + +// Higher resolution = better quality +videoHelper->setRawDataResolution(ZoomSDKResolution_1080P); // Good for recording +``` + +--- + +## Unsubscribing and Cleanup + +### Stop Capturing Video + +```cpp +// Unsubscribe from video stream +if (videoHelper) { + videoHelper->unSubscribe(userId, RAW_DATA_TYPE_VIDEO); +} + +// Stop raw recording +if (recordingCtrl) { + recordingCtrl->StopRawRecording(); +} +``` + +### Cleanup + +```cpp +// Renderer delegate is automatically deleted by SDK +// Don't call delete on videoDelegate yourself! + +// Just null out the pointer +videoHelper = nullptr; +videoDelegate = nullptr; +``` + +--- + +## Audio Raw Data + +Audio is available in **PCM format** through separate callbacks: + +### Set Up Audio Raw Data + +```cpp +class ZoomAudioRawDataDelegate : public IZoomSDKAudioRawDataDelegate { +public: + // Called for each participant's audio (no support for telephone participants) + void onOneWayAudioRawDataReceived(AudioRawData* data_, uint32_t node_id) override { + // Process individual participant's audio + std::cout << "[AUDIO] Received audio from user: " << node_id << std::endl; + } + + // Called for mixed meeting audio (what all participants hear) + void onMixedAudioRawDataReceived(AudioRawData* data_) override { + // Process mixed audio from all participants + SavePCMData(data_); + } +}; + +// Subscribe to audio +IZoomSDKAudioRawDataHelper* audioHelper = GetAudioRawdataHelper(); +ZoomAudioRawDataDelegate* audioDelegate = new ZoomAudioRawDataDelegate(); +audioHelper->subscribe(audioDelegate); +``` + +**Two audio callbacks**: +- `onOneWayAudioRawDataReceived`: Individual participant audio (excludes phone participants) +- `onMixedAudioRawDataReceived`: Combined meeting audio (what you'd hear in the meeting) + +--- + +## Screen Share Raw Data + +You can also capture screen share data (separate from video): + +```cpp +// Subscribe to share data instead of video +videoHelper->subscribe(userId, RAW_DATA_TYPE_SHARE); +``` + +The same `IZoomSDKRendererDelegate::onRawDataFrameReceived()` callback is used, but you'll receive share content instead of camera video. + +--- + +## Alpha Channel Mode (Background Removal) + +Meeting hosts with a raw streaming token can enable **alpha channel mode**, which provides a mask to remove participant backgrounds. This is useful for rendering meeting participants in custom virtual environments. + +### Requirements +- Must be meeting host with raw streaming token +- Used with raw data capture + +### Check and Enable Alpha Channel + +```cpp +IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController(); + +// Check if alpha channel mode can be enabled +if (videoCtrl->CanEnableAlphaChannelMode()) { + // Enable alpha channel mode + SDKError err = videoCtrl->EnableAlphaChannelMode(true); + if (err == SDKERR_SUCCESS) { + std::cout << "[ALPHA] Alpha channel mode enabled" << std::endl; + } +} + +// Check if currently enabled +bool isEnabled = videoCtrl->IsAlphaChannelModeEnabled(); +std::cout << "[ALPHA] Alpha mode active: " << (isEnabled ? "yes" : "no") << std::endl; +``` + +### Listen for Alpha Mode Changes + +Implement `IMeetingVideoCtrlEvent` callback: + +```cpp +class VideoCtrlEventListener : public IMeetingVideoCtrlEvent { +public: + void onVideoAlphaChannelStatusChanged(bool isAlphaModeOn) override { + std::cout << "[ALPHA] Alpha channel mode: " + << (isAlphaModeOn ? "ON" : "OFF") << std::endl; + } + + // ... other IMeetingVideoCtrlEvent methods +}; +``` + +### Access Alpha Data in Raw Frames + +When alpha channel mode is enabled, `YUVRawDataI420` has additional methods: + +```cpp +void onRawDataFrameReceived(YUVRawDataI420* data) override { + // Get standard YUV data + char* yBuffer = data->GetYBuffer(); + char* uBuffer = data->GetUBuffer(); + char* vBuffer = data->GetVBuffer(); + + // Get alpha mask (only available when alpha mode is enabled) + char* alphaBuffer = data->GetAlphaBuffer(); + unsigned int alphaLen = data->GetAlphaBufferLen(); + + if (alphaBuffer != nullptr && alphaLen > 0) { + // Alpha mask is available! + // Use it to remove background from video frame + ProcessWithAlphaMask(data, alphaBuffer, alphaLen); + } else { + // No alpha data (alpha mode not enabled or not available) + ProcessNormalFrame(data); + } +} +``` + +### Alpha Mask Format + +The alpha buffer contains a grayscale mask where: +- **White (255)** = Foreground (participant's body) +- **Black (0)** = Background (to be removed) +- **Gray values** = Edge blending + +### Use Cases + +1. **Custom Virtual Backgrounds**: Replace participant backgrounds with custom scenes +2. **Mixed Reality**: Render participants in 3D virtual environments +3. **Green Screen Alternative**: Remove backgrounds without physical green screen +4. **Video Compositing**: Layer participants over other content + +--- + +## Important Performance Note + +From official documentation: +> **Do not perform heavy operations from within the raw data callbacks.** This may cause delayed or lost data. + +**Recommended pattern**: +```cpp +void onRawDataFrameReceived(YUVRawDataI420* data) { + // Fast: Copy to queue and return immediately + Frame copy; + copy.width = data->GetStreamWidth(); + copy.height = data->GetStreamHeight(); + memcpy(copy.buffer, data->GetYBuffer(), data->GetBufferLen()); + frameQueue.enqueue(copy); // Let separate thread process + + // DON'T: Heavy processing here + // EncodeH264(data); // Blocks callback! + // SaveToNetwork(data); // Too slow! +} +``` + +--- + +## Troubleshooting + +### No Frames Received + +**Checklist**: +- [ ] Called `StartRawRecording()` or `StartRawLiveStream()` BEFORE subscribing +- [ ] Waited ~500ms after starting before subscribing +- [ ] User ID is valid (not 0, exists in participant list) +- [ ] User has their video turned on +- [ ] Subscribed to correct data type (`RAW_DATA_TYPE_VIDEO`) +- [ ] Renderer delegate implements `onRawDataFrameReceived()` correctly +- [ ] You have the required permissions (host/co-host or granted permission) + +**Test**: Subscribe to your own video stream first (easier to control) + +### Frames Look Corrupted + +**Checklist**: +- [ ] Using YUV420 (I420) format, not RGB +- [ ] Buffer size is `width * height * 1.5` bytes +- [ ] Writing/reading planes in correct order: Y, U, V +- [ ] U and V planes are quarter size each, not half size +- [ ] Handling rotation correctly + +**Test**: Save raw YUV and play with ffplay to verify format + +### Performance Issues / Dropped Frames + +**Solutions**: +- Lower resolution: `setRawDataResolution(ZoomSDKResolution_360P)` +- Process frames in separate thread +- Use frame queue/buffer +- Skip frames if processing is slow (process every 2nd or 3rd frame) + +--- + +## Complete Working Example + +See the full working implementation in: +``` +C:\tempsdk\zoom-windows-sdk-sample\src\ +├── ZoomSDKRendererDelegate.h +├── ZoomSDKRendererDelegate.cpp +└── main.cpp (OnInMeeting function) +``` + +Key files to reference: +- Renderer delegate implementation +- Subscription workflow in `OnInMeeting()` +- YUV file saving logic + +--- + +## Related Documentation + +- [Authentication Pattern](authentication-pattern.md) - How to join meetings first +- [Windows Message Loop](../troubleshooting/windows-message-loop.md) - Required for callbacks +- [Interface Methods](../references/interface-methods.md) - Required virtual methods +- [Common Issues](../troubleshooting/common-issues.md) - Troubleshooting guide + +--- + +**Last Updated**: Based on Zoom Windows Meeting SDK v6.7.2.26830 diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/examples/send-raw-data.md b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/send-raw-data.md new file mode 100644 index 00000000..b2e698a1 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/send-raw-data.md @@ -0,0 +1,478 @@ +# Send Raw Data (Virtual Camera & Microphone) + +Send custom video and audio into a Zoom meeting as a virtual camera or microphone. This enables: +- Streaming pre-recorded video files +- AI-generated video/audio +- Custom video effects +- Screen capture injection +- Audio playback into meetings + +--- + +## Overview + +The SDK provides three "send" interfaces: +1. **IZoomSDKVideoSource** - Virtual camera (send video) +2. **IZoomSDKVirtualAudioMicEvent** - Virtual microphone (send audio) +3. **IZoomSDKShareSource** - Virtual share source (send screen share) + +All follow the same pattern: +1. Implement the interface +2. Pass it during meeting join/start +3. SDK calls your callbacks when ready to send + +--- + +## Send Video (Virtual Camera) + +### Interface: IZoomSDKVideoSource + +```cpp +#include + +class ZoomSDKVideoSource : public IZoomSDKVideoSource { +private: + IZoomSDKVideoSender* video_sender_ = nullptr; + string video_source_; + +public: + ZoomSDKVideoSource(string video_source) : video_source_(video_source) {} + + void onInitialize(IZoomSDKVideoSender* sender, + IList* support_cap_list, + VideoSourceCapability& suggest_cap) override { + // Store the sender - you'll use this to send frames + video_sender_ = sender; + std::cout << "Video source initialized" << std::endl; + } + + void onPropertyChange(IList* support_cap_list, + VideoSourceCapability suggest_cap) override { + // SDK suggests resolution/framerate based on network + std::cout << "Suggested: " << suggest_cap.width << "x" + << suggest_cap.height << " @ " << suggest_cap.frame << "fps" << std::endl; + } + + void onStartSend() override { + // SDK is ready - start sending frames in a separate thread + std::thread([this]() { PlayVideo(); }).detach(); + } + + void onStopSend() override { + // Stop sending frames + running_ = false; + } + + void onUninitialized() override { + video_sender_ = nullptr; + } + +private: + bool running_ = false; + + void PlayVideo() { + running_ = true; + + // Open video file with OpenCV + cv::VideoCapture cap(video_source_); + if (!cap.isOpened()) { + std::cerr << "Failed to open video: " << video_source_ << std::endl; + return; + } + + while (running_ && video_sender_) { + cv::Mat frame; + if (!cap.read(frame)) { + cap.set(cv::CAP_PROP_POS_FRAMES, 0); // Loop video + continue; + } + + // Convert BGR to YUV420 (I420) + cv::Mat yuv; + cv::cvtColor(frame, yuv, cv::COLOR_BGR2YUV_I420); + + // Send frame + char* buffer = (char*)yuv.data; + int width = frame.cols; + int height = frame.rows; + int frameLen = yuv.total() * yuv.elemSize(); + + SDKError err = video_sender_->sendVideoFrame(buffer, width, height, frameLen, 0); + if (err != SDKERR_SUCCESS) { + std::cerr << "sendVideoFrame failed: " << err << std::endl; + } + + // Control framerate (e.g., 30fps = 33ms per frame) + std::this_thread::sleep_for(std::chrono::milliseconds(33)); + } + } +}; +``` + +### Register Virtual Camera + +```cpp +// Create your video source +ZoomSDKVideoSource* videoSource = new ZoomSDKVideoSource("my_video.mp4"); + +// Get the raw data helper +IZoomSDKVideoSourceHelper* videoSourceHelper = GetRawdataVideoSourceHelper(); + +// Set as external video source +videoSourceHelper->setExternalVideoSource(videoSource); + +// Join meeting - your video will be the "camera" +meetingService->Join(joinParam); +``` + +### Frame Format: YUV420 (I420) + +``` +YUV420 Layout (1920x1080 example): +┌────────────────────────┐ +│ Y plane │ 1920 x 1080 = 2,073,600 bytes +│ (luminance) │ +├────────────────────────┤ +│ U plane (Cb) │ 960 x 540 = 518,400 bytes +├────────────────────────┤ +│ V plane (Cr) │ 960 x 540 = 518,400 bytes +└────────────────────────┘ +Total: width * height * 1.5 = 3,110,400 bytes +``` + +--- + +## Send Audio (Virtual Microphone) + +### Interface: IZoomSDKVirtualAudioMicEvent + +```cpp +#include + +class ZoomSDKVirtualAudioMic : public IZoomSDKVirtualAudioMicEvent { +private: + IZoomSDKAudioRawDataSender* audio_sender_ = nullptr; + string audio_source_; + bool running_ = false; + +public: + ZoomSDKVirtualAudioMic(string audio_source) : audio_source_(audio_source) {} + + void onMicInitialize(IZoomSDKAudioRawDataSender* pSender) override { + audio_sender_ = pSender; + std::cout << "Virtual mic initialized" << std::endl; + } + + void onMicStartSend() override { + // SDK is ready - start sending audio + std::thread([this]() { PlayAudio(); }).detach(); + } + + void onMicStopSend() override { + running_ = false; + } + + void onMicUninitialized() override { + audio_sender_ = nullptr; + } + +private: + void PlayAudio() { + running_ = true; + + // Open WAV file + std::ifstream file(audio_source_, std::ios::binary); + if (!file.is_open()) { + std::cerr << "Failed to open audio: " << audio_source_ << std::endl; + return; + } + + // Skip WAV header (44 bytes for standard WAV) + file.seekg(44, std::ios::beg); + + // Audio parameters (must match your WAV file!) + const int sampleRate = 48000; // 48kHz + const size_t chunkSize = 640; // Send in 640-byte chunks + + std::vector buffer(chunkSize); + + while (running_ && audio_sender_ && file.good()) { + file.read(buffer.data(), chunkSize); + std::streamsize bytesRead = file.gcount(); + + if (bytesRead > 0) { + // Send audio chunk + SDKError err = audio_sender_->send( + buffer.data(), + bytesRead, + sampleRate, + ZoomSDKAudioChannel_Mono // or ZoomSDKAudioChannel_Stereo + ); + + if (err != SDKERR_SUCCESS) { + std::cerr << "send audio failed: " << err << std::endl; + } + } + + // Control timing based on sample rate and chunk size + // 640 bytes / 2 (16-bit) / 48000 Hz = ~6.67ms + std::this_thread::sleep_for(std::chrono::microseconds(6667)); + } + + file.close(); + } +}; +``` + +### Register Virtual Microphone + +```cpp +// Create your audio source +ZoomSDKVirtualAudioMic* virtualMic = new ZoomSDKVirtualAudioMic("my_audio.wav"); + +// Get audio helper +IZoomSDKAudioRawDataHelper* audioHelper = GetAudioRawdataHelper(); + +// Set as virtual mic +audioHelper->setExternalAudioSource(virtualMic); + +// Join meeting - your audio will be the "microphone" +meetingService->Join(joinParam); +``` + +### Audio Format: PCM 16-bit + +``` +Required format: +- Encoding: PCM (signed 16-bit little-endian) +- Sample rates: 8000, 16000, 32000, 44100, or 48000 Hz +- Channels: Mono (1) or Stereo (2) + +WAV File Structure: +┌──────────────────┐ +│ RIFF Header │ 44 bytes (skip this) +├──────────────────┤ +│ Audio Data │ PCM samples +│ (16-bit signed) │ +└──────────────────┘ +``` + +--- + +## Send Share Screen (Virtual Share) + +### Interface: IZoomSDKShareSource + +```cpp +#include + +class ZoomSDKShareSource : public IZoomSDKShareSource { +private: + IZoomSDKShareSender* share_sender_ = nullptr; + +public: + void onShareSendStarted(IZoomSDKShareSender* pSender) override { + share_sender_ = pSender; + std::thread([this]() { SendShareFrames(); }).detach(); + } + + void onShareSendStopped() override { + share_sender_ = nullptr; + } + +private: + void SendShareFrames() { + while (share_sender_) { + // Capture or generate your share content + cv::Mat frame = CaptureMyContent(); + + // Convert to YUV420 + cv::Mat yuv; + cv::cvtColor(frame, yuv, cv::COLOR_BGR2YUV_I420); + + char* buffer = (char*)yuv.data; + int width = frame.cols; + int height = frame.rows; + int frameLen = yuv.total() * yuv.elemSize(); + + share_sender_->sendShareFrame(buffer, width, height, frameLen); + + std::this_thread::sleep_for(std::chrono::milliseconds(33)); + } + } +}; +``` + +### Start Virtual Share + +```cpp +// Get share helper +IMeetingShareController* shareCtrl = meetingService->GetMeetingShareController(); + +// Create share source +ZoomSDKShareSource* shareSource = new ZoomSDKShareSource(); + +// Start sharing with your source +shareCtrl->StartShareWithPreviewEnabled(shareSource); +``` + +--- + +## Complete Example: Video Bot + +```cpp +#include +#include +#include +#include +#include +#include + +using namespace ZOOMSDK; + +// Global references +IMeetingService* meetingService = nullptr; +IAuthService* authService = nullptr; +ZoomSDKVideoSource* videoSource = nullptr; + +void onInMeeting() { + std::cout << "In meeting - video source active!" << std::endl; +} + +void JoinMeeting() { + CreateMeetingService(&meetingService); + + // Create video source BEFORE joining + videoSource = new ZoomSDKVideoSource("Big_Buck_Bunny.mp4"); + IZoomSDKVideoSourceHelper* helper = GetRawdataVideoSourceHelper(); + helper->setExternalVideoSource(videoSource); + + // Configure join parameters + JoinParam joinParam; + joinParam.userType = SDK_UT_WITHOUT_LOGIN; + + JoinParam4WithoutLogin& params = joinParam.param.withoutloginuserJoin; + params.meetingNumber = 1234567890; + params.psw = L"password"; + params.userName = L"Video Bot"; + params.isVideoOff = false; // Video ON to use virtual camera + params.isAudioOff = true; + + meetingService->SetEvent(new MeetingServiceEventListener(&onInMeeting)); + meetingService->Join(joinParam); +} + +void SDKAuth() { + CreateAuthService(&authService); + authService->SetEvent(new AuthServiceEventListener(&JoinMeeting)); + + AuthContext ctx; + ctx.jwt_token = L"your_jwt_token"; + authService->SDKAuth(ctx); +} + +int main() { + // Initialize + InitParam initParam; + initParam.strWebDomain = L"https://zoom.us"; + InitSDK(initParam); + + // Start auth flow + SDKAuth(); + + // Message loop + MSG msg; + while (GetMessage(&msg, nullptr, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Cleanup + CleanUPSDK(); + return 0; +} +``` + +--- + +## Key Patterns + +### 1. Threading is Required + +Sending frames in callbacks will block the SDK. Always use a separate thread: + +```cpp +void onStartSend() override { + std::thread([this]() { SendFrames(); }).detach(); +} +``` + +### 2. Match Frame Rate to Network + +The SDK suggests resolution/framerate in `onPropertyChange`. Respect these suggestions: + +```cpp +void onPropertyChange(IList* caps, VideoSourceCapability suggest) override { + target_width_ = suggest.width; + target_height_ = suggest.height; + target_fps_ = suggest.frame; +} +``` + +### 3. Handle Stop/Restart Gracefully + +```cpp +void onStopSend() override { + running_ = false; // Signal thread to stop +} + +void onStartSend() override { + running_ = true; // Signal thread to start + std::thread([this]() { SendLoop(); }).detach(); +} +``` + +### 4. YUV420 Conversion + +Using OpenCV: +```cpp +cv::Mat yuv; +cv::cvtColor(bgrFrame, yuv, cv::COLOR_BGR2YUV_I420); +``` + +Manual conversion: +```cpp +// Y = 0.299*R + 0.587*G + 0.114*B +// U = -0.169*R - 0.331*G + 0.500*B + 128 +// V = 0.500*R - 0.419*G - 0.081*B + 128 +``` + +--- + +## Common Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| No video showing | `isVideoOff = true` in join params | Set `isVideoOff = false` | +| Frames not sending | Blocking in callback | Use separate thread | +| Wrong colors | BGR instead of YUV | Convert to YUV420 | +| Choppy video | Wrong timing | Match suggested FPS | +| Audio glitches | Wrong sample rate | Use 48000 Hz | +| Audio silent | Wrong chunk size | Use 640 byte chunks | + +--- + +## Related Documentation + +- [Raw Video Capture](raw-video-capture.md) - Receiving video (opposite direction) +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Universal 3-step pattern +- [Authentication Pattern](authentication-pattern.md) - Complete auth workflow + +--- + +## Sample Projects + +Local samples demonstrating these concepts: +- `C:\tempsdk\zoom_meetingsdk_windows_rawdatademos\SendVideoRawData\` +- `C:\tempsdk\zoom_meetingsdk_windows_rawdatademos\SendAudioRawData\` +- `C:\tempsdk\zoom_meetingsdk_windows_rawdatademos\SendShareScreenRawData\` diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/examples/service-quality.md b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/service-quality.md new file mode 100644 index 00000000..335ffaaf --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/service-quality.md @@ -0,0 +1,249 @@ +# Service Quality Monitoring + +## Overview + +Monitor video, audio, and screen sharing quality during meetings to detect network issues and notify users of unstable conditions. Works with both **Default UI** and **Custom UI**. + +--- + +## Connection Quality Enum + +All quality methods return a `ConnectionQuality` value: + +```cpp +enum ConnectionQuality { + Conn_Quality_Unknown, // Unknown connection status + Conn_Quality_Very_Bad, // Very poor quality + Conn_Quality_Bad, // Poor quality + Conn_Quality_Not_Good, // Not good quality + Conn_Quality_Normal, // Normal quality + Conn_Quality_Good, // Good quality + Conn_Quality_Excellent, // Excellent quality +}; +``` + +--- + +## Video Quality + +Get video connection quality for the local user: + +```cpp +// Get quality of video YOU ARE SENDING +ConnectionQuality sentQuality = meetingService->GetVideoConnQuality(true); +std::cout << "Sent video quality: " << sentQuality << std::endl; + +// Get quality of video YOU ARE RECEIVING +ConnectionQuality receivedQuality = meetingService->GetVideoConnQuality(false); +std::cout << "Received video quality: " << receivedQuality << std::endl; +``` + +**Requirements**: At least 2 users must be sending/receiving video. + +--- + +## Audio Quality + +Get audio connection quality for the local user: + +```cpp +// Get quality of audio YOU ARE SENDING +ConnectionQuality sentQuality = meetingService->GetAudioConnQuality(true); +std::cout << "Sent audio quality: " << sentQuality << std::endl; + +// Get quality of audio YOU ARE RECEIVING +ConnectionQuality receivedQuality = meetingService->GetAudioConnQuality(false); +std::cout << "Received audio quality: " << receivedQuality << std::endl; +``` + +**Requirements**: At least 2 users must be actively sending audio. + +--- + +## Screen Share Quality + +Get screen sharing connection quality for the local user: + +```cpp +// Get quality of screen share YOU ARE SENDING +ConnectionQuality sentQuality = meetingService->GetSharingConnQuality(true); +std::cout << "Sent share quality: " << sentQuality << std::endl; + +// Get quality of screen share YOU ARE RECEIVING +ConnectionQuality receivedQuality = meetingService->GetSharingConnQuality(false); +std::cout << "Received share quality: " << receivedQuality << std::endl; +``` + +**Requirements**: At least 2 users in session, with at least one sharing their screen. + +--- + +## Real-Time Quality Callback + +Get quality updates for ALL users (not just local user) via the `onUserNetworkStatusChanged` callback: + +```cpp +class MeetingServiceEventListener : public IMeetingServiceEvent { +public: + void onUserNetworkStatusChanged( + MeetingComponentType type, // What type: audio, video, or share + ConnectionQuality level, // Quality level + unsigned int userId, // Which user + bool uplink // true=sending, false=receiving + ) override { + const char* typeStr = ""; + switch (type) { + case MeetingComponentType_AUDIO: typeStr = "Audio"; break; + case MeetingComponentType_VIDEO: typeStr = "Video"; break; + case MeetingComponentType_SHARE: typeStr = "Share"; break; + default: typeStr = "Unknown"; break; + } + + std::cout << "[QUALITY] User " << userId + << " " << typeStr + << " " << (uplink ? "send" : "receive") + << " quality: " << level << std::endl; + + // Alert user if quality is poor + if (level <= Conn_Quality_Bad) { + std::cout << "[WARNING] Poor " << typeStr << " quality detected!" << std::endl; + } + } + + // ... other IMeetingServiceEvent methods +}; +``` + +**MeetingComponentType enum**: +```cpp +enum MeetingComponentType { + MeetingComponentType_Def = 0, // Default/unknown + MeetingComponentType_AUDIO, // Audio component + MeetingComponentType_VIDEO, // Video component + MeetingComponentType_SHARE, // Screen share component +}; +``` + +--- + +## Detailed Statistics + +Get detailed statistics (latency, packet loss, etc.) via `ISettingService`: + +```cpp +// Get setting service +ISettingService* settingService = nullptr; +CreateSettingService(&settingService); + +if (settingService) { + IStatisticSettingContext* statsSettings = settingService->GetStatisticSettings(); + + if (statsSettings) { + // Video statistics + ASVSessionStatisticInfo videoInfo; + statsSettings->QueryVideoStatisticInfo(videoInfo); + std::cout << "Video latency: " << videoInfo.latency << "ms" << std::endl; + std::cout << "Video packet loss: " << videoInfo.packetLossAvg << "%" << std::endl; + + // Audio statistics + ASVSessionStatisticInfo audioInfo; + statsSettings->QueryAudioStatisticInfo(audioInfo); + std::cout << "Audio latency: " << audioInfo.latency << "ms" << std::endl; + + // Share statistics + ASVSessionStatisticInfo shareInfo; + statsSettings->QueryShareStatisticInfo(shareInfo); + std::cout << "Share latency: " << shareInfo.latency << "ms" << std::endl; + } +} +``` + +--- + +## Complete Example: Quality Monitor + +```cpp +class QualityMonitor : public IMeetingServiceEvent { +public: + QualityMonitor(IMeetingService* meetingService) + : m_meetingService(meetingService) {} + + void CheckQuality() { + // Check video quality + ConnectionQuality videoSend = m_meetingService->GetVideoConnQuality(true); + ConnectionQuality videoRecv = m_meetingService->GetVideoConnQuality(false); + + // Check audio quality + ConnectionQuality audioSend = m_meetingService->GetAudioConnQuality(true); + ConnectionQuality audioRecv = m_meetingService->GetAudioConnQuality(false); + + // Log current status + std::cout << "=== Connection Quality ===" << std::endl; + std::cout << "Video: Send=" << QualityToString(videoSend) + << ", Recv=" << QualityToString(videoRecv) << std::endl; + std::cout << "Audio: Send=" << QualityToString(audioSend) + << ", Recv=" << QualityToString(audioRecv) << std::endl; + + // Warn if any quality is poor + if (videoSend <= Conn_Quality_Bad || videoRecv <= Conn_Quality_Bad) { + std::cout << "[!] Video quality issues detected" << std::endl; + } + if (audioSend <= Conn_Quality_Bad || audioRecv <= Conn_Quality_Bad) { + std::cout << "[!] Audio quality issues detected" << std::endl; + } + } + + // Real-time callback + void onUserNetworkStatusChanged( + MeetingComponentType type, + ConnectionQuality level, + unsigned int userId, + bool uplink + ) override { + if (level <= Conn_Quality_Bad) { + std::cout << "[ALERT] Poor network quality for user " << userId << std::endl; + // Could trigger UI notification here + } + } + +private: + IMeetingService* m_meetingService; + + const char* QualityToString(ConnectionQuality q) { + switch (q) { + case Conn_Quality_Unknown: return "Unknown"; + case Conn_Quality_Very_Bad: return "Very Bad"; + case Conn_Quality_Bad: return "Bad"; + case Conn_Quality_Not_Good: return "Not Good"; + case Conn_Quality_Normal: return "Normal"; + case Conn_Quality_Good: return "Good"; + case Conn_Quality_Excellent: return "Excellent"; + default: return "Unknown"; + } + } + + // ... implement other required IMeetingServiceEvent methods +}; +``` + +--- + +## Use Cases + +1. **User Notifications**: Alert users when their connection quality drops +2. **Adaptive Quality**: Automatically lower video resolution when quality is poor +3. **Debugging**: Log quality metrics for troubleshooting +4. **Analytics**: Track meeting quality metrics for analysis +5. **Bot Health Monitoring**: Ensure bot has stable connection before processing + +--- + +## Related Documentation + +- [Common Issues](../troubleshooting/common-issues.md) - Network error codes +- [Raw Video Capture](raw-video-capture.md) - Video quality affects frame rate +- [Interface Methods](../references/interface-methods.md) - IMeetingServiceEvent methods + +--- + +**Last Updated**: Based on Zoom Windows Meeting SDK v6.7.2.26830 diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/examples/share-raw-data-capture.md b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/share-raw-data-capture.md new file mode 100644 index 00000000..8738fa50 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/share-raw-data-capture.md @@ -0,0 +1,541 @@ +# Screen Share Raw Data Capture + +## Overview + +The Meeting SDK allows you to receive raw screen share data (YUV420 frames) from participants sharing their screen. This uses the same `IZoomSDKRenderer` and `IZoomSDKRendererDelegate` pattern as video capture, but subscribes to `RAW_DATA_TYPE_SHARE` instead of `RAW_DATA_TYPE_VIDEO`. + +## Architecture + +``` +IMeetingService + ├── GetMeetingShareController() → IMeetingShareController + │ └── GetViewableSharingUserList() + │ + └── GetMeetingRecordingController() → IMeetingRecordingController + └── StartRawRecording() + +createRenderer() → IZoomSDKRenderer + ├── subscribe(userId, RAW_DATA_TYPE_SHARE) + └── unSubscribe() + +IZoomSDKRendererDelegate (your implementation) + └── onRawDataFrameReceived(YUVRawDataI420*) +``` + +## Required Headers + +```cpp +#include +#include +#include +#include +#include +``` + +## Step 1: Implement the Renderer Delegate + +```cpp +// ZoomSDKShareRendererDelegate.h +#pragma once +#include +#include +#include + +class ZoomSDKShareRendererDelegate : public ZOOMSDK::IZoomSDKRendererDelegate { +public: + ZoomSDKShareRendererDelegate(); + virtual ~ZoomSDKShareRendererDelegate(); + + // Called when a share frame is received + virtual void onRawDataFrameReceived(ZOOMSDK::YUVRawDataI420* data) override; + + // Called when renderer status changes + virtual void onRawDataStatusChanged( + ZOOMSDK::RawDataStatus status + ) override; + + // Called when resolution changes + virtual void onRendererBeDestroyed() override; + + // Frame statistics + unsigned int getFrameCount() const { return m_frameCount; } + void resetFrameCount() { m_frameCount = 0; } + + // Enable/disable file output + void enableFileOutput(const std::string& filename); + void disableFileOutput(); + +private: + unsigned int m_frameCount; + std::ofstream m_outputFile; + bool m_writeToFile; + int m_lastWidth; + int m_lastHeight; +}; +``` + +```cpp +// ZoomSDKShareRendererDelegate.cpp +#include "ZoomSDKShareRendererDelegate.h" +#include + +using namespace ZOOMSDK; + +ZoomSDKShareRendererDelegate::ZoomSDKShareRendererDelegate() + : m_frameCount(0) + , m_writeToFile(false) + , m_lastWidth(0) + , m_lastHeight(0) {} + +ZoomSDKShareRendererDelegate::~ZoomSDKShareRendererDelegate() { + disableFileOutput(); +} + +void ZoomSDKShareRendererDelegate::onRawDataFrameReceived(YUVRawDataI420* data) { + if (!data) return; + + m_frameCount++; + + // Get frame dimensions + unsigned int width = data->GetStreamWidth(); + unsigned int height = data->GetStreamHeight(); + + // Check for resolution change + if (width != m_lastWidth || height != m_lastHeight) { + std::cout << "[Share] Resolution changed: " + << width << "x" << height << std::endl; + m_lastWidth = width; + m_lastHeight = height; + } + + // Log periodically + if (m_frameCount % 30 == 0) { + std::cout << "[Share] Frame " << m_frameCount + << " - " << width << "x" << height + << std::endl; + } + + // YUV420 data access + char* yBuffer = data->GetYBuffer(); + char* uBuffer = data->GetUBuffer(); + char* vBuffer = data->GetVBuffer(); + + // Calculate buffer sizes for YUV420 + unsigned int ySize = width * height; + unsigned int uvSize = (width / 2) * (height / 2); + + // Write to file if enabled + if (m_writeToFile && m_outputFile.is_open()) { + m_outputFile.write(yBuffer, ySize); + m_outputFile.write(uBuffer, uvSize); + m_outputFile.write(vBuffer, uvSize); + } + + // Process the frame (e.g., display, encode, analyze) + // The data is in YUV I420 format: + // - Y plane: width * height bytes (luminance) + // - U plane: (width/2) * (height/2) bytes (chrominance) + // - V plane: (width/2) * (height/2) bytes (chrominance) +} + +void ZoomSDKShareRendererDelegate::onRawDataStatusChanged(RawDataStatus status) { + switch (status) { + case RawData_On: + std::cout << "[Share] Raw data: ON" << std::endl; + break; + case RawData_Off: + std::cout << "[Share] Raw data: OFF" << std::endl; + break; + default: + std::cout << "[Share] Raw data status: " << status << std::endl; + } +} + +void ZoomSDKShareRendererDelegate::onRendererBeDestroyed() { + std::cout << "[Share] Renderer being destroyed" << std::endl; + disableFileOutput(); +} + +void ZoomSDKShareRendererDelegate::enableFileOutput(const std::string& filename) { + disableFileOutput(); + m_outputFile.open(filename, std::ios::binary); + if (m_outputFile.is_open()) { + m_writeToFile = true; + std::cout << "[Share] Writing to: " << filename << std::endl; + } +} + +void ZoomSDKShareRendererDelegate::disableFileOutput() { + if (m_outputFile.is_open()) { + m_outputFile.close(); + } + m_writeToFile = false; +} +``` + +## Step 2: Initialize Share Capture + +```cpp +// Global variables +ZoomSDKShareRendererDelegate* g_shareDelegate = nullptr; +IZoomSDKRenderer* g_shareRenderer = nullptr; +IMeetingShareController* g_shareController = nullptr; +IMeetingRecordingController* g_recordController = nullptr; + +void initializeShareCapture(IMeetingService* meetingService) { + // Get controllers + g_shareController = meetingService->GetMeetingShareController(); + g_recordController = meetingService->GetMeetingRecordingController(); + + if (!g_shareController || !g_recordController) { + std::cerr << "Failed to get controllers" << std::endl; + return; + } + + // Create delegate + g_shareDelegate = new ZoomSDKShareRendererDelegate(); + + std::cout << "Share capture initialized" << std::endl; +} +``` + +## Step 3: Find Sharing User and Subscribe + +```cpp +unsigned int getSharingUserId() { + if (!g_shareController) return 0; + + // Get list of users currently sharing + IList* sharingUsers = g_shareController->GetViewableSharingUserList(); + + if (!sharingUsers || sharingUsers->GetCount() == 0) { + std::cout << "No one is sharing" << std::endl; + return 0; + } + + // Get first sharing user + unsigned int userId = sharingUsers->GetItem(0); + std::cout << "User " << userId << " is sharing" << std::endl; + + return userId; +} + +void startShareCapture() { + if (!g_shareDelegate) return; + + // Start raw recording first (required for raw data access) + SDKError err = g_recordController->StartRawRecording(); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to start raw recording: " << err << std::endl; + return; + } + + // Create renderer + err = createRenderer(&g_shareRenderer, g_shareDelegate); + if (err != SDKERR_SUCCESS) { + std::cerr << "Failed to create renderer: " << err << std::endl; + return; + } + + // Get sharing user + unsigned int userId = getSharingUserId(); + if (userId == 0) { + std::cout << "No sharing user to subscribe to" << std::endl; + return; + } + + // Subscribe to share raw data + err = g_shareRenderer->subscribe(userId, RAW_DATA_TYPE_SHARE); + if (err == SDKERR_SUCCESS) { + std::cout << "Subscribed to share from user " << userId << std::endl; + + // Optionally save to file + g_shareDelegate->enableFileOutput("share_capture.yuv"); + } else { + std::cerr << "Failed to subscribe: " << err << std::endl; + } +} + +void stopShareCapture() { + if (g_shareRenderer) { + g_shareRenderer->unSubscribe(); + std::cout << "Unsubscribed from share" << std::endl; + } + + if (g_shareDelegate) { + g_shareDelegate->disableFileOutput(); + } +} +``` + +## Step 4: Handle Share Events + +To know when sharing starts/stops, implement a share controller event listener: + +```cpp +// MeetingShareCtrlEventListener.h +#pragma once +#include + +class MeetingShareCtrlEventListener : public ZOOMSDK::IMeetingShareCtrlEvent { +public: + using ShareStartedCallback = std::function; + using ShareStoppedCallback = std::function; + + MeetingShareCtrlEventListener( + ShareStartedCallback onStarted = nullptr, + ShareStoppedCallback onStopped = nullptr + ); + + // Sharing status changed + virtual void onSharingStatus( + ZOOMSDK::SharingStatus status, + unsigned int userId + ) override; + + // Someone started sharing + virtual void onLockShareStatus(bool bLocked) override; + + // Share content changed + virtual void onShareContentNotification( + ZOOMSDK::ShareInfo* shareInfo + ) override; + + // Multi-share + virtual void onMultiShareSwitchToSingleShareNeedConfirm( + ZOOMSDK::IShareSwitchMultiToSingleConfirmHandler* handler + ) override; + + virtual void onShareSettingTypeChangedNotification( + ZOOMSDK::ShareSettingType type + ) override; + + virtual void onSharedVideoEnded() override; + +private: + ShareStartedCallback m_onStarted; + ShareStoppedCallback m_onStopped; +}; +``` + +```cpp +// MeetingShareCtrlEventListener.cpp +#include "MeetingShareCtrlEventListener.h" +#include + +using namespace ZOOMSDK; + +MeetingShareCtrlEventListener::MeetingShareCtrlEventListener( + ShareStartedCallback onStarted, + ShareStoppedCallback onStopped +) : m_onStarted(onStarted), m_onStopped(onStopped) {} + +void MeetingShareCtrlEventListener::onSharingStatus( + SharingStatus status, + unsigned int userId +) { + switch (status) { + case Sharing_Self_Send_Begin: + std::cout << "Started sharing (self)" << std::endl; + break; + case Sharing_Self_Send_End: + std::cout << "Stopped sharing (self)" << std::endl; + break; + case Sharing_Other_Share_Begin: + std::cout << "User " << userId << " started sharing" << std::endl; + if (m_onStarted) m_onStarted(userId); + break; + case Sharing_Other_Share_End: + std::cout << "User " << userId << " stopped sharing" << std::endl; + if (m_onStopped) m_onStopped(); + break; + case Sharing_View_Other_Sharing: + std::cout << "Viewing share from user " << userId << std::endl; + break; + case Sharing_Pause: + std::cout << "Sharing paused" << std::endl; + break; + case Sharing_Resume: + std::cout << "Sharing resumed" << std::endl; + break; + default: + std::cout << "Sharing status: " << status << std::endl; + } +} + +void MeetingShareCtrlEventListener::onLockShareStatus(bool bLocked) { + std::cout << "Share lock: " << (bLocked ? "LOCKED" : "UNLOCKED") << std::endl; +} + +void MeetingShareCtrlEventListener::onShareContentNotification(ShareInfo* shareInfo) { + if (shareInfo) { + std::cout << "Share content changed" << std::endl; + } +} + +void MeetingShareCtrlEventListener::onMultiShareSwitchToSingleShareNeedConfirm( + IShareSwitchMultiToSingleConfirmHandler* handler +) { + // Handle multi-share to single-share switch +} + +void MeetingShareCtrlEventListener::onShareSettingTypeChangedNotification( + ShareSettingType type +) { + std::cout << "Share setting changed" << std::endl; +} + +void MeetingShareCtrlEventListener::onSharedVideoEnded() { + std::cout << "Shared video ended" << std::endl; +} +``` + +## Complete Integration Example + +```cpp +// Global share event listener +MeetingShareCtrlEventListener* g_shareEventListener = nullptr; + +void onInMeeting(IMeetingService* meetingService) { + // Initialize share capture + initializeShareCapture(meetingService); + + // Set up share event listener + g_shareEventListener = new MeetingShareCtrlEventListener( + // On share started + [](unsigned int userId) { + std::cout << "Starting capture for user " << userId << std::endl; + startShareCapture(); + }, + // On share stopped + []() { + std::cout << "Stopping capture" << std::endl; + stopShareCapture(); + } + ); + + g_shareController->SetEvent(g_shareEventListener); + + // Check if someone is already sharing + unsigned int sharingUser = getSharingUserId(); + if (sharingUser != 0) { + startShareCapture(); + } +} + +// Cleanup +void cleanup() { + stopShareCapture(); + + if (g_shareRenderer) { + // Renderer will be destroyed when unsubscribed + g_shareRenderer = nullptr; + } + + delete g_shareDelegate; + g_shareDelegate = nullptr; + + delete g_shareEventListener; + g_shareEventListener = nullptr; +} +``` + +## Sharing Status Values + +```cpp +enum SharingStatus { + Sharing_Self_Send_Begin, // You started sharing + Sharing_Self_Send_End, // You stopped sharing + Sharing_Other_Share_Begin, // Someone else started sharing + Sharing_Other_Share_End, // Someone else stopped sharing + Sharing_View_Other_Sharing, // Viewing someone's share + Sharing_Pause, // Sharing paused + Sharing_Resume, // Sharing resumed + Sharing_ContentTypeChange, // Content type changed + Sharing_SelfStartAudioShare, // Started audio share + Sharing_SelfStopAudioShare // Stopped audio share +}; +``` + +## Raw Data Types + +```cpp +enum RawDataType { + RAW_DATA_TYPE_VIDEO = 0, // Video frames + RAW_DATA_TYPE_SHARE // Screen share frames +}; +``` + +## Permission Requirements + +To receive screen share raw data, you need: + +1. **Raw Recording Permission**: Call `StartRawRecording()` before subscribing +2. **Recording Permission**: Either be host, co-host, or have recording permission granted + +```cpp +bool checkPermission() { + SDKError err = g_recordController->CanStartRecording(false, 0); + if (err != SDKERR_SUCCESS) { + std::cout << "Requesting recording permission..." << std::endl; + g_recordController->RequestLocalRecordingPrivilege(); + return false; + } + return true; +} +``` + +## Converting YUV to RGB/Image + +The raw data is in YUV420 (I420) format. To convert to RGB or save as image: + +```cpp +// Using OpenCV +#include + +void saveFrameAsImage(YUVRawDataI420* data, const std::string& filename) { + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Create Mat from YUV data + cv::Mat yuvFrame(height + height/2, width, CV_8UC1); + + // Copy Y plane + memcpy(yuvFrame.data, data->GetYBuffer(), width * height); + + // Copy U and V planes (interleaved in I420) + memcpy(yuvFrame.data + width * height, + data->GetUBuffer(), (width/2) * (height/2)); + memcpy(yuvFrame.data + width * height + (width/2) * (height/2), + data->GetVBuffer(), (width/2) * (height/2)); + + // Convert to BGR + cv::Mat bgrFrame; + cv::cvtColor(yuvFrame, bgrFrame, cv::COLOR_YUV2BGR_I420); + + // Save + cv::imwrite(filename, bgrFrame); +} +``` + +## Common Pitfalls + +1. **No share data**: Make sure someone is actually sharing before subscribing +2. **Raw recording**: Must call `StartRawRecording()` before subscribing to share +3. **Permission**: Recording permission is required for raw data access +4. **Multiple sharers**: Use `GetViewableSharingUserList()` to get all sharing users +5. **Resolution changes**: Screen share resolution can change dynamically (window resize) +6. **Frame rate**: Screen share frame rate is typically lower than video (varies by content) +7. **Subscribe timing**: Subscribe after sharing starts, not before + +## Difference from Video Capture + +| Aspect | Video Capture | Share Capture | +|--------|---------------|---------------| +| Data Type | `RAW_DATA_TYPE_VIDEO` | `RAW_DATA_TYPE_SHARE` | +| Source | Webcam | Screen/Window/App | +| Resolution | Fixed camera resolution | Dynamic (window size) | +| Frame Rate | Consistent (30fps) | Variable (content-dependent) | +| User List | Participants controller | Share controller | + +Both use the same `IZoomSDKRenderer` and `IZoomSDKRendererDelegate` interfaces. diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/examples/video-advanced.md b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/video-advanced.md new file mode 100644 index 00000000..6bf55d09 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/examples/video-advanced.md @@ -0,0 +1,343 @@ +# Video Advanced Features: Level 3 Helpers + +## Overview + +The `IMeetingVideoController` provides three Level 3 sub-helpers for advanced video operations: + +| Helper | Getter | Platform | Purpose | +|--------|--------|----------|---------| +| `IMeetingCameraHelper` | `GetMeetingCameraHelper(userid)` | Cross-platform | Remote PTZ camera control | +| `ISetVideoOrderHelper` | `GetSetVideoOrderHelper()` | Windows-only | Batch set video order in gallery | +| `ICameraController` | `GetMyCameraController()` | Windows-only | Control local camera device | + +--- + +## Navigation Path + +``` +IMeetingService + └─► GetMeetingVideoController() + ├─► GetMeetingCameraHelper(userid) // Remote camera PTZ control + ├─► GetSetVideoOrderHelper() // Gallery video order (Windows) + └─► GetMyCameraController() // Local camera device (Windows) +``` + +--- + +## 1. Remote Camera Control (IMeetingCameraHelper) + +Control PTZ (Pan-Tilt-Zoom) cameras of remote participants who have granted permission. + +### Get the Helper + +```cpp +// Get video controller first +IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController(); +if (!videoCtrl) return; + +// Get camera helper for specific user +unsigned int targetUserId = 12345; +IMeetingCameraHelper* cameraHelper = videoCtrl->GetMeetingCameraHelper(targetUserId); +if (!cameraHelper) { + // User doesn't exist or doesn't have controllable camera + return; +} +``` + +### Check Controllability + +```cpp +// Check if we can control this user's camera +if (!cameraHelper->CanControlCamera()) { + // Need to request control first + SDKError err = cameraHelper->RequestControlRemoteCamera(); + if (err != SDKERR_SUCCESS) { + // Request failed - user may not have PTZ camera + } + // Wait for callback to confirm permission granted + return; +} +``` + +### Camera Movement Operations + +```cpp +// All movement methods accept range 10-100 (default: 50) +// Higher = faster/larger movement + +// Pan left/right +cameraHelper->TurnLeft(50); // Pan left +cameraHelper->TurnRight(50); // Pan right + +// Tilt up/down +cameraHelper->TurnUp(50); // Tilt up +cameraHelper->TurnDown(50); // Tilt down + +// Zoom in/out +cameraHelper->ZoomIn(50); // Zoom in +cameraHelper->ZoomOut(50); // Zoom out +``` + +### Release Control + +```cpp +// When done controlling +cameraHelper->GiveUpControlRemoteCamera(); +``` + +### Handle Camera Control Requests (Callback) + +```cpp +class MyCameraEventHandler : public IMeetingVideoCtrlEvent { +public: + void onCameraControlRequestReceived( + unsigned int userId, + CameraControlRequestType requestType, + ICameraControlRequestHandler* pHandler) override + { + if (requestType == CameraControlRequestType_RequestControl) { + // Someone wants to control our camera + // Approve or decline + pHandler->Approve(); // or pHandler->Decline(); + } else if (requestType == CameraControlRequestType_GiveUpControl) { + // User released control of our camera + } + } + + void onCameraControlRequestResult( + unsigned int userId, + CameraControlRequestResult result) override + { + switch (result) { + case CameraControlRequestResult_Approve: + // Our request was approved - can now control camera + break; + case CameraControlRequestResult_Decline: + // Our request was declined + break; + case CameraControlRequestResult_Revoke: + // Our control was revoked + break; + } + } + + // ... other callback implementations +}; +``` + +--- + +## 2. Video Order Control (ISetVideoOrderHelper) - Windows Only + +Batch-set the video order in gallery view. Useful for arranging participant videos in a specific layout. + +### Get the Helper + +```cpp +#if defined(WIN32) +IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController(); +if (!videoCtrl) return; + +ISetVideoOrderHelper* orderHelper = videoCtrl->GetSetVideoOrderHelper(); +if (!orderHelper) return; +#endif +``` + +### Set Video Order (Transaction Pattern) + +```cpp +#if defined(WIN32) +// Step 1: Begin transaction (clears any previous prepared order) +SDKError err = orderHelper->SetVideoOrderTransactionBegin(); +if (err != SDKERR_SUCCESS) return; + +// Step 2: Add users to positions (0-based, max 49 positions) +// Position 0 = first slot in gallery +orderHelper->AddVideoToOrder(hostUserId, 0); // Host at position 0 +orderHelper->AddVideoToOrder(presenter1Id, 1); // Presenter at position 1 +orderHelper->AddVideoToOrder(presenter2Id, 2); // Another presenter at position 2 +// ... add more as needed + +// Step 3: Commit the transaction +err = orderHelper->SetVideoOrderTransactionCommit(); +if (err != SDKERR_SUCCESS) { + // Commit failed +} +#endif +``` + +### Important Notes + +- Maximum 49 users can be ordered +- If same position assigned to multiple users, only last one applies +- Must call `SetVideoOrderTransactionBegin()` before adding users +- Only host/co-host can set video order for all participants +- Use `EnableFollowHostVideoOrder()` to make participants follow host's order + +### Follow Host Video Order + +```cpp +// Check if feature is supported +if (videoCtrl->IsSupportFollowHostVideoOrder()) { + // Enable following host's video order + videoCtrl->EnableFollowHostVideoOrder(true); + + // Check if currently following + bool isFollowing = videoCtrl->IsFollowHostVideoOrderOn(); +} + +// Get current video order list +IList* orderList = videoCtrl->GetVideoOrderList(); +if (orderList) { + for (int i = 0; i < orderList->GetCount(); i++) { + unsigned int userId = orderList->GetItem(i); + // Process ordered user IDs + } +} +``` + +--- + +## 3. Local Camera Control (ICameraController) - Windows Only + +Control the local user's camera device settings. + +### Get the Helper + +```cpp +#if defined(WIN32) +IMeetingVideoController* videoCtrl = meetingService->GetMeetingVideoController(); +if (!videoCtrl) return; + +ICameraController* cameraCtrl = videoCtrl->GetMyCameraController(); +if (!cameraCtrl) return; + +// ICameraController provides device-level camera control +// (Interface details depend on SDK version - check headers) +#endif +``` + +--- + +## Video Order Callbacks + +```cpp +class MyVideoEventHandler : public IMeetingVideoCtrlEvent { +public: + void onHostVideoOrderUpdated(IList* orderList) override { + // Host changed the video order + // Update UI to reflect new order + if (orderList) { + for (int i = 0; i < orderList->GetCount(); i++) { + unsigned int userId = orderList->GetItem(i); + // Reorder video tiles accordingly + } + } + } + + void onLocalVideoOrderUpdated(IList* localOrderList) override { + // Local video order changed (user's personal arrangement) + } + + void onFollowHostVideoOrderChanged(bool bFollow) override { + // Following host video order setting changed + if (bFollow) { + // Now following host's order + } else { + // Using local order + } + } + + // ... other callback implementations +}; +``` + +--- + +## Complete Example: Camera Control Flow + +```cpp +class CameraControlManager : public IMeetingVideoCtrlEvent { +private: + IMeetingVideoController* m_videoCtrl = nullptr; + IMeetingCameraHelper* m_currentCameraHelper = nullptr; + +public: + void Initialize(IMeetingService* meetingService) { + m_videoCtrl = meetingService->GetMeetingVideoController(); + if (m_videoCtrl) { + m_videoCtrl->SetEvent(this); + } + } + + void RequestCameraControl(unsigned int targetUserId) { + if (!m_videoCtrl) return; + + m_currentCameraHelper = m_videoCtrl->GetMeetingCameraHelper(targetUserId); + if (!m_currentCameraHelper) { + // User not found or no controllable camera + return; + } + + if (m_currentCameraHelper->CanControlCamera()) { + // Already have control + OnCameraControlGranted(); + } else { + // Request control + m_currentCameraHelper->RequestControlRemoteCamera(); + } + } + + void OnCameraControlGranted() { + // Now can control camera + // Example: Center the camera + m_currentCameraHelper->TurnLeft(30); + m_currentCameraHelper->TurnUp(20); + } + + void ReleaseCameraControl() { + if (m_currentCameraHelper) { + m_currentCameraHelper->GiveUpControlRemoteCamera(); + m_currentCameraHelper = nullptr; + } + } + + // IMeetingVideoCtrlEvent implementations + void onCameraControlRequestResult(unsigned int userId, + CameraControlRequestResult result) override { + if (result == CameraControlRequestResult_Approve) { + OnCameraControlGranted(); + } else { + m_currentCameraHelper = nullptr; + } + } + + void onCameraControlRequestReceived(unsigned int userId, + CameraControlRequestType requestType, + ICameraControlRequestHandler* pHandler) override { + // Auto-approve camera control requests + if (requestType == CameraControlRequestType_RequestControl) { + pHandler->Approve(); + } + } + + // ... implement other required callbacks + void onUserVideoStatusChange(unsigned int userId, VideoStatus status) override {} + void onSpotlightedUserListChangeNotification(IList* lst) override {} + void onHostRequestStartVideo(IRequestStartVideoHandler* handler) override {} + void onActiveSpeakerVideoUserChanged(unsigned int userid) override {} + void onActiveVideoUserChanged(unsigned int userid) override {} + void onHostVideoOrderUpdated(IList* orderList) override {} + void onLocalVideoOrderUpdated(IList* localOrderList) override {} + void onFollowHostVideoOrderChanged(bool bFollow) override {} + void onUserVideoQualityChanged(VideoConnectionQuality quality, unsigned int userid) override {} + void onVideoAlphaChannelStatusChanged(bool isAlphaModeOn) override {} +}; +``` + +--- + +## Related Documentation + +- [Singleton Hierarchy](../concepts/singleton-hierarchy.md) - Complete navigation map +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Universal 3-step pattern diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/references/deployment.md b/plugins/zoom-developers/skills/meeting-sdk/windows/references/deployment.md new file mode 100644 index 00000000..c063a62d --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/references/deployment.md @@ -0,0 +1,260 @@ +# Deployment Guide + +## Overview + +When releasing or publishing your Meeting SDK app, you need to: +1. Include SDK files from `\bin` +2. Include required Microsoft VC++ runtime libraries +3. NOT re-sign protected SDK files +4. Run cleanup command for upgrades + +--- + +## Required SDK Files + +**Development** requires files from: +- `\lib` - Link libraries +- `\h` - Header files +- `\bin` - Runtime DLLs + +**Deployment/Release** only requires: +- `\bin` - Runtime DLLs (copy to your app's directory) + +--- + +## Protected Files (DO NOT Re-sign) + +These files **cannot be re-signed or have new digital signatures added**. Doing so causes **Error 105035** and fatal errors: + +``` +CptControl.exe +CptHost.exe +CptInstall.exe +CptService.exe +CptShare.dll +zzhost.dll +zzplugin.dll +aomhost64.exe +``` + +**Solution**: Skip these files in your code signing process during build/deployment. + +--- + +## Microsoft VC++ Runtime Libraries + +You must distribute the appropriate Microsoft Visual C++ runtime libraries with your app. + +### x64 Architecture (Most Common) + +**Remove** the `bin/aomhost` directory, then copy these DLLs to the same directory as SDK DLLs: + +``` +concrt140.dll +msvcp140.dll +msvcp140_1.dll +msvcp140_2.dll +msvcp140_codecvt_ids.dll +vccorlib140.dll +vcruntime140.dll +vcruntime140_1.dll +api-ms-win-core-console-l1-1-0.dll +api-ms-win-core-console-l1-2-0.dll +api-ms-win-core-datetime-l1-1-0.dll +api-ms-win-core-debug-l1-1-0.dll +api-ms-win-core-errorhandling-l1-1-0.dll +api-ms-win-core-file-l1-1-0.dll +api-ms-win-core-file-l1-2-0.dll +api-ms-win-core-file-l2-1-0.dll +api-ms-win-core-handle-l1-1-0.dll +api-ms-win-core-heap-l1-1-0.dll +api-ms-win-core-interlocked-l1-1-0.dll +api-ms-win-core-libraryloader-l1-1-0.dll +api-ms-win-core-localization-l1-2-0.dll +api-ms-win-core-memory-l1-1-0.dll +api-ms-win-core-namedpipe-l1-1-0.dll +api-ms-win-core-processenvironment-l1-1-0.dll +api-ms-win-core-processthreads-l1-1-0.dll +api-ms-win-core-processthreads-l1-1-1.dll +api-ms-win-core-profile-l1-1-0.dll +api-ms-win-core-rtlsupport-l1-1-0.dll +api-ms-win-core-string-l1-1-0.dll +api-ms-win-core-synch-l1-1-0.dll +api-ms-win-core-synch-l1-2-0.dll +api-ms-win-core-sysinfo-l1-1-0.dll +api-ms-win-core-timezone-l1-1-0.dll +api-ms-win-core-util-l1-1-0.dll +api-ms-win-crt-conio-l1-1-0.dll +api-ms-win-crt-convert-l1-1-0.dll +api-ms-win-crt-environment-l1-1-0.dll +api-ms-win-crt-filesystem-l1-1-0.dll +api-ms-win-crt-heap-l1-1-0.dll +api-ms-win-crt-locale-l1-1-0.dll +api-ms-win-crt-math-l1-1-0.dll +api-ms-win-crt-multibyte-l1-1-0.dll +api-ms-win-crt-private-l1-1-0.dll +api-ms-win-crt-process-l1-1-0.dll +api-ms-win-crt-runtime-l1-1-0.dll +api-ms-win-crt-stdio-l1-1-0.dll +api-ms-win-crt-string-l1-1-0.dll +api-ms-win-crt-time-l1-1-0.dll +api-ms-win-crt-utility-l1-1-0.dll +ucrtbase.dll +``` + +### x86 Architecture (32-bit) + +Copy these DLLs to **both** `bin` AND `bin/aomhost` directories: + +``` +concrt140.dll +msvcp140.dll +msvcp140_1.dll +msvcp140_2.dll +msvcp140_codecvt_ids.dll +vccorlib140.dll +vcruntime140.dll +api-ms-win-core-console-l1-1-0.dll +api-ms-win-core-console-l1-2-0.dll +api-ms-win-core-datetime-l1-1-0.dll +api-ms-win-core-debug-l1-1-0.dll +api-ms-win-core-errorhandling-l1-1-0.dll +api-ms-win-core-file-l1-1-0.dll +api-ms-win-core-file-l1-2-0.dll +api-ms-win-core-file-l2-1-0.dll +api-ms-win-core-handle-l1-1-0.dll +api-ms-win-core-heap-l1-1-0.dll +api-ms-win-core-interlocked-l1-1-0.dll +api-ms-win-core-libraryloader-l1-1-0.dll +api-ms-win-core-localization-l1-2-0.dll +api-ms-win-core-memory-l1-1-0.dll +api-ms-win-core-namedpipe-l1-1-0.dll +api-ms-win-core-processenvironment-l1-1-0.dll +api-ms-win-core-processthreads-l1-1-0.dll +api-ms-win-core-processthreads-l1-1-1.dll +api-ms-win-core-profile-l1-1-0.dll +api-ms-win-core-rtlsupport-l1-1-0.dll +api-ms-win-core-string-l1-1-0.dll +api-ms-win-core-synch-l1-1-0.dll +api-ms-win-core-synch-l1-2-0.dll +api-ms-win-core-sysinfo-l1-1-0.dll +api-ms-win-core-timezone-l1-1-0.dll +api-ms-win-core-util-l1-1-0.dll +api-ms-win-crt-conio-l1-1-0.dll +api-ms-win-crt-convert-l1-1-0.dll +api-ms-win-crt-environment-l1-1-0.dll +api-ms-win-crt-filesystem-l1-1-0.dll +api-ms-win-crt-heap-l1-1-0.dll +api-ms-win-crt-locale-l1-1-0.dll +api-ms-win-crt-math-l1-1-0.dll +api-ms-win-crt-multibyte-l1-1-0.dll +api-ms-win-crt-private-l1-1-0.dll +api-ms-win-crt-process-l1-1-0.dll +api-ms-win-crt-runtime-l1-1-0.dll +api-ms-win-crt-stdio-l1-1-0.dll +api-ms-win-crt-string-l1-1-0.dll +api-ms-win-crt-time-l1-1-0.dll +api-ms-win-crt-utility-l1-1-0.dll +ucrtbase.dll +API-MS-Win-core-xstate-l2-1-0.dll +``` + +### ARM64 Architecture + +**Remove** the `bin/aomhost` directory, then copy these DLLs: + +``` +msvcp140.dll +msvcp140_1.dll +msvcp140_2.dll +msvcp140_atomic_wait.dll +msvcp140_codecvt_ids.dll +vccorlib140.dll +vcruntime140.dll +concrt140.dll +``` + +--- + +## Upgrade Installation + +When users upgrade from an older version of your app, run this command **with administrator privileges** before installing the new version: + +```cmd +cptinstall.exe -uninstall +``` + +This ensures users who had an older package can use the share function normally. + +--- + +## Visual Studio Project Configuration + +### Output Directory +Set to the `bin` folder: +- Configuration Properties → General → Output Directory: `...\bin` + +### Include Directories +- Configuration Properties → VC++ Directories → Include Directories: `...\h` + +### Library Directories +- Configuration Properties → VC++ Directories → Library Directories: `...\lib` + +### Linker Output +- Linker → General → Output File: Your desired `.exe` location + +### Debug Information (Release builds) +- C/C++ → Debug Information Format: `None` + +--- + +## Deployment Checklist + +- [ ] Copy SDK DLLs from `\bin` to app directory +- [ ] Copy appropriate VC++ runtime DLLs for your architecture +- [ ] For x64/ARM64: Remove `bin/aomhost` directory +- [ ] For x86: Copy runtime DLLs to both `bin` and `bin/aomhost` +- [ ] Exclude protected files from code signing +- [ ] Test on clean machine without Visual Studio installed +- [ ] Include `cptinstall.exe -uninstall` in upgrade process + +--- + +## Common Deployment Issues + +### "Missing DLL" errors +**Cause**: VC++ runtime libraries not included +**Fix**: Copy all required runtime DLLs to app directory + +### Error 105035 +**Cause**: Re-signed protected SDK files +**Fix**: Don't sign `CptControl.exe`, `CptHost.exe`, `CptInstall.exe`, `CptService.exe`, `CptShare.dll`, `zzhost.dll`, `zzplugin.dll`, `aomhost64.exe` + +### Share function not working after upgrade +**Cause**: Old CPT components still registered +**Fix**: Run `cptinstall.exe -uninstall` before installing new version + +### App works on dev machine but not on target +**Cause**: Target machine missing VC++ redistributable +**Fix**: Either bundle runtime DLLs or require VC++ 2019 Redistributable installation + +--- + +## Alternative: Install VC++ Redistributable + +Instead of bundling DLLs, you can require users to install the **Microsoft Visual C++ 2019 Redistributable**: +- [Download VC++ Redistributable](https://docs.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist) + +However, bundling the DLLs gives you more control and ensures compatibility. + +--- + +## Related Documentation + +- [Build Errors Guide](../troubleshooting/build-errors.md) - Project setup +- [Common Issues](../troubleshooting/common-issues.md) - Error 105035 details +- [Authentication Pattern](../examples/authentication-pattern.md) - Complete working code + +--- + +**Last Updated**: Based on Zoom Windows Meeting SDK v6.7.2.26830 diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/references/interface-methods.md b/plugins/zoom-developers/skills/meeting-sdk/windows/references/interface-methods.md new file mode 100644 index 00000000..f545077f --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/references/interface-methods.md @@ -0,0 +1,489 @@ +# Virtual Method Implementation Guide + +## Overview + +The Zoom SDK requires implementing ALL pure virtual methods from interface classes, even platform-specific ones. Missing even a single method results in abstract class errors at compile time. + +This guide shows how to find required methods and implement them correctly. + +--- + +## Critical Rules + +1. **Implement ALL pure virtual methods** - No exceptions +2. **Include WIN32-conditional methods** - They're required even though they're in `#if defined(WIN32)` blocks +3. **SDK version matters** - Different versions have different required methods +4. **Use exact signatures** - Parameter names can differ, but types and order must match exactly + +--- + +## How to Find Required Methods + +### Method 1: Grep the SDK Headers + +```bash +# Find all pure virtual methods (ending with = 0) +grep "= 0" SDK/x64/h/*.h + +# Find methods in specific interface +grep "= 0" SDK/x64/h/auth_service_interface.h +grep "= 0" SDK/x64/h/meeting_service_interface.h +``` + +### Method 2: Read Compiler Errors + +When you forget to implement a method, the compiler tells you: + +``` +error C2259: 'AuthServiceEventListener': cannot instantiate abstract class +note: see declaration of 'AuthServiceEventListener' +note: due to following members: +'void IAuthServiceEvent::onNotificationServiceStatus(SDKNotificationServiceStatus,SDKNotificationServiceError)': + is abstract at auth_service_interface.h(256) +``` + +This tells you: +- **Method name**: `onNotificationServiceStatus` +- **Parameters**: `SDKNotificationServiceStatus status, SDKNotificationServiceError error` +- **Location**: Line 256 in `auth_service_interface.h` + +### Method 3: Read the Header Files Manually + +Open the interface header and look for methods marked with `= 0`: + +```cpp +class IAuthServiceEvent { +public: + virtual ~IAuthServiceEvent() {} + virtual void onAuthenticationReturn(AuthResult ret) = 0; // <-- REQUIRED (= 0) + virtual void onLogout() = 0; // <-- REQUIRED (= 0) +}; +``` + +--- + +## Complete Method Lists + +### IAuthServiceEvent (6 methods required) + +**File**: `SDK/x64/h/auth_service_interface.h` (lines 217-258) + +```cpp +class AuthServiceEventListener : public IAuthServiceEvent { +public: + // Method 1: Authentication result (JWT token validation) + void onAuthenticationReturn(AuthResult ret) override; + + // Method 2: Login result with fail reason (for user login, not JWT) + void onLoginReturnWithReason(LOGINSTATUS ret, IAccountInfo* pAccountInfo, LoginFailReason reason) override; + + // Method 3: Logout notification + void onLogout() override; + + // Method 4: Zoom identity expired (need to regenerate token) + void onZoomIdentityExpired() override; + + // Method 5: Zoom auth identity expiring soon (10 minutes warning) + void onZoomAuthIdentityExpired() override; + + // Method 6: WIN32 ONLY - Notification service status +#if defined(WIN32) + void onNotificationServiceStatus(SDKNotificationServiceStatus status, SDKNotificationServiceError error) override; +#endif +}; +``` + +**Important notes**: +- Methods 1-5 are cross-platform +- Method 6 is Windows-only but **MUST** be implemented on Windows +- For JWT auth, only `onAuthenticationReturn` fires; others are for user login + +--- + +### IMeetingServiceEvent (9 methods required) + +**File**: `SDK/x64/h/meeting_service_interface.h` (lines 830-897) + +```cpp +class MeetingServiceEventListener : public IMeetingServiceEvent { +public: + // Method 1: Meeting status changed (joined, ended, failed, etc.) + void onMeetingStatusChanged(MeetingStatus status, int iResult) override; + + // Method 2: Meeting statistics warning (network issues, etc.) + void onMeetingStatisticsWarningNotification(StatisticsWarningType type) override; + + // Method 3: Meeting parameters (right before meeting starts) + void onMeetingParameterNotification(const MeetingParameter* meeting_param) override; + + // Method 4: Participants activities suspended + void onSuspendParticipantsActivities() override; + + // Method 5: AI Companion status changed + void onAICompanionActiveChangeNotice(bool bActive) override; + + // Method 6: Meeting topic changed + void onMeetingTopicChanged(const zchar_t* sTopic) override; + + // Method 7: Meeting at capacity, provides livestream URL + void onMeetingFullToWatchLiveStream(const zchar_t* sLiveStreamUrl) override; + + // Method 8: User network quality changed + void onUserNetworkStatusChanged(MeetingComponentType type, ConnectionQuality level, unsigned int userId, bool uplink) override; + + // Method 9: WIN32 ONLY - App signal panel updated +#if defined(WIN32) + void onAppSignalPanelUpdated(IMeetingAppSignalHandler* pHandler) override; +#endif +}; +``` + +**Important notes**: +- Methods 1-8 are cross-platform +- Method 9 is Windows-only but **MUST** be implemented on Windows +- Most apps only care about method 1 (`onMeetingStatusChanged`) + +--- + +## Implementation Patterns + +### Minimal Implementation (Empty Stubs) + +If you don't need a method's functionality, implement it as an empty stub: + +```cpp +void AuthServiceEventListener::onLogout() { + // We're not using user login, so this never fires + // Empty implementation is fine +} + +void MeetingServiceEventListener::onAICompanionActiveChangeNotice(bool bActive) { + // We don't care about AI Companion status + // Empty implementation is fine +} +``` + +### Logging Implementation (Recommended for Debugging) + +Add basic logging to see when callbacks fire: + +```cpp +void AuthServiceEventListener::onZoomIdentityExpired() { + std::cout << "[AUTH] Zoom identity expired! Need to regenerate JWT token." << std::endl; +} + +void MeetingServiceEventListener::onMeetingStatisticsWarningNotification(StatisticsWarningType type) { + std::cout << "[MEETING] Statistics warning: " << static_cast(type) << std::endl; +} +``` + +### Full Implementation (For Important Callbacks) + +```cpp +void MeetingServiceEventListener::onMeetingStatusChanged(MeetingStatus status, int iResult) { + switch (status) { + case MEETING_STATUS_IDLE: + std::cout << "[MEETING] Status: IDLE" << std::endl; + break; + case MEETING_STATUS_CONNECTING: + std::cout << "[MEETING] Status: CONNECTING" << std::endl; + break; + case MEETING_STATUS_INMEETING: + std::cout << "[MEETING] Status: IN MEETING" << std::endl; + if (onInMeetingCallback) { + onInMeetingCallback(); // Trigger custom logic + } + break; + case MEETING_STATUS_ENDED: + std::cout << "[MEETING] Status: ENDED (Reason: " << iResult << ")" << std::endl; + if (onMeetingEnded) { + onMeetingEnded(); // Trigger custom logic + } + break; + case MEETING_STATUS_FAILED: + std::cout << "[MEETING] Status: FAILED (Error: " << iResult << ")" << std::endl; + break; + default: + std::cout << "[MEETING] Status: UNKNOWN (" << status << ")" << std::endl; + break; + } +} +``` + +--- + +## Complete Header/Source File Template + +### Header File (AuthServiceEventListener.h) + +```cpp +#pragma once +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class AuthServiceEventListener : public IAuthServiceEvent { +public: + // Constructor with callback + AuthServiceEventListener(void (*onComplete)()); + + // All 6 required methods + void onAuthenticationReturn(AuthResult ret) override; + void onLoginReturnWithReason(LOGINSTATUS ret, IAccountInfo* info, LoginFailReason reason) override; + void onLogout() override; + void onZoomIdentityExpired() override; + void onZoomAuthIdentityExpired() override; +#if defined(WIN32) + void onNotificationServiceStatus(SDKNotificationServiceStatus status, SDKNotificationServiceError error) override; +#endif + +private: + void (*onAuthComplete)(); +}; +``` + +### Source File (AuthServiceEventListener.cpp) + +```cpp +#include "AuthServiceEventListener.h" + +AuthServiceEventListener::AuthServiceEventListener(void (*onComplete)()) + : onAuthComplete(onComplete) {} + +void AuthServiceEventListener::onAuthenticationReturn(AuthResult ret) { + if (ret == AUTHRET_SUCCESS) { + std::cout << "[AUTH] Authentication successful!" << std::endl; + if (onAuthComplete) { + onAuthComplete(); + } + } else { + std::cout << "[AUTH] Authentication failed: " << ret << std::endl; + } +} + +void AuthServiceEventListener::onLoginReturnWithReason(LOGINSTATUS ret, IAccountInfo* info, LoginFailReason reason) { + std::cout << "[AUTH] Login return (not used for JWT): " << ret << std::endl; +} + +void AuthServiceEventListener::onLogout() { + std::cout << "[AUTH] Logout" << std::endl; +} + +void AuthServiceEventListener::onZoomIdentityExpired() { + std::cout << "[AUTH] Zoom identity expired!" << std::endl; +} + +void AuthServiceEventListener::onZoomAuthIdentityExpired() { + std::cout << "[AUTH] Zoom auth identity expiring soon!" << std::endl; +} + +#if defined(WIN32) +void AuthServiceEventListener::onNotificationServiceStatus(SDKNotificationServiceStatus status, SDKNotificationServiceError error) { + std::cout << "[AUTH] Notification service status: " << status << ", error: " << error << std::endl; +} +#endif +``` + +--- + +## Troubleshooting + +### Error: "Cannot instantiate abstract class" + +**Cause**: You're missing one or more pure virtual method implementations. + +**Solution**: +1. Read the compiler error carefully - it lists the missing methods +2. Look up the method signature in the SDK header file +3. Add the method to both your .h and .cpp files +4. Use `override` keyword to catch signature mismatches + +**Example error**: +``` +error C2259: 'AuthServiceEventListener': cannot instantiate abstract class +note: due to following members: +'void IAuthServiceEvent::onNotificationServiceStatus(...)': is abstract +``` + +**Fix**: Add the missing method: +```cpp +// In .h file +#if defined(WIN32) +void onNotificationServiceStatus(SDKNotificationServiceStatus status, SDKNotificationServiceError error) override; +#endif + +// In .cpp file +#if defined(WIN32) +void AuthServiceEventListener::onNotificationServiceStatus(SDKNotificationServiceStatus status, SDKNotificationServiceError error) { + // Empty implementation is fine if you don't need this +} +#endif +``` + +### Error: "No suitable user-defined conversion" + +**Cause**: Method signature doesn't match exactly (wrong parameter types, missing const, etc.). + +**Solution**: Copy the signature EXACTLY from the SDK header file, including: +- `const` qualifiers +- Pointer vs reference (`*` vs `&`) +- Parameter order +- Return type + +### WIN32 Methods Not Compiling + +**Cause**: Forgot `#if defined(WIN32)` wrapper. + +**Solution**: Wrap WIN32-only methods in both .h and .cpp files: +```cpp +#if defined(WIN32) +void onNotificationServiceStatus(...) override; +#endif +``` + +--- + +--- + +## Custom UI Interfaces + +These interfaces are required when using Custom UI mode (`ENABLE_CUSTOMIZED_UI_FLAG`). + +### ICustomizedUIMgrEvent (3 methods required) + +**File**: `SDK/x64/h/customized_ui/customized_ui_mgr.h` + +```cpp +class CustomUIMgrEventListener : public ICustomizedUIMgrEvent { +public: + // Method 1: Video container destroyed by SDK (e.g., meeting ended) + void onVideoContainerDestroyed(ICustomizedVideoContainer* pContainer) override; + + // Method 2: Share render destroyed by SDK + void onShareRenderDestroyed(ICustomizedShareRender* pRender) override; + + // Method 3: Immersive container destroyed by SDK + void onImmersiveContainerDestroyed() override; +}; +``` + +**Important notes**: +- These fire when the SDK destroys containers on its own (e.g., meeting ends) +- You MUST null out your pointers in these callbacks to avoid dangling references +- All 3 are always required + +--- + +### ICustomizedVideoContainerEvent (6 methods required) + +**File**: `SDK/x64/h/customized_ui/customized_video_container.h` + +```cpp +class VideoContainerEventListener : public ICustomizedVideoContainerEvent { +public: + // Method 1: User changed for a video render element + void onRenderUserChanged(IVideoRenderElement* pElement, unsigned int userid) override; + + // Method 2: Data type changed (video, avatar, screen name) + void onRenderDataTypeChanged(IVideoRenderElement* pElement, VideoRenderDataType dataType) override; + + // Method 3: Layout notification — container resized, recompute element positions + void onLayoutNotification(RECT wnd_client_rect) override; + + // Method 4: A video render element was destroyed + void onVideoRenderElementDestroyed(IVideoRenderElement* pElement) override; + + // Method 5: Window messages from SDK child HWND (mouse, keyboard) + void onWindowMsgNotification(UINT uMsg, WPARAM wParam, LPARAM lParam) override; + + // Method 6: Video subscription failed for an element + void onSubscribeUserFail(ZoomSDKVideoSubscribeFailReason fail_reason, IVideoRenderElement* pElement) override; +}; +``` + +**Important notes**: +- `onLayoutNotification` is where you re-layout video elements after container resize +- `onWindowMsgNotification` forwards input from SDK's child HWND (see [Custom UI Architecture](../concepts/custom-ui-architecture.md)) +- `VideoRenderDataType` values: `VideoRenderData_Video`, `VideoRenderData_Avatar`, `VideoRenderData_ScreenName` +- `ZoomSDKVideoSubscribeFailReason` values: `ViewOnly`, `NotInMeeting`, `HasSubscribe1080POr720`, `HasSubscribeTwo720P`, `HasSubscribeExceededLimit`, `TooFrequentCall` + +--- + +### ICustomizedShareRenderEvent (3 methods required) + +**File**: `SDK/x64/h/customized_ui/customized_share_render.h` + +```cpp +class ShareRenderEventListener : public ICustomizedShareRenderEvent { +public: + // Method 1: Started receiving shared content + void onSharingContentStartReceiving() override; + + // Method 2: Share source changed or sharing closed + void onSharingSourceNotification(unsigned int nShareSourceID) override; + + // Method 3: Window messages from share render's child HWND + void onWindowMsgNotification(UINT uMsg, WPARAM wParam, LPARAM lParam) override; +}; +``` + +**Important notes**: +- When `onSharingSourceNotification` fires with a new ID, call `SetShareSourceID(nShareSourceID)` and `Show()` +- When sharing stops, `nShareSourceID` will be 0 — call `Hide()` + +--- + +### Quick Reference: Custom UI Method Counts + +| Interface | Methods | File | +|-----------|---------|------| +| `ICustomizedUIMgrEvent` | 3 | `customized_ui/customized_ui_mgr.h` | +| `ICustomizedVideoContainerEvent` | 6 | `customized_ui/customized_video_container.h` | +| `ICustomizedShareRenderEvent` | 3 | `customized_ui/customized_share_render.h` | +| `ICustomizedImmersiveContainerEvent` | 1 | `customized_ui/customized_immersive_container.h` | +| **Total Custom UI methods** | **13** | | + +--- + +## SDK Version Differences + +Different SDK versions may have different required methods. This guide is for **SDK v6.7.2.26830**. + +If you're using a different version: +1. Run `grep "= 0" SDK/x64/h/auth_service_interface.h` to see your version's methods +2. Run `grep "= 0" SDK/x64/h/meeting_service_interface.h` +3. Compare against this guide to identify new or removed methods + +--- + +## Quick Reference Commands + +```bash +# List all pure virtual methods in SDK +grep "= 0" SDK/x64/h/*.h + +# Count methods per interface +grep -c "= 0" SDK/x64/h/auth_service_interface.h # Should be 6 +grep -c "= 0" SDK/x64/h/meeting_service_interface.h # Should be 9 + +# Find method signature +grep -A 5 "onAuthenticationReturn" SDK/x64/h/auth_service_interface.h + +# Verify your implementation has all methods +grep "override" src/AuthServiceEventListener.h # Should match SDK count +``` + +--- + +## Related Documentation + +- [Build Errors Guide](../troubleshooting/build-errors.md) - Header dependency issues +- [Authentication Pattern](../examples/authentication-pattern.md) - Using IAuthServiceEvent +- [Windows Reference](windows-reference.md) - General SDK setup + +--- + +**Last Updated**: Based on Zoom Windows Meeting SDK v6.7.2.26830 diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/references/windows-reference.md b/plugins/zoom-developers/skills/meeting-sdk/windows/references/windows-reference.md new file mode 100644 index 00000000..188447f4 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/references/windows-reference.md @@ -0,0 +1,790 @@ +# Meeting SDK - Windows Reference + +Complete reference for Zoom Meeting SDK on Windows including dependencies, Visual Studio setup, and troubleshooting. + +## System Requirements + +- **OS**: Windows 10 or later, Windows Server 2016+ +- **Architecture**: x86 (32-bit) and x64 (64-bit) +- **IDE**: Visual Studio 2019, 2022, or later +- **C++ Standard**: C++11 or later + +## Dependencies + +### Required Tools + +```powershell +# Install vcpkg (package manager for C++) +git clone https://github.com/Microsoft/vcpkg.git C:\vcpkg +cd C:\vcpkg +.\bootstrap-vcpkg.bat +.\vcpkg integrate install +``` + +### Required Libraries + +```powershell +# For x64 +.\vcpkg install jsoncpp:x64-windows +.\vcpkg install curl:x64-windows + +# For x86 +.\vcpkg install jsoncpp:x86-windows +.\vcpkg install curl:x86-windows + +# Set default triplet (optional) +$env:VCPKG_DEFAULT_TRIPLET = "x64-windows" +``` + +### Visual Studio Workloads + +Required workloads in Visual Studio Installer: +- Desktop development with C++ +- Windows 10/11 SDK +- C++ CMake tools (optional, for CMake projects) + +## Visual Studio Project Configuration + +### Project Properties Template + +Create a new C++ Console Application, then configure: + +#### C/C++ Settings + +**General → Additional Include Directories:** +``` +$(SolutionDir)SDK\$(PlatformTarget)\h +$(SolutionDir)SDK\$(PlatformTarget)\h\meeting_service_components +$(SolutionDir)SDK\$(PlatformTarget)\h\rawdata +C:\vcpkg\packages\jsoncpp_$(PlatformTarget)-windows\include +C:\vcpkg\packages\curl_$(PlatformTarget)-windows\include +``` + +**Preprocessor → Preprocessor Definitions:** +``` +WIN32 +_DEBUG (for Debug config) +_CONSOLE +_UNICODE +UNICODE +%(PreprocessorDefinitions) +``` + +**Code Generation → Runtime Library:** +- Debug: Multi-threaded Debug DLL (/MDd) +- Release: Multi-threaded DLL (/MD) + +#### Linker Settings + +**General → Additional Library Directories:** +``` +$(SolutionDir)SDK\$(PlatformTarget)\lib +C:\vcpkg\packages\jsoncpp_$(PlatformTarget)-windows\lib +C:\vcpkg\packages\curl_$(PlatformTarget)-windows\lib +``` + +**Input → Additional Dependencies:** +``` +sdk.lib +ws2_32.lib +%(AdditionalDependencies) +``` + +#### Build Events + +**Post-Build Event → Command Line:** +```cmd +xcopy /Y /D "$(SolutionDir)SDK\$(PlatformTarget)\bin\*.*" "$(OutDir)" +xcopy /Y /D "$(ProjectDir)config.json" "$(OutDir)" +``` + +This copies all SDK DLLs and config.json to your output directory. + +### CMakeLists.txt Template (Alternative) + +```cmake +cmake_minimum_required(VERSION 3.16) + +project(ZoomMeetingSDK CXX) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Set output directories +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) + +# Find vcpkg packages +find_package(jsoncpp CONFIG REQUIRED) +find_package(CURL REQUIRED) + +# Include directories +include_directories( + ${CMAKE_SOURCE_DIR}/SDK/x64/h + ${CMAKE_SOURCE_DIR}/SDK/x64/h/meeting_service_components + ${CMAKE_SOURCE_DIR}/SDK/x64/h/rawdata +) + +# Link directories +link_directories( + ${CMAKE_SOURCE_DIR}/SDK/x64/lib + ${CMAKE_SOURCE_DIR}/SDK/x64/bin +) + +# Source files +add_executable(YourApp + YourApp.cpp + AuthServiceEventListener.cpp + MeetingServiceEventListener.cpp + NetworkConnectionHandler.cpp + WebService.cpp + # Add more source files as needed +) + +# Link libraries +target_link_libraries(YourApp + sdk.lib + JsonCpp::JsonCpp + CURL::libcurl +) + +# Copy DLLs to output directory +add_custom_command(TARGET YourApp POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_SOURCE_DIR}/SDK/x64/bin" + $ +) + +# Copy config.json +configure_file( + ${CMAKE_SOURCE_DIR}/config.json + ${CMAKE_SOURCE_DIR}/bin/config.json + COPYONLY +) +``` + +## Configuration File + +### config.json Format + +```json +{ + "sdk_jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "meeting_number": "1234567890", + "passcode": "abc123", + "video_source": "Big_Buck_Bunny_720_10s_1MB.mp4", + "zak": "" +} +``` + +### Reading Config in Code + +```cpp +#include +#include + +void LoadConfig() { + std::ifstream f("config.json"); + Json::Value config; + + if (f.is_open()) { + try { + f >> config; + + // Extract values + std::string jwt = config["sdk_jwt"].asString(); + std::string meetingNum = config["meeting_number"].asString(); + std::string passcode = config["passcode"].asString(); + + // Convert to wstring for SDK + sdk_jwt = std::wstring(jwt.begin(), jwt.end()); + meeting_number = std::stoull(meetingNum); + // ... + } catch (const std::exception& e) { + std::cerr << "Error parsing config.json: " << e.what() << std::endl; + } + } else { + std::cerr << "config.json not found" << std::endl; + } +} +``` + +## Event Listeners + +### Complete Event Listener Templates + +#### AuthServiceEventListener.cpp + +```cpp +#include "AuthServiceEventListener.h" +#include + +AuthServiceEventListener::AuthServiceEventListener(void (*onComplete)()) + : onAuthComplete(onComplete) {} + +void AuthServiceEventListener::onAuthenticationReturn(AuthResult ret) { + switch (ret) { + case AUTHRET_SUCCESS: + std::cout << "Authentication successful" << std::endl; + if (onAuthComplete) onAuthComplete(); + break; + case AUTHRET_KEYORSECRETEMPTY: + std::cerr << "SDK Key or Secret is empty" << std::endl; + break; + case AUTHRET_JWTTOKENWRONG: + std::cerr << "JWT token is invalid" << std::endl; + break; + case AUTHRET_OVERTIME: + std::cerr << "Authentication timeout" << std::endl; + break; + default: + std::cerr << "Authentication failed: " << ret << std::endl; + } +} + +void AuthServiceEventListener::onLoginReturnWithReason( + LOGINSTATUS ret, + IAccountInfo* info, + LoginFailReason reason) { + // Handle login status +} + +void AuthServiceEventListener::onLogout() { + std::cout << "Logged out" << std::endl; +} + +void AuthServiceEventListener::onZoomIdentityExpired() { + std::cout << "Zoom identity expired" << std::endl; +} + +void AuthServiceEventListener::onZoomAuthIdentityExpired() { + std::cout << "Zoom auth identity expired - need to re-authenticate" << std::endl; +} +``` + +#### MeetingServiceEventListener.cpp + +```cpp +#include "MeetingServiceEventListener.h" +#include + +MeetingServiceEventListener::MeetingServiceEventListener( + void (*onJoined)(), + void (*onEnded)(), + void (*onInMeeting)() +) : onMeetingJoined(onJoined), + onMeetingEnded(onEnded), + onInMeetingCallback(onInMeeting) {} + +void MeetingServiceEventListener::onMeetingStatusChanged( + MeetingStatus status, + int iResult) { + + switch (status) { + case MEETING_STATUS_IDLE: + std::cout << "Meeting status: IDLE" << std::endl; + break; + case MEETING_STATUS_CONNECTING: + std::cout << "Meeting status: CONNECTING" << std::endl; + if (onMeetingJoined) onMeetingJoined(); + break; + case MEETING_STATUS_WAITINGFORHOST: + std::cout << "Meeting status: WAITING FOR HOST" << std::endl; + break; + case MEETING_STATUS_INMEETING: + std::cout << "Meeting status: IN MEETING" << std::endl; + if (onInMeetingCallback) onInMeetingCallback(); + break; + case MEETING_STATUS_DISCONNECTING: + std::cout << "Meeting status: DISCONNECTING" << std::endl; + break; + case MEETING_STATUS_RECONNECTING: + std::cout << "Meeting status: RECONNECTING" << std::endl; + break; + case MEETING_STATUS_FAILED: + std::cerr << "Meeting status: FAILED (code: " << iResult << ")" << std::endl; + if (onMeetingEnded) onMeetingEnded(); + break; + case MEETING_STATUS_ENDED: + std::cout << "Meeting status: ENDED" << std::endl; + if (onMeetingEnded) onMeetingEnded(); + break; + default: + std::cout << "Meeting status: UNKNOWN (" << status << ")" << std::endl; + } +} + +void MeetingServiceEventListener::onMeetingStatisticsWarningNotification( + StatisticsWarningType type) { + std::cout << "Meeting statistics warning: " << type << std::endl; +} + +void MeetingServiceEventListener::onMeetingParameterNotification( + const MeetingParameter* param) { + if (param) { + std::cout << "Meeting parameter notification" << std::endl; + } +} + +void MeetingServiceEventListener::onSuspendParticipantsActivities() { + std::cout << "Participants activities suspended" << std::endl; +} + +void MeetingServiceEventListener::onAICompanionActiveChangeNotice(bool isActive) { + std::cout << "AI Companion " << (isActive ? "activated" : "deactivated") << std::endl; +} +``` + +#### MeetingRecordingCtrlEventListener.cpp + +```cpp +#include "MeetingRecordingCtrlEventListener.h" +#include + +void MeetingRecordingCtrlEventListener::onRecordingStatus(RecordingStatus status) { + switch (status) { + case Recording_Start: + std::cout << "Recording started" << std::endl; + break; + case Recording_Stop: + std::cout << "Recording stopped" << std::endl; + break; + case Recording_DiskFull: + std::cerr << "Recording stopped - disk full" << std::endl; + break; + case Recording_Pause: + std::cout << "Recording paused" << std::endl; + break; + case Recording_Connecting: + std::cout << "Recording connecting" << std::endl; + break; + default: + std::cout << "Recording status: " << status << std::endl; + } +} + +void MeetingRecordingCtrlEventListener::onRecordPrivilegeChanged(bool bCanRec) { + std::cout << "Recording privilege: " << (bCanRec ? "granted" : "revoked") << std::endl; +} + +void MeetingRecordingCtrlEventListener::onCloudRecordingStatus(RecordingStatus status) { + std::cout << "Cloud recording status: " << status << std::endl; +} + +void MeetingRecordingCtrlEventListener::onRecordingPrivilegeRequestStatus( + RequestRecPrivilegeStatus status) { + std::cout << "Recording privilege request status: " << status << std::endl; +} + +void MeetingRecordingCtrlEventListener::onLocalRecordingPrivilegeRequestStatus( + RequestLocalRecordingStatus status) { + std::cout << "Local recording privilege request status: " << status << std::endl; +} + +void MeetingRecordingCtrlEventListener::onLocalRecordingPrivilegeResponse( + bool bAccept) { + std::cout << "Local recording privilege " + << (bAccept ? "accepted" : "denied") << std::endl; +} + +void MeetingRecordingCtrlEventListener::onCustomizedLocalRecordingSourceNotification( + ICustomizedLocalRecordingLayoutHelper* layout_helper) { + std::cout << "Customized local recording source notification" << std::endl; +} +``` + +## Raw Data Requirements + +### Recording Permission + +Raw data access requires one of: +1. **Host** status +2. **Co-host** status +3. **Recording permission** from host +4. **Recording token** (app_privilege_token) + +```cpp +bool CheckAndStartRawRecording() { + IMeetingRecordingController* recordCtrl = + meetingService->GetMeetingRecordingController(); + + if (!recordCtrl) { + std::cerr << "Failed to get recording controller" << std::endl; + return false; + } + + SDKError canStart = recordCtrl->CanStartRecording(false, 0); + + if (canStart == SDKERR_SUCCESS) { + SDKError err = recordCtrl->StartRawRecording(); + if (err == SDKERR_SUCCESS) { + std::cout << "Raw recording started" << std::endl; + return true; + } else { + std::cerr << "StartRawRecording failed: " << err << std::endl; + return false; + } + } else { + std::cout << "Need host/cohost/recording permission" << std::endl; + std::cout << "Requesting recording privilege..." << std::endl; + recordCtrl->RequestLocalRecordingPrivilege(); + return false; + } +} +``` + +### Video Resolutions + +```cpp +// Available resolutions +videoHelper->setRawDataResolution(ZoomSDKResolution_90P); // 160x90 +videoHelper->setRawDataResolution(ZoomSDKResolution_180P); // 320x180 +videoHelper->setRawDataResolution(ZoomSDKResolution_360P); // 640x360 +videoHelper->setRawDataResolution(ZoomSDKResolution_720P); // 1280x720 +videoHelper->setRawDataResolution(ZoomSDKResolution_1080P); // 1920x1080 +``` + +### Audio Format + +- **Format**: PCM (raw, uncompressed) +- **Sample Rate**: 32000 Hz (typical) +- **Channels**: Mono (1) or Stereo (2) +- **Bit Depth**: 16-bit +- **Byte Order**: Little-endian + +## API Reference + +### InitParam Structure + +```cpp +struct tagInitParam { + const wchar_t* strWebDomain; // "https://zoom.us" + const wchar_t* strSupportUrl; // Support URL + SDK_LANGUAGE_ID emLanguageID; // LANGUAGE_English, etc. + bool enableGenerateDump; // Enable crash dump + bool enableLogByDefault; // Enable logging + unsigned int uiLogFileSize; // Log file size (MB, default: 5) + const wchar_t* strLogFileFolder; // Custom log folder path + RawDataOptions rawdataOpts; // Raw data options + ConfigurableOptions obConfigOpts; // Config options +}; +``` + +### IAuthService Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `SetEvent(IAuthServiceEvent*)` | Set auth callback | `SDKError` | +| `SDKAuth(AuthContext&)` | Authenticate with JWT | `SDKError` | +| `GetAuthResult()` | Get auth status | `AuthResult` | +| `LogOut()` | Logout | `SDKError` | +| `GetAccountInfo()` | Get account info | `IAccountInfo*` | + +### IMeetingService Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `SetEvent(IMeetingServiceEvent*)` | Set meeting callback | `SDKError` | +| `Join(JoinParam&)` | Join meeting | `SDKError` | +| `Start(StartParam&)` | Start meeting | `SDKError` | +| `Leave(LeaveMeetingCmd)` | Leave meeting | `SDKError` | +| `GetMeetingStatus()` | Get status | `MeetingStatus` | +| `GetMeetingInfo()` | Get meeting info | `IMeetingInfo*` | +| `GetMeetingVideoController()` | Video control | `IMeetingVideoController*` | +| `GetMeetingAudioController()` | Audio control | `IMeetingAudioController*` | +| `GetMeetingRecordingController()` | Recording control | `IMeetingRecordingController*` | +| `GetMeetingParticipantsController()` | Participants | `IMeetingParticipantsController*` | +| `GetMeetingChatController()` | Chat control | `IMeetingChatController*` | + +### JoinParam4WithoutLogin Structure + +```cpp +struct JoinParam4WithoutLogin { + UINT64 meetingNumber; // Meeting number + const wchar_t* userName; // Display name + const wchar_t* psw; // Meeting password + const wchar_t* vanityID; // Personal link name + const wchar_t* customer_key; // Customer key + const wchar_t* webinarToken; // Webinar token + const wchar_t* userZAK; // Zoom Access Key + const wchar_t* app_privilege_token; // App privilege token (for raw data) + const wchar_t* onBehalfToken; // On behalf token + bool isVideoOff; // Start with video off + bool isAudioOff; // Start with audio off + bool isDirectShareDesktop; // Share desktop directly + bool isAudioAutoConnect; // Auto-connect to audio + bool isAutoRecording; // Auto-start recording +}; +``` + +### IZoomSDKRenderer Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `setRawDataResolution(ZoomSDKResolution)` | Set resolution | `SDKError` | +| `subscribe(uint32_t userId, ZoomSDKRawDataType)` | Subscribe to video | `SDKError` | +| `unSubscribe()` | Unsubscribe | `SDKError` | +| `getResolution()` | Get resolution | `ZoomSDKResolution` | +| `getSubscribeId()` | Get user ID | `uint32_t` | + +### YUVRawDataI420 Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `GetStreamWidth()` | Video width | `uint32_t` | +| `GetStreamHeight()` | Video height | `uint32_t` | +| `GetYBuffer()` | Y plane buffer | `char*` | +| `GetUBuffer()` | U plane buffer | `char*` | +| `GetVBuffer()` | V plane buffer | `char*` | +| `GetBufferLen()` | Total buffer length | `uint32_t` | +| `GetRotation()` | Rotation angle | `int` | + +**Video Format:** +- YUV420 (I420) planar format +- Y plane: `width * height` bytes +- U plane: `(width/2) * (height/2)` bytes +- V plane: `(width/2) * (height/2)` bytes +- Total size: `width * height * 1.5` bytes + +### AudioRawData Methods + +| Method | Description | Returns | +|--------|-------------|---------| +| `GetBuffer()` | Audio buffer | `char*` | +| `GetBufferLen()` | Buffer length (bytes) | `uint32_t` | +| `GetSampleRate()` | Sample rate (Hz) | `uint32_t` | +| `GetChannelNum()` | Number of channels | `uint32_t` | + +### Error Codes (SDKError) + +| Code | Description | +|------|-------------| +| `SDKERR_SUCCESS` | Success | +| `SDKERR_INVALID_PARAMETER` | Invalid parameter | +| `SDKERR_UNINITIALIZE` | SDK not initialized | +| `SDKERR_UNAUTHENTICATION` | Not authenticated | +| `SDKERR_NO_PERMISSION` | No permission | +| `SDKERR_NO_AUDIODEVICE_ISFOUND` | No audio device | +| `SDKERR_NO_VIDEODEVICE_ISFOUND` | No video device | +| `SDKERR_INTERNAL_ERROR` | Internal error | +| `SDKERR_SERVICE_FAILED` | Service failed | +| `SDKERR_MEMORY_FAILED` | Memory allocation failed | +| `SDKERR_TOO_FREQUENT_CALL` | API called too frequently | +| `SDKERR_WRONG_USAGE` | Wrong API usage | + +### Authentication Results (AuthResult) + +| Result | Description | +|--------|-------------| +| `AUTHRET_SUCCESS` | Authentication successful | +| `AUTHRET_KEYORSECRETEMPTY` | Key or secret empty | +| `AUTHRET_JWTTOKENWRONG` | JWT token invalid | +| `AUTHRET_OVERTIME` | Operation timed out | + +### Meeting Status (MeetingStatus) + +| Status | Description | +|--------|-------------| +| `MEETING_STATUS_IDLE` | No meeting | +| `MEETING_STATUS_CONNECTING` | Connecting | +| `MEETING_STATUS_INMEETING` | In meeting | +| `MEETING_STATUS_RECONNECTING` | Reconnecting | +| `MEETING_STATUS_FAILED` | Failed | +| `MEETING_STATUS_ENDED` | Meeting ended | +| `MEETING_STATUS_WAITINGFORHOST` | Waiting for host | +| `MEETING_STATUS_DISCONNECTING` | Disconnecting | + +## Troubleshooting + +### Common Build Errors + +#### Error: Cannot open include file 'json/json.h' + +**Cause**: jsoncpp include path not configured + +**Fix**: +1. Ensure vcpkg installed jsoncpp: `.\vcpkg install jsoncpp:x64-windows` +2. Add to **Additional Include Directories**: + ``` + C:\vcpkg\packages\jsoncpp_x64-windows\include + ``` +3. Or in Project Properties → vcpkg → Use vcpkg manifest: Yes + +#### Error: unresolved external symbol "InitSDK" + +**Cause**: sdk.lib not linked + +**Fix**: +1. Add to **Additional Library Directories**: + ``` + $(SolutionDir)SDK\$(PlatformTarget)\lib + ``` +2. Add to **Additional Dependencies**: + ``` + sdk.lib + ``` + +#### Error: sdk.dll not found when running + +**Cause**: DLLs not copied to output directory + +**Fix**: Add Post-Build Event: +```cmd +xcopy /Y /D "$(SolutionDir)SDK\$(PlatformTarget)\bin\*.*" "$(OutDir)" +``` + +### Runtime Errors + +#### Error: InitSDK returns SDKERR_INTERNAL_ERROR + +**Cause**: Invalid SDK initialization parameters or missing DLLs + +**Fix**: +1. Ensure all SDK DLLs are in the same directory as .exe +2. Check InitParam values are valid +3. Ensure strWebDomain is "https://zoom.us" + +#### Error: SDKAuth returns AUTHRET_JWTTOKENWRONG + +**Cause**: Invalid JWT token + +**Fix**: +1. Verify JWT token is correctly generated with SDK Key and Secret +2. Check token expiration time (iat and exp) +3. Ensure meeting number in JWT matches meeting number in join request +4. See [authorization.md](../../references/authorization.md) for JWT generation + +#### Error: Join returns SDKERR_INVALID_PARAMETER + +**Cause**: Invalid join parameters + +**Fix**: +1. Verify meeting number is correct UINT64 +2. Check meeting password is valid +3. Ensure userName is not empty +4. Convert strings to wchar_t* correctly: + ```cpp + std::wstring userName = L"Bot User"; + params.userName = userName.c_str(); + ``` + +#### Cannot start raw recording + +**Cause**: No recording permission + +**Fix**: +1. Wait for host to grant recording permission +2. Use `RequestLocalRecordingPrivilege()` to request permission +3. Or use `app_privilege_token` in JoinParam: + ```cpp + params.app_privilege_token = recording_token.c_str(); + ``` +4. Or join as host using `onBehalfToken` + +### Video/Audio Issues + +#### YUV video file is corrupted when played + +**Cause**: Mixed resolutions or incorrect parameters + +**Fix**: +1. Check console output for resolution changes +2. Create new file when resolution changes +3. Verify ffmpeg command matches actual resolution: + ```cmd + ffmpeg -video_size 1280x720 -pixel_format yuv420p -f rawvideo -i output.yuv output.mp4 + ``` + +#### Audio file has no sound + +**Cause**: Wrong audio format parameters + +**Fix**: +1. Check audio sample rate: `data->GetSampleRate()` +2. Verify channel count: `data->GetChannelNum()` +3. Use correct ffmpeg parameters: + ```cmd + ffplay -f s16le -ar 32000 -ac 1 audio.pcm + ``` + +#### Video subscription fails + +**Cause**: Not in meeting or no recording permission + +**Fix**: +1. Ensure you're in meeting (`MEETING_STATUS_INMEETING`) +2. Start raw recording first: `StartRawRecording()` +3. Check recording permission: `CanStartRecording()` +4. Subscribe after recording started + +## Cleanup + +```cpp +void CleanSDK() { + // Unsubscribe from raw data + if (videoHelper) { + videoHelper->unSubscribe(); + videoHelper = nullptr; + } + + if (audioHelper) { + audioHelper->unSubscribe(); + audioHelper = nullptr; + } + + // Destroy services + if (authService) { + DestroyAuthService(authService); + authService = nullptr; + } + + if (meetingService) { + DestroyMeetingService(meetingService); + meetingService = nullptr; + } + + // Clean up SDK + CleanUPSDK(); +} +``` + +## Docker Support (Windows Containers) + +### Dockerfile + +```dockerfile +# Use Windows Server Core +FROM mcr.microsoft.com/windows/servercore:ltsc2022 + +# Install Visual C++ Redistributables +ADD https://aka.ms/vs/17/release/vc_redist.x64.exe C:\vcredist.exe +RUN C:\vcredist.exe /install /quiet /norestart + +# Copy your application +WORKDIR C:\app +COPY SDK C:\app\SDK +COPY x64\Release\YourApp.exe C:\app\ +COPY config.json C:\app\ + +CMD ["C:\\app\\YourApp.exe"] +``` + +**Note**: Windows containers require Windows-based Docker host. GUI applications are supported but won't display UI. + +## Authentication Requirements (2026 Update) + +> **Important**: Beginning **March 2, 2026**, apps joining meetings outside their account must be authorized. + +Options: +- **App Privilege Token (OBF)** - Recommended for bots (`app_privilege_token`) +- **ZAK Token** - Zoom Access Key (`userZAK`) +- **On Behalf Token** - For specific use cases (`onBehalfToken`) + +See [bot-authentication.md](../../references/bot-authentication.md) for details. + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/meeting-sdk/windows/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/meeting/windows/annotated.html +- **Sample code**: https://github.com/zoom/meetingsdk-windows-raw-recording-sample +- **Developer forum**: https://devforum.zoom.us/ diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/troubleshooting/build-errors.md b/plugins/zoom-developers/skills/meeting-sdk/windows/troubleshooting/build-errors.md new file mode 100644 index 00000000..dd570230 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/troubleshooting/build-errors.md @@ -0,0 +1,409 @@ +# Common Build Errors and Solutions + +## SDK Header Dependency Issues + +The Zoom Windows SDK has several header dependency bugs that cause compilation errors. This guide covers the most common issues and their solutions. + +--- + +## Error 1: `uint32_t` Undefined + +### Symptoms + +``` +Error C2061: syntax error: identifier 'uint32_t' +Error C3646: 'GetAudioJoinType': unknown override specifier +Error C2059: syntax error: ')' +Error C2238: unexpected token(s) preceding ';' +``` + +Errors occur in SDK headers: +- `rawdata/rawdata_renderer_interface.h` (lines 57, 65) +- `meeting_service_components/meeting_participants_ctrl_interface.h` (line 139) + +### Root Cause + +The SDK headers use `uint32_t` but don't include `` where it's defined. + +### Solution + +**Add `#include ` to ALL your header files**, right after ``: + +```cpp +// YourListener.h +#pragma once +#include +#include // CRITICAL: Must come before SDK headers! +#include +``` + +**Required in**: +- All `.h` files that include SDK headers +- `main.cpp` or any `.cpp` that includes SDK headers directly + +### Critical Include Order + +```cpp +// 1. Windows header FIRST +#include + +// 2. Standard int types SECOND (for uint32_t) +#include + +// 3. Other standard headers +#include +#include + +// 4. Zoom SDK headers LAST +#include +#include +``` + +**This order is MANDATORY and must be followed in every file!** + +--- + +## Error 2: `AudioType` Undefined + +### Symptoms + +``` +Error C3646: 'GetAudioJoinType': unknown override specifier +Error C2059: syntax error: ')' +``` + +Error occurs when including: +```cpp +#include +``` + +### Root Cause + +`meeting_participants_ctrl_interface.h` uses `AudioType` enum (line 139) but doesn't include `meeting_audio_interface.h` where `AudioType` is defined. + +### Solution + +**Include `meeting_audio_interface.h` BEFORE `meeting_participants_ctrl_interface.h`**: + +```cpp +// Correct order +#include // FIRST +#include // SECOND +``` + +**Wrong order will fail**: +```cpp +// ❌ This will cause errors! +#include +#include +``` + +--- + +## Error 3: `YUVRawDataI420` Incomplete Type + +### Symptoms + +``` +Error: use of undefined type 'YUVRawDataI420' +Error: incomplete type is not allowed +``` + +### Root Cause + +`rawdata/rawdata_renderer_interface.h` only forward-declares `YUVRawDataI420`: +```cpp +class YUVRawDataI420; // Forward declaration only! +``` + +The full class definition is in `zoom_sdk_raw_data_def.h`. + +### Solution + +**Include `zoom_sdk_raw_data_def.h` in your renderer delegate header**: + +```cpp +// YourRendererDelegate.h +#pragma once +#include +#include +#include +#include // Full YUVRawDataI420 definition +``` + +--- + +## Error 4: Abstract Class Cannot Be Instantiated + +### Symptoms + +``` +Error C2259: 'MeetingServiceEventListener': cannot instantiate abstract class +Error: pure virtual function "IMeetingServiceEvent::onUserNetworkStatusChanged" has no overrider +Error: pure virtual function "IMeetingServiceEvent::onAppSignalPanelUpdated" has no overrider +``` + +### Root Cause + +Missing implementation of pure virtual methods (methods marked with `= 0`) required by SDK interfaces. + +### Solution + +**Implement ALL pure virtual methods** from the SDK interface. + +For `IMeetingServiceEvent` (SDK v6.7.2.26830): +```cpp +class MyMeetingListener : public IMeetingServiceEvent { +public: + // Required by ALL versions + void onMeetingStatusChanged(MeetingStatus status, int iResult) override; + void onMeetingStatisticsWarningNotification(StatisticsWarningType type) override; + void onMeetingParameterNotification(const MeetingParameter* param) override; + void onSuspendParticipantsActivities() override; + void onAICompanionActiveChangeNotice(bool isActive) override; + void onMeetingTopicChanged(const zchar_t* sTopic) override; + void onMeetingFullToWatchLiveStream(const zchar_t* sLiveStreamUrl) override; + void onUserNetworkStatusChanged(MeetingComponentType type, ConnectionQuality level, + unsigned int userId, bool uplink) override; + + // Required when WIN32 is defined + #if defined(WIN32) + void onAppSignalPanelUpdated(IMeetingAppSignalHandler* pHandler) override; + #endif +}; +``` + +### How to Find All Required Methods + +1. Open the SDK header file (e.g., `meeting_service_interface.h`) +2. Search for the interface class (e.g., `class IMeetingServiceEvent`) +3. Look for all methods marked with `= 0` (pure virtual) +4. Implement every single one + +**Example from SDK header**: +```cpp +class IMeetingServiceEvent { +public: + virtual void onMeetingStatusChanged(...) = 0; // Must implement! + virtual void onMeetingStatisticsWarningNotification(...) = 0; // Must implement! + // ... etc +}; +``` + +--- + +## Error 5: Override Specifier Did Not Override + +### Symptoms + +``` +Error C3668: method with override specifier 'override' did not override any base class methods +``` + +### Root Cause + +Method signature doesn't exactly match the base class, or the method doesn't exist in the SDK interface (usually due to conditional compilation). + +### Solution + +**Check for conditional compilation**: + +```cpp +// ❌ Wrong: Will fail if WIN32 is not defined +void onNotificationServiceStatus(...) override; + +// ✅ Correct: Match SDK's conditional compilation +#if defined(WIN32) +void onNotificationServiceStatus(...) override; +#endif +``` + +**Verify method signature matches exactly**: +- Parameter types must match exactly +- `const` qualifiers must match +- Parameter names don't matter, but types do + +--- + +## Complete Include Template + +### For Main Application File + +```cpp +// main.cpp +#include +#include +#include +#include +#include +#include +#include + +// Zoom SDK headers - ORDER MATTERS! +#include +#include +#include +#include +#include // BEFORE participants! +#include +#include +#include + +// Third-party libraries +#include + +// Your headers +#include "AuthServiceEventListener.h" +#include "MeetingServiceEventListener.h" +#include "ZoomSDKRendererDelegate.h" +``` + +### For Event Listener Headers + +```cpp +// AuthServiceEventListener.h +#pragma once +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class AuthServiceEventListener : public IAuthServiceEvent { +public: + // ... methods ... +}; +``` + +### For Renderer Delegate Headers + +```cpp +// ZoomSDKRendererDelegate.h +#pragma once +#include +#include +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +class ZoomSDKRendererDelegate : public IZoomSDKRendererDelegate { +public: + // ... methods ... +}; +``` + +--- + +## Preprocessor Definitions + +### Required Definition: WIN32 + +For correct SDK interface behavior, define `WIN32` in project settings: + +**Visual Studio `.vcxproj`**: +```xml +WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) +``` + +**CMake**: +```cmake +target_compile_definitions(YourTarget PRIVATE WIN32) +``` + +**Why**: SDK uses `#if defined(WIN32)` to conditionally include platform-specific methods. Without it, you'll miss required methods or have methods that don't exist in the interface. + +--- + +## Quick Troubleshooting Checklist + +When you get build errors: + +- [ ] Is `` the FIRST include? +- [ ] Is `` included after `` in ALL headers? +- [ ] Is `` included BEFORE any SDK headers? +- [ ] Is `meeting_audio_interface.h` included before `meeting_participants_ctrl_interface.h`? +- [ ] Is `zoom_sdk_raw_data_def.h` included for raw data delegates? +- [ ] Are ALL pure virtual methods implemented? +- [ ] Is `WIN32` defined in preprocessor definitions? +- [ ] Do method signatures exactly match the SDK interface (including `const`)? +- [ ] Are conditional methods (`#if defined(WIN32)`) handled correctly? + +--- + +## Common Patterns for Each Error + +### Pattern: "identifier 'X' is undefined" +→ Missing include for header that defines `X` +→ Wrong include order (SDK header before ``) + +### Pattern: "unknown override specifier" +→ Type used in method signature is undefined +→ Usually means missing include or wrong include order + +### Pattern: "cannot instantiate abstract class" +→ Missing pure virtual method implementation +→ Check SDK header for ALL methods with `= 0` + +### Pattern: "incomplete type" +→ Only forward declaration available +→ Need to include header with full definition + +### Pattern: "did not override any base class methods" +→ Method doesn't exist in interface (check conditional compilation) +→ Method signature doesn't match exactly + +--- + +## SDK Version Differences + +SDK versions may have different required methods. Always check your specific SDK version's headers. + +**To check required methods**: +```bash +# Search for pure virtual methods in interface +grep "= 0" SDK/x64/h/meeting_service_interface.h +``` + +**SDK v6.7.2.26830 requirements**: +- `IMeetingServiceEvent`: 9 methods (8 + 1 WIN32-specific) +- `IAuthServiceEvent`: 6 methods (5 + 1 WIN32-specific) +- `IZoomSDKRendererDelegate`: 3 methods + +--- + +--- + +## MSBuild Command Pattern + +When building from git bash on Windows, use this invocation pattern: + +```bash +# Git bash requires unix-style path for the exe and //p: (double slash) for switches +"/c/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/MSBuild/Current/Bin/MSBuild.exe" \ + "C:\tempsdk\zoom-windows-sdk-sample\ZoomSDKSample.vcxproj" \ + //p:Configuration=Release //p:Platform=x64 +``` + +**Key gotchas:** +- Use forward slashes for the MSBuild exe path (`/c/Program Files/...`) +- Use `//p:` not `/p:` — git bash interprets single `/p` as a path +- Use `//t:Rebuild` for clean rebuilds +- The `.vcxproj` path can use either forward or backslashes + +**From cmd.exe / PowerShell** (normal Windows paths): +```cmd +"C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe" ^ + ZoomSDKSample.vcxproj /p:Configuration=Release /p:Platform=x64 +``` + +--- + +## See Also + +- [Windows Message Loop](windows-message-loop.md) - Runtime callback issues +- [Virtual Method Implementation](../references/interface-methods.md) - Complete method listings +- [Authentication Pattern](../examples/authentication-pattern.md) - Getting started diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/meeting-sdk/windows/troubleshooting/common-issues.md new file mode 100644 index 00000000..bca58b2f --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/troubleshooting/common-issues.md @@ -0,0 +1,575 @@ +# Common Issues & Troubleshooting Checklist + +## Quick Diagnostic Workflow + +### Build Issues vs Runtime Issues + +**Build issues** = Problems during compilation (Visual Studio errors) +**Runtime issues** = Problems when running the executable (authentication, joining meetings) + +Use this flowchart: + +``` +Does your code compile? +├─ NO → See "Build Issues Checklist" below +└─ YES → Does authentication succeed? + ├─ NO → See "Authentication Issues" below + └─ YES → Does meeting join succeed? + ├─ NO → See "Meeting Join Issues" below + └─ YES → Does video capture work? + ├─ NO → See "Video Capture Issues" below + └─ YES → You're all set! +``` + +--- + +## Build Issues Checklist + +### ✅ Compiler Errors with `uint32_t`, `AudioType`, or `YUVRawDataI420` + +**Symptoms**: +- `Error C2061: syntax error: identifier 'uint32_t'` +- `Error C3646: 'GetAudioJoinType': unknown override specifier` +- `Error: use of undefined type 'YUVRawDataI420'` + +**Checklist**: +- [ ] `#include ` is the FIRST include in every file +- [ ] `#include ` is right after `` in every file +- [ ] `meeting_audio_interface.h` is included BEFORE `meeting_participants_ctrl_interface.h` +- [ ] `zoom_sdk_raw_data_def.h` is included when using raw video data + +**Fix**: See [Build Errors Guide](build-errors.md) for detailed solutions. + +--- + +### ✅ "Cannot Instantiate Abstract Class" Error + +**Symptoms**: +- `Error C2259: 'AuthServiceEventListener': cannot instantiate abstract class` +- `note: due to following members: 'void IAuthServiceEvent::onNotificationServiceStatus(...)': is abstract` + +**Checklist**: +- [ ] Implemented ALL pure virtual methods from `IAuthServiceEvent` (6 methods) +- [ ] Implemented ALL pure virtual methods from `IMeetingServiceEvent` (9 methods) +- [ ] Included WIN32-conditional methods (even though they're in `#if defined(WIN32)`) +- [ ] Used `override` keyword to catch signature mismatches +- [ ] Verified method signatures match SDK headers exactly + +**Fix**: See [Interface Methods Guide](../references/interface-methods.md) for complete method lists. + +--- + +### ✅ Linker Errors + +**Symptoms**: +- `LNK2019: unresolved external symbol` +- `Error: Cannot open file 'sdk.lib'` + +**Checklist**: +- [ ] Added SDK library directory to Project Properties → Linker → General → Additional Library Directories + - `$(SolutionDir)SDK\x64\lib` +- [ ] Added `sdk.lib` to Project Properties → Linker → Input → Additional Dependencies +- [ ] Verified SDK architecture matches project architecture (both x64) +- [ ] SDK DLL (`sdk.dll`) is in the same directory as the executable or in PATH + +**Fix**: +1. Right-click project → Properties +2. Configuration: All Configurations, Platform: x64 +3. Linker → General → Additional Library Directories: Add `$(SolutionDir)SDK\x64\lib` +4. Linker → Input → Additional Dependencies: Add `sdk.lib` +5. Copy `SDK\x64\bin\sdk.dll` to your output directory + +--- + +## Runtime Issues + +### ✅ Authentication Timeout (CRITICAL!) + +**Symptoms**: +- "Still waiting..." messages keep printing +- "ERROR: Authentication timeout after 30 seconds" +- `onAuthenticationReturn()` callback NEVER fires + +**Root Cause**: 99% of the time, this is a **missing Windows message loop**, NOT a JWT token issue! + +**Checklist**: +- [ ] Added `PeekMessage()` loop during authentication wait +- [ ] Added `PeekMessage()` loop in main event loop +- [ ] Using `PM_REMOVE` flag with `PeekMessage()` +- [ ] Calling `TranslateMessage()` and `DispatchMessage()` for each message + +**Fix**: See [Windows Message Loop Guide](windows-message-loop.md) for complete solution. + +**Quick test**: Add this minimal message loop: +```cpp +while (!g_authenticated) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} +``` + +If `onAuthenticationReturn()` suddenly fires, you confirmed the issue was the message loop. + +--- + +### ✅ Authentication Fails with Error Code + +**Symptoms**: +- `onAuthenticationReturn()` fires but with non-success code +- "Authentication failed: [error code]" + +**Authentication Error Codes**: + +| Code | Enum Value | Meaning | Solution | +|------|------------|---------|----------| +| 0 | `AUTHRET_SUCCESS` | ✅ Success | N/A | +| 1 | `AUTHRET_KEYORSECRETEMPTY` | JWT token or app secret is empty | Verify JWT token string is not empty | +| 2 | `AUTHRET_JWTTOKENWRONG` | Invalid JWT token format or signature | Regenerate JWT token, verify app credentials | +| 3 | `AUTHRET_OVERTIME` | JWT token expired | Regenerate JWT with fresh timestamp | +| 4 | `AUTHRET_NETWORKISSUE` | Network connection problem | Check firewall, proxy settings, internet connection | +| 16 | `AUTHRET_CLIENT_INCOMPATIBLE` | SDK version incompatible with Zoom service | Update SDK to latest version | + +**Checklist**: +- [ ] JWT token is correctly formatted (3 parts separated by dots) +- [ ] JWT token was generated within the last hour +- [ ] App credentials (SDK Key/Secret) match JWT token generation +- [ ] System clock is accurate (JWT validation is time-sensitive) +- [ ] Not behind a firewall blocking Zoom domains (*.zoom.us, *.zoomgov.com) + +**How to regenerate JWT token**: +```javascript +// Node.js example +const jwt = require('jsonwebtoken'); +const token = jwt.sign( + { + appKey: 'YOUR_SDK_KEY', + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 7200, // 2 hours + tokenExp: Math.floor(Date.now() / 1000) + 7200 + }, + 'YOUR_SDK_SECRET' +); +``` + +--- + +### ✅ Meeting Join Fails + +**Symptoms**: +- `onMeetingStatusChanged()` fires with `MEETING_STATUS_FAILED` +- "Join meeting failed: [error code]" + +**Meeting Error Codes**: + +| Code | Enum Value | Meaning | Solution | +|------|------------|---------|----------| +| 0 | `MEETING_SUCCESS` | ✅ Success | N/A | +| 1 | `MEETING_FAIL_NETWORK_ERR` | Network error | Check internet connection | +| 2 | `MEETING_FAIL_RECONNECT_ERR` | Reconnection failed | Retry joining | +| 3 | `MEETING_FAIL_MMR_ERR` | Multi-media router error | Contact Zoom support | +| 4 | `MEETING_FAIL_PASSWORD_ERR` | Wrong meeting password | Verify password | +| 5 | `MEETING_FAIL_SESSION_ERR` | Invalid meeting session | Verify meeting number | +| 6 | `MEETING_FAIL_MEETING_OVER` | Meeting has ended | Join a different meeting | +| 7 | `MEETING_FAIL_MEETING_NOT_START` | Meeting hasn't started | Wait for host to start | +| 8 | `MEETING_FAIL_MEETING_NOT_EXIST` | Invalid meeting number | Verify meeting number | +| 9 | `MEETING_FAIL_MEETING_USER_FULL` | Meeting at capacity | Wait for slot or use livestream | +| 10 | `MEETING_FAIL_CLIENT_INCOMPATIBLE` | SDK version incompatible | Update SDK | + +**Checklist**: +- [ ] Meeting number is correct (10-11 digits) +- [ ] Meeting password is correct (if required) +- [ ] Authenticated successfully before joining +- [ ] Meeting is currently active (host has started it) +- [ ] Meeting hasn't reached capacity +- [ ] Using `SDK_UT_WITHOUT_LOGIN` user type for JWT auth + +**Fix for common issues**: +```cpp +// Correct join pattern +JoinParam joinParam; +joinParam.userType = SDK_UT_WITHOUT_LOGIN; // REQUIRED for JWT auth + +JoinParam4WithoutLogin withoutLoginParam; +withoutLoginParam.meetingNumber = 1234567890; // Your meeting number +withoutLoginParam.userName = L"Bot User"; +withoutLoginParam.psw = L"meeting_password"; // Empty if no password +withoutLoginParam.vanityID = nullptr; +withoutLoginParam.customer_key = nullptr; +withoutLoginParam.webinarToken = nullptr; +withoutLoginParam.isVideoOff = false; +withoutLoginParam.isAudioOff = false; + +joinParam.param.withoutloginuserJoin = withoutLoginParam; +meetingService->Join(joinParam); +``` + +--- + +### ✅ Callbacks Not Firing + +**Symptoms**: +- `SetEvent()` called but callbacks never execute +- Authentication/meeting status changes but no output + +**Checklist**: +- [ ] Windows message loop is running (see Authentication Timeout above) +- [ ] Event listener pointer is valid (not deleted prematurely) +- [ ] Event listener is set BEFORE calling SDK methods +- [ ] Using `new` to allocate listener (SDK manages lifecycle) + +**Fix**: +```cpp +// CORRECT: Set listener before SDK actions +AuthServiceEventListener* authListener = new AuthServiceEventListener(&OnAuthComplete); +authService->SetEvent(authListener); +authService->SDKAuth(authContext); // Now callbacks will work + +// WRONG: Set listener after +authService->SDKAuth(authContext); +authService->SetEvent(authListener); // Too late! +``` + +--- + +## Video Capture Issues + +### ✅ Raw Video Data Not Received + +**Symptoms**: +- `onRawDataFrameReceived()` never fires +- `onRawDataStatusChanged()` fires but no frames + +**Checklist**: +- [ ] Called `StartRawRecording()` after joining meeting +- [ ] Subscribed to video streams using `Subscribe(userId, Raw_Video_On)` +- [ ] Implemented `IZoomSDKRendererDelegate` interface correctly +- [ ] Created raw data helper: `CreateRawdataRenderer()` +- [ ] Set renderer delegate: `setRawDataResolution(...)` and `subscribe(...)` + +**Fix**: See [Raw Video Capture Guide](../examples/raw-video-capture.md) for complete workflow. + +**Quick test**: +```cpp +// After successfully joining meeting +IMeetingRecordingController* recordingCtrl = meetingService->GetMeetingRecordingController(); +recordingCtrl->StartRawRecording(); + +IZoomSDKVideoSource* videoSource = rawDataHelper->GetRawdataVideoSourceHelper(); +videoSource->subscribe(userId, Raw_Video_On); +``` + +--- + +### ✅ Video Data Format Issues + +**Symptoms**: +- Receiving frames but video looks corrupted +- Wrong frame size or color + +**Checklist**: +- [ ] Using YUV420 (I420) format, not RGB +- [ ] Buffer size is `width * height * 1.5` bytes (not `width * height * 3`) +- [ ] Y plane: `width * height` bytes +- [ ] U plane: `(width/2) * (height/2)` bytes +- [ ] V plane: `(width/2) * (height/2)` bytes +- [ ] Rotation is handled correctly (0°, 90°, 180°, 270°) + +**YUV420 Layout**: +``` +Width: 1920, Height: 1080 +Total bytes: 1920 * 1080 * 1.5 = 3,110,400 bytes + +Y plane: [0 to 2,073,599] (1920 * 1080 bytes) +U plane: [2,073,600 to 2,592,639] (960 * 540 bytes) +V plane: [2,592,640 to 3,110,399] (960 * 540 bytes) +``` + +--- + +## Network & Firewall Issues + +### ✅ SDK Network Requirements + +**Symptoms**: +- `AUTHRET_NETWORKISSUE` error code +- `MEETING_FAIL_NETWORK_ERR` error code +- Timeouts during initialization + +**Required Firewall Rules**: +- [ ] Allow outbound HTTPS (port 443) to `*.zoom.us` +- [ ] Allow outbound HTTPS (port 443) to `*.zoomgov.com` (for government) +- [ ] Allow UDP ports 8801-8810 for media +- [ ] Not behind a proxy requiring authentication (or proxy configured) + +**How to test connectivity**: +```bash +# Test DNS resolution +ping zoom.us +ping us01web.zoom.us + +# Test HTTPS connectivity +curl https://zoom.us +curl https://us01web.zoom.us +``` + +**Proxy configuration** (if required): +```cpp +InitParam initParam; +initParam.strWebDomain = L"https://zoom.us"; +// Add proxy settings if needed +// initParam.proxy = ...; +``` + +--- + +## General Debugging Tips + +### Enable SDK Logging + +```cpp +InitParam initParam; +initParam.enableLogByDefault = true; // Enable logs +initParam.enableGenerateDump = true; // Enable crash dumps +``` + +Logs location: `%APPDATA%\Zoom\logs\` or `C:\Users\[username]\AppData\Roaming\Zoom\logs\` + +### Add Debug Output to Callbacks + +```cpp +void AuthServiceEventListener::onAuthenticationReturn(AuthResult ret) { + std::cout << "[DEBUG] onAuthenticationReturn called! Code: " << ret << std::endl; + // Your logic here +} +``` + +### Use Windows Debugger + +Set breakpoints in callback methods to verify they're being called: +- `onAuthenticationReturn()` +- `onMeetingStatusChanged()` + +If breakpoints never hit → Message loop issue +If breakpoints hit but code is wrong → Logic issue + +### Verify SDK Version + +Check `SDK/x64/version.txt` or `sdk.dll` properties → Details tab + +Different versions have different: +- Required callback methods +- Error codes +- API behavior + +This guide is for **SDK v6.7.2.26830**. + +--- + +## "If You See X, Do Y" Quick Reference + +| You See | Do This | +|---------|---------| +| `uint32_t` error | Add `#include ` after `` | +| `AudioType` error | Include `meeting_audio_interface.h` before `meeting_participants_ctrl_interface.h` | +| `YUVRawDataI420` error | Include `zoom_sdk_raw_data_def.h` | +| Abstract class error | Implement ALL virtual methods (see [Interface Methods Guide](../references/interface-methods.md)) | +| Authentication timeout | Add Windows message loop (see [Message Loop Guide](windows-message-loop.md)) | +| `AUTHRET_JWTTOKENWRONG` | Regenerate JWT token with correct app credentials | +| `AUTHRET_OVERTIME` | JWT token expired, generate a fresh one | +| `MEETING_FAIL_PASSWORD_ERR` | Wrong meeting password or no password provided | +| `MEETING_FAIL_MEETING_NOT_EXIST` | Invalid meeting number, verify 10-11 digits | +| Callbacks not firing | Add Windows message loop, verify `SetEvent()` called first | +| No video frames | Call `StartRawRecording()` and `Subscribe()` after joining | + +--- + +## Complete SDK Error Code Reference + +This section provides comprehensive error code tables from official Zoom documentation. + +### Global SDK Error Codes (SDKERR_*) + +| Code | Name | Description | +|------|------|-------------| +| 0 | `SDKERR_SUCCESS` | Success | +| 1 | `SDKERR_NO_IMPL` | This feature is currently not available | +| 2 | `SDKERR_WRONG_USEAGE` | Incorrect usage of the feature | +| 3 | `SDKERR_INVALID_PARAMETER` | Wrong parameter | +| 4 | `SDKERR_MODULE_LOAD_FAILED` | Loading module failed | +| 5 | `SDKERR_MEMORY_FAILED` | No memory allocated | +| 6 | `SDKERR_SERVICE_FAILED` | Internal service error | +| 7 | `SDKERR_UNINITIALIZE` | SDK is not initialized before use | +| 8 | `SDKERR_UNAUTHENTICATION` | SDK is not authorized before use | +| 9 | `SDKERR_NORECORDINGINPROCESS` | No recording is in progress | +| 10 | `SDKERR_TRANSCODER_NOFOUND` | Transcoder module is not found | +| 11 | `SDKERR_VIDEO_NOTREADY` | The video service is not ready | +| 12 | `SDKERR_NO_PERMISSION` | No permission | +| 13 | `SDKERR_UNKNOWN` | Unknown error | +| 14 | `SDKERR_OTHER_SDK_INSTANCE_RUNNING` | Another SDK instance is in progress | +| 15 | `SDKERR_INTERNAL_ERROR` | SDK internal error | +| 16 | `SDKERR_NO_AUDIODEVICE_ISFOUND` | No audio device is found | +| 17 | `SDKERR_NO_VIDEODEVICE_ISFOUND` | No video device is found | +| 18 | `SDKERR_TOO_FREQUENT_CALL` | API calls too frequent | +| 19 | `SDKERR_FAIL_ASSIGN_USER_PRIVILEGE` | User cannot be assigned with the new privilege | +| 20 | `SDKERR_MEETING_DONT_SUPPORT_FEATURE` | The current meeting does not support the request feature | +| 21 | `SDKERR_MEETING_NOT_SHARE_SENDER` | The current user is not the presenter | +| 22 | `SDKERR_MEETING_YOU_HAVE_NO_SHARE` | There is no sharing | +| 23 | `SDKERR_MEETING_VIEWTYPE_PARAMETER_IS_WRONG` | Incorrect `ViewType` parameters | +| 24 | `SDKERR_MEETING_ANNOTATION_IS_OFF` | Annotation is disabled | +| 25 | `SDKERR_SETTING_OS_DONT_SUPPORT` | Current OS doesn't support the setting | +| 26 | `SDKERR_EMAIL_LOGIN_IS_DISABLED` | Email login is disabled | +| 27 | `SDKERR_HARDWARE_NOT_MEET_FOR_VB` | Computer doesn't meet minimum requirements for virtual background | +| 28 | `SDKERR_NEED_USER_CONFIRM_RECORD_DISCLAIMER` | Need to process recording disclaimer | +| 29 | `SDKERR_NO_SHARE_DATA` | There is no raw data from sharing | +| 30 | `SDKERR_SHARE_CANNOT_SUBSCRIBE_MYSELF` | Cannot subscribe to my own stream | +| 31 | `SDKERR_NOT_IN_MEETING` | Not in the meeting | + +### Authentication Error Codes (AUTHRET_*) + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| 0 | `AUTHRET_SUCCESS` | Authentication success | N/A | +| 1 | `AUTHRET_KEYORSECRETEMPTY` | SDK key or secret is empty | Verify JWT token string is not empty | +| 2 | `AUTHRET_KEYORSECRETWRONG` | SDK key or secret is incorrect | Check app credentials match | +| 3 | `AUTHRET_ACCOUNTNOTSUPPORT` | Account does not support SDK | Verify account has SDK access | +| 4 | `AUTHRET_ACCOUNTNOTENABLESDK` | Account does not have SDK enabled | Enable SDK for account | +| 5 | `AUTHRET_UNKNOWN` | Unknown error | Check logs | +| 6 | `AUTHRET_SERVICE_BUSY` | Service is busy | Retry later | +| 7 | `AUTHRET_NONE` | Initial status | N/A | +| 8 | `AUTHRET_OVERTIME` | Timeout | Check network, retry | +| 9 | `AUTHRET_NETWORKISSUE` | Network issues | Check firewall/proxy | +| 10 | `AUTHRET_CLIENT_INCOMPATIBLE` | Account does not support this SDK version | Update SDK | +| 11 | `AUTHRET_JWTTOKENWRONG` | JWT token is wrong | Regenerate JWT with correct credentials | + +### Login Error Codes (LoginFail_*) + +| Code | Name | Description | +|------|------|-------------| +| 0 | `LoginFail_None` | Initial status | +| 1 | `LoginFail_EmailLoginDisable` | Email login is disabled | +| 2 | `LoginFail_UserNotExist` | User does not exist | +| 3 | `LoginFail_WrongPassword` | Incorrect password | +| 4 | `LoginFail_AccountLocked` | Account is locked | +| 5 | `LoginFail_SDKNeedUpdate` | SDK version is unsupported | +| 6 | `LoginFail_TooManyFailedAttempts` | Too many failed attempts | +| 7 | `LoginFail_SMSCodeError` | SMS verification code error | +| 8 | `LoginFail_SMSCodeExpired` | SMS verification code expired | +| 9 | `LoginFail_PhoneNumberFormatInValid` | Phone number format invalid | +| 10 | `LoginFail_LoginTokenInvalid` | Login token is invalid | +| 11 | `LoginFail_UserDisagreeLoginDisclaimer` | User disagreed login disclaimer | +| 12 | `LoginFail_Mfa_Required` | MFA is required | +| 13 | `LoginFail_Need_Bitrthday_ask` | Need to provide DOB information | +| 100 | `LoginFail_OtherIssue` | Other issue | + +### Breakout Room Error Codes (BOControllerError_*) + +| Code | Name | Description | +|------|------|-------------| +| 0 | `BOControllerError_NULL_POINTER` | BO controller is null, SDK not initialized | +| 1 | `BOControllerError_WRONG_CURRENT_STATUS` | Incorrect current status | +| 2 | `BOControllerError_TOKEN_NOT_READY` | Token is not ready | +| 3 | `BOControllerError_NO_PRIVILEGE` | No privilege | +| 4 | `BOControllerError_BO_LIST_IS_UPLOADING` | BO list is uploading | +| 5 | `BOControllerError_UPLOAD_FAIL` | BO list upload failed | +| 6 | `BOControllerError_NO_ONE_HAS_BEEN_ASSIGNED` | No user assigned to BO | +| 100 | `BOControllerError_UNKNOWN` | Unknown error | + +### Phone Error Codes (PhoneFailedReason_*) + +| Code | Name | Description | +|------|------|-------------| +| 0 | `PhoneFailedReason_None` | Initial status | +| 1 | `PhoneFailedReason_Busy` | Telephone service is busy | +| 2 | `PhoneFailedReason_Not_Available` | Service not available | +| 3 | `PhoneFailedReason_User_Hangup` | User hung up | +| 4 | `PhoneFailedReason_Other_Fail` | Other failure | +| 5 | `PhoneFailedReason_No_Answer` | No answer | +| 6 | `PhoneFailedReason_Block_No_Host` | Call-out blocked before host joins | +| 7 | `PhoneFailedReason_Block_High_Rate` | Blocked due to high cost | +| 8 | `PhoneFailedReason_Block_Too_Frequent` | Blocked due to high frequency | + +### OBF/Anonymous Join Error Codes (2026 Enforcement) + +**Important Dates**: +- **February 7, 2026**: OBF tokens must be valid (well-formed, not expired) +- **March 2, 2026**: Anonymous joins no longer allowed - must provide valid OBF/ZAK token + +| Code | Name | Description | +|------|------|-------------| +| 503 | `MEETING_FAIL_USER_LEVEL_TOKEN_NOT_HAVE_HOST_ZAK_OBF` | To access raw data with privilege token, must also provide OBF token authorized by host | +| 504 | `MEETING_FAIL_APP_CAN_NOT_ANONYMOUS_JOIN_MEETING` | Anonymous joins not allowed. Provide ZAK or OBF token | +| 6603 | `RESULT_UNKNOWN_ERROR` | Account is blocking your Meeting SDK application | + +### General Network/Server Error Codes + +| Code | Description | Solution | +|------|-------------|----------| +| 5 | Failed to create data connection | Check network connection | +| 15 | Failed to send create meeting command | Check HTTP request configuration | +| 1002 | Wrong user password | Check password | +| 1019 | Web login locked after 6 failed attempts | Contact support to reactivate | +| 3023 | SDK authentication failure: invalid SDK key/secret | Check credentials | +| 3024 | Account does not support using SDK | Verify license type | +| 5003 | No response from server in 30 seconds | Retry later | +| 4502 | Invalid recurring meeting - no meeting occurrence | Verify meeting recurrence | +| 5004 | DNS resolve failure | Check network adaptor | +| 102006 | Conference does not exist | Verify meeting number | +| 102011 | Client version lower than minimum required | Download latest SDK | +| 102012 | Client version higher than maximum allowed | Download latest SDK | +| 102014 | Conference token expired | Get new token | +| 103008 | Server is too busy | Retry later | +| 103024 | Account does not support requested feature | Verify account features | +| 103025 | Account does not support call out | Verify call out feature | +| 103037 | Too many pending requests | Reduce request frequency | +| 103039 | Account is in blacklist | Contact support | +| 102004/103001 | Conference already exists | Use different meeting number | +| 102010/103006 | Attendee limit reached | Contact sales for more attendees | +| 102015/103011 | Conference is locked | Contact host to unlock | +| 102016/103014 | Account restricted | Contact support | + +--- + +## File Signing Error (105035) + +**Symptom**: Error Code `105035` when running the SDK + +**Root Cause**: Re-signing or adding new signatures to protected SDK files + +**Protected Files (DO NOT re-sign)**: +- `CptControl.exe` +- `CptHost.exe` +- `CptInstall.exe` +- `CptService.exe` +- `CptShare.dll` +- `zzhost.dll` +- `zzplugin.dll` +- `aomhost64.exe` + +**Solution**: Skip signing these files during your build/deployment process. If error persists without re-signing, visit the [Zoom Developer Forum](https://devforum.zoom.us/). + +--- + +## Still Having Issues? + +1. **Check SDK logs**: `%APPDATA%\Zoom\logs\` +2. **Enable debug output**: Add `std::cout` to all callbacks +3. **Verify SDK version**: Different versions have different requirements +4. **Review working example**: See complete working code in [Authentication Pattern](../examples/authentication-pattern.md) +5. **Check Zoom Developer Forums**: https://devforum.zoom.us/ + +--- + +## Related Documentation + +- [Windows Message Loop](windows-message-loop.md) - Why callbacks don't fire +- [Build Errors Guide](build-errors.md) - Header dependency issues +- [Interface Methods Guide](../references/interface-methods.md) - Required virtual methods +- [Authentication Pattern](../examples/authentication-pattern.md) - Complete working auth code + +--- + +**Last Updated**: Based on Zoom Windows Meeting SDK v6.7.2.26830 diff --git a/plugins/zoom-developers/skills/meeting-sdk/windows/troubleshooting/windows-message-loop.md b/plugins/zoom-developers/skills/meeting-sdk/windows/troubleshooting/windows-message-loop.md new file mode 100644 index 00000000..dcfc67c5 --- /dev/null +++ b/plugins/zoom-developers/skills/meeting-sdk/windows/troubleshooting/windows-message-loop.md @@ -0,0 +1,401 @@ +# Windows Message Loop Requirement + +## Critical Issue: SDK Callbacks Not Firing + +### The Problem + +**Symptom**: Authentication times out, callbacks never execute +``` +[AUTH] Calling SDKAuth... +[AUTH] Waiting for callback... +[Still waiting after 30 seconds...] +ERROR: Authentication timeout +``` + +**Root Cause**: The Zoom Windows SDK uses the **Windows message pump** to dispatch callbacks. Without processing Windows messages, callbacks are queued but never delivered. + +--- + +## Why This Happens + +The SDK uses COM/Windows messaging for asynchronous operations: + +1. **SDK Thread**: Receives response from Zoom servers +2. **Posts Windows Message**: To application's message queue +3. **Application Must Process**: Via `GetMessage()` or `PeekMessage()` +4. **Message Dispatched**: Callback function finally invoked + +**Without message processing**: Messages queue up → Never dispatched → Callbacks never fire → Timeout + +--- + +## The Solution + +### Pattern 1: Non-Blocking with PeekMessage() (Recommended) + +Use when you need to check conditions or implement timeouts: + +```cpp +bool WaitForAuthentication() { + auto startTime = std::chrono::steady_clock::now(); + + while (!g_authenticated && !g_exit) { + // CRITICAL: Process Windows messages for SDK callbacks! + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Check timeout + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - startTime).count(); + if (elapsed >= 30) { + return false; // Timeout + } + + // Small sleep to avoid CPU spinning + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + return g_authenticated; +} +``` + +### Pattern 2: Blocking with GetMessage() + +Use for main application loop: + +```cpp +int main() { + // ... initialize SDK, authenticate, join meeting ... + + // Main message loop + MSG msg; + while (GetMessage(&msg, nullptr, 0, 0)) { + if (msg.message == WM_QUIT) { + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Cleanup + CleanUPSDK(); + return 0; +} +``` + +### Pattern 3: Hybrid Approach (Our Solution) + +Combines non-blocking message processing with custom exit conditions: + +```cpp +// During authentication wait +while (!g_authenticated && !g_exit) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} + +// Main application loop +while (!g_exit) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + g_exit = true; + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Do other work here + ProcessVideoFrames(); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} +``` + +--- + +## Common Mistakes + +### ❌ Wrong: Just Sleeping + +```cpp +// This will NEVER work - callbacks never dispatched! +while (!g_authenticated) { + std::this_thread::sleep_for(std::chrono::seconds(1)); +} +``` + +### ❌ Wrong: Using std::condition_variable Without Messages + +```cpp +// Callbacks won't fire - no message processing! +std::unique_lock lock(mutex); +cv.wait(lock, []{ return g_authenticated; }); +``` + +### ✅ Correct: Message Loop with Condition Check + +```cpp +while (!g_authenticated) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} +``` + +--- + +## Where Message Processing is Required + +You MUST process Windows messages in these scenarios: + +### 1. Authentication +```cpp +authService->SDKAuth(authContext); + +// MUST process messages while waiting +while (!authenticated) { + ProcessMessages(); +} +``` + +### 2. Joining Meeting +```cpp +meetingService->Join(joinParam); + +// MUST process messages while waiting +while (meetingStatus != IN_MEETING) { + ProcessMessages(); +} +``` + +### 3. Main Application Loop +```cpp +// MUST continuously process messages +while (!exit) { + ProcessMessages(); +} +``` + +### 4. Waiting for Any SDK Callback +Any time you're waiting for: +- `onAuthenticationReturn()` +- `onMeetingStatusChanged()` +- `onRawDataFrameReceived()` +- Any other SDK callback + +You MUST be processing Windows messages! + +--- + +## Debugging Tips + +### How to Tell if Message Loop is Missing + +**Symptoms**: +- Callbacks never fire +- Timeouts after 10-30 seconds +- No error messages from SDK +- Debug output shows "waiting..." but nothing happens + +**Quick Test**: +Add logging in your callback: +```cpp +void onAuthenticationReturn(AuthResult ret) { + std::cout << "CALLBACK FIRED!" << std::endl; // Does this ever print? +} +``` + +If you never see "CALLBACK FIRED!", you're not processing messages. + +### Debug Helper Function + +```cpp +void ProcessMessagesWithDebug() { + MSG msg; + int messageCount = 0; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + messageCount++; + TranslateMessage(&msg); + DispatchMessage(&msg); + } + if (messageCount > 0) { + std::cout << "Processed " << messageCount << " messages" << std::endl; + } +} +``` + +--- + +## PeekMessage vs GetMessage + +| Feature | PeekMessage | GetMessage | +|---------|-------------|------------| +| **Blocking** | No | Yes | +| **Returns if no messages** | Immediately | Waits | +| **Good for** | Timeouts, conditions | Main message loop | +| **CPU Usage** | Can spin (add sleep) | Efficient | +| **Flexibility** | High | Low | + +### When to Use Each + +**PeekMessage**: +- When waiting for SDK callbacks with timeout +- When you need to check other conditions +- When combining with other work + +**GetMessage**: +- Main application message loop +- When you want efficient CPU usage +- Standard Windows application pattern + +--- + +## Complete Example + +```cpp +#include +#include +#include +#include +#include +#include + +using namespace ZOOM_SDK_NAMESPACE; + +bool g_authenticated = false; +bool g_exit = false; + +class MyAuthListener : public IAuthServiceEvent { +public: + void onAuthenticationReturn(AuthResult ret) override { + std::cout << "Auth callback received!" << std::endl; + if (ret == AUTHRET_SUCCESS) { + g_authenticated = true; + } + } + // ... other required methods ... +}; + +bool AuthenticateWithMessageLoop(IAuthService* authService, const wchar_t* jwt) { + authService->SetEvent(new MyAuthListener()); + + AuthContext context; + context.jwt_token = jwt; + + if (authService->SDKAuth(context) != SDKERR_SUCCESS) { + return false; + } + + // Wait for callback with message processing + auto startTime = std::chrono::steady_clock::now(); + while (!g_authenticated && !g_exit) { + // Process Windows messages - CRITICAL! + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Check timeout (30 seconds) + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - startTime).count(); + if (elapsed >= 30) { + std::cerr << "Authentication timeout" << std::endl; + return false; + } + + // Small sleep to avoid CPU spinning + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + return g_authenticated; +} + +int main() { + // Initialize SDK + InitParam initParam; + initParam.strWebDomain = L"https://zoom.us"; + InitSDK(initParam); + + // Create auth service + IAuthService* authService = nullptr; + CreateAuthService(&authService); + + // Authenticate with message loop + if (!AuthenticateWithMessageLoop(authService, L"your-jwt-token")) { + std::cerr << "Authentication failed" << std::endl; + return 1; + } + + std::cout << "Authenticated successfully!" << std::endl; + + // Main application loop with message processing + while (!g_exit) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + g_exit = true; + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Do other work + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // Cleanup + CleanUPSDK(); + return 0; +} +``` + +--- + +## Why SDK Documentation Doesn't Mention This + +1. **Assumed knowledge**: Windows developers are expected to know about message pumps +2. **Sample code uses it**: But in different patterns that might not be obvious +3. **Not explicitly required**: Works fine if you already have a message loop +4. **Platform-specific**: Only affects Windows SDK + +--- + +## Key Takeaways + +1. **Always process Windows messages** when waiting for SDK callbacks +2. **Use PeekMessage()** for non-blocking with timeout +3. **Use GetMessage()** for main application loop +4. **Add sleep** to avoid CPU spinning with PeekMessage() +5. **Test callbacks** to verify message loop is working +6. **This is not optional** - SDK will not work without it + +--- + +## Related Issues + +- **Authentication timeout**: Usually caused by missing message loop +- **Meeting join timeout**: Same issue +- **No video frames**: Check message loop in main application loop +- **Callbacks delayed**: Ensure message processing is frequent enough + +--- + +## See Also + +- [Authentication Flow Pattern](../examples/authentication-pattern.md) +- [Common Build Errors](build-errors.md) +- [SDK Initialization](../SKILL.md#initialization) diff --git a/plugins/zoom-developers/skills/oauth/RUNBOOK.md b/plugins/zoom-developers/skills/oauth/RUNBOOK.md new file mode 100644 index 00000000..3a14487d --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/RUNBOOK.md @@ -0,0 +1,95 @@ +# OAuth 5-Minute Preflight Runbook + +Use this before deep debugging. It catches common OAuth failures fast. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm You Chose the Right Flow + +- S2S (`account_credentials`) for backend automation on your own account. +- User OAuth (`authorization_code`) for acting on behalf of users. +- Device flow for browserless devices. +- Client credentials for chatbot-only scenarios. + +Wrong flow choice causes scope and token errors later. + +## 2) Confirm Endpoint Split + +- Authorize URL: `https://zoom.us/oauth/authorize` +- Token URL: `https://zoom.us/oauth/token` + +If token requests return 404/HTML, verify you are not calling `/oauth/token`. + +## 3) Confirm Redirect URI Exact Match + +- `redirect_uri` in token exchange must exactly match Marketplace config. +- Match scheme, host, path, and trailing slash. + +### State Parameter Guardrail + +- Always generate and verify `state` for user OAuth flows. +- Expire state quickly and consume once. +- If callback has `code` but state is missing/invalid, reject and restart auth. + +## 4) Confirm Scope and App Type Alignment + +- Verify required scopes are added to app. +- Re-authorize after scope changes. +- Ensure app type supports requested behavior. + +## 5) Confirm Token Lifecycle Handling + +- Access token expires ~1 hour. +- Store latest refresh token after each refresh. +- Handle refresh failure with re-auth fallback. + +### Refresh Rotation Reminder + +- Treat refresh tokens as rotating credentials. +- Persist new refresh token returned by refresh response. +- Using stale refresh tokens causes intermittent auth failures later. + +## 6) Quick Probes + +- Token endpoint returns JSON with `access_token`. +- API call to `/v2/users/me` succeeds with bearer token. +- Redirect callback receives `code` and valid `state`. + +### Copy/Paste Validation Commands + +Use these to verify OAuth plumbing in under a minute. + +```bash +# 1) S2S token request +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(printf '%s:%s' "$ZOOM_CLIENT_ID" "$ZOOM_CLIENT_SECRET" | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=account_credentials&account_id=$ZOOM_ACCOUNT_ID" + +# 2) User auth-code exchange +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(printf '%s:%s' "$ZOOM_CLIENT_ID" "$ZOOM_CLIENT_SECRET" | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=authorization_code&code=$ZOOM_AUTH_CODE&redirect_uri=$ZOOM_REDIRECT_URI" + +# 3) Token health check +curl -X GET "https://api.zoom.us/v2/users/me" \ + -H "Authorization: Bearer $ZOOM_ACCESS_TOKEN" +``` + +## 7) Fast Decision Tree + +- **4709 redirect mismatch** -> fix exact redirect URI. +- **4702/4704 invalid client** -> wrong client credentials or app. +- **4733/4734 code errors** -> auth code expired/invalid, restart consent flow. +- **Scopes missing** -> add scopes + re-authorize. + +## 8) Flow-to-App-Type Guardrail + +- If using `account_credentials`, app must support S2S flow. +- If using `authorization_code`, app and redirect configuration must support user consent. +- If auth appears valid but API fails, verify app type, scope level, and account ownership assumptions. diff --git a/plugins/zoom-developers/skills/oauth/SKILL.md b/plugins/zoom-developers/skills/oauth/SKILL.md new file mode 100644 index 00000000..78154b3f --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/SKILL.md @@ -0,0 +1,27 @@ +--- +name: zoom-oauth +description: Use when implementing OAuth. +--- + +# Zoom OAuth + +Use this skill for concrete Zoom authentication implementation and troubleshooting. Prefer `setup-zoom-oauth` for the first-pass setup plan, then return here for exact flows, scope behavior, refresh handling, and error diagnosis. + +## Workflow + +1. Identify the app type and actor: user-level OAuth, account-level OAuth, server-to-server OAuth where officially supported, SDK JWT, or Build-platform credentials. +2. Confirm the target API, SDK, or app surface, because scopes and token audiences differ by surface. +3. Choose the grant flow: authorization code with PKCE for public clients, authorization code for confidential web apps, device authorization where appropriate, or account credentials for supported account-level automation. +4. Store refresh tokens as single-use values: persist the replacement refresh token returned by each refresh response before reusing the old one. +5. Validate requests against redirect URI, account ID, scopes, app publication state, and token expiration before changing application code. +6. For local development, keep access tokens out of logs and treat refresh tokens as single-use when rotating credentials. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- OAuth flows: [concepts/oauth-flows.md](concepts/oauth-flows.md) +- Token lifecycle: [concepts/token-lifecycle.md](concepts/token-lifecycle.md) +- Scope architecture: [concepts/scopes-architecture.md](concepts/scopes-architecture.md) +- Granular scopes: [references/granular-scopes.md](references/granular-scopes.md) +- OAuth errors: [references/oauth-errors.md](references/oauth-errors.md) +- Redirect URI issues: [troubleshooting/redirect-uri-issues.md](troubleshooting/redirect-uri-issues.md) diff --git a/plugins/zoom-developers/skills/oauth/concepts/oauth-flows.md b/plugins/zoom-developers/skills/oauth/concepts/oauth-flows.md new file mode 100644 index 00000000..9c03d644 --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/concepts/oauth-flows.md @@ -0,0 +1,530 @@ +# Zoom OAuth Flows + +Zoom supports 4 OAuth 2.0 flows. This guide helps you choose the right one and understand how each works. + +Endpoint split to remember: +- Authorization URL: `https://zoom.us/oauth/authorize` +- Token URL: `https://zoom.us/oauth/token` + +## Quick Decision Matrix + +| Your Scenario | Flow | Grant Type | +|---------------|------|------------| +| Backend automation on **your own account** | S2S OAuth | `account_credentials` | +| SaaS app for **other Zoom users** | User OAuth | `authorization_code` | +| Device without browser (TV, kiosk, IoT) | Device Flow | `urn:ietf:params:oauth:grant-type:device_code` | +| **Team Chat bot only** | Chatbot | `client_credentials` | + +## Two-Legged vs Three-Legged + +| Type | User Involved? | Zoom Flows | +|------|----------------|------------| +| **Two-legged** | No (app acts on its own) | S2S OAuth, Chatbot | +| **Three-legged** | Yes (user authorizes app) | User OAuth, Device Flow | + +--- + +## 1. Server-to-Server (S2S) OAuth + +**When to use:** +- Backend automation on your own Zoom account +- No end-user interaction needed +- Account-wide API access + +**Grant type:** `account_credentials` + +**Token lifetime:** +- Access token: 1 hour +- Refresh token: None (request new token when expired) + +**Credentials required:** +- Account ID +- Client ID +- Client Secret + +### Flow Diagram + +``` +┌──────────────┐ ┌──────────────┐ +│ Your App │ │ Zoom OAuth │ +│ (Backend) │ │ Server │ +└──────┬───────┘ └──────┬───────┘ + │ │ + │ POST /oauth/token │ + │ grant_type=account_credentials │ + │ account_id={ACCOUNT_ID} │ + │ Authorization: Basic {CLIENT_ID:CLIENT_SECRET} │ + │──────────────────────────────────────────────────>│ + │ │ + │ │ Validate + │ │ credentials + │ │ + │ { access_token, expires_in, scope } │ + │<──────────────────────────────────────────────────│ + │ │ + │ API Requests with Bearer token │ + │ (valid for 1 hour) │ + │ │ +``` + +### Implementation + +```javascript +const axios = require('axios'); +const qs = require('query-string'); + +const getToken = async () => { + const response = await axios.post( + 'https://zoom.us/oauth/token', + qs.stringify({ + grant_type: 'account_credentials', + account_id: process.env.ZOOM_ACCOUNT_ID + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + return response.data; // { access_token, expires_in, scope, token_type } +}; +``` + +### Key Points + +✅ **Simple:** No redirect URIs, no user interaction +✅ **Secure:** Credentials stored server-side only +✅ **Account-wide:** Single token for all account operations +⚠️ **No refresh token:** Just request a new token when expired (cache with TTL) + +--- + +## 2. User Authorization OAuth + +**When to use:** +- Building a SaaS app for other Zoom users +- Users authorize your app to act on their behalf +- Need per-user access control + +**Grant type:** `authorization_code` + +**Token lifetime:** +- Access token: 1 hour +- Refresh token: lifetime varies; ~90 days is common for some user-based flows (treat as changeable behavior) + +**Credentials required:** +- Client ID +- Client Secret +- Redirect URI (must match marketplace app config) + +### Flow Diagram + +``` +┌────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ User │ │ Your App │ │ Zoom OAuth │ │ Zoom API │ +│Browser │ │ (Server) │ │ Server │ │ Server │ +└────┬───┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ + │ │ │ │ + │ 1. Click "Add App" │ │ │ + │─────────────────────>│ │ │ + │ │ │ │ + │ 2. Redirect to authorize │ │ + │https://zoom.us/oauth/authorize? │ │ + │ client_id={ID} │ │ + │ redirect_uri={URI} │ │ + │ response_type=code │ │ + │ state={RANDOM} │ │ + │<─────────────────────│ │ │ + │ │ │ │ + │ 3. User sees "Allow" page │ │ + │─────────────────────────────────────────────────>│ │ + │ │ │ │ + │ 4. User clicks "Allow" │ │ + │─────────────────────────────────────────────────>│ │ + │ │ │ │ + │ 5. Redirect to callback │ │ + │ {REDIRECT_URI}?code={CODE}&state={STATE} │ │ + │<─────────────────────────────────────────────────│ │ + │ │ │ │ + │ 6. Send code to app │ │ │ + │─────────────────────>│ │ │ + │ │ │ │ + │ │ 7. Exchange code for token │ + │ │ POST /oauth/token │ + │ │ grant_type=authorization_code │ + │ │ code={CODE} │ + │ │ redirect_uri={URI} │ + │ │ Authorization: Basic {CLIENT_ID:CLIENT_SECRET} │ + │ │─────────────────────────────────────────────────────>│ + │ │ │ │ + │ │ 8. Return tokens │ │ + │ │ { access_token, refresh_token, expires_in } │ + │ │<─────────────────────────────────────────────────────│ + │ │ │ │ + │ │ 9. Store tokens (encrypted) │ + │ │ per user │ │ + │ │ │ │ + │ │ 10. API requests │ + │ │ Authorization: Bearer {ACCESS_TOKEN} │ + │ │─────────────────────────────────────────────────────────────────>│ +``` + +### Implementation + +#### Step 1: Redirect to Authorization + +```javascript +const express = require('express'); +const crypto = require('crypto'); + +app.get('/auth', (req, res) => { + const state = crypto.randomBytes(16).toString('hex'); + req.session.oauthState = state; // Store for verification + + const authURL = new URL('https://zoom.us/oauth/authorize'); + authURL.searchParams.set('response_type', 'code'); + authURL.searchParams.set('client_id', process.env.ZOOM_CLIENT_ID); + authURL.searchParams.set('redirect_uri', process.env.ZOOM_REDIRECT_URL); + authURL.searchParams.set('state', state); + + res.redirect(authURL.toString()); +}); +``` + +#### Step 2: Handle Callback and Exchange Code + +```javascript +app.get('/callback', async (req, res) => { + const { code, state } = req.query; + + // Verify state to prevent CSRF + if (state !== req.session.oauthState) { + return res.status(403).send('Invalid state parameter'); + } + + try { + const response = await axios.post( + 'https://zoom.us/oauth/token', + qs.stringify({ + grant_type: 'authorization_code', + code: code, + redirect_uri: process.env.ZOOM_REDIRECT_URL + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + const { access_token, refresh_token } = response.data; + + // Store tokens securely (encrypted) per user + await saveUserTokens(req.session.userId, { + access_token, + refresh_token + }); + + res.send('Authorization successful!'); + } catch (error) { + res.status(500).send('Token exchange failed'); + } +}); +``` + +### Key Points + +✅ **User-controlled:** Users authorize access to their own account +✅ **Per-user tokens:** Each user gets their own access/refresh tokens +✅ **Refresh support:** Tokens can be refreshed while the refresh token remains valid (lifetime varies; ~90 days is common) +⚠️ **Redirect URI must match exactly:** Including trailing slash, protocol, port +⚠️ **State parameter required:** Prevent CSRF attacks +⚠️ **Authorization code expires in 5 minutes:** Exchange immediately + +--- + +## 3. Device Authorization Flow + +**When to use:** +- Devices without a browser (smart TVs, kiosks, IoT devices) +- Devices with limited input capabilities +- User authorizes on a separate device (phone/computer) + +**Grant type:** `urn:ietf:params:oauth:grant-type:device_code` + +**Token lifetime:** +- Access token: 1 hour +- Refresh token: lifetime varies; ~90 days is common for some user-based flows (treat as changeable behavior) + +**Credentials required:** +- Client ID +- Client Secret + +### Flow Diagram + +``` +┌────────────┐ ┌──────────────┐ ┌────────────┐ +│ Device │ │ Zoom OAuth │ │User's Phone│ +│ (TV/Kiosk) │ │ Server │ │ / Computer │ +└──────┬─────┘ └──────┬───────┘ └─────┬──────┘ + │ │ │ + │ 1. POST /oauth/devicecode │ + │ client_id={CLIENT_ID} │ + │───────────────────────>│ │ + │ │ │ + │ 2. Return device_code, user_code, verification_uri, interval + │ { device_code, user_code, verification_uri, interval } + │<───────────────────────│ │ + │ │ │ + │ 3. Display to user: │ │ + │ "Go to zoom.us/activate" │ + │ "Enter code: ABC-DEF" │ │ + │ │ │ + │ │ 4. User visits URL │ + │ │ and enters user_code │ + │ │<────────────────────────│ + │ │ │ + │ │ 5. User clicks "Allow" │ + │ │<────────────────────────│ + │ │ │ + │ 6. Poll for token (every {interval} seconds) │ + │ POST /oauth/token │ + │ grant_type=urn:ietf:params:oauth:grant-type:device_code + │ device_code={DEVICE_CODE} │ + │───────────────────────>│ │ + │ │ │ + │ 7. Response (repeat until success or timeout) │ + │ - authorization_pending (keep polling) │ + │ - slow_down (increase interval) │ + │ - expired_token (restart flow) │ + │ - { access_token, refresh_token } (success!) │ + │<───────────────────────│ │ +``` + +### Implementation + +#### Step 1: Request Device Code + +```javascript +const requestDeviceCode = async () => { + const response = await axios.post( + 'https://zoom.us/oauth/devicecode', + qs.stringify({ + client_id: process.env.ZOOM_CLIENT_ID + }), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + return response.data; + /* + { + device_code: "GmRhmhcxhwAzkoEqiMEg_DnyEysNmsh6JCl-fNkAghaUg", + user_code: "ABC-DEF", + verification_uri: "https://zoom.us/activate", + expires_in: 900, // 15 minutes + interval: 5 // Poll every 5 seconds + } + */ +}; +``` + +#### Step 2: Display User Code + +```javascript +const { device_code, user_code, verification_uri, interval } = await requestDeviceCode(); + +console.log(`\nGo to: ${verification_uri}`); +console.log(`Enter code: ${user_code}\n`); +``` + +#### Step 3: Poll for Token + +```javascript +const pollForToken = async (device_code, interval) => { + const pollInterval = interval * 1000; // Convert to milliseconds + let currentInterval = pollInterval; + + return new Promise((resolve, reject) => { + const poll = async () => { + try { + const response = await axios.post( + 'https://zoom.us/oauth/token', + qs.stringify({ + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: device_code + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + // Success! Got tokens + resolve(response.data); + + } catch (error) { + const errorCode = error.response?.data?.error; + + if (errorCode === 'authorization_pending') { + // User hasn't authorized yet, keep polling + setTimeout(poll, currentInterval); + + } else if (errorCode === 'slow_down') { + // Zoom wants us to slow down, increase interval by 5s + currentInterval += 5000; + setTimeout(poll, currentInterval); + + } else if (errorCode === 'expired_token') { + // Device code expired (15 minutes), restart flow + reject(new Error('Device code expired. Please restart authorization.')); + + } else { + // Other error + reject(error); + } + } + }; + + // Start polling + poll(); + }); +}; +``` + +### Key Points + +✅ **No browser required:** User authorizes on separate device +✅ **Simple user experience:** Just enter a short code +✅ **Polling-based:** Device polls until user authorizes +⚠️ **Must enable in app settings:** "Use App on Device" feature flag +⚠️ **Device code expires in 15 minutes:** User must complete authorization quickly +⚠️ **Respect polling interval:** Returned by `/devicecode` endpoint (usually 5s) +⚠️ **Handle slow_down:** Increase interval by 5s when requested + +--- + +## 4. Client Authorization (Chatbot) + +**When to use:** +- Building a Team Chat bot ONLY +- App needs `imchat:bot` scope +- Simpler than S2S OAuth + +**Grant type:** `client_credentials` + +**Token lifetime:** +- Access token: 1 hour +- Refresh token: None (request new token when expired) + +**Credentials required:** +- Client ID +- Client Secret + +### Flow Diagram + +``` +┌──────────────┐ ┌──────────────┐ +│ Chatbot App │ │ Zoom OAuth │ +│ (Backend) │ │ Server │ +└──────┬───────┘ └──────┬───────┘ + │ │ + │ POST /oauth/token │ + │ grant_type=client_credentials │ + │ Authorization: Basic {CLIENT_ID:CLIENT_SECRET} │ + │──────────────────────────────────────────────────>│ + │ │ + │ { access_token, expires_in, scope } │ + │<──────────────────────────────────────────────────│ + │ │ + │ Chatbot API Requests with Bearer token │ + │ (valid for 1 hour) │ + │ │ +``` + +### Implementation + +```javascript +const getChatbotToken = async () => { + const response = await axios.post( + 'https://zoom.us/oauth/token', + qs.stringify({ + grant_type: 'client_credentials' + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + return response.data; // { access_token, expires_in, scope, token_type } +}; +``` + +### Key Points + +✅ **Simplest flow:** Just request token with credentials +✅ **Chatbot-specific:** Limited to Team Chat bot operations +⚠️ **No refresh token:** Request new token when expired +⚠️ **Scope limited:** Primarily `imchat:bot` scope + +--- + +## Comparison Table + +| Feature | S2S OAuth | User OAuth | Device Flow | Chatbot | +|---------|-----------|------------|-------------|---------| +| **Grant Type** | `account_credentials` | `authorization_code` | `device_code` | `client_credentials` | +| **User Interaction** | No | Yes (browser) | Yes (separate device) | No | +| **Access Token Lifetime** | 1 hour | 1 hour | 1 hour | 1 hour | +| **Refresh Token** | ❌ None | ✅ ~90 days (commonly) | ✅ ~90 days (commonly) | ❌ None | +| **Redirect URI** | ❌ Not needed | ✅ Required | ❌ Not needed | ❌ Not needed | +| **PKCE Support** | ❌ N/A | ✅ Optional | ❌ N/A | ❌ N/A | +| **State Parameter** | ❌ N/A | ✅ Recommended | ❌ N/A | ❌ N/A | +| **Account Access** | Account-wide | Per-user | Per-user | Account-wide | +| **Token Storage** | Redis (ephemeral) | Database (persistent) | Database (persistent) | Redis (ephemeral) | +| **Use Case** | Backend automation | SaaS apps | TV/kiosk apps | Chat bots | + +--- + +## OAuth 2.0 Standards + +Zoom OAuth follows these RFCs: + +- **RFC 6749**: OAuth 2.0 Authorization Framework + - https://datatracker.ietf.org/doc/html/rfc6749 + +- **RFC 7636**: PKCE (Proof Key for Code Exchange) + - https://datatracker.ietf.org/doc/html/rfc7636 + +- **RFC 8628**: Device Authorization Grant + - https://datatracker.ietf.org/doc/html/rfc8628 + +--- + +## Next Steps + +- **Understand token lifecycle** → [token-lifecycle.md](token-lifecycle.md) +- **Learn about PKCE** → [pkce.md](pkce.md) +- **Implement your flow:** + - S2S → [../examples/s2s-oauth-redis.md](../examples/s2s-oauth-redis.md) + - User → [../examples/user-oauth-mysql.md](../examples/user-oauth-mysql.md) + - Device → [../examples/device-flow.md](../examples/device-flow.md) diff --git a/plugins/zoom-developers/skills/oauth/concepts/pkce.md b/plugins/zoom-developers/skills/oauth/concepts/pkce.md new file mode 100644 index 00000000..8e5e17c8 --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/concepts/pkce.md @@ -0,0 +1,415 @@ +# PKCE (Proof Key for Code Exchange) + +PKCE (pronounced "pixy") is a security extension to OAuth 2.0 for public clients that cannot securely store a client secret. + +## When PKCE is Required + +| Client Type | Can Store Secrets? | PKCE Required? | Examples | +|-------------|-------------------|----------------|----------| +| **Confidential** | ✅ Yes (server-side) | ❌ Optional | Backend servers, traditional web apps | +| **Public** | ❌ No (client-side) | ✅ **Required** | Mobile apps, SPAs, desktop apps | + +###Why Public Clients Can't Keep Secrets + +```javascript +// ❌ INSECURE: Client secret embedded in mobile/SPA code +const CLIENT_SECRET = "abc123"; // Anyone can decompile/inspect and find this! + +// Attacker can: +// 1. Extract CLIENT_SECRET from app +// 2. Intercept authorization code +// 3. Exchange code for tokens using stolen secret +``` + +## How PKCE Works + +PKCE prevents authorization code interception attacks **without requiring a client secret**. + +### Flow Diagram + +``` +┌─────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Mobile App │ │ Zoom OAuth │ │ Attacker │ +│ (Public) │ │ Server │ │ (Intercepting│ +└──────┬──────┘ └──────┬───────┘ └──────┬───────┘ + │ │ │ + │ 1. Generate code_verifier │ │ + │ (random 43-128 chars) │ │ + │ │ │ + │ 2. Create code_challenge │ │ + │ SHA256(code_verifier) │ │ + │ │ │ + │ 3. Authorize with challenge │ │ + │https://zoom.us/oauth/authorize?│ │ + │ code_challenge={HASH} │ │ + │ code_challenge_method=S256 │ │ + │──────────────────────────────>│ │ + │ │ │ + │ 4. User authorizes │ │ + │──────────────────────────────>│ │ + │ │ │ + │ 5. Return authorization code │ │ + │<──────────────────────────────│ │ + │ │ │ + │ │ Attacker intercepts code │ + │ │<───────────────────────────────│ + │ │ │ + │ │ Attacker tries to exchange │ + │ │ (but doesn't have verifier!) │ + │ │<───────────────────────────────│ + │ │ │ + │ │ REJECTED: Missing verifier │ + │ │───────────────────────────────>│ + │ │ │ + │ 6. Exchange code with verifier│ │ + │ POST /oauth/token │ │ + │ code={CODE} │ │ + │ code_verifier={ORIGINAL} │ │ + │──────────────────────────────>│ │ + │ │ │ + │ │ Verify: │ + │ │ SHA256(code_verifier) │ + │ │ == code_challenge? │ + │ │ │ + │ 7. Return tokens │ │ + │<──────────────────────────────│ │ + │ │ │ +``` + +### Key Concept + +- **code_verifier:** Random secret generated by app (kept secret) +- **code_challenge:** SHA256 hash of code_verifier (sent to Zoom) +- **Zoom stores challenge:** During authorization +- **App proves possession:** By providing original verifier during token exchange +- **Attacker fails:** Even with intercepted code, can't generate matching verifier + +--- + +## Implementation + +### Step 1: Generate PKCE Parameters + +```javascript +const crypto = require('crypto'); + +function generatePKCE() { + // Generate random code_verifier (43-128 characters) + const verifier = crypto + .randomBytes(32) + .toString('base64url'); // base64url encoding (no padding) + + // Create code_challenge: SHA256(code_verifier) + const challenge = crypto + .createHash('sha256') + .update(verifier) + .digest('base64url'); + + return { + code_verifier: verifier, + code_challenge: challenge + }; +} + +// Example output: +// { +// code_verifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", +// code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" +// } +``` + +### Step 2: Store code_verifier Securely + +```javascript +// Store in session (server-side) or secure storage (mobile) +req.session.pkce_verifier = code_verifier; + +// For mobile apps, use secure storage: +// - iOS: Keychain +// - Android: EncryptedSharedPreferences +``` + +### Step 3: Redirect to Authorization with code_challenge + +```javascript +app.get('/auth', (req, res) => { + const { code_verifier, code_challenge } = generatePKCE(); + + // Store verifier for later (Step 5) + req.session.pkce_verifier = code_verifier; + + const authURL = new URL('https://zoom.us/oauth/authorize'); + authURL.searchParams.set('response_type', 'code'); + authURL.searchParams.set('client_id', process.env.ZOOM_CLIENT_ID); + authURL.searchParams.set('redirect_uri', process.env.ZOOM_REDIRECT_URL); + authURL.searchParams.set('code_challenge', code_challenge); + authURL.searchParams.set('code_challenge_method', 'S256'); // SHA256 + + res.redirect(authURL.toString()); +}); +``` + +### Step 4: Exchange Code with code_verifier + +```javascript +app.get('/callback', async (req, res) => { + const { code } = req.query; + const code_verifier = req.session.pkce_verifier; // Retrieve stored verifier + + try { + const response = await axios.post( + 'https://zoom.us/oauth/token', + qs.stringify({ + grant_type: 'authorization_code', + code: code, + redirect_uri: process.env.ZOOM_REDIRECT_URL, + code_verifier: code_verifier // Prove possession of original verifier + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + const { access_token, refresh_token } = response.data; + + // Success! Store tokens + await saveTokens({ access_token, refresh_token }); + + // Clean up verifier + delete req.session.pkce_verifier; + + res.send('Authorization successful!'); + } catch (error) { + res.status(500).send('Token exchange failed'); + } +}); +``` + +--- + +## PKCE Methods + +Zoom supports two code_challenge_method values: + +| Method | Description | Security | Support | +|--------|-------------|----------|---------| +| **S256** | SHA256 hash of verifier | ✅ **Recommended** | All OAuth 2.0 servers | +| **plain** | Verifier sent as-is (no hash) | ⚠️ Weaker | Legacy support only | + +**Always use S256:** + +```javascript +// ✅ RECOMMENDED +authURL.searchParams.set('code_challenge_method', 'S256'); +``` + +```javascript +// ❌ AVOID (less secure) +authURL.searchParams.set('code_challenge_method', 'plain'); +``` + +--- + +## Security Benefits + +### Without PKCE (Vulnerable) + +``` +Attacker intercepts authorization code + ↓ +Attacker exchanges code with client_secret + ↓ +Attacker gets access_token and refresh_token + ↓ +Attacker has full account access +``` + +### With PKCE (Protected) + +``` +Attacker intercepts authorization code + ↓ +Attacker tries to exchange code + ↓ +Zoom: "Provide code_verifier" + ↓ +Attacker doesn't have original verifier + ↓ +Token exchange FAILS + ↓ +Legitimate app exchanges with correct verifier + ↓ +Legitimate app gets tokens +``` + +--- + +## Common Mistakes + +### 1. Not Storing code_verifier + +```javascript +// ❌ WRONG: Generating new verifier on callback +app.get('/callback', async (req, res) => { + const { code_challenge } = generatePKCE(); // New verifier! + // This won't match the original challenge +}); +``` + +```javascript +// ✅ CORRECT: Retrieve stored verifier +app.get('/callback', async (req, res) => { + const code_verifier = req.session.pkce_verifier; // Original verifier +}); +``` + +### 2. Using 'plain' Method + +```javascript +// ❌ AVOID +code_challenge_method: 'plain' // Less secure +``` + +```javascript +// ✅ RECOMMENDED +code_challenge_method: 'S256' // SHA256 hashing +``` + +### 3. Exposing code_verifier + +```javascript +// ❌ WRONG: Including verifier in URL +authURL.searchParams.set('code_verifier', verifier); // Don't send verifier during auth! +``` + +```javascript +// ✅ CORRECT: Only send code_challenge +authURL.searchParams.set('code_challenge', challenge); +authURL.searchParams.set('code_challenge_method', 'S256'); +``` + +--- + +## Mobile App Implementation + +### iOS (Swift) + +```swift +import CryptoKit + +func generatePKCE() -> (verifier: String, challenge: String) { + // Generate random verifier + var buffer = [UInt8](repeating: 0, count: 32) + _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer) + let verifier = Data(buffer).base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + + // Create SHA256 challenge + let data = verifier.data(using: .utf8)! + let hash = SHA256.hash(data: data) + let challenge = Data(hash).base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + + return (verifier, challenge) +} + +// Store verifier in Keychain +KeychainWrapper.standard.set(verifier, forKey: "pkce_verifier") +``` + +### Android (Kotlin) + +```kotlin +import java.security.MessageDigest +import java.security.SecureRandom +import android.util.Base64 + +fun generatePKCE(): Pair { + // Generate random verifier + val bytes = ByteArray(32) + SecureRandom().nextBytes(bytes) + val verifier = Base64.encodeToString(bytes, + Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) + + // Create SHA256 challenge + val digest = MessageDigest.getInstance("SHA-256") + val hash = digest.digest(verifier.toByteArray()) + val challenge = Base64.encodeToString(hash, + Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) + + return Pair(verifier, challenge) +} + +// Store verifier in EncryptedSharedPreferences +val encryptedPrefs = EncryptedSharedPreferences.create(...) +encryptedPrefs.edit().putString("pkce_verifier", verifier).apply() +``` + +--- + +## Testing PKCE Implementation + +### 1. Verify code_challenge Format + +```javascript +const { code_verifier, code_challenge } = generatePKCE(); + +console.log('Verifier length:', code_verifier.length); // Should be 43-128 +console.log('Challenge length:', code_challenge.length); // Should be 43 for S256 +console.log('Verifier chars:', /^[A-Za-z0-9_-]+$/.test(code_verifier)); // true +console.log('Challenge chars:', /^[A-Za-z0-9_-]+$/.test(code_challenge)); // true +``` + +### 2. Verify SHA256 Hashing + +```javascript +const crypto = require('crypto'); + +const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; +const expected_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"; + +const computed_challenge = crypto + .createHash('sha256') + .update(verifier) + .digest('base64url'); + +console.log(computed_challenge === expected_challenge); // Should be true +``` + +### 3. Test Token Exchange + +```bash +curl -X POST https://zoom.us/oauth/token \ + -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=authorization_code" \ + -d "code=YOUR_AUTH_CODE" \ + -d "redirect_uri=YOUR_REDIRECT_URI" \ + -d "code_verifier=YOUR_CODE_VERIFIER" + +# Should return { access_token, refresh_token, ... } +``` + +--- + +## PKCE Specification + +PKCE follows **RFC 7636**: +- https://datatracker.ietf.org/doc/html/rfc7636 + +--- + +## Next Steps + +- **Implement PKCE in your app** → [../examples/pkce-implementation.md](../examples/pkce-implementation.md) +- **Add state parameter** → [state-parameter.md](state-parameter.md) +- **Understand OAuth flows** → [oauth-flows.md](oauth-flows.md) diff --git a/plugins/zoom-developers/skills/oauth/concepts/scopes-architecture.md b/plugins/zoom-developers/skills/oauth/concepts/scopes-architecture.md new file mode 100644 index 00000000..e1bc4bdc --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/concepts/scopes-architecture.md @@ -0,0 +1,244 @@ +# Scopes Architecture + +Zoom OAuth uses scopes to limit API access. Understanding Classic vs Granular scopes is critical. + +## Scope Types + +| Type | Format | Example | Status | +|------|--------|---------|--------| +| **Classic** | `resource:level` | `meeting:write:admin` | Active | +| **Granular** | `service:action:data_claim:access` | `meeting:write:meeting:admin` | Active (newer) | + +## Classic Scopes + +### Format + +``` +{resource}:{action}:{level} +``` + +**Examples:** +- `meeting:read` - Read user's own meetings +- `meeting:write:admin` - Create/update meetings for all account users +- `recording:read:master` - Read recordings across all sub-accounts + +### Scope Levels + +| Level | Access | Who Can Authorize | Example | +|-------|--------|-------------------|---------| +| **(none)** | Own data only | Any user | `meeting:read` | +| `:admin` | Account-wide | Admin role required | `meeting:write:admin` | +| `:master` | Multi-account | Account owner only | `user:master` | + +### Common Classic Scopes + +``` +meeting:read # View own meetings +meeting:write # Create/edit own meetings +meeting:write:admin # Manage all account meetings + +user:read # View own profile +user:write:admin # Manage account users + +recording:read # View own recordings +recording:write:admin # Manage account recordings + +webinar:read # View own webinars +webinar:write:admin # Manage account webinars + +imchat:bot # Team Chat bot access +``` + +## Granular Scopes + +### Format + +``` +{service}:{action}:{data_claim}:{access_level} +``` + +**Examples:** +- `meeting:read:meeting:user` - Read user's own meetings +- `meeting:write:invite_links:admin` - Create invite links for account +- `recording:delete:recording_file:admin` - Delete recording files account-wide + +### Components + +1. **service**: API category (meeting, user, recording, etc.) +2. **action**: Operation (read, write, delete, etc.) +3. **data_claim**: Specific data type (meeting, participant, invite_links, etc.) +4. **access_level**: Scope of access (user, admin, account, etc.) + +### Access Levels (Granular) + +| Level | Access | Example | +|-------|--------|---------| +| `user` | Own data | `meeting:read:meeting:user` | +| `admin` | Account-wide | `meeting:write:meeting:admin` | +| `account` | Account settings | `account:read:settings:account` | + +## Classic vs Granular Comparison + +### Meetings Scope Example + +| Classic | Granular Equivalent | +|---------|---------------------| +| `meeting:read` | `meeting:read:meeting:user` + `meeting:read:list_meetings:user` | +| `meeting:write:admin` | `meeting:write:meeting:admin` + `meeting:write:settings:admin` + more | + +### Why Granular Scopes Exist + +**Classic scopes** are broad: +- `meeting:write:admin` grants ALL meeting write permissions account-wide +- Includes create, update, delete, settings, etc. + +**Granular scopes** are specific: +- `meeting:write:meeting:admin` - Only create/update meetings +- `meeting:delete:meeting:admin` - Only delete meetings +- `meeting:write:settings:admin` - Only update settings + +**Principle of Least Privilege:** Request only the granular scopes you need. + +## Choosing Between Classic and Granular + +### Use Classic When: +- You need broad access (e.g., full meeting management) +- Simpler scope management preferred +- Legacy app migration + +### Use Granular When: +- You need specific permissions only +- Implementing principle of least privilege +- Building security-sensitive apps + +### Can You Mix? + +✅ **Yes**, you can request both Classic and Granular scopes in the same app. + +``` +scope=meeting:read user:write:admin meeting:write:invite_links:admin +``` + +## Requesting Scopes + +### During App Creation (S2S OAuth, Chatbot) + +Scopes are configured in Zoom Marketplace: +1. Go to your app in https://marketplace.zoom.us +2. Click "Scopes" tab +3. Select required scopes (Classic or Granular) +4. Click "Continue" + +Token will include all configured scopes. + +### During Authorization (User OAuth, Device Flow) + +Scopes are requested in authorization URL: + +```javascript +const authURL = new URL('https://zoom.us/oauth/authorize'); +authURL.searchParams.set('response_type', 'code'); +authURL.searchParams.set('client_id', CLIENT_ID); +authURL.searchParams.set('redirect_uri', REDIRECT_URI); + +// Request specific scopes (space-separated) +authURL.searchParams.set('scope', 'meeting:read user:read recording:read'); + +// User sees consent screen listing these scopes +``` + +## Scope Consent Screen + +When user authorizes your app, they see: + +``` +[Your App Name] wants to: + +✓ View your meetings (meeting:read) +✓ View your profile (user:read) +✓ View your recordings (recording:read) + +[Deny] [Authorize] +``` + +## Scope Errors + +### Error 4711: Scope Mismatch + +**Cause:** Token's scopes don't include required scope for API endpoint. + +**Example:** +```javascript +// Token has: meeting:read +// API requires: meeting:write +await axios.post('https://api.zoom.us/v2/users/me/meetings', {...}, { + headers: { Authorization: `Bearer ${token}` } +}); +// Error 4711: Insufficient scope +``` + +**Solution:** +1. Add required scope in Zoom Marketplace (S2S/Chatbot) +2. OR request scope in authorization URL (User/Device) +3. Re-authorize user to grant new scopes + +## Checking Token Scopes + +### Decode Access Token Scopes + +```javascript +// S2S OAuth: scopes returned in token response +const { access_token, scope } = tokenResponse.data; +console.log('Scopes:', scope); // "meeting:read user:read recording:write" + +// User OAuth: scopes returned during token exchange +const { access_token, scope } = tokenResponse.data; +console.log('Granted scopes:', scope.split(' ')); // ['meeting:read', 'user:read', ...] +``` + +### API to Get Token Scopes + +```bash +curl -H "Authorization: Bearer {access_token}" \ + https://zoom.us/oauth/token +``` + +## Best Practices + +### 1. Request Minimum Scopes Needed + +```javascript +// ❌ AVOID: Requesting broad admin access when not needed +scope: "meeting:write:admin user:write:admin recording:write:admin" + +// ✅ PREFER: Request only what you need +scope: "meeting:read user:read" +``` + +### 2. Use Granular Scopes for Specific Operations + +```javascript +// ❌ Classic (broad) +scope: "meeting:write:admin" // Includes create, update, delete, settings, etc. + +// ✅ Granular (specific) +scope: "meeting:write:meeting:admin" // Only create/update meetings +``` + +### 3. Document Required Scopes + +```javascript +/** + * Create a meeting for a user + * Required scope: meeting:write:admin (Classic) or meeting:write:meeting:admin (Granular) + */ +async function createMeeting(userId, meetingData) { + // ... +} +``` + +## Reference Documentation + +- **Classic Scopes** → [../references/classic-scopes.md](../references/classic-scopes.md) +- **Granular Scopes** → [../references/granular-scopes.md](../references/granular-scopes.md) +- **Scope Errors** → [../troubleshooting/scope-issues.md](../troubleshooting/scope-issues.md) diff --git a/plugins/zoom-developers/skills/oauth/concepts/state-parameter.md b/plugins/zoom-developers/skills/oauth/concepts/state-parameter.md new file mode 100644 index 00000000..fb2a7992 --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/concepts/state-parameter.md @@ -0,0 +1,234 @@ +# State Parameter (CSRF Protection) + +The `state` parameter prevents Cross-Site Request Forgery (CSRF) attacks in OAuth flows. + +## What is CSRF in OAuth? + +### Attack Scenario (Without State) + +``` +1. Attacker initiates OAuth flow for victim's account +2. Attacker gets authorization code in callback +3. Attacker tricks victim into visiting callback URL with attacker's code +4. Victim's app exchanges code and links attacker's Zoom account to victim's app account +5. Attacker now has access to victim's app data +``` + +### Protection (With State) + +``` +1. App generates random state before redirecting to OAuth +2. App stores state in user's session +3. Zoom includes state in callback +4. App verifies state matches session +5. If state doesn't match → Reject (CSRF detected) +``` + +## Implementation + +### Step 1: Generate Random State + +```javascript +const crypto = require('crypto'); + +app.get('/auth', (req, res) => { + // Generate cryptographically secure random state + const state = crypto.randomBytes(16).toString('hex'); + + // Store in session (server-side) + req.session.oauthState = state; + + const authURL = new URL('https://zoom.us/oauth/authorize'); + authURL.searchParams.set('response_type', 'code'); + authURL.searchParams.set('client_id', process.env.ZOOM_CLIENT_ID); + authURL.searchParams.set('redirect_uri', process.env.ZOOM_REDIRECT_URL); + authURL.searchParams.set('state', state); // Include state + + res.redirect(authURL.toString()); +}); +``` + +### Step 2: Verify State in Callback + +```javascript +app.get('/callback', async (req, res) => { + const { code, state } = req.query; + const sessionState = req.session.oauthState; + + // Verify state matches + if (state !== sessionState) { + return res.status(403).send('Invalid state parameter - possible CSRF attack'); + } + + // Clean up state (one-time use) + delete req.session.oauthState; + + // Proceed with token exchange + const tokens = await exchangeCodeForToken(code); + // ... +}); +``` + +## State Parameter Flow + +``` +┌─────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Your App │ │ User Session│ │ Zoom OAuth │ +└──────┬──────┘ └──────┬───────┘ └──────┬───────┘ + │ │ │ + │ 1. Generate state │ │ + │ state = "abc123" │ │ + │ │ │ + │ 2. Store in session │ │ + │────────────────────────────>│ session.oauthState = "abc123" + │ │ │ + │ 3. Redirect to authorize with state │ + │https://zoom.us/oauth/authorize?state=abc123 │ + │───────────────────────────────────────────────────────────>│ + │ │ │ + │ 4. User authorizes │ │ + │ │ │ + │ 5. Redirect to callback with state │ + │ /callback?code=xyz&state=abc123 │ + │<───────────────────────────────────────────────────────────│ + │ │ │ + │ 6. Retrieve session state │ │ + │<────────────────────────────│ sessionState = "abc123" │ + │ │ │ + │ 7. Verify state === sessionState │ + │ "abc123" === "abc123" ✓ │ │ + │ │ │ + │ 8. Exchange code for token │ │ + │───────────────────────────────────────────────────────────>│ + │ │ │ +``` + +## Common Mistakes + +### 1. Not Verifying State + +```javascript +// ❌ WRONG: Accepting any state +app.get('/callback', async (req, res) => { + const { code } = req.query; + // No state verification! + await exchangeCodeForToken(code); +}); +``` + +```javascript +// ✅ CORRECT: Verifying state +app.get('/callback', async (req, res) => { + const { code, state } = req.query; + + if (state !== req.session.oauthState) { + return res.status(403).send('CSRF detected'); + } + + await exchangeCodeForToken(code); +}); +``` + +### 2. Using Predictable State + +```javascript +// ❌ WRONG: Predictable state +const state = Date.now().toString(); // Attacker can predict! +``` + +```javascript +// ✅ CORRECT: Cryptographically random state +const state = crypto.randomBytes(16).toString('hex'); +``` + +### 3. Reusing State + +```javascript +// ❌ WRONG: Not deleting state after use +if (state === req.session.oauthState) { + // State remains in session - can be reused! +} +``` + +```javascript +// ✅ CORRECT: Delete state after verification +if (state === req.session.oauthState) { + delete req.session.oauthState; // One-time use +} +``` + +## State + PKCE Together + +For maximum security, use both: + +```javascript +app.get('/auth', (req, res) => { + // Generate state (CSRF protection) + const state = crypto.randomBytes(16).toString('hex'); + req.session.oauthState = state; + + // Generate PKCE (authorization code interception protection) + const { code_verifier, code_challenge } = generatePKCE(); + req.session.pkceVerifier = code_verifier; + + const authURL = new URL('https://zoom.us/oauth/authorize'); + authURL.searchParams.set('response_type', 'code'); + authURL.searchParams.set('client_id', CLIENT_ID); + authURL.searchParams.set('redirect_uri', REDIRECT_URI); + authURL.searchParams.set('state', state); // CSRF protection + authURL.searchParams.set('code_challenge', code_challenge); // PKCE + authURL.searchParams.set('code_challenge_method', 'S256'); + + res.redirect(authURL.toString()); +}); + +app.get('/callback', async (req, res) => { + const { code, state } = req.query; + + // Verify state (CSRF) + if (state !== req.session.oauthState) { + return res.status(403).send('CSRF detected'); + } + + // Exchange with code_verifier (PKCE) + const code_verifier = req.session.pkceVerifier; + const tokens = await exchangeCode(code, code_verifier); + + // Clean up + delete req.session.oauthState; + delete req.session.pkceVerifier; +}); +``` + +## Mobile Apps + +For mobile apps without server-side sessions: + +```javascript +// Store state in secure storage +const state = generateRandomString(); +await SecureStore.setItemAsync('oauth_state', state); + +// Verify in callback +const storedState = await SecureStore.getItemAsync('oauth_state'); +if (receivedState !== storedState) { + throw new Error('CSRF detected'); +} + +// Clean up +await SecureStore.deleteItemAsync('oauth_state'); +``` + +## Best Practices + +1. **Always use state** for User OAuth and Device Flow +2. **Generate cryptographically random state** (not timestamps, sequential IDs) +3. **Store state server-side** in session (not client-side cookies) +4. **Delete state after verification** (one-time use) +5. **Combine with PKCE** for mobile/SPA apps + +## Next Steps + +- **Implement state + PKCE** → [../examples/pkce-implementation.md](../examples/pkce-implementation.md) +- **Understand PKCE** → [pkce.md](pkce.md) +- **Fix OAuth errors** → [../troubleshooting/common-errors.md](../troubleshooting/common-errors.md) diff --git a/plugins/zoom-developers/skills/oauth/concepts/token-lifecycle.md b/plugins/zoom-developers/skills/oauth/concepts/token-lifecycle.md new file mode 100644 index 00000000..1873dc3f --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/concepts/token-lifecycle.md @@ -0,0 +1,484 @@ +# Token Lifecycle + +Understanding how Zoom OAuth tokens are created, expire, refresh, and revoke is critical for building reliable integrations. + +## Token Types + +### Access Token +- **Purpose:** Authenticate API requests +- **Lifetime:** 1 hour (all OAuth flows) +- **Usage:** `Authorization: Bearer {access_token}` header +- **Format:** Opaque string (not JWT) + +### Refresh Token +- **Purpose:** Obtain new access tokens without user re-authorization +- **Lifetime:** Varies by flow/account/app configuration; ~90 days is common for some user-based flows (treat as changeable behavior) +- **Availability:** S2S OAuth and Chatbot do NOT have refresh tokens +- **Rotation:** Each refresh returns a NEW refresh token (old one becomes invalid) + +### Authorization Code +- **Purpose:** Temporary code exchanged for access token +- **Lifetime:** 5 minutes +- **Usage:** User OAuth and Device Flow only +- **One-time use:** Code becomes invalid after exchange + +--- + +## Expiration Summary + +| Flow | Access Token | Refresh Token | Strategy | +|------|--------------|---------------|----------| +| **S2S OAuth** | 1 hour | None | Request new token before expiration | +| **User OAuth** | 1 hour | ~90 days (commonly) | Use refresh token to get new access token | +| **Device Flow** | 1 hour | ~90 days (commonly) | Use refresh token to get new access token | +| **Chatbot** | 1 hour | None | Request new token before expiration | + +--- + +## S2S OAuth & Chatbot Token Lifecycle + +### Timeline + +``` +┌────────────────────────────────────────────────────┐ +│ │ +│ Token Request │ +│ │ │ +│ v │ +│ [ Access Token Valid ] │ +│ │ +│ ├───────────────────── 1 hour ──────────────────┤ │ +│ │ +│ Token │ +│ Expires │ +│ │ │ +│ v │ +│ Request New Token ────────────────> [ New Access Token Valid ] +│ │ +└────────────────────────────────────────────────────┘ +``` + +### Strategy: Cache with TTL + +```javascript +const redis = require('redis'); +const client = redis.createClient(); + +const getToken = async () => { + // Check cache first + let token = await client.get('zoom_access_token'); + + if (!token) { + // Request new token + const response = await axios.post('https://zoom.us/oauth/token', ...); + const { access_token, expires_in } = response.data; + + // Cache with TTL (10 second buffer before actual expiration) + await client.setex('zoom_access_token', expires_in - 10, access_token); + + token = access_token; + } + + return token; +}; +``` + +**Key Points:** +- ✅ Cache token in Redis with TTL matching expiration +- ✅ Use 10-second buffer to prevent race conditions +- ✅ Single token shared across all requests +- ❌ Do NOT request new token on every API call +- ❌ Do NOT try to "refresh" (no refresh token exists) + +--- + +## User OAuth & Device Flow Token Lifecycle + +### Timeline + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ │ +│ User Authorizes │ +│ │ │ +│ v │ +│ [ Access Token Valid ] │ +│ [ Refresh Token Valid ]────────────────────────────────────────┐ │ +│ │ │ +│ ├───────────── 1 hour ────────────┤ │ │ +│ │ │ +│ Access Token Expires │ │ +│ │ │ │ +│ v │ │ +│ Refresh Request ──────────> [ New Access Token Valid ] │ │ +│ [ New Refresh Token Valid ]────┐ │ │ +│ │ │ │ +│ ├───────────── 1 hour ────────────┤ │ │ │ +│ │ │ │ +│ Access Token Expires │ │ │ +│ │ │ │ │ +│ v │ │ │ +│ Refresh Request ──────────> [ New Access Token Valid ] │ │ │ +│ [ New Refresh Token Valid ] │ │ │ +│ │ │ │ +│ ... Continue refreshing up to ~90 days (commonly) ... │ │ │ +│ │ │ │ +│ ├──────────────────────── ~90 days (commonly) ───────────┤ │ │ +│ │ │ +│ Refresh Token Expires │ │ +│ │ │ │ +│ v │ │ +│ User Must Re-Authorize (restart OAuth flow) │ │ +│ │ +└────────────────────────────────────────────────────────────────────┘ +``` + +### Strategy: Auto-Refresh Middleware + +```javascript +const tokenMiddleware = async (req, res, next) => { + const userId = req.session.userId; + + // Get user's tokens from database + let { access_token, refresh_token, token_expiry } = await getUserTokens(userId); + + // Check if access token is expired or will expire soon (5 minute buffer) + const now = Date.now(); + const expiresIn = token_expiry - now; + + if (expiresIn < 300000) { // Less than 5 minutes remaining + // Refresh the token + const response = await axios.post( + 'https://zoom.us/oauth/token', + qs.stringify({ + grant_type: 'refresh_token', + refresh_token: refresh_token + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${CLIENT_ID}:${CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + const { access_token: new_access_token, refresh_token: new_refresh_token, expires_in } = response.data; + + // CRITICAL: Update BOTH tokens in database + await updateUserTokens(userId, { + access_token: new_access_token, + refresh_token: new_refresh_token, // NEW refresh token + token_expiry: now + (expires_in * 1000) + }); + + access_token = new_access_token; + } + + // Attach token to request + req.zoomToken = access_token; + next(); +}; +``` + +**Key Points:** +- ✅ Refresh BEFORE token expires (5-minute buffer recommended) +- ✅ **ALWAYS save the NEW refresh token** (old one becomes invalid) +- ✅ Store tokens per user in database +- ✅ Encrypt tokens at rest (AES-256 minimum) +- ❌ Do NOT reuse old refresh token after refresh +- ❌ Do NOT wait for API 401 errors to trigger refresh + +--- + +## Refresh Token Rotation + +**CRITICAL:** Zoom rotates refresh tokens on every refresh. + +### What Happens During Refresh + +``` +Before Refresh: + access_token: "abc123" (expired) + refresh_token: "xyz789" (valid) + +Request: + POST /oauth/token + grant_type=refresh_token + refresh_token=xyz789 + +Response: + { + "access_token": "def456", // NEW access token + "refresh_token": "uvw012", // NEW refresh token + "expires_in": 3600 + } + +After Refresh: + access_token: "def456" (valid for 1 hour) + refresh_token: "uvw012" (lifetime varies; ~90 days is common) + + OLD refresh_token "xyz789" is NOW INVALID +``` + +### Common Mistake + +```javascript +// ❌ WRONG: Not saving new refresh token +const response = await refreshToken(old_refresh_token); +const { access_token } = response.data; // Only saving access token + +await updateUserTokens(userId, { access_token }); // Refresh token not updated! + +// Next refresh will fail with error 4735 "Invalid refresh token" +``` + +```javascript +// ✅ CORRECT: Saving both tokens +const response = await refreshToken(old_refresh_token); +const { access_token, refresh_token } = response.data; + +await updateUserTokens(userId, { + access_token, + refresh_token // MUST save new refresh token +}); +``` + +--- + +## Authorization Code Expiration + +**Lifetime:** 5 minutes + +### Timeline + +``` +User Clicks "Allow" + │ + v +Authorization Code Issued (expires in 5 minutes) + │ + │ ← Exchange code for token within 5 minutes + v +[ Access Token + Refresh Token ] + +If code not exchanged within 5 minutes: + → Error 4733 "Invalid authorization code" + → User must re-authorize +``` + +### Implementation + +```javascript +app.get('/callback', async (req, res) => { + const { code } = req.query; + + try { + // Exchange code for token IMMEDIATELY + const response = await axios.post('https://zoom.us/oauth/token', { + grant_type: 'authorization_code', + code: code, + redirect_uri: process.env.REDIRECT_URI + }, ...); + + // Store tokens + await saveTokens(response.data); + + } catch (error) { + if (error.response?.data?.error === 'invalid_grant') { + // Code expired (4733) or already used + res.send('Authorization code expired. Please re-authorize.'); + } + } +}); +``` + +**Key Points:** +- ✅ Exchange authorization code **immediately** upon receiving it +- ✅ Authorization codes are one-time use +- ❌ Do NOT cache or store authorization codes +- ❌ Do NOT delay token exchange + +--- + +## Token Revocation + +### When Tokens Are Revoked + +1. **User re-authorizes your app:** + - All previous tokens for that user become invalid + - New tokens are issued + +2. **User removes your app:** + - All tokens for that user become invalid + - User must re-authorize to grant access again + +3. **Explicit revocation:** + - Your app calls `https://zoom.us/oauth/revoke` endpoint + - Tokens become invalid immediately + +4. **Refresh token expires (lifetime varies):** + - Can no longer refresh + - User must re-authorize + +### Revoke Token API + +```javascript +const revokeToken = async (access_token) => { + await axios.post( + 'https://zoom.us/oauth/revoke', + qs.stringify({ + token: access_token + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${CLIENT_ID}:${CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + // Token is now revoked + // Delete from database + await deleteUserTokens(userId); +}; +``` + +**What Gets Revoked:** +- Access token becomes invalid immediately +- Refresh token becomes invalid immediately +- All API requests with revoked token return 401 + +--- + +## Error Codes + +| Code | Error | Meaning | Action | +|------|-------|---------|--------| +| **4733** | Invalid authorization code | Code expired (5 min) or already used | User must re-authorize | +| **4735** | Invalid refresh token | Refresh token expired or rotated | User must re-authorize | +| **4741** | Token has been revoked | Token was explicitly revoked | User must re-authorize | +| **401** | Unauthorized | Access token expired or invalid | Refresh token (if available) or re-authorize | + +--- + +## Best Practices + +### 1. Cache S2S Tokens + +```javascript +// ✅ Cache in Redis with TTL +await redis.setex('zoom_token', expires_in - 10, access_token); +``` + +```javascript +// ❌ Request new token on every API call +const token = await getToken(); // Every time? No! +await makeAPIRequest(token); +``` + +### 2. Refresh BEFORE Expiration + +```javascript +// ✅ Refresh with buffer (5 minutes before expiry) +if (expiresIn < 300000) { + await refreshToken(); +} +``` + +```javascript +// ❌ Wait for 401 error +try { + await makeAPIRequest(token); +} catch (err) { + if (err.status === 401) { + await refreshToken(); // Too late! + } +} +``` + +### 3. Always Save New Refresh Token + +```javascript +// ✅ Update both tokens +const { access_token, refresh_token } = await refresh(); +await saveTokens({ access_token, refresh_token }); +``` + +```javascript +// ❌ Only save access token +const { access_token } = await refresh(); +await saveTokens({ access_token }); // Refresh token not saved! +``` + +### 4. Encrypt Tokens at Rest + +```javascript +// ✅ Encrypt before storing +const encrypted = encrypt(access_token, CIPHER_KEY); +await db.query('UPDATE users SET token = ? WHERE id = ?', [encrypted, userId]); +``` + +```javascript +// ❌ Store in plain text +await db.query('UPDATE users SET token = ? WHERE id = ?', [access_token, userId]); +``` + +### 5. Handle Revocation Gracefully + +```javascript +// ✅ Detect revoked tokens and prompt re-auth +if (error.code === 4741) { + await deleteUserTokens(userId); + res.redirect('/auth'); // Re-authorize +} +``` + +--- + +## Debugging Token Issues + +### Symptom: "Token expired" immediately after getting it + +**Cause:** Server clock is incorrect + +**Solution:** +```bash +# Sync server time +sudo ntpdate -s time.nist.gov +``` + +### Symptom: Refresh fails with "Invalid refresh token" (4735) + +**Cause:** Using old refresh token after it was rotated + +**Solution:** +- Check database: Are you saving the NEW refresh token? +- Check code: Are you updating BOTH access_token AND refresh_token? + +### Symptom: Authorization code fails with "Invalid grant" (4733) + +**Cause:** Code expired (5 minutes passed) or already used + +**Solution:** +- Exchange code immediately in callback +- Codes are one-time use (don't cache) + +### Symptom: All tokens revoked unexpectedly + +**Cause:** User re-authorized your app or removed it + +**Solution:** +- Detect 401/4741 errors +- Prompt user to re-authorize + +--- + +## Next Steps + +- **Implement auto-refresh** → [../examples/token-refresh.md](../examples/token-refresh.md) +- **Fix token errors** → [../troubleshooting/token-issues.md](../troubleshooting/token-issues.md) +- **Understand OAuth flows** → [oauth-flows.md](oauth-flows.md) diff --git a/plugins/zoom-developers/skills/oauth/examples/device-flow.md b/plugins/zoom-developers/skills/oauth/examples/device-flow.md new file mode 100644 index 00000000..89c450bb --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/examples/device-flow.md @@ -0,0 +1,9 @@ +# Device Flow + +See [../concepts/oauth-flows.md](../concepts/oauth-flows.md) and official Zoom samples for implementation details. + +Official sample repositories: +- https://github.com/zoom/oauth-sample-app +- https://github.com/zoom/server-to-server-oauth-token +- https://github.com/zoom/server-to-server-oauth-starter-api +- https://github.com/zoom/user-level-oauth-starter diff --git a/plugins/zoom-developers/skills/oauth/examples/pkce-implementation.md b/plugins/zoom-developers/skills/oauth/examples/pkce-implementation.md new file mode 100644 index 00000000..5388259b --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/examples/pkce-implementation.md @@ -0,0 +1,9 @@ +# Pkce Implementation + +See [../concepts/oauth-flows.md](../concepts/oauth-flows.md) and official Zoom samples for implementation details. + +Official sample repositories: +- https://github.com/zoom/oauth-sample-app +- https://github.com/zoom/server-to-server-oauth-token +- https://github.com/zoom/server-to-server-oauth-starter-api +- https://github.com/zoom/user-level-oauth-starter diff --git a/plugins/zoom-developers/skills/oauth/examples/s2s-oauth-basic.md b/plugins/zoom-developers/skills/oauth/examples/s2s-oauth-basic.md new file mode 100644 index 00000000..dee7cee9 --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/examples/s2s-oauth-basic.md @@ -0,0 +1,9 @@ +# S2s Oauth Basic + +See [../concepts/oauth-flows.md](../concepts/oauth-flows.md) and official Zoom samples for implementation details. + +Official sample repositories: +- https://github.com/zoom/oauth-sample-app +- https://github.com/zoom/server-to-server-oauth-token +- https://github.com/zoom/server-to-server-oauth-starter-api +- https://github.com/zoom/user-level-oauth-starter diff --git a/plugins/zoom-developers/skills/oauth/examples/s2s-oauth-redis.md b/plugins/zoom-developers/skills/oauth/examples/s2s-oauth-redis.md new file mode 100644 index 00000000..69822341 --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/examples/s2s-oauth-redis.md @@ -0,0 +1,295 @@ +# S2S OAuth with Redis Caching (Production Pattern) + +Production-ready Server-to-Server OAuth implementation with Redis token caching and auto-refresh middleware. + +## Architecture + +``` +Express App + ↓ +tokenCheck Middleware (automatic token management) + ↓ +Redis Cache (TTL-based expiration) + ↓ +Zoom API Routes (protected) +``` + +## Complete Implementation + +### 1. Dependencies + +```bash +npm install express redis axios query-string dotenv +``` + +### 2. Redis Configuration + +```javascript +// configs/redis.js +const redis = require('redis'); + +const client = redis.createClient({ + url: process.env.REDIS_URL || 'redis://YOUR_REDIS_HOST:6379' +}); + +client.on('error', (err) => console.error('Redis error:', err)); +client.on('connect', () => console.log('Connected to Redis')); + +module.exports = client; +``` + +### 3. Token Utilities + +```javascript +// utils/token.js +const axios = require('axios'); +const qs = require('query-string'); + +const { ZOOM_ACCOUNT_ID, ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET } = process.env; + +const getToken = async () => { + try { + const response = await axios.post( + 'https://zoom.us/oauth/token', + qs.stringify({ + grant_type: 'account_credentials', + account_id: ZOOM_ACCOUNT_ID + }), + { + headers: { + 'Authorization': `Basic ${Buffer.from( + `${ZOOM_CLIENT_ID}:${ZOOM_CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + return response.data; // { access_token, expires_in, scope } + } catch (error) { + throw new Error(`Token request failed: ${error.response?.data?.message || error.message}`); + } +}; + +const setToken = async (redis, { access_token, expires_in }) => { + // Cache with TTL (10 second buffer before actual expiration) + await redis.setex('access_token', expires_in - 10, access_token); +}; + +module.exports = { getToken, setToken }; +``` + +### 4. Token Check Middleware + +```javascript +// middlewares/tokenCheck.js +const redis = require('../configs/redis'); +const { getToken, setToken } = require('../utils/token'); + +const tokenCheck = async (req, res, next) => { + let token = await redis.get('access_token'); + + // Redis returns null if key doesn't exist + if (!token) { + try { + const { access_token, expires_in, error } = await getToken(); + + if (error) { + return res.status(401).json({ + message: `Authentication failed: ${error.message}` + }); + } + + // Cache token + await setToken(redis, { access_token, expires_in }); + token = access_token; + } catch (err) { + return res.status(500).json({ + message: 'Token generation failed', + error: err.message + }); + } + } + + // Attach token to request for route handlers + req.headerConfig = { + headers: { + Authorization: `Bearer ${token}` + } + }; + + next(); +}; + +module.exports = { tokenCheck }; +``` + +### 5. Main Application + +```javascript +// index.js +require('dotenv').config(); + +const express = require('express'); +const redis = require('./configs/redis'); +const { tokenCheck } = require('./middlewares/tokenCheck'); + +const app = express(); +const PORT = process.env.PORT || 8080; + +// Connect to Redis +(async () => { + await redis.connect(); +})(); + +// Add global middlewares +app.use(express.json()); + +// Apply tokenCheck to all API routes +app.use('/api/users', tokenCheck, require('./routes/api/users')); +app.use('/api/meetings', tokenCheck, require('./routes/api/meetings')); + +const server = app.listen(PORT, () => { + console.log(`Server listening on port ${PORT}`); +}); + +// Graceful shutdown +const cleanup = async () => { + console.log('Shutting down gracefully...'); + await redis.del('access_token'); // Clear cached token + server.close(() => { + redis.quit(() => process.exit()); + }); +}; + +process.on('SIGTERM', cleanup); +process.on('SIGINT', cleanup); +``` + +### 6. Example API Route + +```javascript +// routes/api/users.js +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); + +const ZOOM_API_BASE = 'https://api.zoom.us/v2'; + +// List users +router.get('/', async (req, res) => { + try { + const response = await axios.get( + `${ZOOM_API_BASE}/users`, + req.headerConfig // Token from middleware + ); + res.json(response.data); + } catch (error) { + res.status(error.response?.status || 500).json({ + message: 'Failed to list users', + error: error.response?.data || error.message + }); + } +}); + +// Get user +router.get('/:userId', async (req, res) => { + try { + const response = await axios.get( + `${ZOOM_API_BASE}/users/${req.params.userId}`, + req.headerConfig + ); + res.json(response.data); + } catch (error) { + res.status(error.response?.status || 500).json({ + message: 'Failed to get user', + error: error.response?.data || error.message + }); + } +}); + +module.exports = router; +``` + +### 7. Environment Variables + +```bash +# .env +ZOOM_ACCOUNT_ID=your_account_id +ZOOM_CLIENT_ID=your_client_id +ZOOM_CLIENT_SECRET=your_client_secret +REDIS_URL=redis://YOUR_REDIS_HOST:6379 +PORT=8080 +``` + +## How It Works + +1. **Request arrives** at protected route (e.g., GET /api/users) +2. **tokenCheck middleware** runs: + - Checks Redis for cached token + - If missing: Requests new token from Zoom + - Caches token with TTL (expires_in - 10 seconds) +3. **Token attached** to `req.headerConfig` +4. **Route handler** makes API request with token +5. **Token auto-refreshes** when Redis TTL expires + +## Benefits + +✅ **Automatic token management** - No manual refresh logic +✅ **Single token for all requests** - Account-wide access +✅ **TTL-based expiration** - Redis handles cleanup +✅ **10-second buffer** - Prevents race conditions +✅ **Graceful shutdown** - Clears token on exit + +## Testing + +```bash +# Start Redis +docker run -d -p 6379:6379 redis + +# Start app +npm start + +# Test endpoints +API_BASE_URL="http://YOUR_API_HOST:8080" +curl "$API_BASE_URL/api/users" +``` + +## Docker Deployment + +```dockerfile +# Dockerfile +FROM node:18 +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +CMD ["node", "index.js"] +``` + +```yaml +# docker-compose.yml +version: '3.8' +services: + redis: + image: redis:7-alpine + ports: + - "6379:6379" + + app: + build: . + ports: + - "8080:8080" + environment: + - ZOOM_ACCOUNT_ID=${ZOOM_ACCOUNT_ID} + - ZOOM_CLIENT_ID=${ZOOM_CLIENT_ID} + - ZOOM_CLIENT_SECRET=${ZOOM_CLIENT_SECRET} + - REDIS_URL=redis://redis:6379 + depends_on: + - redis +``` + +## Related + +- **S2S OAuth flow** → [../concepts/oauth-flows.md#server-to-server-s2s-oauth](../concepts/oauth-flows.md#server-to-server-s2s-oauth) +- **Token lifecycle** → [../concepts/token-lifecycle.md](../concepts/token-lifecycle.md) diff --git a/plugins/zoom-developers/skills/oauth/examples/token-refresh.md b/plugins/zoom-developers/skills/oauth/examples/token-refresh.md new file mode 100644 index 00000000..96079d89 --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/examples/token-refresh.md @@ -0,0 +1,9 @@ +# Token Refresh + +See [../concepts/oauth-flows.md](../concepts/oauth-flows.md) and official Zoom samples for implementation details. + +Official sample repositories: +- https://github.com/zoom/oauth-sample-app +- https://github.com/zoom/server-to-server-oauth-token +- https://github.com/zoom/server-to-server-oauth-starter-api +- https://github.com/zoom/user-level-oauth-starter diff --git a/plugins/zoom-developers/skills/oauth/examples/user-oauth-basic.md b/plugins/zoom-developers/skills/oauth/examples/user-oauth-basic.md new file mode 100644 index 00000000..685bb38e --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/examples/user-oauth-basic.md @@ -0,0 +1,9 @@ +# User Oauth Basic + +See [../concepts/oauth-flows.md](../concepts/oauth-flows.md) and official Zoom samples for implementation details. + +Official sample repositories: +- https://github.com/zoom/oauth-sample-app +- https://github.com/zoom/server-to-server-oauth-token +- https://github.com/zoom/server-to-server-oauth-starter-api +- https://github.com/zoom/user-level-oauth-starter diff --git a/plugins/zoom-developers/skills/oauth/examples/user-oauth-mysql.md b/plugins/zoom-developers/skills/oauth/examples/user-oauth-mysql.md new file mode 100644 index 00000000..72eaae12 --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/examples/user-oauth-mysql.md @@ -0,0 +1,9 @@ +# User Oauth Mysql + +See [../concepts/oauth-flows.md](../concepts/oauth-flows.md) and official Zoom samples for implementation details. + +Official sample repositories: +- https://github.com/zoom/oauth-sample-app +- https://github.com/zoom/server-to-server-oauth-token +- https://github.com/zoom/server-to-server-oauth-starter-api +- https://github.com/zoom/user-level-oauth-starter diff --git a/plugins/zoom-developers/skills/oauth/references/classic-scopes.md b/plugins/zoom-developers/skills/oauth/references/classic-scopes.md new file mode 100644 index 00000000..a73cfb7b --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/references/classic-scopes.md @@ -0,0 +1,74 @@ +# Classic OAuth Scopes + +Source: https://developers.zoom.us/docs/integrations/oauth-scopes/ + +This file is intentionally compact. Do not mirror the full Zoom classic scope catalog into the plugin; it changes over time and is too large for a local Codex reference bundle. Use the official source above as the authoritative endpoint-to-scope lookup. + +## Format + +Classic scopes are broader than granular scopes and usually follow this shape: + +```text +{resource}:{action}:{level} +``` + +Common parts: + +| Part | Examples | Meaning | +|---|---|---| +| `resource` | `meeting`, `recording`, `user`, `webinar`, `chat`, `phone`, `docs` | Zoom product/API area | +| `action` | `read`, `write` | Read or manage capability | +| `level` | omitted, `admin`, `master` | Own-user, account admin, or master-account access | + +Examples: + +```text +meeting:read +meeting:write +meeting:read:admin +meeting:write:admin +recording:read +recording:read:admin +user:read +user:write:admin +imchat:bot +phone:read +phone:write:admin +``` + +## When To Use Classic Scopes + +Use classic scopes when: + +- The endpoint documentation only lists classic scopes. +- You are maintaining an older Marketplace app that already uses classic scopes. +- The app legitimately needs broad access across a resource family. + +Prefer granular scopes when: + +- The endpoint supports granular scopes. +- The app only needs a narrow operation. +- The consent screen should expose the smallest possible access surface. + +## Access Levels + +| Level | Access pattern | Notes | +|---|---|---| +| omitted | Current user’s own resources | Usually available to normal user authorization if the app is installed | +| `admin` | Account-level resources | Requires suitable app type, scope approval, and admin authorization | +| `master` | Master/sub-account operations | Only use when the API and account model explicitly require it | + +## Lookup Workflow + +1. Identify the exact REST endpoint. +2. Open the official classic scope catalog or endpoint reference. +3. Search by endpoint name, operation ID, or resource term. +4. Confirm whether granular alternatives are available. +5. Add only the required scopes and re-authorize users after changes. + +## Common Mistakes + +- Do not request `:admin` scopes for user-owned workflows unless account-wide access is required. +- Do not use classic scopes as a shortcut when granular scopes are available and narrower. +- Do not assume Server-to-Server OAuth supports every endpoint; verify the endpoint and app type. +- Do not keep stale access tokens after changing scopes; complete OAuth again. diff --git a/plugins/zoom-developers/skills/oauth/references/environment-variables.md b/plugins/zoom-developers/skills/oauth/references/environment-variables.md new file mode 100644 index 00000000..f678dbbf --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/references/environment-variables.md @@ -0,0 +1,23 @@ +# Zoom OAuth Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_CLIENT_ID` | Yes | OAuth client identity | Zoom Marketplace -> OAuth app -> App Credentials | +| `ZOOM_CLIENT_SECRET` | Yes | OAuth client secret | Zoom Marketplace -> OAuth app -> App Credentials | +| `ZOOM_REDIRECT_URI` | User-level OAuth | Authorization code callback | Zoom Marketplace -> OAuth redirect/allow list | +| `ZOOM_ACCOUNT_ID` | S2S OAuth | Account-level token grant | Zoom Marketplace -> Server-to-Server OAuth app credentials | + +## Runtime-only values + +- `ZOOM_AUTH_CODE` +- `ZOOM_ACCESS_TOKEN` +- `ZOOM_REFRESH_TOKEN` + +Generate these at runtime and keep in secure storage. + +## Notes + +- Use S2S OAuth where user consent is not required. +- Use Authorization Code flow when acting on behalf of a Zoom user. diff --git a/plugins/zoom-developers/skills/oauth/references/full-guide.md b/plugins/zoom-developers/skills/oauth/references/full-guide.md new file mode 100644 index 00000000..a4ea15e2 --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/references/full-guide.md @@ -0,0 +1,879 @@ +# Zoom OAuth + +Background reference for Zoom auth and token lifecycle behavior. Prefer `setup-zoom-oauth` first, then use this skill for the exact flow, scope, and error details. + +# Zoom OAuth + +Authentication and authorization for Zoom APIs. + +## 📖 Complete Documentation + +For comprehensive guides, production patterns, and troubleshooting, see **Integrated Index section below**. + +Quick navigation: +- **[5-Minute Runbook](../RUNBOOK.md)** - Preflight checks before deep debugging +- **[OAuth Flows](../concepts/oauth-flows.md)** - Which flow to use and how each works +- **[Token Lifecycle](../concepts/token-lifecycle.md)** - Expiration, refresh, and revocation +- **[Production Examples](../examples/s2s-oauth-redis.md)** - Redis caching, MySQL storage, auto-refresh +- **[Troubleshooting](../troubleshooting/common-errors.md)** - Error codes 4700-4741 + +## Prerequisites + +- Zoom app created in [Marketplace](https://marketplace.zoom.us/) +- Client ID and Client Secret +- For S2S OAuth: Account ID + +## Four Authorization Use Cases + +| Use Case | App Type | Grant Type | Industry Name | +|----------|----------|------------|---------------| +| **Account Authorization** | Server-to-Server | `account_credentials` | Client Credentials Grant, M2M, Two-legged OAuth | +| **User Authorization** | General | `authorization_code` | Authorization Code Grant, Three-legged OAuth | +| **Device Authorization** | General | `urn:ietf:params:oauth:grant-type:device_code` | Device Authorization Grant (RFC 8628) | +| **Client Authorization** | General | `client_credentials` | Client Credentials Grant (chatbot-scoped) | + +### Industry Terminology + +| Term | Meaning | +|------|---------| +| **Two-legged OAuth** | No user involved (client ↔ server) | +| **Three-legged OAuth** | User involved (user ↔ client ↔ server) | +| **M2M** | Machine-to-Machine (backend services) | +| **Public client** | Can't keep secrets (mobile, SPA) → use PKCE | +| **Confidential client** | Can keep secrets (backend servers) | +| **PKCE** | Proof Key for Code Exchange (RFC 7636), pronounced "pixy" | + +### Which Flow Should I Use? + +``` + ┌─────────────────────┐ + │ What are you │ + │ building? │ + └──────────┬──────────┘ + │ + ┌────────────────────┼────────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ Backend │ │ App for other │ │ Chatbot only │ + │ automation │ │ users/accounts │ │ (Team Chat) │ + │ (your account) │ │ │ │ │ + └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ + │ │ │ + ▼ │ ▼ + ┌─────────────────┐ │ ┌─────────────────┐ + │ ACCOUNT │ │ │ CLIENT │ + │ (S2S OAuth) │ │ │ (Chatbot) │ + └─────────────────┘ │ └─────────────────┘ + │ + ▼ + ┌─────────────────────┐ + │ Does device have │ + │ a browser? │ + └──────────┬──────────┘ + │ + ┌───────────────┴───────────────┐ + │ NO YES│ + ▼ ▼ + ┌─────────────────────────┐ ┌─────────────────┐ + │ DEVICE │ │ USER │ + │ (Device Flow) │ │ (Auth Code) │ + │ │ │ │ + │ Examples: │ │ + PKCE if │ + │ • Smart TV │ │ public client │ + │ • Meeting SDK device │ │ │ + └─────────────────────────┘ └─────────────────┘ +``` + +--- + +## Account Authorization (Server-to-Server OAuth) + +For backend automation without user interaction. + +### Request Access Token + +```bash +POST https://zoom.us/oauth/token?grant_type=account_credentials&account_id={ACCOUNT_ID} + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +### Response + +```json +{ + "access_token": "eyJ...", + "token_type": "bearer", + "expires_in": 3600, + "scope": "user:read:user:admin", + "api_url": "https://api.zoom.us" +} +``` + +### Refresh + +Access tokens expire after **1 hour**. No separate refresh flow - just request a new token. + +--- + +## User Authorization (Authorization Code Flow) + +For apps that act on behalf of users. + +### Step 1: Redirect User to Authorize + +``` +https://zoom.us/oauth/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI} +``` + +Use `https://zoom.us/oauth/authorize` for consent, but `https://zoom.us/oauth/token` for token exchange. + +**Optional Parameters:** + +| Parameter | Description | +|-----------|-------------| +| `state` | CSRF protection, maintains state through flow | +| `code_challenge` | For PKCE (see below) | +| `code_challenge_method` | `S256` or `plain` (default: plain) | + +### Step 2: User Authorizes + +- User signs in and grants permission +- Redirects to `redirect_uri` with authorization code: + ``` + https://example.com/?code={AUTHORIZATION_CODE} + ``` + +### Step 3: Exchange Code for Token + +```bash +POST https://zoom.us/oauth/token?grant_type=authorization_code&code={CODE}&redirect_uri={REDIRECT_URI} + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +**With PKCE:** Add `code_verifier` parameter. + +### Response + +```json +{ + "access_token": "eyJ...", + "token_type": "bearer", + "refresh_token": "eyJ...", + "expires_in": 3600, + "scope": "user:read:user", + "api_url": "https://api.zoom.us" +} +``` + +### Refresh Token + +```bash +POST https://zoom.us/oauth/token?grant_type=refresh_token&refresh_token={REFRESH_TOKEN} + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +- Access tokens expire after **1 hour** +- Refresh token lifetime can vary; ~90 days is common for some user-based flows. Treat it as configuration/behavior that can change and rely on runtime errors + re-auth fallback. +- Always use the latest refresh token for the next request +- If refresh token expires, redirect user to authorization URL to restart flow + +### User-Level vs Account-Level Apps + +| Type | Who Can Authorize | Scope Access | +|------|-------------------|--------------| +| **User-level** | Any individual user | Scoped to themselves | +| **Account-level** | User with admin permissions | Account-wide access (admin scopes) | + +--- + +## Device Authorization (Device Flow) + +For devices without browsers (e.g., Meeting SDK apps). + +### Prerequisites + +Enable "Use App on Device" in: Features > Embed > Enable Meeting SDK + +### Step 1: Request Device Code + +```bash +POST https://zoom.us/oauth/devicecode?client_id={CLIENT_ID} + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +### Response + +```json +{ + "device_code": "DEVICE_CODE", + "user_code": "abcd1234", + "verification_uri": "https://zoom.us/oauth_device", + "verification_uri_complete": "https://zoom.us/oauth/device/complete/{CODE}", + "expires_in": 900, + "interval": 5 +} +``` + +### Step 2: User Authorization + +Direct user to: +- `verification_uri` and display `user_code` for manual entry, OR +- `verification_uri_complete` (user code prefilled) + +User signs in and allows the app. + +### Step 3: Poll for Token + +Poll at the `interval` (5 seconds) until user authorizes: + +```bash +POST https://zoom.us/oauth/token?grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code={DEVICE_CODE} + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +### Response + +```json +{ + "access_token": "eyJ...", + "token_type": "bearer", + "refresh_token": "eyJ...", + "expires_in": 3599, + "scope": "user:read:user user:read:token", + "api_url": "https://api.zoom.us" +} +``` + +### Polling Responses + +| Response | Meaning | Action | +|----------|---------|--------| +| Token returned | User authorized | Store tokens, done | +| `error: authorization_pending` | User hasn't authorized yet | Keep polling at interval | +| `error: slow_down` | Polling too fast | Increase interval by 5 seconds | +| `error: expired_token` | Device code expired (15 min) | Restart flow from Step 1 | +| `error: access_denied` | User denied authorization | Handle denial, don't retry | + +### Polling Implementation + +```javascript +async function pollForToken(deviceCode, interval) { + while (true) { + await sleep(interval * 1000); + + try { + const response = await axios.post( + `https://zoom.us/oauth/token?grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${deviceCode}`, + null, + { headers: { 'Authorization': `Basic ${credentials}` } } + ); + return response.data; // Success - got tokens + } catch (error) { + const err = error.response?.data?.error; + if (err === 'authorization_pending') continue; + if (err === 'slow_down') { interval += 5; continue; } + throw error; // expired_token or access_denied + } + } +} +``` + +### Refresh + +Same as User Authorization. If refresh token expires, restart device flow from Step 1. + +--- + +## Client Authorization (Chatbot) + +For chatbot message operations only. + +### Request Token + +```bash +POST https://zoom.us/oauth/token?grant_type=client_credentials + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +### Response + +```json +{ + "access_token": "eyJ...", + "token_type": "bearer", + "expires_in": 3600, + "scope": "imchat:bot", + "api_url": "https://api.zoom.us" +} +``` + +### Refresh + +Tokens expire after **1 hour**. No refresh flow - just request a new token. + +--- + +## Using Access Tokens + +### Call API + +```bash +GET https://api.zoom.us/v2/users/me + +Headers: +Authorization: Bearer {ACCESS_TOKEN} +``` + +### Me Context + +Replace `userID` with `me` to target the token's associated user: + +| Endpoint | Methods | +|----------|---------| +| `/v2/users/me` | GET, PATCH | +| `/v2/users/me/token` | GET | +| `/v2/users/me/meetings` | GET, POST | + +--- + +## Revoke Access Token + +Works for all authorization types. + +```bash +POST https://zoom.us/oauth/revoke?token={ACCESS_TOKEN} + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +### Response + +```json +{ + "status": "success" +} +``` + +--- + +## PKCE (Proof Key for Code Exchange) + +For public clients that can't securely store secrets (mobile apps, SPAs, desktop apps). + +### When to Use PKCE + +| Client Type | Use PKCE? | Why | +|-------------|-----------|-----| +| Mobile app | **Yes** | Can't securely store client secret | +| Single Page App (SPA) | **Yes** | JavaScript is visible to users | +| Desktop app | **Yes** | Binary can be decompiled | +| Meeting SDK (client-side) | **Yes** | Runs on user's device | +| Backend server | Optional | Can keep secrets, but PKCE adds security | + +### How PKCE Works + +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ Client │ │ Zoom │ │ Zoom │ +│ App │ │ Auth │ │ Token │ +└────┬─────┘ └────┬─────┘ └────┬─────┘ + │ │ │ + │ 1. Generate code_verifier (random) │ │ + │ 2. Create code_challenge = SHA256(verifier) │ + │ │ │ + │ ─────── /authorize + code_challenge ──► │ │ + │ │ │ + │ ◄────── authorization_code ──────────── │ │ + │ │ │ + │ ─────────────── /token + code_verifier ─┼────────────────────────────► │ + │ │ │ + │ │ Verify: SHA256(verifier) │ + │ │ == challenge │ + │ │ │ + │ ◄───────────────────────────────────────┼─────── access_token ──────── │ + │ │ │ +``` + +### Implementation (Node.js) + +```javascript +const crypto = require('crypto'); + +function generatePKCE() { + const verifier = crypto.randomBytes(32).toString('base64url'); + const challenge = crypto.createHash('sha256').update(verifier).digest('base64url'); + return { verifier, challenge }; +} + +const pkce = generatePKCE(); + +const authUrl = `https://zoom.us/oauth/authorize?` + + `response_type=code&` + + `client_id=${CLIENT_ID}&` + + `redirect_uri=${REDIRECT_URI}&` + + `code_challenge=${pkce.challenge}&` + + `code_challenge_method=S256`; + +// Store pkce.verifier in session for callback +``` + +### Token Exchange with PKCE + +```bash +POST https://zoom.us/oauth/token?grant_type=authorization_code&code={CODE}&redirect_uri={REDIRECT_URI}&code_verifier={VERIFIER} + +Headers: +Authorization: Basic {Base64(ClientID:ClientSecret)} +``` + +--- + +## Deauthorization + +When a user removes your app, Zoom sends a webhook to your Deauthorization Notification Endpoint URL. + +### Webhook Event + +```json +{ + "event": "app_deauthorized", + "event_ts": 1740439732278, + "payload": { + "account_id": "ACCOUNT_ID", + "user_id": "USER_ID", + "signature": "SIGNATURE", + "deauthorization_time": "2019-06-17T13:52:28.632Z", + "client_id": "CLIENT_ID" + } +} +``` + +### Requirements + +- **Delete all associated user data** after receiving this event +- **Verify webhook signature** (use secret token, verification token deprecated Oct 2023) +- Only public apps receive deauthorization webhooks (not private/dev apps) + +--- + +## Pre-Approval Flow + +Some Zoom accounts require Marketplace admin pre-approval before users can authorize apps. + +- Users can request pre-approval from their admin +- Account-level apps (admin scopes) require appropriate role permissions + +--- + +## Active Apps Notifier (AAN) + +In-meeting feature showing apps with real-time access to content. + +- Displays icon + tooltip with app info, content type being accessed, approving account +- Supported: Zoom client 5.6.7+, Meeting SDK 5.9.0+ + +--- + +## OAuth Scopes + +### Scope Types + +| Type | Description | For | +|------|-------------|-----| +| **Classic scopes** | Legacy scopes (user, admin, master levels) | Existing apps | +| **Granular scopes** | New fine-grained scopes with optional support | New apps | + +### Classic Scopes + +For previously-created apps. Three levels: +- **User-level**: Access to individual user's data +- **Admin-level**: Account-wide access, requires admin role +- **Master-level**: For master-sub account setups, requires account owner + +Full list: https://developers.zoom.us/docs/integrations/oauth-scopes/ + +### Granular Scopes + +For new apps. Format: `:::` + +| Component | Values | +|-----------|--------| +| **service** | `meeting`, `webinar`, `user`, `recording`, etc. | +| **action** | `read`, `write`, `update`, `delete` | +| **data_claim** | Data category (e.g., `participants`, `settings`) | +| **access** | empty (user), `admin`, `master` | + +Example: `meeting:read:list_meetings:admin` + +Full list: https://developers.zoom.us/docs/integrations/oauth-scopes-granular/ + +### Optional Scopes + +Granular scopes can be marked as **optional** - users choose whether to grant them. + +**Basic authorization** (uses build flow defaults): +``` +https://zoom.us/oauth/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI} +``` + +**Advanced authorization** (custom scopes per request): +``` +https://zoom.us/oauth/authorize?client_id={CLIENT_ID}&response_type=code&redirect_uri={REDIRECT_URI}&scope={required_scopes}&optional_scope={optional_scopes} +``` + +**Include previously granted scopes:** +``` +https://zoom.us/oauth/authorize?...&include_granted_scopes&scope={additional_scopes} +``` + +### Migrating Classic to Granular + +1. Manage > select app > edit +2. Scope page > Development tab > click **Migrate** +3. Review auto-assigned granular scopes, remove unnecessary, mark optional +4. Test +5. Production tab > click **Migrate** + +**Notes:** +- No review needed if only migrating or reducing scopes +- Existing user tokens continue with classic scope values until re-authorization +- New users get granular scopes after migration + +--- + +## Common Error Codes + +| Code | Message | Solution | +|------|---------|----------| +| 4700 | Token cannot be empty | Check Authorization header has valid token | +| 4702/4704 | Invalid client | Verify Client ID and Client Secret | +| 4705 | Grant type not supported | Use: `account_credentials`, `authorization_code`, `urn:ietf:params:oauth:grant-type:device_code`, or `client_credentials` | +| 4706 | Client ID or secret missing | Add credentials to header or request params | +| 4709 | Redirect URI mismatch | Ensure redirect_uri matches app configuration exactly (including trailing slash) | +| 4711 | Refresh token invalid | Token scopes don't match client scopes | +| 4717 | App has been disabled | Contact Zoom support | +| 4733 | Code is expired | Authorization codes expire in 5 minutes - restart flow | +| 4734 | Invalid authorization code | Regenerate authorization code | +| 4735 | Owner of token does not exist | User was removed from account - re-authorize | +| 4741 | Token has been revoked | Use the most recent token from latest authorization | + +See `references/oauth-errors.md` for complete error list. + +--- + +## Quick Reference + +| Flow | Grant Type | Token Expiry | Refresh | +|------|------------|--------------|---------| +| Account (S2S) | `account_credentials` | 1 hour | Request new token | +| User | `authorization_code` | 1 hour | Use refresh_token (90 day expiry) | +| Device | `urn:ietf:params:oauth:grant-type:device_code` | 1 hour | Use refresh_token (90 day expiry) | +| Client (Chatbot) | `client_credentials` | 1 hour | Request new token | + +--- + +## Demo Guidance + +If you build an OAuth demo app, document its runtime base URL in that demo project's own +README or `.env.example`, not in this shared skill. + +## Resources + +- **OAuth docs**: https://developers.zoom.us/docs/integrations/oauth/ +- **S2S OAuth docs**: https://developers.zoom.us/docs/internal-apps/s2s-oauth/ +- **PKCE blog**: https://developers.zoom.us/blog/pcke-oauth-with-postman-rest-api/ +- **Classic scopes**: https://developers.zoom.us/docs/integrations/oauth-scopes/ +- **Granular scopes**: https://developers.zoom.us/docs/integrations/oauth-scopes-granular/ + +--- + +## Integrated Index + +_This section was migrated from `SKILL.md`._ + +## Quick Start Path + +**If you're new to Zoom OAuth, follow this order:** + +1. **Run preflight checks first** → [RUNBOOK.md](../RUNBOOK.md) + +2. **Choose your OAuth flow** → [concepts/oauth-flows.md](../concepts/oauth-flows.md) + - 4 flows: S2S (backend), User (SaaS), Device (no browser), Chatbot + - Decision matrix: Which flow fits your use case? + +3. **Understand token lifecycle** → [concepts/token-lifecycle.md](../concepts/token-lifecycle.md) + - **CRITICAL**: How tokens expire, refresh, and revoke + - Common pitfalls: refresh token rotation + +4. **Implement your flow** → Jump to examples: + - Backend automation → [examples/s2s-oauth-redis.md](../examples/s2s-oauth-redis.md) + - SaaS app → [examples/user-oauth-mysql.md](../examples/user-oauth-mysql.md) + - Mobile/SPA → [examples/pkce-implementation.md](../examples/pkce-implementation.md) + - Device (TV/kiosk) → [examples/device-flow.md](../examples/device-flow.md) + +5. **Fix redirect URI issues** → [troubleshooting/redirect-uri-issues.md](../troubleshooting/redirect-uri-issues.md) + - Most common OAuth error: Redirect URI mismatch + +6. **Implement token refresh** → [examples/token-refresh.md](../examples/token-refresh.md) + - Automatic middleware pattern + - Handle refresh token rotation + +7. **Troubleshoot errors** → [troubleshooting/common-errors.md](../troubleshooting/common-errors.md) + - Error code tables (4700-4741 range) + - Quick diagnostic workflow + +--- + +## Documentation Structure + +``` +oauth/ +├── SKILL.md # Main skill overview +├── SKILL.md # This file - navigation guide +│ +├── concepts/ # Core OAuth concepts +│ ├── oauth-flows.md # 4 flows: S2S, User, Device, Chatbot +│ ├── token-lifecycle.md # Expiration, refresh, revocation +│ ├── pkce.md # PKCE security for public clients +│ ├── scopes-architecture.md # Classic vs Granular scopes +│ └── state-parameter.md # CSRF protection with state +│ +├── examples/ # Complete working code +│ ├── s2s-oauth-basic.md # S2S OAuth minimal example +│ ├── s2s-oauth-redis.md # S2S OAuth with Redis caching (production) +│ ├── user-oauth-basic.md # User OAuth minimal example +│ ├── user-oauth-mysql.md # User OAuth with MySQL + encryption (production) +│ ├── device-flow.md # Device authorization flow +│ ├── pkce-implementation.md # PKCE for SPAs/mobile apps +│ └── token-refresh.md # Auto-refresh middleware pattern +│ +├── troubleshooting/ # Problem solving guides +│ ├── common-errors.md # Error codes 4700-4741 +│ ├── redirect-uri-issues.md # Most common OAuth error +│ ├── token-issues.md # Expired, revoked, invalid tokens +│ └── scope-issues.md # Scope mismatch errors +│ +└── references/ # Reference documentation + ├── oauth-errors.md # Complete error code reference + ├── classic-scopes.md # Classic scope lookup guide + └── granular-scopes.md # Granular scope lookup guide +``` + +--- + +## By Use Case + +### I want to automate Zoom tasks on my own account +1. [OAuth Flows](../concepts/oauth-flows.md) - S2S OAuth explained +2. [S2S OAuth Redis](../examples/s2s-oauth-redis.md) - Production pattern with Redis caching +3. [Token Lifecycle](../concepts/token-lifecycle.md) - 1hr token, no refresh + +### I want to build a SaaS app for other Zoom users +1. [OAuth Flows](../concepts/oauth-flows.md) - User OAuth explained +2. [User OAuth MySQL](../examples/user-oauth-mysql.md) - Production pattern with encryption +3. [Token Refresh](../examples/token-refresh.md) - Automatic refresh middleware +4. [Redirect URI Issues](../troubleshooting/redirect-uri-issues.md) - Fix most common error + +### I want to build a mobile or SPA app +1. [PKCE](../concepts/pkce.md) - Why PKCE is required for public clients +2. [PKCE Implementation](../examples/pkce-implementation.md) - Complete code example +3. [State Parameter](../concepts/state-parameter.md) - CSRF protection + +### I want to build an app for devices without browsers (TV, kiosk) +1. [OAuth Flows](../concepts/oauth-flows.md) - Device flow explained +2. [Device Flow Example](../examples/device-flow.md) - Complete polling implementation +3. [Common Errors](../troubleshooting/common-errors.md) - Device-specific errors + +### I'm building a Team Chat bot +1. [OAuth Flows](../concepts/oauth-flows.md) - Chatbot flow explained +2. [S2S OAuth Basic](../examples/s2s-oauth-basic.md) - Similar pattern, different grant type +3. [Scopes Architecture](../concepts/scopes-architecture.md) - Chatbot-specific scopes + +### I'm getting redirect URI errors (4709) +1. [Redirect URI Issues](../troubleshooting/redirect-uri-issues.md) - **START HERE!** +2. [Common Errors](../troubleshooting/common-errors.md) - Error details +3. [User OAuth Basic](../examples/user-oauth-basic.md) - See correct pattern + +### I'm getting token errors (4700-4741) +1. [Token Issues](../troubleshooting/token-issues.md) - Diagnostic workflow +2. [Token Lifecycle](../concepts/token-lifecycle.md) - Understand expiration +3. [Token Refresh](../examples/token-refresh.md) - Implement auto-refresh +4. [Common Errors](../troubleshooting/common-errors.md) - Error code tables + +### I'm getting scope errors (4711) +1. [Scope Issues](../troubleshooting/scope-issues.md) - Mismatch causes +2. [Scopes Architecture](../concepts/scopes-architecture.md) - Classic vs Granular +3. [Classic Scopes](../references/classic-scopes.md) - Lookup workflow and official source +4. [Granular Scopes](../references/granular-scopes.md) - Lookup workflow and official source + +### I need to refresh tokens +1. [Token Lifecycle](../concepts/token-lifecycle.md) - When to refresh +2. [Token Refresh](../examples/token-refresh.md) - Middleware pattern +3. [Token Issues](../troubleshooting/token-issues.md) - Common mistakes + +### I want to understand the difference between Classic and Granular scopes +1. [Scopes Architecture](../concepts/scopes-architecture.md) - **Complete comparison** +2. [Classic Scopes](../references/classic-scopes.md) - `resource:level` format and official source +3. [Granular Scopes](../references/granular-scopes.md) - `service:action:data_claim:access` format and official source + +### I need to secure my OAuth implementation +1. [PKCE](../concepts/pkce.md) - Public client security +2. [State Parameter](../concepts/state-parameter.md) - CSRF protection +3. [User OAuth MySQL](../examples/user-oauth-mysql.md) - Token encryption at rest + +### I want to migrate from JWT app to S2S OAuth +1. [S2S OAuth Redis](../examples/s2s-oauth-redis.md) - Modern replacement +2. [Token Lifecycle](../concepts/token-lifecycle.md) - Different token behavior + +> **Note**: JWT App Type was deprecated in June 2023. Migrate to S2S OAuth for server-to-server automation. + +--- + +## Most Critical Documents + +### 1. OAuth Flows (DECISION DOCUMENT) +**[concepts/oauth-flows.md](../concepts/oauth-flows.md)** + +Understand which of the 4 flows to use: +- **S2S OAuth**: Backend automation (your account) +- **User OAuth**: SaaS apps (users authorize you) +- **Device Flow**: Devices without browsers +- **Chatbot**: Team Chat bots only + +### 2. Token Lifecycle (MOST COMMON ISSUE) +**[concepts/token-lifecycle.md](../concepts/token-lifecycle.md)** + +99% of OAuth issues stem from misunderstanding: +- Token expiration (1 hour for all flows) +- Refresh token rotation (must save new refresh token) +- Revocation behavior (invalidates all tokens) + +### 3. Redirect URI Issues (MOST COMMON ERROR) +**[troubleshooting/redirect-uri-issues.md](../troubleshooting/redirect-uri-issues.md)** + +Error 4709 ("Redirect URI mismatch") is the #1 OAuth error. +Must match EXACTLY (including trailing slash, http vs https). + +--- + +## Key Learnings + +### Critical Discoveries: + +1. **Refresh Token Rotation** + - Each refresh returns a NEW refresh token + - Old refresh token becomes invalid + - Failure to save new token causes 4735 errors + - See: [Token Refresh](../examples/token-refresh.md) + +2. **S2S OAuth Uses Redis, User OAuth Uses Database** + - S2S: Single token for entire account → Redis (ephemeral) + - User: Per-user tokens → Database (persistent) + - See: [S2S OAuth Redis](../examples/s2s-oauth-redis.md) vs [User OAuth MySQL](../examples/user-oauth-mysql.md) + +3. **Redirect URI Must Match EXACTLY** + - Trailing slash matters: `/callback` ≠ `/callback/` + - Protocol matters: `http://` ≠ `https://` + - Port matters: `:3000` ≠ `:3001` + - See: [Redirect URI Issues](../troubleshooting/redirect-uri-issues.md) + +4. **PKCE Required for Public Clients** + - Mobile apps CANNOT keep secrets + - SPAs CANNOT keep secrets + - PKCE prevents authorization code interception + - See: [PKCE](../concepts/pkce.md) + +5. **State Parameter Prevents CSRF** + - Generate random state before redirect + - Store in session + - Verify on callback + - See: [State Parameter](../concepts/state-parameter.md) + +6. **Token Storage Must Be Encrypted** + - NEVER store tokens in plain text + - Use AES-256 minimum + - See: [User OAuth MySQL](../examples/user-oauth-mysql.md) + +7. **JWT App Type is Deprecated (June 2023)** + - No new JWT apps can be created + - Existing apps still work but will eventually be sunset + - Migrate to S2S OAuth or User OAuth + +8. **Scope Levels Determine Authorization Requirements** + - No suffix (user-level): Any user can authorize + - `:admin`: Requires admin role + - `:master`: Requires account owner (multi-account) + - See: [Scopes Architecture](../concepts/scopes-architecture.md) + +9. **Authorization Codes Expire in 5 Minutes** + - Exchange code for token immediately + - Don't cache authorization codes + - See: [Token Lifecycle](../concepts/token-lifecycle.md) + +10. **Device Flow Requires Polling** + - Poll at interval returned by `/devicecode` (usually 5s) + - Handle `authorization_pending`, `slow_down`, `expired_token` + - See: [Device Flow](../examples/device-flow.md) + +--- + +## Quick Reference + +### "Which OAuth flow should I use?" +→ [OAuth Flows](../concepts/oauth-flows.md) + +### "Redirect URI mismatch error (4709)" +→ [Redirect URI Issues](../troubleshooting/redirect-uri-issues.md) + +### "Token expired or invalid" +→ [Token Issues](../troubleshooting/token-issues.md) + +### "Refresh token invalid (4735)" +→ [Token Refresh](../examples/token-refresh.md) - Must save new refresh token + +### "Scope mismatch error (4711)" +→ [Scope Issues](../troubleshooting/scope-issues.md) + +### "How do I secure my OAuth app?" +→ [PKCE](../concepts/pkce.md) + [State Parameter](../concepts/state-parameter.md) + +### "How do I implement auto-refresh?" +→ [Token Refresh](../examples/token-refresh.md) + +### "What's the difference between Classic and Granular scopes?" +→ [Scopes Architecture](../concepts/scopes-architecture.md) + +### "What error code means what?" +→ [Common Errors](../troubleshooting/common-errors.md) + +--- + +## Document Version + +Based on **Zoom OAuth API v2** (2024+) + +**Deprecated:** JWT App Type (June 2023) + +--- + +**Happy coding!** + +Remember: Start with [OAuth Flows](../concepts/oauth-flows.md) to understand which flow fits your use case! + +## Environment Variables + +- See [references/environment-variables.md](../references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/plugins/zoom-developers/skills/oauth/references/granular-scopes.md b/plugins/zoom-developers/skills/oauth/references/granular-scopes.md new file mode 100644 index 00000000..2cea5d60 --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/references/granular-scopes.md @@ -0,0 +1,38 @@ +# Granular OAuth Scopes + +Source: https://developers.zoom.us/docs/integrations/oauth-scopes-granular/ + +This file is intentionally compact. Do not mirror the full Zoom granular scope catalog into the plugin; it changes over time and is too large for a local Codex reference bundle. Use the official source above as the authoritative endpoint-to-scope lookup. + +## Format + +Granular scopes are narrower than classic scopes and usually follow this shape: + +```text +{service}:{action}:{data_claim}:{access_level} +``` + +Common parts: + +| Part | Examples | Meaning | +|---|---|---| +| `service` | `meeting`, `cloud_recording`, `docs`, `whiteboard`, `contact_center` | Zoom product/API area | +| `action` | `read`, `write`, `update`, `delete` | Operation type | +| `data_claim` | `assets`, `search`, `content`, `whiteboard`, `collaborator` | Specific resource or capability | +| `access_level` | `user`, `admin`, `master` | Own-user, account admin, or master-account access | + +Prefer granular scopes when the app only needs specific capabilities. They reduce consent surface and avoid overbroad classic scopes. + +## Lookup Workflow + +1. Identify the exact API endpoint or SDK capability. +2. Open the official granular scope catalog. +3. Search by endpoint name, operation ID, or resource term. +4. Add only the listed scopes for the operation. +5. Re-authorize the user after adding scopes; existing user tokens will not automatically gain newly added scopes. + +## Common Mistakes + +- Do not guess granular scope names from the URL path; verify against the official scope catalog. +- Do not mix user-level and admin-level assumptions. A user token cannot perform admin operations unless the app and authorizing user have the required admin scope and permissions. +- Do not rely on old tokens after adding scopes. Re-run OAuth authorization and token exchange. diff --git a/plugins/zoom-developers/skills/oauth/references/oauth-errors.md b/plugins/zoom-developers/skills/oauth/references/oauth-errors.md new file mode 100644 index 00000000..17e82d11 --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/references/oauth-errors.md @@ -0,0 +1,41 @@ +# OAuth Error Messages + +> Source: https://developers.zoom.us/docs/integrations/oauth/ + +This table lists OAuth error messages, possible causes, and recommended mitigations. + +| Error Code | Error Message | Description | Guidance | +|------------|---------------|-------------|----------| +| 4700 | *(empty)* | The cause can vary depending on the API. | Use the tracking ID to find additional information in the logs and contact Zoom for further assistance. | +| 4700 | Token cannot be empty. | The token is missing. | Verify that the token is present in the header and that its value is correct. | +| 4700 | Exception message | This is a catch all for unexpected errors. | Report the error code to Zoom for further assistance. | +| 4702, 4704 | Invalid client. / Invalid client secret. | Client ID does not match authenticated client. The client ID or client secret isn't entered correctly, or the related app doesn't exist. | Verify that the client ID and client secret are entered in the header correctly. If they are correct then contact Zoom for further assistance. | +| 4705 | Grant type is not supported from token endpoint. | The grant type is not supported by the token endpoint. | Use a valid grant type against `https://zoom.us/oauth/token` (for example `authorization_code`, `refresh_token`, `account_credentials`, `client_credentials`, `urn:ietf:params:oauth:grant-type:device_code`). | +| 4706 | Client ID or client secret is missing. | The client ID and client secret are missing either in the header or in the request parameter. | Verify that the client ID and client secret are entered correctly in the header or request parameter. | +| 4706 | Missing grant type. | OAuth requires the grant type, and it is missing in the header. | Verify that the grant type is entered in the header. | +| 4709 | Redirect URI mismatch. | The redirect_uri is missing or the value is null or is incorrect. | Verify that the redirect_uri is entered correctly. | +| 4711 | Refresh token invalid. | The token scopes do not match with the client scopes. | Verify that there isn't a mismatch between the token's scopes and the client's scopes. | +| 4717 | The app has been disabled | The app has been disabled. | Contact Zoom support to enable the app. | +| 4724 | Exception error message. | An invalid JWT token is passed in the header. | Verify that the JWT token is correctly signed and that the token passed in the header is valid. | +| 4732 | Creating authorization code error. | The lookup service may be down. ELK logs usually throw a `/lookup/v1/indexes POST 5005` Internal Server error. | Contact your DNS lookup service provider to verify the server status, or contact Zoom for further support. | +| 4733 | Code is expired | Authorization codes have an expiration time of 5 minutes. | Regenerate the authorization code. | +| 4734 | Invalid authorization code. | The authorization code is invalid. | Regenerate the authorization code. | +| 4735 | The owner of the token does not exist. | The user ID of the token doesn't exist. This might happen if the refresh token was issued for a user who has since been removed from an account. The user ID is stored in the `uid` field of a token. | Verify the `uid` for the token is valid and entered correctly. | +| 4737 | Can not find the authentication for the access token. | The refresh token isn't found in the DynamoDB table. | Contact Zoom and request to reauthorize the app. | +| 4738 | The token is disabled by admin. | An admin turned off pre-approval for the related app for users under an account. | Contact Zoom for further support. | +| 4740 | The token ID is out of the token tolerance range. | The maximum number of times a refresh token is allowed to be used has been surpassed. Tolerance errors happen with version 7 tokens. Version 8 and later tokens do not use the tolerance mechanism. | Contact Zoom for assistance with reconfiguring the tolerance range. | +| 4741 | The token has been revoked. | This happens when you perform multiple authorizations. With multiple authorizations, the last token issued is considered valid, and the previous ones are invalidated. | Make sure you are using the latest and valid authorization token. | + +## Common Issues Quick Reference + +| Symptom | Check | +|---------|-------| +| Empty error (4700) | Check tracking ID in logs | +| Invalid client (4702/4704) | Verify Client ID and Client Secret | +| Grant type error (4705) | Use: `refresh_token`, `authorization_code`, `device_auth`, `account_credentials` | +| Missing credentials (4706) | Ensure Client ID/Secret in header or request params | +| Redirect mismatch (4709) | Verify redirect_uri matches app configuration | +| Token scope mismatch (4711) | Compare token scopes vs client scopes | +| Code expired (4733) | Authorization codes expire in 5 minutes | +| Invalid code (4734) | Regenerate authorization code | +| Token revoked (4741) | Use the most recent token from latest authorization | diff --git a/plugins/zoom-developers/skills/oauth/troubleshooting/common-errors.md b/plugins/zoom-developers/skills/oauth/troubleshooting/common-errors.md new file mode 100644 index 00000000..376e6f1d --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/troubleshooting/common-errors.md @@ -0,0 +1,13 @@ +# Common Errors + +See [../references/oauth-errors.md](../references/oauth-errors.md) for complete error reference. + +Common OAuth error codes: 4700-4741 + +For specific error details, consult the error reference documentation. + +## High-Frequency Endpoint Mistake + +- Use `https://zoom.us/oauth/authorize` for user consent. +- Use `https://zoom.us/oauth/token` for token exchange. +- If token calls return HTML or 404, check that you are not calling `/oauth/token`. diff --git a/plugins/zoom-developers/skills/oauth/troubleshooting/redirect-uri-issues.md b/plugins/zoom-developers/skills/oauth/troubleshooting/redirect-uri-issues.md new file mode 100644 index 00000000..6a21b3ba --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/troubleshooting/redirect-uri-issues.md @@ -0,0 +1,7 @@ +# Redirect Uri Issues + +See [../references/oauth-errors.md](../references/oauth-errors.md) for complete error reference. + +Common OAuth error codes: 4700-4741 + +For specific error details, consult the error reference documentation. diff --git a/plugins/zoom-developers/skills/oauth/troubleshooting/scope-issues.md b/plugins/zoom-developers/skills/oauth/troubleshooting/scope-issues.md new file mode 100644 index 00000000..31ec61f5 --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/troubleshooting/scope-issues.md @@ -0,0 +1,7 @@ +# Scope Issues + +See [../references/oauth-errors.md](../references/oauth-errors.md) for complete error reference. + +Common OAuth error codes: 4700-4741 + +For specific error details, consult the error reference documentation. diff --git a/plugins/zoom-developers/skills/oauth/troubleshooting/token-issues.md b/plugins/zoom-developers/skills/oauth/troubleshooting/token-issues.md new file mode 100644 index 00000000..36a1126b --- /dev/null +++ b/plugins/zoom-developers/skills/oauth/troubleshooting/token-issues.md @@ -0,0 +1,7 @@ +# Token Issues + +See [../references/oauth-errors.md](../references/oauth-errors.md) for complete error reference. + +Common OAuth error codes: 4700-4741 + +For specific error details, consult the error reference documentation. diff --git a/plugins/zoom-developers/skills/phone/RUNBOOK.md b/plugins/zoom-developers/skills/phone/RUNBOOK.md new file mode 100644 index 00000000..6dde6f8e --- /dev/null +++ b/plugins/zoom-developers/skills/phone/RUNBOOK.md @@ -0,0 +1,47 @@ +# Zoom Phone 5-Minute Preflight Runbook + +Use this before deep debugging. + +## 1) Confirm Product Prerequisites + +- Zoom Phone licenses assigned. +- Admin access available for Phone settings. +- If SMS is required, 10DLC/SMS setup is complete. + +## 2) Confirm App and OAuth + +- App type: General OAuth app for user/admin flows. +- Redirect URI and allow list are exact and current. +- Required Phone scopes are added. +- App is installed/re-authorized after scope changes. + +## 3) Confirm Integration Surface + +- Smart Embed: iframe/script loaded and approved domain configured. +- API/Webhook: access token valid and webhook endpoint reachable. +- URI launch: endpoint uses supported scheme and client is signed in. + +## 4) Confirm Event/Data Correlation + +- Persist `call_id` for real-time events. +- Persist `call_history_uuid` and `call_element_id` for post-call lookup. +- Keep idempotency logic for duplicate event deliveries. + +## 5) Confirm Migration Posture + +- Do not build new features on legacy v1 call logs. +- Webhook consumers are ready for `call_element` event names/fields. +- Field-mapping adapter exists for old/new payload shapes. + +## 6) Confirm Security Controls + +- Smart Embed `postMessage` enforces trusted origin. +- Webhook signatures validated with secret token. +- OAuth secrets are server-side only. + +## 7) Fast Decision Tree + +- Smart Embed iframe visible but no events -> missing init sequence or bad origin filtering. +- OAuth works but API fails with 401/403 -> scope mismatch or stale authorization. +- Data pipeline breaks after endpoint/event upgrade -> missing v2/v3 field mapping. +- URI click does nothing -> unsupported platform/client state, or wrong scheme. diff --git a/plugins/zoom-developers/skills/phone/SKILL.md b/plugins/zoom-developers/skills/phone/SKILL.md new file mode 100644 index 00000000..430989f7 --- /dev/null +++ b/plugins/zoom-developers/skills/phone/SKILL.md @@ -0,0 +1,25 @@ +--- +name: build-zoom-phone-integration +description: Use when building Phone. +--- + +# Build Zoom Phone Integration + +Use this skill when the target workflow is Zoom Phone, CTI, CRM calling, call events, or Smart Embed behavior. + +## Workflow + +1. Classify the integration: REST API automation, webhook event processing, Smart Embed, URI launch, CRM dialer, or call-handling workflow. +2. Confirm the actor, account settings, phone entitlements, and OAuth scopes before implementation. +3. Keep call control, call records, number management, and webhook processing as separate modules. +4. For Smart Embed, validate postMessage event contracts, embedding constraints, and CRM state mapping. +5. Debug by checking scopes, phone license, role permissions, number ownership, webhook subscriptions, and event payload shape. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- Architecture and lifecycle: [concepts/architecture-and-lifecycle.md](concepts/architecture-and-lifecycle.md) +- Phone API service pattern: [examples/phone-api-service-pattern.md](examples/phone-api-service-pattern.md) +- Smart Embed bridge: [examples/smart-embed-postmessage-bridge.md](examples/smart-embed-postmessage-bridge.md) +- Event contract: [references/smart-embed-event-contract.md](references/smart-embed-event-contract.md) +- Common issues: [troubleshooting/common-issues.md](troubleshooting/common-issues.md) diff --git a/plugins/zoom-developers/skills/phone/concepts/architecture-and-lifecycle.md b/plugins/zoom-developers/skills/phone/concepts/architecture-and-lifecycle.md new file mode 100644 index 00000000..bd25fa0d --- /dev/null +++ b/plugins/zoom-developers/skills/phone/concepts/architecture-and-lifecycle.md @@ -0,0 +1,57 @@ +# Zoom Phone Architecture and Lifecycle + +## Architecture + +```text +User/Agent UI + | + | (A) Smart Embed postMessage events + v +Smart Embed Iframe (applications.zoom.us) + | + | event stream + call controls + v +CRM Web App (event bridge + UI state) + | + | OAuth token on server only + v +Backend API Layer + |\ + | \-- Zoom Phone REST APIs (call history, call handling, contacts) + | + \---- Webhook endpoint (phone.* events) +``` + +## Lifecycle Workflow + +1. Provision: +- Account has Zoom Phone and optional SMS enablement. + +2. Authorize: +- OAuth app installed and scoped for required Phone operations. + +3. Initialize UI: +- Load Smart Embed iframe/script. +- Wait for `onZoomPhoneIframeApiReady`. +- Send `zp-init-config` and register event handlers. + +4. Engage: +- Start calls/SMS via `zp-make-call` or `zp-input-sms`. +- Receive events (`zp-call-*`, `zp-sms-log-event`, optional AI/contact/notes events). + +5. Persist: +- Save event snapshots keyed by `callId`. +- Reconcile to call history/call element records after completion. + +6. Post-call: +- Save call notes/disposition and optional recording/voicemail links. + +7. Operate: +- Track deprecations and apply endpoint/event mapping updates. + +## Version Drift Strategy + +- Normalize inbound payloads in one adapter layer. +- Keep endpoint constants centralized by version target. +- Feature-flag optional payload fields. +- Keep webhook + Smart Embed event handlers tolerant to added fields and enum expansion. diff --git a/plugins/zoom-developers/skills/phone/examples/phone-api-service-pattern.md b/plugins/zoom-developers/skills/phone/examples/phone-api-service-pattern.md new file mode 100644 index 00000000..f3d2bf41 --- /dev/null +++ b/plugins/zoom-developers/skills/phone/examples/phone-api-service-pattern.md @@ -0,0 +1,41 @@ +# Phone API Service Pattern (Migration-Safe) + +## Pattern goals + +- Isolate OAuth token usage to server code. +- Support current call history/call element model. +- Keep compatibility with old payload fields while migrating. + +## Service example + +```javascript +export async function getCallHistory(accessToken, from, to) { + const qs = new URLSearchParams({ from, to }).toString(); + const res = await fetch(`https://api.zoom.us/v2/phone/call_history?${qs}`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + if (!res.ok) throw new Error(`call_history failed: ${res.status}`); + + const data = await res.json(); + + // Normalize v2/v3 style for downstream code. + return (data.call_history || data.call_logs || []).map((row) => ({ + callHistoryUuid: row.call_history_uuid || row.id, + callId: row.call_id, + raw: row, + })); +} + +export async function getCallElement(accessToken, callElementId) { + const res = await fetch(`https://api.zoom.us/v2/phone/call_element/${callElementId}`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + if (!res.ok) throw new Error(`call_element failed: ${res.status}`); + return res.json(); +} +``` + +## Operational notes + +- Add explicit logging when fallback fields (`call_logs`, `call_path`) are encountered. +- Remove fallback path once migration is complete. diff --git a/plugins/zoom-developers/skills/phone/examples/smart-embed-postmessage-bridge.md b/plugins/zoom-developers/skills/phone/examples/smart-embed-postmessage-bridge.md new file mode 100644 index 00000000..b88bb310 --- /dev/null +++ b/plugins/zoom-developers/skills/phone/examples/smart-embed-postmessage-bridge.md @@ -0,0 +1,49 @@ +# Smart Embed postMessage Bridge Pattern + +## Why this pattern + +Smart Embed event/control flow is `window.postMessage` based. Reliability depends on strict initialization order and origin validation. + +## Pattern + +```javascript +const ZOOM_ORIGIN = 'https://applications.zoom.us'; +const iframe = document.querySelector('#zoom-embeddable-phone-iframe'); + +function initSmartEmbed(config) { + iframe?.contentWindow?.postMessage({ + type: 'zp-init-config', + data: config, + }, ZOOM_ORIGIN); +} + +function makeCall(number, callerId) { + iframe?.contentWindow?.postMessage({ + type: 'zp-make-call', + data: { number, callerId, autoDial: true }, + }, ZOOM_ORIGIN); +} + +window.addEventListener('message', (event) => { + if (event.origin !== ZOOM_ORIGIN) return; + const payload = event.data; + if (!payload?.type) return; + + switch (payload.type) { + case 'zp-call-ringing-event': + case 'zp-call-connected-event': + case 'zp-call-ended-event': + case 'zp-call-log-completed-event': + handlePhoneEvent(payload); + break; + default: + break; + } +}); +``` + +## Operational notes + +- Call APIs only after iframe readiness callback. +- Persist `event.id` (if present) for idempotency. +- Keep event dispatcher tolerant to new event types. diff --git a/plugins/zoom-developers/skills/phone/references/call-handling-patterns.md b/plugins/zoom-developers/skills/phone/references/call-handling-patterns.md new file mode 100644 index 00000000..26d9a667 --- /dev/null +++ b/plugins/zoom-developers/skills/phone/references/call-handling-patterns.md @@ -0,0 +1,34 @@ +# Call Handling API Patterns + +## Endpoint family + +- `POST /phone/extension/{extensionId}/call_handling/settings/{settingType}` +- `PATCH /phone/extension/{extensionId}/call_handling/settings/{settingType}` +- `GET /phone/extension/{extensionId}/call_handling/settings` + +## Supported extension targets + +- Users +- Auto receptionists +- Call queues + +## Common subsettings + +- `custom_hours` +- `holiday` +- `call_handling` +- `call_forwarding` (user-focused) + +## Practical implementation pattern + +1. Read current settings snapshot with `GET`. +2. Build small, typed patch payloads by subsetting. +3. Update business/closed/holiday hours independently. +4. Validate E.164 formatting for external phone numbers. +5. Store previous settings for rollback. + +## Drift watchpoints + +- Enum/action values may evolve. +- Routing field names differ between docs sections and old implementations. +- Keep a server-side validator to reject malformed call-handling payloads before API call. diff --git a/plugins/zoom-developers/skills/phone/references/crm-sample-validation.md b/plugins/zoom-developers/skills/phone/references/crm-sample-validation.md new file mode 100644 index 00000000..dca3910a --- /dev/null +++ b/plugins/zoom-developers/skills/phone/references/crm-sample-validation.md @@ -0,0 +1,36 @@ +# CRM Sample Validation (https://github.com/zoom/CRM-Sample) + +## Useful architecture patterns adopted + +- Smart Embed as dedicated iframe sidebar component. +- Server-only OAuth token handling with `next-auth` callbacks. +- API route pattern that reads session token and calls Phone APIs. +- Client-side event listener for Smart Embed message events. + +## Environment keys observed in sample + +- `ZOOM_CLIENT_ID` +- `ZOOM_CLIENT_SECRET` +- `NEXTAUTH_URL` +- `NEXTAUTH_SECRET` + +## Lifecycle pattern extracted + +1. User authenticates with Zoom OAuth. +2. Server stores access/refresh token session state. +3. UI renders Smart Embed iframe. +4. UI sends click-to-call command and listens for events. +5. Backend fetches call history/contact data for CRM views. + +## Contradictions and drift issues found + +- Sample still maps response via `data.call_logs` (legacy shape) while migration docs push toward call history/call element shapes. +- README references `.env.example`, repository provides `.env.sample`. +- Middleware matcher and route naming are inconsistent (`/call-log` vs `/call-logs`, missing `/api/calls/[id]` route used by modal). +- Sample contains hardcoded demo records in some screens alongside live API calls. + +## Guidance + +- Treat sample as architectural reference, not canonical API contract. +- Apply migration-safe normalizers for call history fields. +- Validate each endpoint payload against current Phone API docs. diff --git a/plugins/zoom-developers/skills/phone/references/deprecations-and-migrations.md b/plugins/zoom-developers/skills/phone/references/deprecations-and-migrations.md new file mode 100644 index 00000000..ebfc31dd --- /dev/null +++ b/plugins/zoom-developers/skills/phone/references/deprecations-and-migrations.md @@ -0,0 +1,30 @@ +# Deprecations and Migration Notes (Zoom Phone) + +## Timeline extracted from docs + +- Legacy Call Logs API (v1) full deprecation: **April 2026**. +- Legacy Call Log webhooks (v1) full deprecation: **May 2026**. +- Legacy array fields deprecation: +- `call_log` array deprecation: **November 2026**. +- `call_path` array deprecation: **November 2026**. + +## API migration map + +- `GET /phone/call_logs` -> `GET /phone/call_history` +- `GET /phone/call_logs/{callLogId}` -> `GET /phone/call_history/{call_history_uuid}` +- `GET /phone/call_history_detail/{callHistoryId}` -> `GET /phone/call_element/{call_element_id}` + +## Webhook migration map + +- `phone.call_log_deleted` -> `phone.call_history_deleted` -> `phone.call_element_deleted` +- `phone.callee_call_log_completed` -> `phone.callee_call_history_completed` -> `phone.callee_call_element_completed` +- `phone.caller_call_log_completed` -> `phone.caller_call_history_completed` -> `phone.caller_call_element_completed` + +## Compatibility strategy + +- Standardize storage fields: +- `call_id` +- `call_history_uuid` +- `call_element_id` +- Add adapters for old/new field names during transition windows. +- Prefer v3 naming for all new features and schemas. diff --git a/plugins/zoom-developers/skills/phone/references/environment-variables.md b/plugins/zoom-developers/skills/phone/references/environment-variables.md new file mode 100644 index 00000000..5a626ac5 --- /dev/null +++ b/plugins/zoom-developers/skills/phone/references/environment-variables.md @@ -0,0 +1,26 @@ +# Zoom Phone Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_CLIENT_ID` | Yes | OAuth app identity for Phone APIs | Zoom Marketplace -> General OAuth app -> App Credentials | +| `ZOOM_CLIENT_SECRET` | Yes | OAuth token exchange | Zoom Marketplace -> General OAuth app -> App Credentials | +| `ZOOM_REDIRECT_URI` | Yes (user OAuth) | OAuth callback URL | Zoom Marketplace -> OAuth redirect/allow list | +| `ZOOM_ACCOUNT_ID` | Optional (S2S patterns) | Account-level service integrations | Zoom Marketplace -> Server-to-Server OAuth app credentials | +| `ZOOM_WEBHOOK_SECRET` or `WEBHOOK_SECRET_TOKEN` | Recommended | Webhook signature verification | Zoom Marketplace -> Features -> Event Subscriptions -> Secret Token | +| `ZOOM_PHONE_SMART_EMBED_URL` | Optional | Smart Embed iframe URL override | Zoom Phone Smart Embed docs (`applications.zoom.us` path) | +| `ZOOM_PHONE_SMART_EMBED_ORIGIN` | Recommended | Allowed postMessage origin | Set to `https://applications.zoom.us` | + +## Common runtime keys + +- `NEXTAUTH_URL` (if using NextAuth) +- `NEXTAUTH_SECRET` (if using NextAuth) +- `PORT` +- `NODE_ENV` + +## Notes + +- Keep OAuth secrets server-side only. +- Smart Embed approved domains are configured in Marketplace app settings, not in `.env`. +- Re-authorize app after changing scopes. diff --git a/plugins/zoom-developers/skills/phone/references/forum-top-questions.md b/plugins/zoom-developers/skills/phone/references/forum-top-questions.md new file mode 100644 index 00000000..939d0076 --- /dev/null +++ b/plugins/zoom-developers/skills/phone/references/forum-top-questions.md @@ -0,0 +1,95 @@ +--- +title: "Forum-Derived Top Questions (Phone)" +--- + +# Forum-Derived Top Questions (Phone) + +Use this as a checklist of the most common recent Developer Forum asks for Zoom Phone integrations. + +## Fast Routing Questions (Ask First) + +- Integration surface: Smart Embed, Phone REST API, webhooks, or URI launch (`zoomphonecall://`, `zoomphonesms://`). +- App/auth type: Server-to-Server OAuth vs user OAuth, and who the token is acting as. +- Account posture: Zoom Phone license assigned, user enabled, admin permissions, site/queue scope. +- Exact failure: HTTP status + Zoom `code`/`message` + endpoint/event name + sample payload. +- Correlation IDs available: `call_id`, `call_history_uuid`, `call_element_id`, recording ID. + +## Smart Embed Sign-In or Calling Fails + +Common asks: +- Smart Embed shows login but never completes. +- Widget loads, but outbound/inbound calling does not work. +- `zp-make-call`/search-and-match behaviors are inconsistent. + +Answer pattern: +- Confirm approved Smart Embed domain matches the real runtime origin exactly. +- Confirm `origin` parameter is domain-level where required and not path-mismatched. +- Verify Zoom client sign-in state and account licensing prerequisites. +- Add strict `postMessage` origin handling and validate event init sequence. + +## `call_logs` to `call_history` Migration Gaps + +Common asks: +- Missing fields after migrating to `call_history`. +- Existing call analytics pipelines break after deprecation migration. + +Answer pattern: +- Treat migration as a schema migration, not a drop-in endpoint swap. +- Build a mapping layer from legacy fields to current call history/call element fields. +- Persist both legacy and new IDs during transition for reconciliation. +- Update downstream reports that assumed removed fields. + +## Recording and Download URL Auth Errors + +Common asks: +- `download_url` returns 401/403. +- `Invalid access token, does not contain scopes` on recordings/transcripts. + +Answer pattern: +- Generate a fresh token from the app that owns the needed scopes. +- Re-authorize after scope changes; verify token scope set, not just app config. +- Handle redirects while preserving auth headers where needed. +- Keep a fallback retry path for temporary scope/permission regressions. + +## "Zoom Phone Has Not Been Enabled" (`2013`/`2031`) + +Common asks: +- Token works for some APIs/users but Phone endpoints return not enabled. + +Answer pattern: +- Verify `account_id` is present in S2S token request and token is from expected account. +- Verify target users actually have Zoom Phone entitlement. +- Verify caller/admin context has permission for account-level Phone resources. +- Re-test with one known-good licensed admin and one known-good licensed user. + +## Webhooks: Missing Events or Duplicates + +Common asks: +- Expected call events not received. +- Missed-call events delivered more than once. + +Answer pattern: +- Acknowledge webhooks quickly with `200`/`204` and process asynchronously. +- Implement idempotency keyed by event ID/call identifiers. +- Expect retries and occasional ordering variance. +- Validate event subscription scope and verify webhook logs before blaming delivery. + +## Correlating Calls Across APIs and Events + +Common asks: +- Hard to tie recordings, call path/history, and webhook events to one interaction. + +Answer pattern: +- Persist all call identifiers emitted at each lifecycle phase. +- Build a correlation table keyed by your internal interaction ID. +- Do not rely on a single identifier across all endpoints. + +## Pagination and Incomplete Result Sets + +Common asks: +- `/phone/users` or call list endpoints appear to miss records. + +Answer pattern: +- Always iterate `next_page_token` until exhausted. +- Keep query filters stable between page requests. +- Add dedupe + page-audit logging to detect loops or repeated pages. diff --git a/plugins/zoom-developers/skills/phone/references/full-guide.md b/plugins/zoom-developers/skills/phone/references/full-guide.md new file mode 100644 index 00000000..45ce9f9d --- /dev/null +++ b/plugins/zoom-developers/skills/phone/references/full-guide.md @@ -0,0 +1,69 @@ +# /build-zoom-phone-integration + +Background reference for Zoom Phone integrations across API, webhook, Smart Embed, and URI-launch workflows. + +Implementation guidance for Zoom Phone integrations across API, webhook/event, Smart Embed, and URI-launch workflows. + +Official docs: +- https://developers.zoom.us/docs/phone/ +- CRM sample reference: https://github.com/zoom/CRM-Sample + +## Routing Guardrail + +- If the user needs embedded softphone behavior in a web app, use Smart Embed ([examples/smart-embed-postmessage-bridge.md](../examples/smart-embed-postmessage-bridge.md)). +- If the user needs call records, analytics, or automation, use Phone REST API and webhooks ([references/deprecations-and-migrations.md](../references/deprecations-and-migrations.md)). +- If the user needs click-to-dial/SMS launch from external UI, use URI schemes (`zoomphonecall://`, `zoomphonesms://`). +- If the user mixes Zoom Phone and Contact Center, chain with [../contact-center/SKILL.md](../../contact-center/SKILL.md). + +## Quick Links + +Start here: +1. [concepts/architecture-and-lifecycle.md](../concepts/architecture-and-lifecycle.md) +2. [scenarios/high-level-scenarios.md](../scenarios/high-level-scenarios.md) +3. [references/deprecations-and-migrations.md](../references/deprecations-and-migrations.md) +4. [references/forum-top-questions.md](../references/forum-top-questions.md) +5. [references/smart-embed-event-contract.md](../references/smart-embed-event-contract.md) +6. [references/call-handling-patterns.md](../references/call-handling-patterns.md) +7. [references/environment-variables.md](../references/environment-variables.md) +8. [references/crm-sample-validation.md](../references/crm-sample-validation.md) +9. [troubleshooting/common-issues.md](../troubleshooting/common-issues.md) +10. [RUNBOOK.md](../RUNBOOK.md) +11. [examples/smart-embed-postmessage-bridge.md](../examples/smart-embed-postmessage-bridge.md) +12. [examples/phone-api-service-pattern.md](../examples/phone-api-service-pattern.md) +13. [references/source-map.md](../references/source-map.md) + +## Common Lifecycle Pattern + +1. Provision account prerequisites (Zoom Phone license, admin setup, SMS readiness). +2. Create OAuth app and scopes in Marketplace. +3. Choose integration surface: +- Smart Embed (iframe + postMessage) +- REST + webhooks +- URI launch (`callto`, `tel`, `zoomphonecall`, `zoomphonesms`) +4. Capture real-time events (Smart Embed events and/or webhooks). +5. Persist call identifiers and correlate records (`call_id`, `call_history_uuid`, `call_element_id`). +6. Apply migration-safe data mapping (v1 -> v2 -> v3) and handle renamed fields. +7. Harden security (origin validation, webhook signature validation, least-privilege scopes). + +## High-Level Scenarios + +- CRM softphone pane using Smart Embed + contact search/match callbacks. +- Click-to-call from account/contact table via `zp-make-call`. +- Call disposition workflow using `zp-save-log-event` and custom notes page. +- SMS engagement workflow with `zoomphonesms://` and `zp-sms-log-event`. +- Real-time operational board driven by `phone.*` webhook events. +- Call analytics migration from legacy call logs to call history/call elements. +- Admin automation for user/auto-receptionist/call-queue call-handling settings. + +See [scenarios/high-level-scenarios.md](../scenarios/high-level-scenarios.md) for details. + +## Chaining + +- OAuth setup/token lifecycle: [../oauth/SKILL.md](../../oauth/SKILL.md) +- Phone and account resources via REST: [../rest-api/SKILL.md](../../rest-api/SKILL.md) +- Event delivery and signature validation: [../webhooks/SKILL.md](../../webhooks/SKILL.md) +- Contact Center blended journey: [../contact-center/SKILL.md](../../contact-center/SKILL.md) + +## Environment Variables + +- See [references/environment-variables.md](../references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/plugins/zoom-developers/skills/phone/references/smart-embed-event-contract.md b/plugins/zoom-developers/skills/phone/references/smart-embed-event-contract.md new file mode 100644 index 00000000..01a9d413 --- /dev/null +++ b/plugins/zoom-developers/skills/phone/references/smart-embed-event-contract.md @@ -0,0 +1,37 @@ +# Smart Embed Event Contract + +## Core initialization and command messages + +- `zp-init-config` +- `zp-make-call` +- `zp-input-sms` +- `zp-contact-search-response` +- `zp-contact-match-response` + +## Core emitted event types + +- `zp-call-ringing-event` +- `zp-call-connected-event` +- `zp-call-ended-event` +- `zp-call-log-completed-event` +- `zp-call-recording-completed-event` +- `zp-call-voicemail-received-event` +- `zp-ai-call-summary-event` +- `zp-sms-log-event` +- `zp-save-log-event` +- `zp-contact-search-event` +- `zp-contact-match-event` +- `zp-notes-save-event` + +## Field-level reliability notes + +- `callId` appears early in lifecycle. +- `callLogId` appears in completion-oriented events. +- `event.id` can be used for deduplication/idempotency. +- Additional flags can appear (for example `enableAutoLog` behavior fields). + +## Security and resilience + +- Validate `event.origin === https://applications.zoom.us`. +- Keep a permissive parser for new optional fields. +- Route unknown event types into structured logs, not hard failures. diff --git a/plugins/zoom-developers/skills/phone/references/source-map.md b/plugins/zoom-developers/skills/phone/references/source-map.md new file mode 100644 index 00000000..6462d864 --- /dev/null +++ b/plugins/zoom-developers/skills/phone/references/source-map.md @@ -0,0 +1,28 @@ +# Zoom Phone Source Map + +Crawled docs source: +- `https://developers.zoom.us/docs/phone/` +- Crawl config used: depth `10`, concurrency `10`, Android excluded. + +## Processed pages + +- `call-data.md` +- `call-handling.md` +- `create-app.md` +- `first-app.md` +- `integrate-with-zoom-phone.md` +- `migrate.md` +- `outbound-call.md` +- `outbound-sms.md` +- `smart-embed-guide.md` +- `smart-embed.md` +- `start.md` +- `webhook-migrate.md` + +## Mapping to skill docs + +- App setup + OAuth -> [../SKILL.md](../SKILL.md), [environment-variables.md](environment-variables.md) +- Smart Embed lifecycle/events -> `examples/smart-embed-postmessage-bridge.md`, `references/smart-embed-event-contract.md` +- Call handling admin API -> `references/call-handling-patterns.md` +- API/webhook migration timeline -> `references/deprecations-and-migrations.md` +- CRM sample validation -> `references/crm-sample-validation.md` diff --git a/plugins/zoom-developers/skills/phone/scenarios/high-level-scenarios.md b/plugins/zoom-developers/skills/phone/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..1b7c5865 --- /dev/null +++ b/plugins/zoom-developers/skills/phone/scenarios/high-level-scenarios.md @@ -0,0 +1,33 @@ +# Zoom Phone High-Level Scenarios + +## 1) Smart Embed CRM Softphone + +Use Smart Embed in a CRM sidebar to place and receive calls, then log outcomes back into CRM records. + +## 2) Click-to-Call from Lead Table + +Use `zp-make-call` or URI scheme launch from contact rows; subscribe to call status events for UI updates. + +## 3) SMS Follow-Up Automation + +Use `zoomphonesms://` and Smart Embed SMS events to trigger follow-up tasks and SLA timers. + +## 4) Call Disposition + Notes Pipeline + +Use `zp-save-log-event` and `zp-notes-save-event` to capture custom dispositions and sync to third-party systems. + +## 5) Real-Time Supervisor Dashboard + +Use `phone.*` webhooks and call events to track active calls, misses, rejects, and queue pressure. + +## 6) Call History Modernization + +Migrate from legacy call log fields to call history/call element IDs while maintaining backward compatibility for old records. + +## 7) Call Handling Admin Automation + +Use call handling APIs to standardize business/closed/holiday routing for users, auto receptionists, and call queues. + +## 8) Blended Phone + Contact Center Journey + +Route phone interactions into Contact Center follow-up or escalation workflows using shared CRM context. diff --git a/plugins/zoom-developers/skills/phone/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/phone/troubleshooting/common-issues.md new file mode 100644 index 00000000..fcc44ec1 --- /dev/null +++ b/plugins/zoom-developers/skills/phone/troubleshooting/common-issues.md @@ -0,0 +1,39 @@ +# Zoom Phone Common Issues + +## Smart Embed event listener gets nothing + +Checks: +- Iframe is loaded from `https://applications.zoom.us`. +- `onZoomPhoneIframeApiReady` sequence is respected. +- `postMessage` origin checks are correct. +- Approved domains configured in Zoom Phone Smart Embed app settings. + +## OAuth works but API calls fail (401/403) + +Checks: +- Required scopes are present and app was re-authorized. +- Access token is current (refresh flow works). +- Right app type and account context are used. + +## Data fields missing after migration + +Checks: +- Code expects old fields (`call_logs`, `call_path`) only. +- Endpoint path still points to legacy call log URLs. +- Webhook processor supports `call_element_id` fields. + +## URI launch inconsistencies + +Checks: +- Client is installed and signed in. +- Scheme is valid (`callto:`, `tel:`, `zoomphonecall://`, `zoomphonesms://`). +- Platform caveats are handled. +- Android caveat: docs explicitly note no `zoomphonecall`/`tel` support due to system limitations. + +## Call handling API patch fails + +Checks: +- `extensionId` target type is correct. +- Payload subsetting matches endpoint context. +- Phone numbers are E.164 where required. +- Enum/action values are valid for current API version. diff --git a/plugins/zoom-developers/skills/plan-zoom-integration/SKILL.md b/plugins/zoom-developers/skills/plan-zoom-integration/SKILL.md new file mode 100644 index 00000000..d1ae0bb7 --- /dev/null +++ b/plugins/zoom-developers/skills/plan-zoom-integration/SKILL.md @@ -0,0 +1,40 @@ +--- +name: plan-zoom-integration +description: Use when planning Zoom integrations. +--- + +# /plan-zoom-integration + +> For local plugin installation and app mapping details, see [README.md](../../README.md). + +Create a practical build plan for a Zoom integration or app. + +## Usage + +```text +/plan-zoom-integration $ARGUMENTS +``` + +## Workflow + +1. Capture the target user flow and success criteria. +2. Choose the correct Zoom surface and supporting services. +3. Define auth requirements, scopes, and account assumptions. +4. Break implementation into phases: prototype, core integration, reliability, and launch. +5. Call out hard risks early: OAuth setup, webhook verification, SDK environment limits, or marketplace review. +6. End with the smallest deliverable that proves the architecture. + +## Output + +- Architecture summary +- Zoom products and APIs required +- Auth and scope checklist +- Delivery phases +- Risks, open questions, and immediate next action + +## Related Skills + +- [start](../start/SKILL.md) +- [setup-zoom-oauth](../setup-zoom-oauth/SKILL.md) +- [build-zoom-meeting-app](../build-zoom-meeting-app/SKILL.md) +- [build-zoom-bot](../build-zoom-bot/SKILL.md) diff --git a/plugins/zoom-developers/skills/plan-zoom-product/SKILL.md b/plugins/zoom-developers/skills/plan-zoom-product/SKILL.md new file mode 100644 index 00000000..a4046364 --- /dev/null +++ b/plugins/zoom-developers/skills/plan-zoom-product/SKILL.md @@ -0,0 +1,38 @@ +--- +name: plan-zoom-product +description: Use when choosing products. +--- + +# /plan-zoom-product + +> For local plugin installation and app mapping details, see [README.md](../../README.md). + +Choose between Zoom REST API, Webhooks, WebSockets, Meeting SDK, Video SDK, Zoom Apps SDK, Phone, or Contact Center for a specific use case. + +## Usage + +```text +/plan-zoom-product $ARGUMENTS +``` + +## Workflow + +1. Identify the user's actual goal. +2. Classify whether the problem is automation, embedded meetings, custom video, in-client app behavior, event delivery, AI tooling, or support/phone/contact-center work. +3. If the request is ambiguous, ask one short clarifier before locking the recommendation. +4. Recommend the primary Zoom surface and list the minimum supporting pieces. +5. Explain why the rejected alternatives are worse for this case. +6. End with a concrete next-step plan. + +## Output + +- Recommended Zoom surface +- Supporting components required +- Key tradeoffs and constraints +- Suggested implementation sequence +- Relevant skill links for the next step + +## Related Skills + +- [start](../start/SKILL.md) +- [choose-zoom-approach](../choose-zoom-approach/SKILL.md) diff --git a/plugins/zoom-developers/skills/probe-sdk/RUNBOOK.md b/plugins/zoom-developers/skills/probe-sdk/RUNBOOK.md new file mode 100644 index 00000000..e7bad23a --- /dev/null +++ b/plugins/zoom-developers/skills/probe-sdk/RUNBOOK.md @@ -0,0 +1,67 @@ +# Probe SDK 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. +- API/option naming can drift by version; validate against current Probe SDK reference. + +## 1) Confirm Integration Surface + +- Confirm this is a web diagnostics use case, not meeting/session join runtime. +- Confirm whether you need only device checks, only network checks, or full diagnostics. +- Confirm renderer target strategy (`video-tag` or canvas-based renderer). + +## 2) Confirm Required Inputs + +- No Zoom Marketplace credentials are required for core Probe SDK diagnostics. +- Device IDs are required for explicit audio input/output and camera diagnostics. +- For comprehensive network diagnostics, verify optional JS/WASM URL override strategy. + +## 3) Confirm Lifecycle Order + +1. `requestMediaDevicePermission()`. +2. `requestMediaDevices()`. +3. `diagnoseAudio(...)` / `diagnoseVideo(...)`. +4. `startToDiagnose(jsUrl, wasmUrl, config, statsListener)`. +5. `stopToDiagnose*` and `cleanup()` on exit. + +## 4) Confirm Event/State Handling + +- Keep stream lifecycle explicit (`releaseMediaStream`). +- Keep stats callback lightweight and avoid blocking UI thread. +- Persist final report snapshot before cleanup. + +## 5) Confirm Cleanup + Upgrade Posture + +- Always stop active diagnostics before page unload/navigation. +- Re-check renderer option naming and report field names on upgrades. +- Re-check browser compatibility assumptions against current docs. + +## 6) Quick Probes + +- Permissions prompt appears and resolves expectedly. +- Devices list includes expected microphone/speaker/camera. +- Video diagnostic renders to selected target. +- Network diagnostic emits stats and final report. + +## 7) Fast Decision Tree + +- No media diagnostics -> permissions denied or insecure context. +- Video diagnostics fail -> renderer/target mismatch or unsupported renderer type. +- Network diagnostic incomplete -> timeout/domain/config mismatch. +- Report schema mismatch -> version drift between docs and installed package. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/probe-sdk/ +- https://marketplacefront.zoom.us/sdk/probe/index.html + +### Raw docs in repo + +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/probe-sdk/` +- `tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/probe/` diff --git a/plugins/zoom-developers/skills/probe-sdk/SKILL.md b/plugins/zoom-developers/skills/probe-sdk/SKILL.md new file mode 100644 index 00000000..8d04b980 --- /dev/null +++ b/plugins/zoom-developers/skills/probe-sdk/SKILL.md @@ -0,0 +1,25 @@ +--- +name: probe-sdk +description: Use when using Probe SDK. +--- + +# Zoom Probe SDK + +Use this skill when the app needs readiness checks before a user joins a meeting or video session. + +## Workflow + +1. Define the preflight gate: browser support, camera, microphone, speaker, network, CPU, or diagnostic report. +2. Run diagnostics before the Meeting SDK or Video SDK join flow. +3. Decide whether failed checks should block join, warn the user, or capture support telemetry. +4. Keep probe results separate from meeting/session tokens and avoid sending unnecessary device details downstream. +5. Debug by isolating browser permissions, device enumeration, HTTPS requirements, firewall behavior, and unsupported environments. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- Probe overview: [probe-sdk.md](probe-sdk.md) +- Architecture and lifecycle: [concepts/architecture-and-lifecycle.md](concepts/architecture-and-lifecycle.md) +- Diagnostic page pattern: [examples/diagnostic-page-pattern.md](examples/diagnostic-page-pattern.md) +- Network pattern: [examples/comprehensive-network-pattern.md](examples/comprehensive-network-pattern.md) +- Common issues: [troubleshooting/common-issues.md](troubleshooting/common-issues.md) diff --git a/plugins/zoom-developers/skills/probe-sdk/concepts/architecture-and-lifecycle.md b/plugins/zoom-developers/skills/probe-sdk/concepts/architecture-and-lifecycle.md new file mode 100644 index 00000000..a624dac1 --- /dev/null +++ b/plugins/zoom-developers/skills/probe-sdk/concepts/architecture-and-lifecycle.md @@ -0,0 +1,57 @@ +# Probe SDK Architecture and Lifecycle + +## Purpose + +Probe SDK answers one question before real-time media starts: +- `Can this user/device/network support an acceptable experience?` + +## Architecture Model + +```text +User Browser + -> Probe SDK (Prober, Reporter) + -> Media APIs (permissions/devices) + -> Renderer path (video tag / WebGL / WebGL2 / WebGPU) + -> Network probing runtime (JS/WASM + domain endpoint) + -> Diagnostic stats stream + final report + -> UI gating decision (allow join / warn / block) +``` + +## Lifecycle Workflow + +1. Initialize +- `const prober = new Prober()` +- `const reporter = new Reporter()` (optional for standalone feature/basic reports) + +2. Permissions and devices +- `requestMediaDevicePermission({ audio: true, video: true })` +- `requestMediaDevices()` + +3. Targeted diagnostics +- `diagnoseAudio(inputConstraints, outputConstraints, duration)` +- `diagnoseVideo(constraints, { rendererType, target })` + +4. Comprehensive diagnostics +- `startToDiagnose(jsUrl, wasmUrl, config, statsListener)` +- stream live stats and wait for final report payload + +5. Tear-down and cleanup +- `stopToDiagnose()` / `stopToDiagnoseVideo(stream?)` +- `releaseMediaStream(stream)` +- `cleanup()` + +## Readiness Policy Calibration + +- Keep readiness policy product-specific and versioned (for example, `policy_version=2026-02`). +- Define explicit thresholds per output signal (`allow`, `warn`, `block`) for network/audio/video outcomes. +- Recalibrate policy thresholds whenever upgrading Probe SDK or changing browser support baseline. +- Log policy version with each final report so support can reproduce decisions. + +## Data Model Notes + +Typical final report includes: +- network diagnostic result +- basic info entries +- supported feature entries + +Field naming may vary by version (`basicInfo` vs `basicInfoEntries`, `supportedFeatures` vs `featureEntries`), so version-aware adapters are recommended. diff --git a/plugins/zoom-developers/skills/probe-sdk/examples/comprehensive-network-pattern.md b/plugins/zoom-developers/skills/probe-sdk/examples/comprehensive-network-pattern.md new file mode 100644 index 00000000..48f67292 --- /dev/null +++ b/plugins/zoom-developers/skills/probe-sdk/examples/comprehensive-network-pattern.md @@ -0,0 +1,40 @@ +# Comprehensive Network Diagnostic Pattern + +```javascript +import { Prober } from "@zoom/probesdk"; + +const prober = new Prober(); + +export async function runComprehensiveDiagnostic() { + const jsUrl = ""; // optional custom hosted prober.js + const wasmUrl = ""; // optional custom hosted prober.wasm loader + + const config = { + probeDuration: 120 * 1000, + connectTimeout: 20 * 1000, + domain: "zoom.us", + }; + + const statsHistory = []; + + const report = await prober.startToDiagnose(jsUrl, wasmUrl, config, (stats) => { + statsHistory.push(stats); + }); + + return { + report, + statsHistory, + }; +} + +export async function stopEarlyAndCollect() { + const partial = await prober.stopToDiagnose(); + prober.cleanup(); + return partial; +} +``` + +## Notes + +- Use stats callback for realtime charting; keep callback lightweight. +- Wrap final report fields behind adapter layer for version drift. diff --git a/plugins/zoom-developers/skills/probe-sdk/examples/diagnostic-page-pattern.md b/plugins/zoom-developers/skills/probe-sdk/examples/diagnostic-page-pattern.md new file mode 100644 index 00000000..d7c14922 --- /dev/null +++ b/plugins/zoom-developers/skills/probe-sdk/examples/diagnostic-page-pattern.md @@ -0,0 +1,42 @@ +# Diagnostic Page Pattern + +```javascript +import { Prober } from "@zoom/probesdk"; + +const prober = new Prober(); + +export async function runDeviceChecks(videoCanvas) { + const permission = await prober.requestMediaDevicePermission({ audio: true, video: true }); + if (permission.error) return { ok: false, stage: "permission", error: permission.error }; + + const devices = await prober.requestMediaDevices(); + if (devices.error) return { ok: false, stage: "devices", error: devices.error }; + + const cameraId = devices.devices?.find((d) => d.kind === "videoinput")?.deviceId || "default"; + const micId = devices.devices?.find((d) => d.kind === "audioinput")?.deviceId || "default"; + const speakerId = devices.devices?.find((d) => d.kind === "audiooutput")?.deviceId; + + const audioResult = await prober.diagnoseAudio( + { audio: { deviceId: micId }, video: false }, + { audio: { deviceId: speakerId }, video: false }, + 5000 + ); + + const videoResult = await prober.diagnoseVideo( + { video: { deviceId: cameraId } }, + { rendererType: 2, target: videoCanvas } + ); + + return { ok: true, devices: devices.devices, audioResult, videoResult }; +} + +export function cleanupStream(stream) { + prober.releaseMediaStream(stream); + prober.cleanup(); +} +``` + +## Notes + +- `rendererType` target requirements must match chosen renderer. +- Always release media streams when diagnostics are complete. diff --git a/plugins/zoom-developers/skills/probe-sdk/probe-sdk.md b/plugins/zoom-developers/skills/probe-sdk/probe-sdk.md new file mode 100644 index 00000000..1b1514fe --- /dev/null +++ b/plugins/zoom-developers/skills/probe-sdk/probe-sdk.md @@ -0,0 +1,15 @@ +# Zoom Probe SDK (Overview) + +Probe SDK is a web diagnostics SDK used to validate media devices, network quality, and browser capability before meeting/session workflows. + +For full documentation and navigation, start at [SKILL.md](SKILL.md). + +## Quick Links + +- [Architecture and Lifecycle](concepts/architecture-and-lifecycle.md) +- [High-Level Scenarios](scenarios/high-level-scenarios.md) +- [Diagnostic Page Pattern](examples/diagnostic-page-pattern.md) +- [Comprehensive Network Pattern](examples/comprehensive-network-pattern.md) +- [Probe Reference Map](references/probe-reference-map.md) +- [Sample Validation](references/samples-validation.md) +- [Common Issues](troubleshooting/common-issues.md) diff --git a/plugins/zoom-developers/skills/probe-sdk/references/environment-variables.md b/plugins/zoom-developers/skills/probe-sdk/references/environment-variables.md new file mode 100644 index 00000000..c3e84468 --- /dev/null +++ b/plugins/zoom-developers/skills/probe-sdk/references/environment-variables.md @@ -0,0 +1,23 @@ +# Probe SDK Environment Variables + +Probe SDK does not require Zoom Marketplace credentials for core diagnostics. + +## Required `.env` keys + +- None required by the SDK itself. + +## Optional `.env` keys (app-level conventions) + +| Key | Required | Description | Where to find value | +|-----|----------|-------------|---------------------| +| `PROBE_JS_URL` | Optional | Override URL for probe runtime JS | Hosted by your app/infrastructure (or empty to use defaults) | +| `PROBE_WASM_URL` | Optional | Override URL for probe runtime WASM loader | Hosted by your app/infrastructure (or empty to use defaults) | +| `PROBE_DOMAIN` | Optional | Domain target for diagnostic probes | Usually `zoom.us` or your approved diagnostic domain | +| `PROBE_DURATION_MS` | Optional | Probe duration in milliseconds | Chosen by your product policy (defaults align with SDK guidance) | +| `PROBE_CONNECT_TIMEOUT_MS` | Optional | Probe connect timeout in milliseconds | Chosen by your product policy | + +## Notes + +- Because no OAuth credentials are required, Probe SDK is suitable as a lightweight preflight page before auth-sensitive flows. +- Do not require `ZOOM_CLIENT_ID`, `ZOOM_CLIENT_SECRET`, or account-level OAuth tokens for core Probe diagnostics. +- Keep optional URLs/versioning aligned with package version to avoid JS/WASM mismatch. diff --git a/plugins/zoom-developers/skills/probe-sdk/references/full-guide.md b/plugins/zoom-developers/skills/probe-sdk/references/full-guide.md new file mode 100644 index 00000000..bee0577e --- /dev/null +++ b/plugins/zoom-developers/skills/probe-sdk/references/full-guide.md @@ -0,0 +1,65 @@ +# Zoom Probe SDK + +Background reference for preflight diagnostics on user devices and networks before meeting or session workflows. + +Official docs: +- https://developers.zoom.us/docs/probe-sdk/ +- https://marketplacefront.zoom.us/sdk/probe/index.html + +Reference sample: +- https://github.com/zoom/probesdk-web + +## Routing Guardrail + +- Use Probe SDK when the user needs client-side diagnostics and readiness scoring (device/network/browser capability), not meeting/session join. +- If user needs embedded meeting flows, route to [../meeting-sdk/SKILL.md](../../meeting-sdk/SKILL.md). +- If user needs custom real-time session UX, route to [../video-sdk/SKILL.md](../../video-sdk/SKILL.md). +- If user needs backend orchestration of events/APIs, chain with [../rivet-sdk/SKILL.md](../../rivet-sdk/SKILL.md), [../oauth/SKILL.md](../../oauth/SKILL.md), and [../rest-api/SKILL.md](../../rest-api/SKILL.md). + +## Quick Links + +Start here: +1. [probe-sdk.md](../probe-sdk.md) +2. [concepts/architecture-and-lifecycle.md](../concepts/architecture-and-lifecycle.md) +3. [scenarios/high-level-scenarios.md](../scenarios/high-level-scenarios.md) +4. [examples/diagnostic-page-pattern.md](../examples/diagnostic-page-pattern.md) +5. [examples/comprehensive-network-pattern.md](../examples/comprehensive-network-pattern.md) +6. [references/probe-reference-map.md](../references/probe-reference-map.md) +7. [references/environment-variables.md](../references/environment-variables.md) +8. [references/versioning-and-compatibility.md](../references/versioning-and-compatibility.md) +9. [references/samples-validation.md](../references/samples-validation.md) +10. [references/source-map.md](../references/source-map.md) +11. [troubleshooting/common-issues.md](../troubleshooting/common-issues.md) +12. [RUNBOOK.md](../RUNBOOK.md) + +## Common Lifecycle Pattern + +1. Initialize `Prober` / `Reporter`. +2. Request media permissions and enumerate devices. +3. Run targeted diagnostics (`diagnoseAudio`, `diagnoseVideo`). +4. Run comprehensive network diagnostic (`startToDiagnose`) and stream stats to UI. +5. Produce final report and apply readiness gates. +6. Stop/cleanup (`stopToDiagnose`, `stopToDiagnoseVideo`, `releaseMediaStream`, `cleanup`). + +## High-Level Scenarios + +- Pre-join diagnostics page before Meeting SDK join action. +- Support workflow that captures structured report for customer troubleshooting. +- Device certification flow for kiosk or controlled endpoint environments. +- Browser capability gating for advanced media features. + +See [scenarios/high-level-scenarios.md](../scenarios/high-level-scenarios.md) for details. + +## Chaining + +- Meeting pre-join gate: [../meeting-sdk/web/SKILL.md](../../meeting-sdk/web/SKILL.md) +- Video session readiness gate: [../video-sdk/web/SKILL.md](../../video-sdk/web/SKILL.md) +- Telemetry/report ingestion backend: [../rivet-sdk/SKILL.md](../../rivet-sdk/SKILL.md) + [../rest-api/SKILL.md](../../rest-api/SKILL.md) + +## Environment Variables + +- See [references/environment-variables.md](../references/environment-variables.md) for optional `.env` keys and how to source values. + +## Operations + +- [RUNBOOK.md](../RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/probe-sdk/references/probe-reference-map.md b/plugins/zoom-developers/skills/probe-sdk/references/probe-reference-map.md new file mode 100644 index 00000000..dc7fd593 --- /dev/null +++ b/plugins/zoom-developers/skills/probe-sdk/references/probe-reference-map.md @@ -0,0 +1,43 @@ +# Probe SDK Reference Map + +## Canonical Documentation + +- Product docs: https://developers.zoom.us/docs/probe-sdk/ +- Get started: https://developers.zoom.us/docs/probe-sdk/get-started/ +- API reference: https://marketplacefront.zoom.us/sdk/probe/index.html + +## Core Classes + +- `Prober` +- `Reporter` + +## Core Methods + +From global/class reference surfaces: +- `requestMediaDevicePermission` +- `requestMediaDevices` +- `diagnoseAudio` +- `diagnoseVideo` +- `startToDiagnose` +- `stopToDiagnose` +- `stopToDiagnoseVideo` +- `releaseMediaStream` +- `reportBasicInfo` +- `reportFeatures` +- `cleanup` + +## Key Enums/Constants + +- `RENDERER_TYPE` +- `NETWORK_QUALITY_LEVEL` +- `BANDWIDTH_QUALITY_LEVEL` +- `PROTOCOL_TYPE` +- `ERR_CODE` +- `SUPPORTED_FEATURE_INDEX` +- `BASIC_INFO_ATTR_INDEX` + +## Changelog and Package + +- Changelog: https://developers.zoom.us/changelog/probe-sdk/ +- npm package: https://www.npmjs.com/package/@zoom/probesdk +- sample source: https://github.com/zoom/probesdk-web diff --git a/plugins/zoom-developers/skills/probe-sdk/references/samples-validation.md b/plugins/zoom-developers/skills/probe-sdk/references/samples-validation.md new file mode 100644 index 00000000..10cfc632 --- /dev/null +++ b/plugins/zoom-developers/skills/probe-sdk/references/samples-validation.md @@ -0,0 +1,25 @@ +# Probe SDK Sample Validation and Drift Notes + +Validated sample: +- `zoom/probesdk-web` + +## Lifecycle and Architecture Patterns Confirmed + +- `Prober` initialization followed by staged diagnostics. +- Explicit separation between targeted checks (`diagnoseAudio`, `diagnoseVideo`) and comprehensive network probe (`startToDiagnose`). +- Cleanup and stream lifecycle methods are critical for stable page behavior. + +## Contradictions and Drift Indicators + +- Doc snippet drift: +- Some docs use video options key `type` while reference/sample prefer `rendererType`. +- Report shape drift: +- Docs show `basicInfo`/`supportedFeatures` whereas sample README also references `basicInfoEntries`/`featureEntries`. +- Timeout example variance: +- Docs and sample show different `connectTimeout` defaults in code snippets. + +## Recommendations + +- Use adapter layer for diagnostic report fields. +- Normalize renderer options through a shared utility in your app. +- Keep browser matrix in your own QA docs and update quarterly. diff --git a/plugins/zoom-developers/skills/probe-sdk/references/source-map.md b/plugins/zoom-developers/skills/probe-sdk/references/source-map.md new file mode 100644 index 00000000..194a0858 --- /dev/null +++ b/plugins/zoom-developers/skills/probe-sdk/references/source-map.md @@ -0,0 +1,13 @@ +# Probe SDK Source Map + +## Crawled Docs (local raw-docs) + +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/probe-sdk/get-started.md` +- `tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/probe/index.md` +- `tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/probe/Prober.md` +- `tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/probe/Reporter.md` +- `tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/probe/global.md` + +## External Validation Source + +- `zoom/probesdk-web` diff --git a/plugins/zoom-developers/skills/probe-sdk/references/versioning-and-compatibility.md b/plugins/zoom-developers/skills/probe-sdk/references/versioning-and-compatibility.md new file mode 100644 index 00000000..1c60278e --- /dev/null +++ b/plugins/zoom-developers/skills/probe-sdk/references/versioning-and-compatibility.md @@ -0,0 +1,24 @@ +# Probe SDK Versioning and Compatibility + +## Upgrade Strategy + +Use the standard upgrade workflow: +- [../../general/references/sdk-upgrade-workflow.md](../../general/references/sdk-upgrade-workflow.md) + +## Compatibility Risks + +- Renderer option naming drift (`type` vs `rendererType`) across docs/examples. +- Report object field naming drift (`basicInfo` vs `basicInfoEntries`, `supportedFeatures` vs `featureEntries`). +- Browser support table age vs current browser versions. +- Runtime JS/WASM URL override mismatches. + +## Safe Upgrade Checklist + +- Pin and record current/target `@zoom/probesdk` version. +- Compare get-started docs, API reference, and sample repo behavior. +- Validate all renderer targets in your browser matrix. +- Validate both full diagnostic completion and early stop path. +- Validate report schema adapter and downstream consumers. +- Pin JS/WASM assets to the same Probe SDK release and prevent mixed-version loading. +- Add cache-busting strategy for JS/WASM updates (asset fingerprinting or versioned URLs). +- Confirm CDN/browser cache invalidation behavior before production rollout. diff --git a/plugins/zoom-developers/skills/probe-sdk/scenarios/high-level-scenarios.md b/plugins/zoom-developers/skills/probe-sdk/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..c26e094f --- /dev/null +++ b/plugins/zoom-developers/skills/probe-sdk/scenarios/high-level-scenarios.md @@ -0,0 +1,31 @@ +# Probe SDK High-Level Scenarios + +## 1) Meeting Pre-Join Readiness Gate + +- Run Probe diagnostics before showing Meeting SDK join button. +- Block join for severe failures (no media permissions, fatal network score). +- Show guidance and retry path for recoverable failures. + +## 2) Video Session Quality Predictor + +- Pair Probe results with Video SDK session UX. +- Choose default video quality and renderer path based on capability checks. +- Fall back to audio-first profile when network quality is poor. + +## 3) Support/Triage Diagnostic Capture + +- Collect final diagnostic report for helpdesk workflows. +- Attach report to support ticket to reduce reproduction time. +- Compare report against known-good baseline by browser/OS cohort. + +## 4) Managed Device Certification + +- Run automated checks across approved browser/device matrix. +- Persist pass/fail score and feature support profile. +- Use certification output to guide endpoint policy and rollout. + +## 5) Incident Response Validation + +- During outage/performance alerts, run Probe tests from affected geos. +- Distinguish local device failures from Zoom/service-zone path issues. +- Route incident response based on network and protocol-specific findings. diff --git a/plugins/zoom-developers/skills/probe-sdk/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/probe-sdk/troubleshooting/common-issues.md new file mode 100644 index 00000000..3c933dd6 --- /dev/null +++ b/plugins/zoom-developers/skills/probe-sdk/troubleshooting/common-issues.md @@ -0,0 +1,65 @@ +# Probe SDK Common Issues + +## Permissions denied or missing streams + +Symptoms: +- Permission request returns error. +- No stream returned for diagnostics. + +Checks: +- HTTPS/secure context is used. +- Not in insecure context (`http://`, mixed content, blocked iframe permissions policy). +- Browser-level camera/mic permissions are granted. +- Device is not locked by another app. + +## No media devices detected + +Symptoms: +- `requestMediaDevices` returns empty sets for mic/camera/speaker. + +Checks: +- OS-level privacy settings allow browser device access. +- External USB/Bluetooth devices are connected before page load. +- Virtual device drivers are installed and recognized by the browser. +- Browser enterprise policies are not blocking media device enumeration. + +## Video diagnostic fails to render + +Symptoms: +- `diagnoseVideo` error or blank target. + +Checks: +- Renderer option key and target match selected renderer. +- `video-tag` renderer uses HTMLVideoElement target. +- WebGL/WebGL2/WebGPU renderers use canvas/offscreen canvas target. + +## Network diagnostic never completes + +Symptoms: +- `startToDiagnose` does not return final report in expected duration. + +Checks: +- `probeDuration` and `connectTimeout` values are reasonable. +- Domain and optional JS/WASM URLs are reachable. +- Browser/network policies do not block probing paths. + +## Report field mismatch in app code + +Symptoms: +- Undefined fields when parsing final report. + +Checks: +- Add compatibility adapter for `basicInfo` vs `basicInfoEntries`. +- Add compatibility adapter for `supportedFeatures` vs `featureEntries`. +- Pin SDK version and align parser tests to that version. + +## Residual resource usage after diagnostics + +Symptoms: +- Camera indicator remains active. +- Memory/network usage persists after leaving page. + +Checks: +- Call `stopToDiagnoseVideo` and/or `releaseMediaStream`. +- Call `stopToDiagnose` on early exit. +- Call `cleanup()` on route/page teardown. diff --git a/plugins/zoom-developers/skills/rest-api/RUNBOOK.md b/plugins/zoom-developers/skills/rest-api/RUNBOOK.md new file mode 100644 index 00000000..53924091 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/RUNBOOK.md @@ -0,0 +1,89 @@ +# REST API 5-Minute Preflight Runbook + +Use this before deep debugging. It catches common Zoom REST API integration failures fast. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Auth Flow and Endpoint + +- Choose matching OAuth flow for use case (S2S/User/PKCE/Device). +- Use token URL `https://zoom.us/oauth/token`. + +Wrong flow or token endpoint causes immediate auth failures. + +## 2) Confirm Scope and Account Context + +- Verify token contains required scopes. +- For admin/account-level operations, verify app/account permissions. +- Re-authorize after scope changes. + +## 3) Confirm ID Semantics + +- Distinguish Meeting ID vs Meeting UUID. +- Apply required URL encoding (double-encoding for UUID where needed). + +### ID Sanity Rules + +- Use numeric Meeting ID for many standard meeting operations. +- Use Meeting UUID for some past-instance/recording/report operations. +- If endpoint docs mention UUID and your value contains `/` or `+`, encode carefully. + +If a resource "exists" in UI but API returns not found, ID type/encoding mismatch is a top cause. + +## 4) Confirm Pagination and Rate Limits + +- Handle `next_page_token` where applicable. +- Implement retry/backoff on 429 and transient 5xx. + +### Minimal Retry Policy + +- 429 or 5xx: exponential backoff with jitter. +- Respect retry headers when provided. +- Put high-volume endpoints behind queue/batch workers. + +## 5) Confirm Webhook-Driven Workflows + +- If pipeline is event-driven, validate webhook signatures and retry behavior. +- Respond quickly and process asynchronously. + +## 6) Quick Probes + +- `GET /v2/users/me` succeeds with current token. +- Representative endpoint (e.g., list meetings) returns expected schema. +- Error payload includes actionable code/details (not HTML response). + +### Copy/Paste Validation Commands + +```bash +# 1) Get S2S access token +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(printf '%s:%s' "$ZOOM_CLIENT_ID" "$ZOOM_CLIENT_SECRET" | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=account_credentials&account_id=$ZOOM_ACCOUNT_ID" + +# 2) Validate token can access account context +curl -X GET "https://api.zoom.us/v2/users/me" \ + -H "Authorization: Bearer $ZOOM_ACCESS_TOKEN" + +# 3) List meetings for current user (quick schema sanity) +curl -X GET "https://api.zoom.us/v2/users/me/meetings?page_size=30" \ + -H "Authorization: Bearer $ZOOM_ACCESS_TOKEN" +``` + +Expected: JSON responses with HTTP 200 (or clear JSON error codes), not HTML error pages. + +## 7) Fast Decision Tree + +- **401/invalid token** -> wrong flow, expired token, or scope mismatch. +- **404-like behavior** -> wrong endpoint path/version or wrong resource ID. +- **429 spikes** -> missing backoff/queue strategy. + +## 8) Common Integration Mixups + +- REST `join_url` is a browser link, not a Meeting SDK join payload. +- REST API creates/manages Zoom resources; Meeting SDK and Video SDK are separate integration surfaces. +- If auth works but operation fails, check scope and resource ownership before endpoint debugging. diff --git a/plugins/zoom-developers/skills/rest-api/SKILL.md b/plugins/zoom-developers/skills/rest-api/SKILL.md new file mode 100644 index 00000000..72e2ea4b --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/SKILL.md @@ -0,0 +1,28 @@ +--- +name: build-zoom-rest-api-app +description: Use when calling REST APIs. +--- + +# Build Zoom REST API App + +Use this skill when the task needs deterministic Zoom API calls or resource management from application code. + +## Workflow + +1. Define the resource and actor: user, meeting, webinar, recording, docs, chat, phone, account, or admin-level workflow. +2. Select the endpoint and required scopes from the reference files before coding. +3. Confirm auth fit: user-level OAuth for user-owned resources, account-level OAuth for admin workflows, and only use server-to-server OAuth where the target API documents support for it. +4. Implement narrow API wrappers with explicit pagination, retry, idempotency, and rate-limit handling. +5. Treat webhook processing as a separate event-ingestion path with signature verification and replay protection. +6. Debug by checking token audience, missing scopes, resource ownership, account settings, API enablement, and rate-limit headers. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- API architecture: [concepts/api-architecture.md](concepts/api-architecture.md) +- Authentication flows: [concepts/authentication-flows.md](concepts/authentication-flows.md) +- Rate limits: [references/rate-limits.md](references/rate-limits.md) +- Meetings: [references/meetings.md](references/meetings.md) +- Recordings: [references/recordings.md](references/recordings.md) +- Users: [references/users.md](references/users.md) +- Token and scope playbook: [troubleshooting/token-scope-playbook.md](troubleshooting/token-scope-playbook.md) diff --git a/plugins/zoom-developers/skills/rest-api/concepts/api-architecture.md b/plugins/zoom-developers/skills/rest-api/concepts/api-architecture.md new file mode 100644 index 00000000..987ce39d --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/concepts/api-architecture.md @@ -0,0 +1,307 @@ +# API Architecture + +Core design patterns for the Zoom REST API — base URLs, regional routing, identifiers, time formats, and request conventions. + +## Base URL + +All requests use HTTPS with API version `/v2` in the path: + +``` +https://api.zoom.us/v2/ +``` + +**GraphQL** uses a separate versioned endpoint: + +``` +https://api.zoom.us/v3/graphql +``` + +## Regional Base URLs + +The OAuth token response includes an `api_url` field indicating the user's data region. Use this for data residency compliance: + +```json +{ + "access_token": "eyJ...", + "api_url": "https://api-eu.zoom.us" +} +``` + +Construct your regional base URL by appending `/v2/`: + +| Region | API URL | Base URL | +|--------|---------|----------| +| Global (default) | `https://api.zoom.us` | `https://api.zoom.us/v2` | +| Australia | `https://api-au.zoom.us` | `https://api-au.zoom.us/v2` | +| Canada | `https://api-ca.zoom.us` | `https://api-ca.zoom.us/v2` | +| European Union | `https://api-eu.zoom.us` | `https://api-eu.zoom.us/v2` | +| India | `https://api-in.zoom.us` | `https://api-in.zoom.us/v2` | +| Saudi Arabia | `https://api-sa.zoom.us` | `https://api-sa.zoom.us/v2` | +| Singapore | `https://api-sg.zoom.us` | `https://api-sg.zoom.us/v2` | +| United Kingdom | `https://api-uk.zoom.us` | `https://api-uk.zoom.us/v2` | +| United States | `https://api-us.zoom.us` | `https://api-us.zoom.us/v2` | +| Vanity account | `https://{vanity}.zoom.us` | `https://{vanity}.zoom.us/v2` | + +**Important:** The global URL `https://api.zoom.us` always works regardless of user region. Regional URLs are for compliance, not required. + +### Node.js — Dynamic Base URL from Token + +```javascript +async function getZoomClient(accountId, clientId, clientSecret) { + const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); + + const tokenRes = await fetch('https://zoom.us/oauth/token', { + method: 'POST', + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: `grant_type=account_credentials&account_id=${accountId}` + }); + + const tokenData = await tokenRes.json(); + const baseUrl = tokenData.api_url + ? `${tokenData.api_url}/v2` + : 'https://api.zoom.us/v2'; + + return { + accessToken: tokenData.access_token, + baseUrl, + async request(method, path, body = null) { + const res = await fetch(`${this.baseUrl}${path}`, { + method, + headers: { + 'Authorization': `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json' + }, + body: body ? JSON.stringify(body) : undefined + }); + if (!res.ok) { + const err = await res.json(); + throw new Error(`Zoom API ${res.status}: ${err.message}`); + } + return res.json(); + } + }; +} +``` + +## The `me` Keyword + +The `me` keyword substitutes for `userId` or `accountId` in API paths. Its behavior varies by app type: + +| App Type | `me` Behavior | When to Use | +|----------|---------------|-------------| +| **User-level OAuth** | Resolves to the authenticated user | **MUST use** — providing `userId` causes invalid token error | +| **Server-to-Server OAuth** | Not supported | **MUST NOT use** — provide actual `userId` or email | +| **Account-level OAuth** | Resolves to the user who installed the app | Can use either `me` or `userId` | + +### Examples + +```bash +# User OAuth app — MUST use me +GET /v2/users/me +GET /v2/users/me/meetings + +# S2S OAuth app — MUST use actual userId or email +GET /v2/users/abc123def +GET /v2/users/john@example.com +GET /v2/users/john@example.com/meetings +``` + +### Common Error + +Using `userId` with a User-level OAuth token: + +```json +{ + "code": 4700, + "message": "Invalid access token, does not contain scopes." +} +``` + +**Fix:** Replace the `userId` with `me`. + +## Meeting ID vs UUID + +- **Meeting ID**: Numeric identifier for the meeting. Reusable for recurring meetings. Expires 30 days after last use. +- **UUID**: Unique identifier for a specific meeting *instance*. Never expires. Generated per occurrence of recurring meetings. + +### When to Use Which + +| Use Case | Use | +|----------|-----| +| Get a scheduled meeting | Meeting ID | +| Get a past meeting instance | UUID | +| Get recordings for a specific session | UUID | +| Report on a specific occurrence | UUID | + +### Double-Encoding UUIDs + +UUIDs that begin with `/` or contain `//` **must be double URL-encoded**: + +```javascript +function encodeUUID(uuid) { + // Check if double-encoding is needed + if (uuid.startsWith('/') || uuid.includes('//')) { + return encodeURIComponent(encodeURIComponent(uuid)); + } + return encodeURIComponent(uuid); +} + +// UUID: /abcABC123== +// Single encode: %2FabcABC123%3D%3D +// Double encode: %252FabcABC123%253D%253D ← Required + +const meetingUUID = '/abcABC123=='; +const url = `https://api.zoom.us/v2/past_meetings/${encodeUUID(meetingUUID)}`; +``` + +### Python + +```python +from urllib.parse import quote + +def encode_uuid(uuid_str): + if uuid_str.startswith('/') or '//' in uuid_str: + return quote(quote(uuid_str, safe=''), safe='') + return quote(uuid_str, safe='') + +uuid = '/abcABC123==' +url = f'https://api.zoom.us/v2/past_meetings/{encode_uuid(uuid)}' +``` + +## Time Formats + +Zoom API uses ISO 8601 with two variants: + +| Format | Meaning | Example | +|--------|---------|---------| +| `yyyy-MM-ddTHH:mm:ssZ` | **UTC time** (Z suffix) | `2025-03-15T10:00:00Z` | +| `yyyy-MM-ddTHH:mm:ss` | **Local time** (no Z, uses `timezone` field) | `2025-03-15T10:00:00` | + +### Setting Meeting Time + +```json +{ + "topic": "Team Meeting", + "type": 2, + "start_time": "2025-03-15T10:00:00", + "timezone": "America/Los_Angeles", + "duration": 60 +} +``` + +Or using UTC directly: + +```json +{ + "topic": "Team Meeting", + "type": 2, + "start_time": "2025-03-15T17:00:00Z", + "duration": 60 +} +``` + +**Note:** Some Report APIs only accept UTC format. Always check the endpoint reference for the accepted format. + +### Date-Only Parameters + +Some endpoints (e.g., recordings list) use `YYYY-MM-DD` format: + +```bash +GET /v2/users/me/recordings?from=2025-01-01&to=2025-01-31 +``` + +## Download URLs + +Recording `download_url` values in API responses and webhook payloads are dynamically generated. They require authentication: + +### Authentication Methods + +1. **Bearer token in Authorization header** (recommended): +```bash +curl -L -H "Authorization: Bearer ACCESS_TOKEN" \ + "https://zoom.us/rec/archive/download/xyz" +``` + +2. **`download_access_token`** from webhook payload (for webhook-triggered downloads): +```bash +curl -L -H "Authorization: Bearer DOWNLOAD_ACCESS_TOKEN" \ + "https://zoom.us/rec/archive/download/xyz" +``` + +### Follow Redirects + +Download URLs may return HTTP 301/302 redirects. Always follow redirects: + +```javascript +// Node.js — fetch follows redirects by default +const response = await fetch(downloadUrl, { + headers: { 'Authorization': `Bearer ${accessToken}` }, + redirect: 'follow' +}); + +const fileBuffer = await response.arrayBuffer(); +``` + +```python +# Python — requests follows redirects by default +import requests + +response = requests.get( + download_url, + headers={'Authorization': f'Bearer {access_token}'}, + allow_redirects=True, + stream=True +) + +with open('recording.mp4', 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) +``` + +## Personal Meeting ID (PMI) + +Users can create meetings with their PMI. The API returns a unique meeting ID in the response, but webhook events still reference the PMI. Use the PMI when passing IDs to API endpoints for PMI-based meetings. + +## Shared Access Permissions + +Users with Schedule Privilege or role-based access can act on behalf of other users. If your app accesses resources of a user other than the one who installed the app, that user must have authorized shared access permissions. + +**Error when shared access is not granted:** + +```json +{ + "code": 403, + "message": "authenticated user has not permitted access to the targeted resource" +} +``` + +**Resolution:** Direct the user to enable shared access permissions in their Zoom settings. See [Zoom Help Center](https://support.zoom.us/hc/en-us/articles/4413265586189) for the user-facing instructions. + +## Email Address Display Rules + +External participant emails are only shown if: +- The participant entered their email during registration +- The host provided the email via calendar integration, authentication exception, or breakout room assignment +- A CSV was imported for webinar panelists/attendees + +## High API Failure Rates + +If your app has a consistently high error-to-request ratio, Zoom may disable it. Build robust error handling and graceful retry logic. + +## Request Authentication + +All API requests require a Bearer token in the Authorization header: + +``` +Authorization: Bearer {access_token} +``` + +> **Full auth implementation:** See [Authentication Flows](authentication-flows.md) or the **[zoom-oauth](../../oauth/SKILL.md)** skill. + +## Resources + +- **Using Zoom APIs**: https://developers.zoom.us/docs/api/using-zoom-apis/ +- **API Reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/ diff --git a/plugins/zoom-developers/skills/rest-api/concepts/authentication-flows.md b/plugins/zoom-developers/skills/rest-api/concepts/authentication-flows.md new file mode 100644 index 00000000..bc40cd84 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/concepts/authentication-flows.md @@ -0,0 +1,295 @@ +# Authentication Flows + +All Zoom REST API requests require OAuth 2.0 authentication. This guide covers all supported OAuth flows and when to use each. + +> **Complete OAuth implementation guide:** See the **[zoom-oauth](../../oauth/SKILL.md)** skill for full code examples, token storage, and production patterns. + +## Flow Selection + +| Flow | Use Case | User Interaction | Token Lifetime | +|------|----------|------------------|----------------| +| **Server-to-Server OAuth** | Backend automation, bots, integrations | None | 1 hour | +| **Authorization Code** | User-facing web apps | User consent flow | 1 hour (refresh: 15 years) | +| **Authorization Code + PKCE** | SPAs, mobile apps | User consent flow | 1 hour (refresh: 15 years) | +| **Device Code** | TV/IoT devices, CLI tools | User enters code on separate device | 1 hour (refresh: 15 years) | +| ~~**JWT**~~ | ~~Legacy~~ | ~~None~~ | **DEPRECATED** — migrate to S2S OAuth | + +## Server-to-Server OAuth (Recommended for Backend) + +No user interaction required. Best for automation, scheduled tasks, and backend services. + +### Setup + +1. Go to [Zoom App Marketplace](https://marketplace.zoom.us/) → **Develop** → **Build App** +2. Select **Server-to-Server OAuth** +3. Note: **Account ID**, **Client ID**, **Client Secret** +4. Add required scopes (e.g., `meeting:write:admin`, `user:read:admin`) + +### Get Access Token + +```bash +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=account_credentials&account_id=ACCOUNT_ID" +``` + +### Response + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiJ9...", + "token_type": "bearer", + "expires_in": 3600, + "scope": "meeting:read meeting:write user:read", + "api_url": "https://api.zoom.us" +} +``` + +### Node.js — Token Manager with Auto-Refresh + +```javascript +class ZoomS2SAuth { + constructor(accountId, clientId, clientSecret) { + this.accountId = accountId; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.token = null; + this.tokenExpiry = 0; + } + + async getAccessToken() { + // Return cached token if valid (with 60s buffer) + if (this.token && Date.now() < this.tokenExpiry - 60000) { + return this.token; + } + + const credentials = Buffer.from( + `${this.clientId}:${this.clientSecret}` + ).toString('base64'); + + const response = await fetch('https://zoom.us/oauth/token', { + method: 'POST', + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: `grant_type=account_credentials&account_id=${this.accountId}` + }); + + if (!response.ok) { + const err = await response.json(); + throw new Error(`Token error: ${err.error} - ${err.reason}`); + } + + const data = await response.json(); + this.token = data.access_token; + this.tokenExpiry = Date.now() + (data.expires_in * 1000); + + return this.token; + } + + async request(method, path, body = null) { + const token = await this.getAccessToken(); + + const response = await fetch(`https://api.zoom.us/v2${path}`, { + method, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: body ? JSON.stringify(body) : undefined + }); + + if (!response.ok) { + const err = await response.json().catch(() => ({})); + throw new Error(`Zoom API ${response.status}: ${JSON.stringify(err)}`); + } + + // Some endpoints return 204 No Content + if (response.status === 204) return null; + return response.json(); + } +} + +// Usage +const zoom = new ZoomS2SAuth( + process.env.ZOOM_ACCOUNT_ID, + process.env.ZOOM_CLIENT_ID, + process.env.ZOOM_CLIENT_SECRET +); + +const users = await zoom.request('GET', '/users?page_size=300'); +const meeting = await zoom.request('POST', '/users/user@example.com/meetings', { + topic: 'API Meeting', type: 2, duration: 30 +}); +``` + +### Python — Token Manager + +```python +import requests +import time +from base64 import b64encode + +class ZoomS2SAuth: + def __init__(self, account_id, client_id, client_secret): + self.account_id = account_id + self.client_id = client_id + self.client_secret = client_secret + self.token = None + self.token_expiry = 0 + + def get_access_token(self): + if self.token and time.time() < self.token_expiry - 60: + return self.token + + credentials = b64encode( + f'{self.client_id}:{self.client_secret}'.encode() + ).decode() + + response = requests.post( + 'https://zoom.us/oauth/token', + headers={ + 'Authorization': f'Basic {credentials}', + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data=f'grant_type=account_credentials&account_id={self.account_id}' + ) + response.raise_for_status() + + data = response.json() + self.token = data['access_token'] + self.token_expiry = time.time() + data['expires_in'] + return self.token + + def request(self, method, path, json_data=None): + token = self.get_access_token() + response = requests.request( + method, + f'https://api.zoom.us/v2{path}', + headers={'Authorization': f'Bearer {token}'}, + json=json_data + ) + response.raise_for_status() + return response.json() if response.content else None +``` + +## User OAuth (Authorization Code) + +For apps that act on behalf of individual Zoom users. + +### Flow + +``` +1. User clicks "Connect to Zoom" +2. Redirect to: https://zoom.us/oauth/authorize?response_type=code&client_id=XXX&redirect_uri=YYY&state=ZZZ +3. User grants permission +4. Zoom redirects to callback: https://yourapp.com/callback?code=AUTH_CODE&state=ZZZ +5. Exchange code for tokens +6. Use access_token for API calls +7. Refresh when expired +``` + +### Exchange Code for Token + +```bash +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=https://yourapp.com/callback" +``` + +### Refresh Token + +```bash +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=refresh_token&refresh_token=REFRESH_TOKEN" +``` + +### Important: `me` Keyword + +User OAuth apps **must** use `me` instead of `userId` in API paths: + +```bash +# CORRECT for user OAuth +GET /v2/users/me/meetings + +# WRONG for user OAuth — will return "Invalid access token" +GET /v2/users/abc123/meetings +``` + +## Common Scopes + +| Scope | Description | +|-------|-------------| +| `user:read` | Read user profile | +| `user:read:admin` | Read all users (admin) | +| `user:write:admin` | Manage all users (admin) | +| `meeting:read` | Read meeting data | +| `meeting:write` | Create/update meetings | +| `meeting:write:admin` | Create/update any user's meetings | +| `recording:read` | Access recordings | +| `recording:write` | Manage recordings | +| `webinar:read` | Read webinar data | +| `webinar:write` | Manage webinars | +| `report:read:admin` | View reports | + +**Best practice:** Request only the scopes you need. Fewer scopes = less user friction and faster app approval. + +## Token Storage Best Practices + +```javascript +// DO: Encrypt tokens at rest +const encrypted = encrypt(accessToken, process.env.ENCRYPTION_KEY); +await db.tokens.upsert({ userId, encrypted, expiresAt }); + +// DO: Use httpOnly secure cookies for web apps +res.cookie('zoom_session', sessionId, { + httpOnly: true, secure: true, sameSite: 'strict', maxAge: 3600000 +}); + +// DON'T: Store tokens in localStorage or log them +localStorage.setItem('zoom_token', token); // INSECURE +console.log('Token:', accessToken); // LEAKS CREDENTIALS +``` + +## Error Handling + +| Error | Cause | Solution | +|-------|-------|----------| +| `invalid_grant` | Expired/used auth code or refresh token | Restart OAuth flow or re-authenticate | +| `invalid_client` | Wrong client ID or secret | Verify credentials | +| `invalid_scope` | Scope not approved for your app | Check app scopes in Marketplace | +| `access_denied` | User denied permission | Handle gracefully in UI | + +```javascript +try { + const token = await refreshAccessToken(refreshToken); +} catch (error) { + if (error.response?.data?.error === 'invalid_grant') { + // Refresh token revoked or expired — re-authenticate + redirectToOAuthFlow(); + } +} +``` + +## Migration from JWT (Deprecated) + +The JWT app type on Zoom Marketplace is deprecated. This does **not** affect JWT token signatures used elsewhere (e.g., Video SDK). + +**Steps:** +1. Create a Server-to-Server OAuth app +2. Request the same scopes +3. Replace JWT token generation with OAuth token endpoint +4. Test all endpoints +5. Delete the JWT app + +## Resources + +- **OAuth Guide**: https://developers.zoom.us/docs/integrations/oauth/ +- **S2S OAuth**: https://developers.zoom.us/docs/internal-apps/s2s-oauth/ +- **Scopes Reference**: https://developers.zoom.us/docs/integrations/oauth-scopes/ +- **Full OAuth Skill**: See **[zoom-oauth](../../oauth/SKILL.md)** diff --git a/plugins/zoom-developers/skills/rest-api/concepts/meeting-urls-and-sdk-joining.md b/plugins/zoom-developers/skills/rest-api/concepts/meeting-urls-and-sdk-joining.md new file mode 100644 index 00000000..9d3c38a5 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/concepts/meeting-urls-and-sdk-joining.md @@ -0,0 +1,38 @@ +--- +title: "Meeting URLs vs Meeting SDK Joining" +--- + +# Meeting URLs vs Meeting SDK Joining + +Forum confusion pattern: + +- “How do I generate a Zoom meeting URL server-side?” +- “How do I join a meeting via API?” +- “Can I use `join_url` with Meeting SDK?” + +## REST API: What You Get + +When you create a meeting via REST API, you typically get: + +- `join_url`: for participants to join using Zoom clients/web join links +- `start_url`: for the host (often time-limited, and tied to the host context) +- `id` / meeting number: the meeting identifier + +## Meeting SDK: What It Uses + +Meeting SDK integrations generally use: + +- `meetingNumber` (meeting id) +- Meeting SDK **signature** (generated server-side) +- `role` (0 join, 1 start) +- passcode (if required) + +So: you usually do **not** “feed join_url into Meeting SDK”. You use the meeting number + SDK signature. + +## “Join via API” Clarification + +The REST API does not “join” a meeting as a client. If the goal is to build an embedded or automated participant, you’re typically looking at: + +- Meeting SDK (embed/join/start flows) +- or bot-style patterns (Linux Meeting SDK, or RTMS for media access), depending on the end goal + diff --git a/plugins/zoom-developers/skills/rest-api/concepts/rate-limiting-strategy.md b/plugins/zoom-developers/skills/rest-api/concepts/rate-limiting-strategy.md new file mode 100644 index 00000000..18477fd3 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/concepts/rate-limiting-strategy.md @@ -0,0 +1,347 @@ +# Rate Limiting Strategy + +Zoom API rate limits by plan, category, and strategies for handling them in production. + +## Rate Limits by Account Plan + +Rate limits are **per-account** (shared by all users and all apps on the account): + +### Main REST API + +| Category | Free | Pro | Business+ | +|----------|------|-----|-----------| +| **Light** | 4/sec, 6,000/day | 30/sec | 80/sec | +| **Medium** | 2/sec, 2,000/day | 20/sec | 60/sec | +| **Heavy** | 1/sec, 1,000/day | 10/sec* | 40/sec* | +| **Resource-Intensive** | 10/min, 30,000/day | 10/min* | 20/min* | + +**\* Combined daily limits:** +- **Pro**: 30,000/day (Heavy + Resource-Intensive shared) +- **Business+**: 60,000/day (Heavy + Resource-Intensive shared) + +**Business+** includes: Business, Education, Enterprise, and Partners. + +### Zoom Phone API + +| Category | Pro | Business+ | +|----------|-----|-----------| +| **Light** | 20/sec | 40/sec | +| **Medium** | 10/sec | 20/sec | +| **Heavy** | 5/sec, 15,000/day* | 10/sec, 30,000/day* | +| **Resource-Intensive** | 5/min, 15,000/day* | 10/min, 30,000/day* | + +**\* Daily limit shared** between Heavy and Resource-Intensive. + +### Zoom Contact Center API + +| Category | Pro | Business+ | +|----------|-----|-----------| +| **Light** | 20/sec | 40/sec | +| **Medium** | 10/sec | 20/sec | +| **Heavy** | 5/sec, 15,000/day* | 10/sec, 30,000/day* | + +**\* Daily limit shared** with Resource-Intensive APIs. + +### Video SDK Account Rate Limits + +| Plan | Uses Limits | +|------|-------------| +| Pay As You Go (Deprecated) | Pro | +| Annual Prepay Monthly Usage | Pro | +| All other plans | Business+ | + +## Endpoint Category Examples + +| Light | Medium | Heavy | +|-------|--------|-------| +| Get A Meeting | Create Meeting | Get Daily Usage Report | +| Get Meeting Recordings | List All Recordings | List Devices | +| Add Meeting Registrant | Get Past Meeting Participants | — | +| Update A Meeting | List Meetings | — | + +## Per-User Daily Limits + +These are separate from account-level rate limits: + +| Operation | Limit | Reset | +|-----------|-------|-------| +| Meeting/Webinar Create/Update | **100/day per user** | 00:00 UTC | +| Registrant Addition | **3/day per registrant** | 00:00 UTC | +| Registrant Status Updates | **10/day per registrant** | 00:00 UTC | + +**The 100/day limit** applies to all Meeting/Webinar IDs hosted by a specific user. To bulk-create meetings, distribute across multiple host users. + +## Concurrent Request Limits (Lock-Key) + +Zoom enforces single-concurrency on certain resource operations: + +| Scenario | Behavior | +|----------|----------| +| Multiple DELETE on same userId | Only 1 concurrent DELETE allowed | +| POST to `/v2/users` | Blocks GET/PATCH/PUT/DELETE until complete | + +**Error:** +```json +{ + "code": 429, + "message": "Too many concurrent requests. A request to disassociate this user has already been made." +} +``` + +## Response Headers + +Every API response includes rate limit information: + +| Header | Description | +|--------|-------------| +| `X-RateLimit-Category` | `Light`, `Medium`, `Heavy`, or `Resource-intensive` | +| `X-RateLimit-Type` | `QPS` (per-second) or `Daily-limit` | +| `X-RateLimit-Limit` | Max requests in current window | +| `X-RateLimit-Remaining` | Requests remaining | +| `X-RateLimit-Reset` | Unix timestamp when per-second limit resets | +| `Retry-After` | ISO 8601 datetime when daily limit resets | + +### Example — Normal Response + +``` +X-RateLimit-Category: Medium +X-RateLimit-Type: QPS +X-RateLimit-Limit: 60 +X-RateLimit-Remaining: 55 +``` + +### Example — Per-Second Rate Limited + +``` +HTTP/1.1 429 Too Many Requests +X-RateLimit-Category: Light +X-RateLimit-Type: QPS +X-RateLimit-Limit: 80 +X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 1705312800 +``` + +### Example — Daily Rate Limited + +``` +HTTP/1.1 429 Too Many Requests +X-RateLimit-Category: Heavy +X-RateLimit-Type: Daily-limit +X-RateLimit-Limit: 60000 +X-RateLimit-Remaining: 0 +Retry-After: 2025-01-20T00:00:00Z +``` + +## Strategy 1: Exponential Backoff with Jitter + +The simplest retry strategy for handling 429 responses: + +```javascript +async function callZoomAPI(url, options, maxRetries = 5) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + const response = await fetch(url, options); + + if (response.status === 429) { + // Check for daily limit (Retry-After header) + const retryAfter = response.headers.get('Retry-After'); + if (retryAfter) { + const waitMs = new Date(retryAfter) - Date.now(); + console.warn(`Daily limit hit. Retry after: ${retryAfter}`); + if (waitMs > 0 && waitMs < 86400000) { + await sleep(waitMs); + continue; + } + throw new Error(`Daily rate limit hit. Retry after ${retryAfter}`); + } + + // Per-second limit — exponential backoff with jitter + const baseDelay = Math.pow(2, attempt) * 1000; + const jitter = baseDelay * 0.2 * Math.random(); + const delay = baseDelay + jitter; + console.warn(`Rate limited. Retrying in ${Math.round(delay)}ms (attempt ${attempt + 1})`); + await sleep(delay); + continue; + } + + return response; + } + throw new Error('Max retries exceeded for Zoom API'); +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} +``` + +## Strategy 2: Proactive Throttling + +Monitor remaining quota and slow down before hitting limits: + +```javascript +async function throttledRequest(url, options) { + const response = await fetch(url, options); + + const remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '999'); + const limit = parseInt(response.headers.get('X-RateLimit-Limit') || '999'); + const category = response.headers.get('X-RateLimit-Category'); + + // Proactive throttling when under 10% quota + if (remaining < limit * 0.1) { + const resetTs = response.headers.get('X-RateLimit-Reset'); + if (resetTs) { + const waitMs = (parseInt(resetTs) * 1000) - Date.now(); + if (waitMs > 0 && waitMs < 10000) { + console.warn(`[${category}] ${remaining}/${limit} remaining — throttling ${waitMs}ms`); + await sleep(waitMs); + } + } else { + await sleep(1000); + } + } + + return response; +} +``` + +## Strategy 3: Request Queue (High-Volume) + +For applications making many concurrent requests: + +```javascript +class ZoomRateLimitedQueue { + constructor(requestsPerSecond = 10, minDelayMs = 100) { + this.queue = []; + this.running = 0; + this.maxConcurrent = requestsPerSecond; + this.minDelayMs = minDelayMs; + this.processing = false; + } + + async add(requestFn) { + return new Promise((resolve, reject) => { + this.queue.push({ requestFn, resolve, reject }); + this.process(); + }); + } + + async process() { + if (this.processing) return; + this.processing = true; + + while (this.queue.length > 0) { + if (this.running >= this.maxConcurrent) { + await sleep(this.minDelayMs); + continue; + } + + const { requestFn, resolve, reject } = this.queue.shift(); + this.running++; + + requestFn() + .then(resolve) + .catch(reject) + .finally(() => { + this.running--; + }); + + await sleep(this.minDelayMs); + } + + this.processing = false; + } +} + +// Usage — process 10 requests/sec max +const queue = new ZoomRateLimitedQueue(10, 100); + +const userIds = ['user1', 'user2', 'user3', /* ... */]; +const results = await Promise.all( + userIds.map(id => + queue.add(() => zoom.request('GET', `/users/${id}`)) + ) +); +``` + +## Best Practices + +### 1. Cache GET Responses + +```javascript +const cache = new Map(); + +async function cachedGet(path, ttlMs = 60000) { + const cached = cache.get(path); + if (cached && Date.now() - cached.time < ttlMs) { + return cached.data; + } + const data = await zoom.request('GET', path); + cache.set(path, { data, time: Date.now() }); + return data; +} +``` + +### 2. Use Webhooks Instead of Polling + +```javascript +// DON'T: Poll for meeting status changes +setInterval(async () => { + const meetings = await zoom.request('GET', `/users/${userId}/meetings`); +}, 60000); + +// DO: Receive webhook events +app.post('/webhook', (req, res) => { + handleEvent(req.body); + res.status(200).send(); +}); +``` + +> See **[zoom-webhooks](../../webhooks/SKILL.md)** for webhook implementation. + +### 3. Use List Endpoints with Pagination + +```javascript +// DON'T: Fetch users one by one (N API calls) +for (const id of userIds) { + const user = await zoom.request('GET', `/users/${id}`); +} + +// DO: Fetch in bulk (1 API call per page) +const allUsers = await zoom.request('GET', '/users?page_size=300'); +``` + +### 4. Distribute Bulk Creates Across Users + +```javascript +// Avoid hitting the 100/day per-user limit +const hosts = ['host1@co.com', 'host2@co.com', 'host3@co.com']; +let hostIndex = 0; + +for (const meeting of meetingsToCreate) { + const host = hosts[hostIndex % hosts.length]; + await zoom.request('POST', `/users/${host}/meetings`, meeting); + hostIndex++; + await sleep(100); // Prevent per-second burst +} +``` + +### 5. Use QSS for Quality Data + +For Quality of Service data, use QSS (push-based) instead of polling Reports API: +- Streams telemetry via webhooks/WebSocket +- Pushes data 4-6 times per minute +- Drastically reduces API call volume + +## Common Gotchas + +| Issue | Solution | +|-------|----------| +| 429 on first request of the day | Another app on account used quota | +| Different limits than documented | Check account type (Free/Pro/Business+) | +| Meeting create fails at 100/day | Per-user limit — distribute across hosts | +| Concurrent DELETE errors | Serialize DELETE operations on same user | +| Daily limit hit unexpectedly | Heavy + Resource-Intensive share quota | + +## Resources + +- **Rate Limits Documentation**: https://developers.zoom.us/docs/api/rest/rate-limits/ +- **Detailed Reference**: [references/rate-limits.md](../references/rate-limits.md) diff --git a/plugins/zoom-developers/skills/rest-api/examples/graphql-queries.md b/plugins/zoom-developers/skills/rest-api/examples/graphql-queries.md new file mode 100644 index 00000000..c1194659 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/examples/graphql-queries.md @@ -0,0 +1,14 @@ +# GraphQL Queries (Zoom) + +Use this when a forum question is really about "how do I fetch X without calling 10 REST endpoints". + +## Practical Guidance + +- Treat GraphQL as an alternative query surface. Not all REST resources are available. +- Auth is still OAuth; the most common failure mode is missing scopes. + +## Pitfalls Seen In Forum Threads + +- Confusing GraphQL "cursor" pagination with REST `next_page_token`. +- Assuming GraphQL replaces webhooks. It does not. + diff --git a/plugins/zoom-developers/skills/rest-api/examples/meeting-lifecycle.md b/plugins/zoom-developers/skills/rest-api/examples/meeting-lifecycle.md new file mode 100644 index 00000000..93946e15 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/examples/meeting-lifecycle.md @@ -0,0 +1,599 @@ +# Meeting Lifecycle - Complete CRUD with Webhook Integration + +Complete working examples for the full meeting lifecycle: Create → Update → Start → End → Delete, with webhook event integration. + +## Prerequisites + +- Server-to-Server OAuth token (see [Authentication Flows](../concepts/authentication-flows.md)) +- Scopes: `meeting:write`, `meeting:read` (minimum) +- Base URL: `https://api.zoom.us/v2` + +## Step 1: Create a Meeting + +### Instant Meeting (No Fixed Time) + +```bash +curl -X POST "https://api.zoom.us/v2/users/me/meetings" \ + -H "Authorization: Bearer ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "topic": "Quick Team Sync", + "type": 1, + "settings": { + "join_before_host": false, + "waiting_room": true, + "approval_type": 2 + } + }' +``` + +### Scheduled Meeting + +```bash +curl -X POST "https://api.zoom.us/v2/users/me/meetings" \ + -H "Authorization: Bearer ACCESS_TOKEN" \ + -H "Content-Type": application/json" \ + -d '{ + "topic": "Q1 Planning Meeting", + "type": 2, + "start_time": "2025-03-15T10:00:00Z", + "duration": 60, + "timezone": "America/New_York", + "agenda": "Discuss Q1 goals and milestones", + "settings": { + "host_video": true, + "participant_video": false, + "join_before_host": false, + "waiting_room": true, + "mute_upon_entry": true, + "approval_type": 2, + "auto_recording": "cloud", + "alternative_hosts": "alt.host@example.com" + } + }' +``` + +### Node.js Example + +```javascript +async function createMeeting(accessToken, meetingData) { + const response = await fetch('https://api.zoom.us/v2/users/me/meetings', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(meetingData) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(`Failed to create meeting: ${error.message}`); + } + + return await response.json(); +} + +// Usage +const meetingData = { + topic: 'Team Standup', + type: 2, // Scheduled + start_time: '2025-03-15T14:00:00Z', + duration: 30, + settings: { + join_before_host: false, + waiting_room: true + } +}; + +const meeting = await createMeeting(accessToken, meetingData); +console.log('Meeting created:', meeting.id); +console.log('Join URL:', meeting.join_url); +``` + +### Response + +```json +{ + "id": 93123456789, + "uuid": "xyzAbC1234==", + "host_id": "abc123def456", + "topic": "Q1 Planning Meeting", + "type": 2, + "start_time": "2025-03-15T10:00:00Z", + "duration": 60, + "timezone": "America/New_York", + "created_at": "2025-02-09T12:30:00Z", + "join_url": "https://zoom.us/j/93123456789", + "start_url": "https://zoom.us/s/93123456789?zak=...", + "settings": { + "host_video": true, + "participant_video": false, + "waiting_room": true, + "auto_recording": "cloud" + } +} +``` + +### Meeting Types + +| Type | Value | Description | +|------|-------|-------------| +| Instant | `1` | Start immediately, no fixed time | +| Scheduled | `2` | Fixed date/time | +| Recurring (no fixed time) | `3` | PMI meetings | +| Recurring (fixed time) | `8` | Series with schedule | + +## Step 2: Update a Meeting + +### Update Meeting Details + +```bash +curl -X PATCH "https://api.zoom.us/v2/meetings/93123456789" \ + -H "Authorization: Bearer ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "topic": "Q1 Planning - Updated", + "start_time": "2025-03-15T14:00:00Z", + "duration": 90, + "settings": { + "waiting_room": false + } + }' +``` + +### Node.js Example + +```javascript +async function updateMeeting(accessToken, meetingId, updates) { + const response = await fetch(`https://api.zoom.us/v2/meetings/${meetingId}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(updates) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(`Failed to update meeting: ${error.message}`); + } + + // PATCH returns 204 No Content on success + return response.status === 204; +} + +// Usage +const updates = { + topic: 'Q1 Planning - Updated Agenda', + duration: 90 +}; + +await updateMeeting(accessToken, 93123456789, updates); +console.log('Meeting updated successfully'); +``` + +### Partial Updates + +You only need to include fields you want to change: + +```javascript +// Only update topic +await updateMeeting(accessToken, meetingId, { + topic: 'New Topic' +}); + +// Only update settings +await updateMeeting(accessToken, meetingId, { + settings: { + waiting_room: true, + mute_upon_entry: true + } +}); +``` + +## Step 3: Get Meeting Details + +```bash +curl "https://api.zoom.us/v2/meetings/93123456789" \ + -H "Authorization: Bearer ACCESS_TOKEN" +``` + +### Node.js Example + +```javascript +async function getMeeting(accessToken, meetingId) { + const response = await fetch( + `https://api.zoom.us/v2/meetings/${meetingId}`, + { + headers: { + 'Authorization': `Bearer ${accessToken}` + } + } + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error(`Failed to get meeting: ${error.message}`); + } + + return await response.json(); +} + +// Usage +const meeting = await getMeeting(accessToken, 93123456789); +console.log('Meeting:', meeting.topic); +console.log('Start time:', meeting.start_time); +console.log('Join URL:', meeting.join_url); +``` + +## Step 4: List User's Meetings + +```bash +curl "https://api.zoom.us/v2/users/me/meetings?type=scheduled&page_size=30" \ + -H "Authorization: Bearer ACCESS_TOKEN" +``` + +### Node.js with Pagination + +```javascript +async function listAllMeetings(accessToken, userId = 'me') { + let allMeetings = []; + let nextPageToken = ''; + + while (true) { + const params = new URLSearchParams({ + type: 'scheduled', + page_size: 300, + ...(nextPageToken && { next_page_token: nextPageToken }) + }); + + const response = await fetch( + `https://api.zoom.us/v2/users/${userId}/meetings?${params}`, + { + headers: { + 'Authorization': `Bearer ${accessToken}` + } + } + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error(`Failed to list meetings: ${error.message}`); + } + + const data = await response.json(); + allMeetings = allMeetings.concat(data.meetings); + + nextPageToken = data.next_page_token; + if (!nextPageToken) break; + } + + return allMeetings; +} + +// Usage +const meetings = await listAllMeetings(accessToken); +console.log(`Found ${meetings.length} meetings`); +meetings.forEach(m => console.log(`- ${m.topic} (${m.start_time})`)); +``` + +### Meeting List Types + +| Type | Value | Description | +|------|-------|-------------| +| Scheduled | `scheduled` | Future meetings | +| Live | `live` | Currently active | +| Upcoming | `upcoming` | Within next 30 days | + +## Step 5: Delete a Meeting + +```bash +curl -X DELETE "https://api.zoom.us/v2/meetings/93123456789" \ + -H "Authorization: Bearer ACCESS_TOKEN" +``` + +### Node.js Example + +```javascript +async function deleteMeeting(accessToken, meetingId, options = {}) { + const params = new URLSearchParams(); + + // Optional: Cancel single occurrence of recurring meeting + if (options.occurrenceId) { + params.append('occurrence_id', options.occurrenceId); + } + + // Optional: Send cancellation email + if (options.scheduleForReminder !== undefined) { + params.append('schedule_for_reminder', options.scheduleForReminder); + } + + const url = `https://api.zoom.us/v2/meetings/${meetingId}${params.toString() ? '?' + params.toString() : ''}`; + + const response = await fetch(url, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${accessToken}` + } + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(`Failed to delete meeting: ${error.message}`); + } + + // DELETE returns 204 No Content on success + return response.status === 204; +} + +// Usage +await deleteMeeting(accessToken, 93123456789); +console.log('Meeting deleted successfully'); +``` + +## Webhook Integration + +To receive real-time events for meeting lifecycle, set up webhooks. See [Webhook Server Example](webhook-server.md) for full implementation. + +### Key Meeting Events + +| Event | When It Fires | +|-------|---------------| +| `meeting.created` | Meeting is created | +| `meeting.updated` | Meeting details changed | +| `meeting.deleted` | Meeting is deleted | +| `meeting.started` | Meeting begins | +| `meeting.ended` | Meeting ends | +| `meeting.participant_joined` | Participant joins | +| `meeting.participant_left` | Participant leaves | +| `recording.completed` | Cloud recording finishes processing | + +### Webhook Payload Example + +**Event:** `meeting.started` + +```json +{ + "event": "meeting.started", + "event_ts": 1707486720000, + "payload": { + "account_id": "abc123", + "object": { + "id": "93123456789", + "uuid": "xyzAbC1234==", + "host_id": "def456", + "topic": "Q1 Planning Meeting", + "type": 2, + "start_time": "2025-03-15T14:00:00Z", + "duration": 60, + "timezone": "America/New_York" + } + } +} +``` + +### Webhook Handler Example + +```javascript +// Express.js webhook endpoint +app.post('/webhook', express.json(), (req, res) => { + const { event, payload } = req.body; + + switch (event) { + case 'meeting.started': + console.log(`Meeting started: ${payload.object.topic}`); + // Trigger recording, send notifications, etc. + break; + + case 'meeting.ended': + console.log(`Meeting ended: ${payload.object.topic}`); + // Process analytics, download recordings, etc. + break; + + case 'recording.completed': + console.log(`Recording ready: ${payload.object.topic}`); + // Download recording (see recording-pipeline.md) + break; + + default: + console.log(`Unhandled event: ${event}`); + } + + // Respond with 200 to acknowledge receipt + res.status(200).send(); +}); +``` + +## Complete Lifecycle Workflow + +### Automated Meeting Management + +```javascript +class MeetingManager { + constructor(accessToken) { + this.accessToken = accessToken; + } + + async createScheduledMeeting(topic, startTime, duration = 60) { + const meetingData = { + topic, + type: 2, + start_time: startTime, + duration, + settings: { + join_before_host: false, + waiting_room: true, + auto_recording: 'cloud' + } + }; + + const meeting = await this.createMeeting(meetingData); + console.log(`Created meeting: ${meeting.id}`); + + return meeting; + } + + async updateMeetingTime(meetingId, newStartTime) { + await this.updateMeeting(meetingId, { + start_time: newStartTime + }); + console.log(`Updated meeting ${meetingId} start time`); + } + + async cancelMeeting(meetingId) { + await this.deleteMeeting(meetingId, { + schedule_for_reminder: true // Send cancellation email + }); + console.log(`Cancelled meeting ${meetingId}`); + } + + async getUpcomingMeetings() { + const meetings = await this.listAllMeetings(); + const now = new Date(); + + return meetings.filter(m => { + const startTime = new Date(m.start_time); + return startTime > now; + }); + } + + // Helper methods (implementations from above examples) + async createMeeting(data) { /* ... */ } + async updateMeeting(id, updates) { /* ... */ } + async deleteMeeting(id, options) { /* ... */ } + async listAllMeetings() { /* ... */ } +} + +// Usage +const manager = new MeetingManager(accessToken); + +// Create meeting +const meeting = await manager.createScheduledMeeting( + 'Team Standup', + '2025-03-15T10:00:00Z', + 30 +); + +// Update if needed +await manager.updateMeetingTime(meeting.id, '2025-03-15T14:00:00Z'); + +// Get all upcoming meetings +const upcoming = await manager.getUpcomingMeetings(); +console.log(`${upcoming.length} upcoming meetings`); + +// Cancel if needed +await manager.cancelMeeting(meeting.id); +``` + +## Common Patterns + +### Recurring Meeting Series + +```javascript +const recurringMeeting = { + topic: 'Weekly Team Sync', + type: 8, // Recurring with fixed time + start_time: '2025-03-15T10:00:00Z', + duration: 30, + recurrence: { + type: 2, // Weekly + repeat_interval: 1, + weekly_days: '1,3,5', // Monday, Wednesday, Friday + end_times: 20 // 20 occurrences + } +}; + +const meeting = await createMeeting(accessToken, recurringMeeting); +``` + +### Meeting with Registration + +```javascript +const meetingWithRegistration = { + topic: 'Product Demo', + type: 2, + start_time: '2025-03-15T14:00:00Z', + duration: 60, + settings: { + approval_type: 0, // Automatic approval + registration_type: 1, // Attendees register once + meeting_authentication: false + } +}; + +const meeting = await createMeeting(accessToken, meetingWithRegistration); +console.log('Registration URL:', meeting.registration_url); +``` + +### PMI Meeting + +```javascript +const pmiMeeting = { + topic: 'My Personal Room', + type: 3, // Recurring with no fixed time (PMI) + settings: { + use_pmi: true, + join_before_host: true + } +}; + +const meeting = await createMeeting(accessToken, pmiMeeting); +``` + +## Error Handling + +### Per-User Daily Limit + +Meeting/webinar create/update operations are limited to **100 per day per user** (resets at 00:00 UTC). + +```javascript +async function createMeetingWithRetry(accessToken, meetingData, maxRetries = 3) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await createMeeting(accessToken, meetingData); + } catch (error) { + if (error.message.includes('Too many requests')) { + console.log(`Hit per-user limit. Attempt ${attempt}/${maxRetries}`); + if (attempt < maxRetries) { + await sleep(5000); // Wait 5 seconds + continue; + } + } + throw error; + } + } +} +``` + +### Validation Errors + +```javascript +try { + await createMeeting(accessToken, meetingData); +} catch (error) { + if (error.message.includes('Invalid field')) { + console.error('Validation error:', error); + // Check start_time format, type value, etc. + } else if (error.message.includes('User does not exist')) { + console.error('Invalid userId'); + } else { + throw error; + } +} +``` + +## Related Documentation + +- **[API Architecture](../concepts/api-architecture.md)** - Base URLs, `me` keyword, time formats +- **[Authentication Flows](../concepts/authentication-flows.md)** - Get access tokens +- **[Webhook Server](webhook-server.md)** - Receive meeting events +- **[Recording Pipeline](recording-pipeline.md)** - Download meeting recordings +- **[Meetings Reference](../references/meetings.md)** - Complete endpoint documentation + +## Resources + +- [Create Meeting API](https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetingCreate) +- [Update Meeting API](https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetingUpdate) +- [Meeting Events](https://developers.zoom.us/docs/api/meetings/events/) diff --git a/plugins/zoom-developers/skills/rest-api/examples/recording-pipeline.md b/plugins/zoom-developers/skills/rest-api/examples/recording-pipeline.md new file mode 100644 index 00000000..3c4e9348 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/examples/recording-pipeline.md @@ -0,0 +1,17 @@ +# Recording Pipeline (Webhook -> Download -> Store) + +Goal: automatically ingest cloud recordings after meetings end. + +## High-Level Steps + +1. Subscribe to recording-related webhooks (e.g. `recording.completed`). +2. On webhook: fetch recording files via the recordings endpoints. +3. Download files using authenticated requests (often `download_url` requires an Authorization header). +4. Store in your system (S3/GCS/etc) and track status. + +## Common Pitfalls + +- Following `download_url` without attaching a bearer token. +- Not handling redirect responses from `download_url`. +- Assuming recording is available immediately after meeting ends (processing delays). + diff --git a/plugins/zoom-developers/skills/rest-api/examples/user-management.md b/plugins/zoom-developers/skills/rest-api/examples/user-management.md new file mode 100644 index 00000000..aaba5cd5 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/examples/user-management.md @@ -0,0 +1,17 @@ +# User Management (Create/List/Update) + +This doc targets the common "how do I list users / create users / search users" forum clusters. + +## Common Tasks + +- List users with pagination +- Create user (custCreate / SSO users vary by account settings) +- Update user type/license +- Deactivate/delete users + +## Pitfalls + +- Page size vs plan limits. +- Admin-only scopes needed for many user operations. +- "Search by first name" is not always supported as a direct filter; you may need to page and filter client-side. + diff --git a/plugins/zoom-developers/skills/rest-api/examples/webhook-server.md b/plugins/zoom-developers/skills/rest-api/examples/webhook-server.md new file mode 100644 index 00000000..1af4f07f --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/examples/webhook-server.md @@ -0,0 +1,589 @@ +# Webhook Server - Express.js with CRC Validation and Signature Verification + +Production-ready webhook server implementation for receiving Zoom webhook events with CRC (Challenge-Response Check) validation and HMAC signature verification. + +> **For comprehensive webhook documentation**, see the **[webhooks skill](../../webhooks/SKILL.md)**. + +## Quick Start + +### 1. Install Dependencies + +```bash +npm install express body-parser crypto +``` + +### 2. Basic Webhook Server + +```javascript +const express = require('express'); +const crypto = require('crypto'); +const app = express(); + +// Zoom webhook secret token (from your app's Feature page) +const WEBHOOK_SECRET_TOKEN = process.env.ZOOM_WEBHOOK_SECRET; + +// Parse JSON bodies +app.use(express.json()); + +// Webhook endpoint +app.post('/webhook', (req, res) => { + const { event, payload } = req.body; + + // Handle CRC validation (Challenge-Response Check) + if (event === 'endpoint.url_validation') { + return handleCRC(req, res); + } + + // Verify signature + if (!verifySignature(req)) { + console.error('Invalid signature'); + return res.status(401).send('Unauthorized'); + } + + // Handle events + handleEvent(event, payload); + + // Always respond with 200 within 3 seconds + res.status(200).send(); +}); + +app.listen(3000, () => { + console.log('Webhook server running on port 3000'); +}); +``` + +## CRC (Challenge-Response Check) Validation + +When you add a webhook URL or make changes, Zoom sends a validation request. You must respond within 3 seconds. + +### CRC Flow + +1. Zoom sends POST with `event: "endpoint.url_validation"` +2. Your server hashes the `plainToken` using your webhook secret +3. Respond with JSON containing both `plainToken` and `encryptedToken` + +### Implementation + +```javascript +function handleCRC(req, res) { + const { plainToken } = req.body.payload; + + // Hash the plainToken with HMAC-SHA256 + const encryptedToken = crypto + .createHmac('sha256', WEBHOOK_SECRET_TOKEN) + .update(plainToken) + .digest('hex'); + + // Respond within 3 seconds + res.status(200).json({ + plainToken, + encryptedToken + }); + + console.log('CRC validation successful'); +} +``` + +### CRC Request Example + +```json +{ + "event": "endpoint.url_validation", + "payload": { + "plainToken": "qgg8vlvZRS6UYooatFL8Aw" + }, + "event_ts": 1654503849680 +} +``` + +### CRC Response Example + +```json +{ + "plainToken": "qgg8vlvZRS6UYooatFL8Aw", + "encryptedToken": "23a89b634c017e5364a1c8d9c8ea909b60dd5599e2bb04bb1558d9c3a121faa5" +} +``` + +## Signature Verification + +Verify that webhook requests actually come from Zoom by checking the HMAC signature. + +### Signature Verification Flow + +1. Extract `x-zm-signature` and `x-zm-request-timestamp` headers +2. Construct message: `v0:{timestamp}:{body}` +3. Hash message with HMAC-SHA256 using your webhook secret +4. Prepend `v0=` to the hash +5. Compare with `x-zm-signature` header + +### Implementation + +```javascript +function verifySignature(req) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + + if (!signature || !timestamp) { + console.error('Missing signature headers'); + return false; + } + + // Construct the message + const message = `v0:${timestamp}:${JSON.stringify(req.body)}`; + + // Hash the message + const hashForVerify = crypto + .createHmac('sha256', WEBHOOK_SECRET_TOKEN) + .update(message) + .digest('hex'); + + // Prepend v0= + const computedSignature = `v0=${hashForVerify}`; + + // Compare signatures + return signature === computedSignature; +} +``` + +### Signature Headers Example + +```http +POST /webhook HTTP/1.1 +Host: example.com +x-zm-signature: v0=a05d830fa017433bc47887f835a00b9ff33d3882f22f63a2986a8es270341 +x-zm-request-timestamp: 1658940994 +Content-Type: application/json + +{"event":"meeting.started","payload":{...}} +``` + +## Event Handling + +### Event Router + +```javascript +function handleEvent(event, payload) { + switch (event) { + case 'meeting.created': + handleMeetingCreated(payload); + break; + + case 'meeting.started': + handleMeetingStarted(payload); + break; + + case 'meeting.ended': + handleMeetingEnded(payload); + break; + + case 'meeting.participant_joined': + handleParticipantJoined(payload); + break; + + case 'recording.completed': + handleRecordingCompleted(payload); + break; + + default: + console.log(`Unhandled event: ${event}`); + } +} +``` + +### Event Handlers + +```javascript +function handleMeetingStarted(payload) { + const { id, uuid, topic, start_time } = payload.object; + console.log(`Meeting started: ${topic} (ID: ${id})`); + + // Your logic: Send notifications, start recording, etc. + // Example: Trigger auto-recording + // await startCloudRecording(id); +} + +function handleMeetingEnded(payload) { + const { id, uuid, topic, duration } = payload.object; + console.log(`Meeting ended: ${topic} (Duration: ${duration}min)`); + + // Your logic: Process analytics, trigger workflows, etc. +} + +function handleRecordingCompleted(payload) { + const { id, uuid, topic, recording_files } = payload.object; + console.log(`Recording ready: ${topic}`); + + // Download recordings (see recording-pipeline.md) + recording_files.forEach(file => { + console.log(`- ${file.file_type}: ${file.download_url}`); + // downloadRecording(file.download_url, file.id); + }); +} + +function handleParticipantJoined(payload) { + const { participant } = payload.object; + console.log(`Participant joined: ${participant.user_name}`); + + // Your logic: Track attendance, send welcome message, etc. +} +``` + +## Complete Production Server + +```javascript +const express = require('express'); +const crypto = require('crypto'); +const app = express(); + +const WEBHOOK_SECRET_TOKEN = process.env.ZOOM_WEBHOOK_SECRET; +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(express.json()); + +// Request logging +app.use((req, res, next) => { + console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`); + next(); +}); + +// Webhook endpoint +app.post('/webhook', async (req, res) => { + try { + const { event, payload, event_ts } = req.body; + + // CRC validation + if (event === 'endpoint.url_validation') { + return handleCRC(req, res); + } + + // Verify signature + if (!verifySignature(req)) { + console.error('Signature verification failed'); + return res.status(401).send('Unauthorized'); + } + + // Log event + console.log(`Event received: ${event} at ${new Date(event_ts)}`); + + // Handle event asynchronously + setImmediate(() => { + handleEvent(event, payload).catch(error => { + console.error('Error handling event:', error); + }); + }); + + // Respond immediately (within 3 seconds) + res.status(200).send(); + + } catch (error) { + console.error('Webhook error:', error); + res.status(500).send('Internal Server Error'); + } +}); + +// CRC validation +function handleCRC(req, res) { + const { plainToken } = req.body.payload; + + const encryptedToken = crypto + .createHmac('sha256', WEBHOOK_SECRET_TOKEN) + .update(plainToken) + .digest('hex'); + + res.status(200).json({ plainToken, encryptedToken }); + console.log('CRC validation successful'); +} + +// Signature verification +function verifySignature(req) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + + if (!signature || !timestamp) { + return false; + } + + const message = `v0:${timestamp}:${JSON.stringify(req.body)}`; + + const hashForVerify = crypto + .createHmac('sha256', WEBHOOK_SECRET_TOKEN) + .update(message) + .digest('hex'); + + const computedSignature = `v0=${hashForVerify}`; + + return signature === computedSignature; +} + +// Event handler +async function handleEvent(event, payload) { + switch (event) { + case 'meeting.started': + await handleMeetingStarted(payload); + break; + + case 'meeting.ended': + await handleMeetingEnded(payload); + break; + + case 'recording.completed': + await handleRecordingCompleted(payload); + break; + + // Add more event handlers as needed + default: + console.log(`Unhandled event: ${event}`); + } +} + +async function handleMeetingStarted(payload) { + console.log(`Meeting started: ${payload.object.topic}`); + // Your business logic +} + +async function handleMeetingEnded(payload) { + console.log(`Meeting ended: ${payload.object.topic}`); + // Your business logic +} + +async function handleRecordingCompleted(payload) { + console.log(`Recording completed: ${payload.object.topic}`); + // Download logic (see recording-pipeline.md) +} + +// Health check +app.get('/health', (req, res) => { + res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`Webhook server running on port ${PORT}`); + console.log(`Webhook endpoint: ${process.env.PUBLIC_BASE_URL || 'https://YOUR_PUBLIC_BASE_URL'}/webhook`); +}); + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log('SIGTERM received, shutting down gracefully'); + process.exit(0); +}); +``` + +## Webhook Retry Policy + +Zoom automatically retries failed webhooks **3 times** with exponential backoff: + +1. **First retry**: 5 minutes after initial failure +2. **Second retry**: 20 minutes after first retry +3. **Third retry**: 60 minutes after second retry + +### Retry Conditions + +Zoom retries for: +- HTTP status codes ≥ 500 +- Network errors (connection refused, timeout, etc.) + +Zoom does **NOT** retry for: +- HTTP status codes 200-299 (success) +- HTTP status codes 300-399 (redirects) +- HTTP status codes 400-499 (client errors) + +### Handling Retries + +```javascript +// Track processed events to avoid duplicate processing +const processedEvents = new Set(); + +app.post('/webhook', (req, res) => { + const { event, event_ts, payload } = req.body; + + // Create unique event ID + const eventId = `${event}-${event_ts}-${payload.object?.id || ''}`; + + // Check if already processed (duplicate due to retry) + if (processedEvents.has(eventId)) { + console.log(`Duplicate event: ${eventId}`); + return res.status(200).send(); // Still return 200 + } + + // Mark as processed + processedEvents.add(eventId); + + // Handle event + handleEvent(event, payload); + + res.status(200).send(); + + // Clean up old entries after 2 hours + setTimeout(() => processedEvents.delete(eventId), 2 * 60 * 60 * 1000); +}); +``` + +## Webhook Revalidation + +Zoom automatically revalidates webhook endpoints every **72 hours**. If revalidation fails 6 consecutive times, Zoom disables the webhook. + +### Revalidation Notifications + +- **2 failures**: First email notification +- **4 failures**: Second email notification +- **6 failures**: Webhook disabled + +### Ensure Uptime + +```javascript +// Health check with monitoring +app.get('/health', (req, res) => { + // Check dependencies (database, external APIs, etc.) + const isHealthy = checkDependencies(); + + if (isHealthy) { + res.status(200).json({ + status: 'ok', + timestamp: new Date().toISOString(), + uptime: process.uptime() + }); + } else { + res.status(503).json({ + status: 'unhealthy', + timestamp: new Date().toISOString() + }); + } +}); + +function checkDependencies() { + // Check database connection, external APIs, etc. + return true; +} +``` + +## Environment Variables + +```bash +# .env +ZOOM_WEBHOOK_SECRET=your_webhook_secret_token_here +PORT=3000 +NODE_ENV=production +``` + +### Loading Environment Variables + +```javascript +require('dotenv').config(); + +const WEBHOOK_SECRET_TOKEN = process.env.ZOOM_WEBHOOK_SECRET; + +if (!WEBHOOK_SECRET_TOKEN) { + throw new Error('ZOOM_WEBHOOK_SECRET environment variable is required'); +} +``` + +## Deployment + +### Requirements + +1. **HTTPS required** - Zoom only sends to HTTPS endpoints +2. **Public URL** - Endpoint must be publicly accessible +3. **TLS 1.2+** - Valid certificate from a Certificate Authority (CA) +4. **FQDN** - Fully qualified domain name (not IP address) +5. **Response time** - Respond within 3 seconds + +### Deployment Options + +- **Heroku**: `git push heroku main` +- **AWS Lambda**: Use API Gateway + Lambda function +- **Vercel/Netlify**: Serverless functions +- **Self-hosted**: Nginx + Node.js + Let's Encrypt + +### ngrok for Local Development + +```bash +# Install ngrok +npm install -g ngrok + +# Start your server +node server.js + +# In another terminal, expose to public URL +ngrok http 3000 + +# Use the HTTPS URL in Zoom webhook configuration +# Example: https://abc123.ngrok.io/webhook +``` + +## Testing + +### Test CRC Validation + +```bash +WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:3000" + +curl -X POST "$WEBHOOK_BASE_URL/webhook" \ + -H "Content-Type: application/json" \ + -d '{ + "event": "endpoint.url_validation", + "payload": { + "plainToken": "test_token_123" + }, + "event_ts": 1654503849680 + }' +``` + +Expected response: +```json +{ + "plainToken": "test_token_123", + "encryptedToken": "..." +} +``` + +### Test Event Handling + +```bash +# Generate valid signature +TIMESTAMP=$(date +%s) +MESSAGE="v0:${TIMESTAMP}:{\"event\":\"meeting.started\",\"payload\":{\"object\":{\"id\":\"123\",\"topic\":\"Test\"}}}" +SIGNATURE="v0=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "YOUR_SECRET" -binary | xxd -p)" + +WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:3000" + +curl -X POST "$WEBHOOK_BASE_URL/webhook" \ + -H "Content-Type: application/json" \ + -H "x-zm-signature: $SIGNATURE" \ + -H "x-zm-request-timestamp: $TIMESTAMP" \ + -d '{"event":"meeting.started","payload":{"object":{"id":"123","topic":"Test Meeting"}}}' +``` + +## Common Event Types + +| Event | Description | +|-------|-------------| +| `meeting.created` | Meeting created | +| `meeting.updated` | Meeting details changed | +| `meeting.deleted` | Meeting deleted | +| `meeting.started` | Meeting begins | +| `meeting.ended` | Meeting ends | +| `meeting.participant_joined` | Participant joins | +| `meeting.participant_left` | Participant leaves | +| `recording.completed` | Cloud recording ready | +| `recording.transcript_completed` | Transcript ready | +| `user.created` | User created | +| `user.updated` | User updated | +| `user.deleted` | User deleted | + +> **See complete event catalog**: [webhooks skill](../../webhooks/SKILL.md) + +## Related Documentation + +- **[webhooks skill](../../webhooks/SKILL.md)** - Comprehensive webhook documentation +- **[Recording Pipeline](recording-pipeline.md)** - Download recordings from webhook events +- **[Meeting Lifecycle](meeting-lifecycle.md)** - Create/update/delete meetings +- **[Common Issues](../troubleshooting/common-issues.md)** - Webhook troubleshooting + +## Resources + +- [Using Webhooks](https://developers.zoom.us/docs/api/webhooks/) +- [Webhook Sample App](https://github.com/zoom/webhook-sample-node.js) +- [Event Reference](https://developers.zoom.us/docs/api/rest/webhook-reference/) diff --git a/plugins/zoom-developers/skills/rest-api/references/accounts.md b/plugins/zoom-developers/skills/rest-api/references/accounts.md new file mode 100644 index 00000000..3ee47bc1 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/accounts.md @@ -0,0 +1,125 @@ +# Zoom Accounts API + +Authoritative endpoint inventory for Accounts. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/accounts/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 59 | +| Path templates | 46 | +| Tags | 6 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Accounts | 11 | +| Dashboards | 26 | +| Data Requests | 5 | +| Information Barriers | 5 | +| Roles | 8 | +| Survey Management | 4 | + +## Endpoints by Tag + +### Accounts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/accounts/{accountId}/lock_settings` | Get locked settings | `getAccountLockSettings` | +| PATCH | `/accounts/{accountId}/lock_settings` | Update locked settings | `UpdateLockedSettings` | +| GET | `/accounts/{accountId}/managed_domains` | Get account's managed domains | `accountManagedDomain` | +| PUT | `/accounts/{accountId}/owner` | Update the account owner | `UpdateTheAccountOwner` | +| GET | `/accounts/{accountId}/settings` | Get account settings | `accountSettings` | +| PATCH | `/accounts/{accountId}/settings` | Update account settings | `accountSettingsUpdate` | +| GET | `/accounts/{accountId}/settings/registration` | Get an account's webinar registration settings | `accountSettingsRegistration` | +| PATCH | `/accounts/{accountId}/settings/registration` | Update an account's webinar registration settings | `accountSettingsRegistrationUpdate` | +| DELETE | `/accounts/{accountId}/settings/virtual_backgrounds` | Delete virtual background files | `delVB` | +| POST | `/accounts/{accountId}/settings/virtual_backgrounds` | Upload virtual background files | `uploadVB` | +| GET | `/accounts/{accountId}/trusted_domains` | Get account's trusted domains | `accountTrustedDomain` | + +### Dashboards + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/metrics/chat` | Get chat metrics | `dashboardChat` | +| GET | `/metrics/client/feedback` | List Zoom meetings client feedback | `dashboardClientFeedback` | +| GET | `/metrics/client/feedback/{feedbackId}` | Get zoom meetings client feedback | `dashboardClientFeedbackDetail` | +| GET | `/metrics/client/satisfaction` | List client meeting satisfaction | `listMeetingSatisfaction` | +| GET | `/metrics/client_versions` | List the client versions | `getClientVersions` | +| GET | `/metrics/crc` | Get CRC port usage | `dashboardCRC` | +| GET | `/metrics/issues/zoomrooms` | Get top 25 Zoom Rooms with issues | `dashboardIssueZoomRoom` | +| GET | `/metrics/issues/zoomrooms/{zoomroomId}` | Get issues of Zoom Rooms | `dashboardIssueDetailZoomRoom` | +| GET | `/metrics/meetings` | List meetings | `dashboardMeetings` | +| GET | `/metrics/meetings/{meetingId}` | Get meeting details | `dashboardMeetingDetail` | +| GET | `/metrics/meetings/{meetingId}/participants` | List meeting participants | `dashboardMeetingParticipants` | +| GET | `/metrics/meetings/{meetingId}/participants/qos` | List meeting participants QoS | `dashboardMeetingParticipantsQOS` | +| GET | `/metrics/meetings/{meetingId}/participants/satisfaction` | Get post meeting feedback | `participantFeedback` | +| GET | `/metrics/meetings/{meetingId}/participants/sharing` | Get meeting sharing/recording details | `dashboardMeetingParticipantShare` | +| GET | `/metrics/meetings/{meetingId}/participants/{participantId}/qos` | Get meeting participant QoS | `dashboardMeetingParticipantQOS` | +| GET | `/metrics/quality` | Get meeting quality scores | `dashboardQuality` | +| GET | `/metrics/webinars` | List webinars | `dashboardWebinars` | +| GET | `/metrics/webinars/{webinarId}` | Get webinar details | `dashboardWebinarDetail` | +| GET | `/metrics/webinars/{webinarId}/participants` | Get webinar participants | `dashboardWebinarParticipants` | +| GET | `/metrics/webinars/{webinarId}/participants/qos` | List webinar participant QoS | `dashboardWebinarParticipantsQOS` | +| GET | `/metrics/webinars/{webinarId}/participants/satisfaction` | Get post webinar feedback | `participantWebinarFeedback` | +| GET | `/metrics/webinars/{webinarId}/participants/sharing` | Get webinar sharing/recording details | `dashboardWebinarParticipantShare` | +| GET | `/metrics/webinars/{webinarId}/participants/{participantId}/qos` | Get webinar participant QoS | `dashboardWebinarParticipantQOS` | +| GET | `/metrics/zoomrooms` | List Zoom Rooms | `dashboardZoomRooms` | +| GET | `/metrics/zoomrooms/issues` | Get top 25 issues of Zoom Rooms | `dashboardZoomRoomIssue` | +| GET | `/metrics/zoomrooms/{zoomroomId}` | Get Zoom Rooms details | `dashboardZoomRoom` | + +### Data Requests + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/data_requests/files/{fileId}/url` | Get download link for data access request file | `DownloadfilesfromDataRequest` | +| GET | `/data_requests/requests` | List data request history | `GetDataRequestsHistory` | +| POST | `/data_requests/requests` | Create data (export/deletion) request | `CreateDataAccessRequest` | +| DELETE | `/data_requests/requests/{requestId}` | Cancel data deletion request | `CancelDataRequest` | +| GET | `/data_requests/requests/{requestId}` | List downloadable files for export data request | `GetDownloadableFilesforDataRequest` | + +### Information Barriers + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/information_barriers/policies` | List information Barrier policies | `InformationBarriersList` | +| POST | `/information_barriers/policies` | Create an Information Barrier policy | `InformationBarriersCreate` | +| DELETE | `/information_barriers/policies/{policyId}` | Remove an Information Barrier policy | `InformationBarriersDelete` | +| GET | `/information_barriers/policies/{policyId}` | Get an Information Barrier policy by ID | `InformationBarriersGet` | +| PATCH | `/information_barriers/policies/{policyId}` | Update an Information Barriers policy | `InformationBarriersUpdate` | + +### Roles + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/roles` | List roles | `roles` | +| POST | `/roles` | Create a role | `createRole` | +| DELETE | `/roles/{roleId}` | Delete a role | `deleteRole` | +| GET | `/roles/{roleId}` | Get role information | `getRoleInformation` | +| PATCH | `/roles/{roleId}` | Update role information | `updateRole` | +| GET | `/roles/{roleId}/members` | List members in a role | `roleMembers` | +| POST | `/roles/{roleId}/members` | Assign a role | `AddRoleMembers` | +| DELETE | `/roles/{roleId}/members/{memberId}` | Unassign a role | `roleMemberDelete` | + +### Survey Management + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/surveys` | Get surveys | `getAccountSurveys` | +| GET | `/surveys/{surveyId}` | Get survey info | `getSurveyInfo` | +| GET | `/surveys/{surveyId}/answers` | Get survey answers | `getSurveyAnswers` | +| GET | `/surveys/{surveyId}/instances` | Get survey instances | `getSurveyInstancesInfo` | diff --git a/plugins/zoom-developers/skills/rest-api/references/ai-companion.md b/plugins/zoom-developers/skills/rest-api/references/ai-companion.md new file mode 100644 index 00000000..f73e41e5 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/ai-companion.md @@ -0,0 +1,37 @@ +# Zoom AI Companion API + +Authoritative endpoint inventory for AI Companion. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/ai-companion/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 1 | +| Path templates | 1 | +| Tags | 1 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Archive | 1 | + +## Endpoints by Tag + +### Archive + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/aic/users/{userId}/conversation_archive` | Get AI Companion conversation archives | `GetAICconversationarchives` | diff --git a/plugins/zoom-developers/skills/rest-api/references/ai-services.md b/plugins/zoom-developers/skills/rest-api/references/ai-services.md new file mode 100644 index 00000000..b25e5c8d --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/ai-services.md @@ -0,0 +1,43 @@ +# Zoom AI Services API + +Authoritative endpoint inventory for AI Services / Scribe. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/ai-services/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Product skill: [../../scribe/SKILL.md](../../scribe/SKILL.md) +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Current OpenAPI surface is Scribe-focused. +- Auth uses Build-platform JWT, which differs from the standard OAuth-centric paths in most REST API product areas. +- Use this file for endpoint discovery and inventory. Use the `scribe` skill for workflow, webhook, and mode-selection guidance. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 6 | +| Path templates | 4 | +| Tags | 1 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Scribe | 6 | + +## Endpoints by Tag + +### Scribe + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/aiservices/scribe/jobs` | List Batch Jobs | `listBatchJobs` | +| POST | `/aiservices/scribe/jobs` | Submit Batch Scribe Job | `submitBatchAsr` | +| GET | `/aiservices/scribe/jobs/{jobId}` | Get Batch Job Status | `getBatchJobStatus` | +| DELETE | `/aiservices/scribe/jobs/{jobId}` | Cancel Batch Job | `cancelBatchJob` | +| GET | `/aiservices/scribe/jobs/{jobId}/files` | List Batch Job Files | `listBatchJobFiles` | +| POST | `/aiservices/scribe/transcribe` | Scribe (Synchronous) | `createFastAsr` | diff --git a/plugins/zoom-developers/skills/rest-api/references/authentication.md b/plugins/zoom-developers/skills/rest-api/references/authentication.md new file mode 100644 index 00000000..4d1add9a --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/authentication.md @@ -0,0 +1,438 @@ +# Authentication Guide + +Comprehensive guide to Zoom API authentication methods: OAuth 2.0, Server-to-Server OAuth, and JWT (legacy). + +## Overview + +Zoom APIs support multiple authentication methods depending on your use case: + +| Method | Use Case | Token Lifetime | +|--------|----------|----------------| +| **User OAuth 2.0** | Act on behalf of users | 1 hour (refresh: 15 years) | +| **Server-to-Server OAuth** | Backend automation | 1 hour | +| **JWT (Deprecated)** | Legacy integrations | Custom | + +## Server-to-Server OAuth (Recommended for Backend) + +For backend services that don't need user interaction. + +### Setup + +1. Go to [Zoom App Marketplace](https://marketplace.zoom.us/) +2. Click **Develop** → **Build App** +3. Select **Server-to-Server OAuth** +4. Note your credentials: + - Account ID + - Client ID + - Client Secret + +### Get Access Token + +```bash +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(echo -n '{clientId}:{clientSecret}' | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=account_credentials&account_id={accountId}" +``` + +### Response + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiJ9...", + "token_type": "bearer", + "expires_in": 3600, + "scope": "meeting:read meeting:write user:read" +} +``` + +### Code Example (Node.js) + +```javascript +const axios = require('axios'); + +class ZoomAuth { + constructor(accountId, clientId, clientSecret) { + this.accountId = accountId; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.token = null; + this.tokenExpiry = null; + } + + async getAccessToken() { + // Return cached token if still valid + if (this.token && this.tokenExpiry > Date.now() + 60000) { + return this.token; + } + + const credentials = Buffer.from( + `${this.clientId}:${this.clientSecret}` + ).toString('base64'); + + const response = await axios.post( + 'https://zoom.us/oauth/token', + `grant_type=account_credentials&account_id=${this.accountId}`, + { + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + this.token = response.data.access_token; + this.tokenExpiry = Date.now() + (response.data.expires_in * 1000); + + return this.token; + } + + async apiRequest(method, endpoint, data = null) { + const token = await this.getAccessToken(); + + const config = { + method, + url: `https://api.zoom.us/v2${endpoint}`, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }; + + if (data) { + config.data = data; + } + + return axios(config); + } +} + +// Usage +const zoom = new ZoomAuth( + process.env.ZOOM_ACCOUNT_ID, + process.env.ZOOM_CLIENT_ID, + process.env.ZOOM_CLIENT_SECRET +); + +const users = await zoom.apiRequest('GET', '/users'); +``` + +## User OAuth 2.0 (For User Actions) + +For applications that act on behalf of individual users. + +### OAuth Flow + +``` +1. User clicks "Connect to Zoom" + ↓ +2. Redirect to Zoom authorization URL + ↓ +3. User grants permission + ↓ +4. Zoom redirects to your callback with code + ↓ +5. Exchange code for access token + ↓ +6. Use access token for API calls + ↓ +7. Refresh token when expired +``` + +### Step 1: Create OAuth App + +1. Go to [Zoom App Marketplace](https://marketplace.zoom.us/) +2. Click **Develop** → **Build App** +3. Select **OAuth** +4. Configure: + - Redirect URI(s) + - Required scopes + - App information + +### Step 2: Authorization URL + +Redirect users to: + +``` +https://zoom.us/oauth/authorize?response_type=code&client_id={clientId}&redirect_uri={redirectUri}&state={state} +``` + +| Parameter | Description | +|-----------|-------------| +| `response_type` | Always `code` | +| `client_id` | Your OAuth app client ID | +| `redirect_uri` | Must match registered URI | +| `state` | Random string for CSRF protection | + +### Step 3: Handle Callback + +User is redirected to your callback URL: + +``` +https://yourapp.com/callback?code=AUTH_CODE&state=STATE +``` + +### Step 4: Exchange Code for Token + +```bash +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(echo -n '{clientId}:{clientSecret}' | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=authorization_code&code={authCode}&redirect_uri={redirectUri}" +``` + +### Response + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiJ9...", + "token_type": "bearer", + "refresh_token": "eyJhbGciOiJIUzI1NiJ9...", + "expires_in": 3600, + "scope": "meeting:read meeting:write user:read" +} +``` + +### Step 5: Refresh Token + +```bash +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(echo -n '{clientId}:{clientSecret}' | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=refresh_token&refresh_token={refreshToken}" +``` + +### Complete OAuth Example (Express.js) + +```javascript +const express = require('express'); +const axios = require('axios'); +const crypto = require('crypto'); + +const app = express(); + +const ZOOM_CLIENT_ID = process.env.ZOOM_CLIENT_ID; +const ZOOM_CLIENT_SECRET = process.env.ZOOM_CLIENT_SECRET; +const REDIRECT_URI = 'https://yourapp.com/auth/zoom/callback'; + +// Step 2: Initiate OAuth +app.get('/auth/zoom', (req, res) => { + const state = crypto.randomBytes(16).toString('hex'); + req.session.oauthState = state; + + const authUrl = new URL('https://zoom.us/oauth/authorize'); + authUrl.searchParams.set('response_type', 'code'); + authUrl.searchParams.set('client_id', ZOOM_CLIENT_ID); + authUrl.searchParams.set('redirect_uri', REDIRECT_URI); + authUrl.searchParams.set('state', state); + + res.redirect(authUrl.toString()); +}); + +// Step 3 & 4: Handle callback and exchange code +app.get('/auth/zoom/callback', async (req, res) => { + const { code, state } = req.query; + + // Verify state + if (state !== req.session.oauthState) { + return res.status(400).send('Invalid state'); + } + + try { + // Exchange code for token + const credentials = Buffer.from( + `${ZOOM_CLIENT_ID}:${ZOOM_CLIENT_SECRET}` + ).toString('base64'); + + const tokenResponse = await axios.post( + 'https://zoom.us/oauth/token', + `grant_type=authorization_code&code=${code}&redirect_uri=${REDIRECT_URI}`, + { + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + const { access_token, refresh_token, expires_in } = tokenResponse.data; + + // Store tokens securely (database) + await storeTokens(req.user.id, { + accessToken: access_token, + refreshToken: refresh_token, + expiresAt: Date.now() + (expires_in * 1000) + }); + + res.redirect('/dashboard'); + } catch (error) { + console.error('OAuth error:', error.response?.data); + res.status(500).send('Authentication failed'); + } +}); + +// Helper: Get valid access token (refresh if needed) +async function getValidToken(userId) { + const tokens = await getStoredTokens(userId); + + // Check if token is expired (with 1 min buffer) + if (tokens.expiresAt < Date.now() + 60000) { + const credentials = Buffer.from( + `${ZOOM_CLIENT_ID}:${ZOOM_CLIENT_SECRET}` + ).toString('base64'); + + const response = await axios.post( + 'https://zoom.us/oauth/token', + `grant_type=refresh_token&refresh_token=${tokens.refreshToken}`, + { + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + await storeTokens(userId, { + accessToken: response.data.access_token, + refreshToken: response.data.refresh_token, + expiresAt: Date.now() + (response.data.expires_in * 1000) + }); + + return response.data.access_token; + } + + return tokens.accessToken; +} +``` + +## Scopes + +### Common Scopes + +| Scope | Description | +|-------|-------------| +| `user:read` | Read user profile | +| `user:write` | Update user profile | +| `meeting:read` | Read meeting data | +| `meeting:write` | Create/update meetings | +| `recording:read` | Access recordings | +| `recording:write` | Manage recordings | +| `phone:read` | Read Zoom Phone data | +| `phone:write` | Manage Zoom Phone | + +### Admin Scopes + +| Scope | Description | +|-------|-------------| +| `user:read:admin` | Read all users | +| `user:write:admin` | Manage all users | +| `meeting:read:admin` | Read all meetings | +| `account:read:admin` | Read account settings | + +### Scope Selection + +Request only scopes you need: +- More scopes = more user friction +- Less scopes = better approval chance +- Add scopes incrementally as features grow + +## Token Storage Best Practices + +```javascript +// DO: Encrypt tokens at rest +const encryptedToken = encrypt(accessToken, encryptionKey); +await db.tokens.save({ userId, encryptedToken }); + +// DO: Use secure, httpOnly cookies for web apps +res.cookie('zoom_token', accessToken, { + httpOnly: true, + secure: true, + sameSite: 'strict', + maxAge: 3600000 +}); + +// DON'T: Store tokens in localStorage +localStorage.setItem('zoom_token', token); // BAD + +// DON'T: Log tokens +console.log('Token:', accessToken); // BAD +``` + +## Error Handling + +### Common OAuth Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| `invalid_grant` | Expired/used code | Restart OAuth flow | +| `invalid_client` | Wrong credentials | Check client ID/secret | +| `invalid_scope` | Unauthorized scope | Request only approved scopes | +| `access_denied` | User denied permission | Handle gracefully | + +### Token Refresh Errors + +```javascript +try { + const newToken = await refreshToken(refreshToken); +} catch (error) { + if (error.response?.data?.error === 'invalid_grant') { + // Refresh token expired or revoked + // User needs to re-authorize + redirectToOAuth(); + } +} +``` + +## Webhook Verification + +For webhook security, verify the request signature: + +```javascript +const crypto = require('crypto'); + +function verifyWebhook(req, secret) { + const message = `v0:${req.headers['x-zm-request-timestamp']}:${JSON.stringify(req.body)}`; + const signature = crypto + .createHmac('sha256', secret) + .update(message) + .digest('hex'); + + const expected = `v0=${signature}`; + return req.headers['x-zm-signature'] === expected; +} + +app.post('/webhooks/zoom', (req, res) => { + if (!verifyWebhook(req, process.env.ZOOM_WEBHOOK_SECRET)) { + return res.status(401).send('Invalid signature'); + } + + // Process webhook + const event = req.body; + // ... +}); +``` + +## Migration from JWT (Deprecated) + +JWT apps are deprecated. Migrate to Server-to-Server OAuth: + +1. Create new Server-to-Server OAuth app +2. Request same scopes +3. Update code to use OAuth token endpoint +4. Test thoroughly +5. Delete JWT app + +```javascript +// OLD (JWT - Deprecated) +const token = jwt.sign(payload, apiSecret); + +// NEW (Server-to-Server OAuth) +const token = await getServerToServerToken(); +``` + +## Resources + +- **OAuth Guide**: https://developers.zoom.us/docs/integrations/oauth/ +- **Server-to-Server OAuth**: https://developers.zoom.us/docs/internal-apps/s2s-oauth/ +- **Scopes Reference**: https://developers.zoom.us/docs/integrations/oauth-scopes/ +- **Migration Guide**: https://developers.zoom.us/docs/internal-apps/jwt-app-migration/ diff --git a/plugins/zoom-developers/skills/rest-api/references/auto-dialer.md b/plugins/zoom-developers/skills/rest-api/references/auto-dialer.md new file mode 100644 index 00000000..b0c62a84 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/auto-dialer.md @@ -0,0 +1,62 @@ +# Zoom Auto Dialer API + +Authoritative endpoint inventory for Auto Dialer. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/auto-dialer/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 14 | +| Path templates | 8 | +| Tags | 3 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Call History & Reporting | 2 | +| Call List Management | 5 | +| Prospect Management | 7 | + +## Endpoints by Tag + +### Call History & Reporting + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/dialer/call-histories/{callHistoryId}` | Get Call History by ID | `GetCallDetailsbyCallID` | +| GET | `/dialer/call-history` | Get Call History | `GetCallHistory` | + +### Call List Management + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/dialer/call-lists` | List Call Lists | `ListCallLists` | +| POST | `/dialer/call-lists` | Create Call List | `CreateCallList` | +| DELETE | `/dialer/call-lists/{callListId}` | Delete Call List | `DeleteCallList` | +| GET | `/dialer/call-lists/{callListId}` | Get Call List by ID | `GetCallListbyID` | +| PATCH | `/dialer/call-lists/{callListId}` | Update Call List | `UpdateCallList` | + +### Prospect Management + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/dialer/call-lists/{callListId}/prospects` | List All Prospects in Call List | `ListAllProspectsInCallList` | +| PATCH | `/dialer/call-lists/{callListId}/prospects` | Update Prospects batch | `UpdateProspects` | +| POST | `/dialer/call-lists/{callListId}/prospects` | Create Prospect | `CreateProspect` | +| POST | `/dialer/call-lists/{callListId}/prospects/batch` | Create Prospects batch | `CreateProspects` | +| DELETE | `/dialer/call-lists/{callListId}/prospects/{prospectId}` | Delete Prospect | `DeleteProspect` | +| PATCH | `/dialer/call-lists/{callListId}/prospects/{prospectId}` | Update Prospect | `UpdateProspect` | +| GET | `/dialer/prospects/{prospectId}` | Get Prospect by ID | `GetProspectbyID` | diff --git a/plugins/zoom-developers/skills/rest-api/references/calendar.md b/plugins/zoom-developers/skills/rest-api/references/calendar.md new file mode 100644 index 00000000..1ba41c18 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/calendar.md @@ -0,0 +1,100 @@ +# Zoom Calendar API + +Authoritative endpoint inventory for Calendar. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/calendar/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 28 | +| Path templates | 16 | +| Tags | 7 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| acl | 5 | +| calendar list | 5 | +| calendars | 4 | +| colors | 1 | +| events | 9 | +| freebusy | 1 | +| settings | 3 | + +## Endpoints by Tag + +### acl + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/calendars/{calId}/acl` | List ACL rules of specified calendar | `Listacl` | +| POST | `/calendars/{calId}/acl` | Create a new ACL rule | `Insertacl` | +| DELETE | `/calendars/{calId}/acl/{aclId}` | Delete an existing ACL rule | `Deleteacl` | +| GET | `/calendars/{calId}/acl/{aclId}` | Get the specified ACL rule | `Getacl` | +| PATCH | `/calendars/{calId}/acl/{aclId}` | Update the specified ACL rule | `Patchacl` | + +### calendar list + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/calendars/users/{userIdentifier}/calendarList` | List the calendars in the user's own calendarList | `ListcalendarList` | +| POST | `/calendars/users/{userIdentifier}/calendarList` | Insert an existing calendar to the user's own calendarList | `InsertcalendarList` | +| DELETE | `/calendars/users/{userIdentifier}/calendarList/{calendarId}` | Delete an existing calendar from the user's own calendarList | `DeletecalendarList` | +| GET | `/calendars/users/{userIdentifier}/calendarList/{calendarId}` | Get a specified calendar from the user's own calendarList | `GetcalendarList` | +| PATCH | `/calendars/users/{userIdentifier}/calendarList/{calendarId}` | Update an existing calendar in the user's own calendarList | `PatchcalendarList` | + +### calendars + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/calendars` | Create a new secondary calendar | `Insertcalendar` | +| DELETE | `/calendars/{calId}` | Delete a calendar owned by a user | `Deletecalendar` | +| GET | `/calendars/{calId}` | Get the specified calendar | `Getcalendar` | +| PATCH | `/calendars/{calId}` | Update the specified calendar | `Patchcalendar` | + +### colors + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/calendars/colors` | Get the color definitions for calendars and events | `Getcolor` | + +### events + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/calendars/{calId}/events` | List events on the specified calendar | `Listevent` | +| POST | `/calendars/{calId}/events` | Insert a new event to the specified calendar | `Insertevent` | +| POST | `/calendars/{calId}/events/import` | Import event to the specified calendar | `Importevent` | +| POST | `/calendars/{calId}/events/quickAdd` | Quick add an event to the specified calendar | `Quickaddevent` | +| DELETE | `/calendars/{calId}/events/{eventId}` | Delete an existing event from the specified calendar | `Deleteevent` | +| GET | `/calendars/{calId}/events/{eventId}` | Get the specified event on the specified calendar | `Getevent` | +| PATCH | `/calendars/{calId}/events/{eventId}` | Update the specified event on the specified calendar | `Patchevent` | +| GET | `/calendars/{calId}/events/{eventId}/instances` | List all instances of the specified recurring event | `Instanceevent` | +| POST | `/calendars/{calId}/events/{eventId}/move` | Move the specified event from a calendar to another specified calendar | `Moveevent` | + +### freebusy + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/calendars/freeBusy` | Query freebusy information for a set of calendars | `Queryfreebusy` | + +### settings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/calendars/users/{userIdentifier}/settings` | List all user calendar settings of the authenticated user | `Listsettings` | +| GET | `/calendars/users/{userIdentifier}/settings/{settingId}` | Get the specified user calendar settings of the authenticated user | `Getsetting` | +| PATCH | `/calendars/users/{userIdentifier}/settings/{settingId}` | Patch the specified user calendar settings of the authenticated user | `Patchsetting` | diff --git a/plugins/zoom-developers/skills/rest-api/references/chatbot.md b/plugins/zoom-developers/skills/rest-api/references/chatbot.md new file mode 100644 index 00000000..5c162641 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/chatbot.md @@ -0,0 +1,40 @@ +# Zoom Chatbot API + +Authoritative endpoint inventory for Chatbot. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/chatbot/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 4 | +| Path templates | 3 | +| Tags | 1 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Chatbot Messages | 4 | + +## Endpoints by Tag + +### Chatbot Messages + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/im/chat/messages` | Send Chatbot messages | `sendChatbot` | +| DELETE | `/im/chat/messages/{message_id}` | Delete a Chatbot message | `deleteAChatbotMessage` | +| PUT | `/im/chat/messages/{message_id}` | Edit a Chatbot message | `editChatbotMessage` | +| POST | `/im/chat/users/{userId}/unfurls/{triggerId}` | Link Unfurls | `unfurlingLink` | diff --git a/plugins/zoom-developers/skills/rest-api/references/clips.md b/plugins/zoom-developers/skills/rest-api/references/clips.md new file mode 100644 index 00000000..aca587e2 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/clips.md @@ -0,0 +1,85 @@ +# Zoom Clips API + +Authoritative endpoint inventory for Clips. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/clips/methods/endpoints.json +- Base URL: `https://api.zoom.us/` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 13 | +| Path templates | 11 | +| Tags | 7 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Clips | 3 | +| Collaborator | 1 | +| Comment | 2 | +| Download | 1 | +| Single | 1 | +| Transfer | 2 | +| Upload | 3 | + +## Endpoints by Tag + +### Clips + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/clips` | List all clips | `GetUserClips` | +| GET | `/clips/{clipId}` | Get a clip | `GetClipById` | +| GET | `/clips/{clipId}/collaborators` | Get collaborators of a clip | `GetClipCollaborators` | + +### Collaborator + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/clips/{clipId}/collaborators` | Remove the collaborator from a clip | `DeleteCollaborator` | + +### Comment + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/clips/{clipId}/comments` | List clip comments | `Listclipcomments` | +| DELETE | `/clips/{clipId}/comments/{commentId}` | Delete a comment | `Deleteacomment` | + +### Download + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/clips/{clipId}/download` | Download a clip | `downloadClip` | + +### Single + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/clips/{clipId}` | Delete a clip(soft delete) | `DeleteClip` | + +### Transfer + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/clips/transfers` | Transfer clips owner | `Transferclipsowner` | +| GET | `/clips/transfers/{taskId}` | Transfer task status check | `Transfertaskstatuscheck` | + +### Upload + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/clips/files` | Upload clip file | `UploadClipFile` | +| POST | `/clips/files/multipart` | Upload clip multipart files | `UploadIqMultipartClipFile` | +| POST | `/clips/files/multipart/upload_events` | Initiate and complete the multipart file upload for a clip | `InitiateAndCompleteAClipMultipartUpload.` | diff --git a/plugins/zoom-developers/skills/rest-api/references/cobrowse-sdk-api.md b/plugins/zoom-developers/skills/rest-api/references/cobrowse-sdk-api.md new file mode 100644 index 00000000..48dfa08f --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/cobrowse-sdk-api.md @@ -0,0 +1,40 @@ +# Zoom Cobrowse SDK API + +Authoritative endpoint inventory for Cobrowse SDK. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/cobrowse-sdk/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 4 | +| Path templates | 4 | +| Tags | 1 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Sessions | 4 | + +## Endpoints by Tag + +### Sessions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/cobrowsesdk/live_sessions` | List live sessions | `Listlivesessions` | +| GET | `/cobrowsesdk/past_sessions` | List past sessions | `Listpastsession` | +| GET | `/cobrowsesdk/sessions/{sessionId}` | Get session details | `Getasession` | +| GET | `/cobrowsesdk/sessions/{sessionId}/users` | List session users | `Listsessionusers` | diff --git a/plugins/zoom-developers/skills/rest-api/references/commerce.md b/plugins/zoom-developers/skills/rest-api/references/commerce.md new file mode 100644 index 00000000..e310a5a3 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/commerce.md @@ -0,0 +1,111 @@ +# Zoom Commerce API + +Authoritative endpoint inventory for Commerce. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/commerce/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 33 | +| Path templates | 31 | +| Tags | 8 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Account Management | 4 | +| Billing | 3 | +| Deal Registration | 5 | +| Order | 4 | +| Platform | 3 | +| Product Catalog | 3 | +| Quote | 6 | +| Subscription | 5 | + +## Endpoints by Tag + +### Account Management + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/commerce/account` | Create an end customer account | `createAccount` | +| POST | `/commerce/account/{accountKey}/contacts` | Add contacts to an existing end customer or your own account. | `addAccountContact` | +| GET | `/commerce/accounts` | Get the list of all accounts associated with a Zoom Partner/Sub-Reseller, by the account type | `getAllAccounts` | +| GET | `/commerce/accounts/{accountKey}` | Get the account details for a Zoom Partner/Subreseller/End Customer | `getAccountDetails` | + +### Billing + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/commerce/billing_documents` | Gets all billing documents for a distributor or a reseller | `getAllBillingDocs` | +| GET | `/commerce/billing_documents/{documentNumber}/document` | Gets the PDF document for the billing document ID | `downloadBillingDoc` | +| GET | `/commerce/invoices/{invoiceNumber}` | Get detailed information about a specific invoice for a distributor or a reseller | `getInvoiceDetail` | + +### Deal Registration + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/commerce/campaigns` | Retrieves all valid Zoom Campaigns which a deal registration can be associated with. | `getCampaigns` | +| POST | `/commerce/deal_registration` | Creates a new deal registration for a partner | `createDealReg` | +| GET | `/commerce/deal_registrations` | Gets all valid Deal Registrations for a partner | `getAllDealRegs` | +| GET | `/commerce/deal_registrations/{dealRegKey}` | Get details of a deal registration by registration number | `getDealRegDetails` | +| PATCH | `/commerce/deal_registrations/{dealRegKey}` | Updates an existing deal registration | `Updatesanexistingdealregistration` | + +### Order + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/commerce/order` | Create a subscription order for a Zoom partner | `createOrder` | +| POST | `/commerce/order/preview` | Preview delta order metrics and subscriptions in an order | `createOrderPreview` | +| GET | `/commerce/orders` | Gets all orders for a Zoom partner. | `getAllOrders` | +| GET | `/commerce/orders/{orderReferenceId}` | Get order details by order reference ID | `getOrderDetails` | + +### Platform + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/commerce/file` | Upload an attachment pdf file in context of a deal registration or quote | `uploadFile` | +| GET | `/commerce/files/{associatedReferenceId}/details` | Gets details of all files associated with a quote or deal registration | `allFileDetails` | +| GET | `/commerce/files/{documentReferenceId}` | Download a file associated with a quote or deal registration. | `downloadFile.` | + +### Product Catalog + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/commerce/catalog` | Gets Zoom Product Catalog for a Zoom Partner | `getOffers` | +| GET | `/commerce/catalog/{offerId}` | Gets the details for a Zoom product or offer. | `getOfferDetail` | +| GET | `/commerce/pricebooks` | Gets the pricebook in a downloadable file | `downloadPricebook` | + +### Quote + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/commerce/quote` | Create a subscription quote for a Zoom Partner | `createQuote` | +| POST | `/commerce/quote/preview` | Preview delta quote metrics and subscriptions in a quote | `createQuotePreview` | +| GET | `/commerce/quotes` | Gets all quotes for a Zoom partner | `getAllQuotes` | +| GET | `/commerce/quotes/{quoteReferenceId}` | Get quote details by quote reference ID | `getQuoteDetails` | +| PATCH | `/commerce/quotes/{quoteReferenceId}` | Update a subscription quote for a Zoom partner | `updateQuote` | +| PATCH | `/commerce/quotes/{quoteReferenceId}/fulfillment` | Submits a subscription quote for provisioning | `provisionQuote` | + +### Subscription + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/commerce/subscriptions` | Gets paid subscriptions for a Zoom partner. | `getAllSubscriptions` | +| GET | `/commerce/subscriptions/{subscriptionNumber}` | Gets subscription details for a given subscription number | `getSubscriptionDetails` | +| GET | `/commerce/subscriptions/{subscriptionNumber}/versions` | Gets subscription changes/versions for a given subscription number. | `getSubscriptionVersions` | +| GET | `/commerce/trials` | Get trial subscriptions for a Zoom partner | `getAllTrialSubscriptions` | +| GET | `/commerce/trials/{trialReferenceId}` | Get trial details for an end customer by their Zoom account number or the trial ID | `getTrialDetails` | diff --git a/plugins/zoom-developers/skills/rest-api/references/contact-center.md b/plugins/zoom-developers/skills/rest-api/references/contact-center.md new file mode 100644 index 00000000..8199dd7e --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/contact-center.md @@ -0,0 +1,458 @@ +# Zoom Contact Center API + +Authoritative endpoint inventory for Contact Center. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/contact-center/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 278 | +| Path templates | 157 | +| Tags | 25 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Address Books | 21 | +| Agent Statuses | 5 | +| Asset Library | 12 | +| Call Control | 3 | +| Campaigns | 21 | +| Dispositions | 10 | +| Engagements | 8 | +| Flows | 11 | +| Inboxes | 18 | +| Logs | 5 | +| Messaging | 1 | +| Notes | 4 | +| Operating Hours | 14 | +| Queues | 35 | +| Recordings | 4 | +| Regions | 7 | +| Reports V2(CX Analytics) | 11 | +| Reports(Legacy Reports) | 7 | +| Roles | 10 | +| Routing Profiles | 10 | +| Skills | 11 | +| SMS | 1 | +| Teams | 14 | +| Users | 22 | +| Variables | 13 | + +## Endpoints by Tag + +### Address Books + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/address_books` | List address books | `listAddressBooks` | +| POST | `/contact_center/address_books` | Create an address book | `createAddressBook` | +| GET | `/contact_center/address_books/contacts/{contactId}/custom_fields` | List a contact's custom fields | `ListContactCustomFields` | +| GET | `/contact_center/address_books/custom_fields` | List an address book's custom fields | `Listaddressbookcustomfields` | +| POST | `/contact_center/address_books/custom_fields` | Create an address book custom field | `Createacustomfield` | +| DELETE | `/contact_center/address_books/custom_fields/{customFieldId}` | Delete an address book custom field | `Deleteancustomfield` | +| GET | `/contact_center/address_books/custom_fields/{customFieldId}` | Get an address book's custom field | `Getaaddressbookcustomfield` | +| PATCH | `/contact_center/address_books/custom_fields/{customFieldId}` | Update an address book custom field | `Updateacustomfield` | +| GET | `/contact_center/address_books/units` | List address book units | `listUnits` | +| POST | `/contact_center/address_books/units` | Create an address book unit | `createUnit` | +| DELETE | `/contact_center/address_books/units/{unitId}` | Delete an address book unit | `deleteUnit` | +| GET | `/contact_center/address_books/units/{unitId}` | Get an address book unit | `getUnit` | +| PATCH | `/contact_center/address_books/units/{unitId}` | Update an address book unit | `updateUnit` | +| DELETE | `/contact_center/address_books/{addressBookId}` | Delete an address book | `deleteAddressBook` | +| GET | `/contact_center/address_books/{addressBookId}` | Get an address book | `getAddressBook` | +| PATCH | `/contact_center/address_books/{addressBookId}` | Update an address book | `updateAddressBook` | +| GET | `/contact_center/address_books/{addressBookId}/contacts` | List address book contacts | `listContacts` | +| POST | `/contact_center/address_books/{addressBookId}/contacts` | Create an address book contact | `createContact` | +| DELETE | `/contact_center/address_books/{addressBookId}/contacts/{contactId}` | Delete an address book contact | `contactDelete` | +| GET | `/contact_center/address_books/{addressBookId}/contacts/{contactId}` | Get an address book contact | `getContact` | +| PATCH | `/contact_center/address_books/{addressBookId}/contacts/{contactId}` | Update an address book contact | `updateContact` | + +### Agent Statuses + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/system_statuses` | List system statuses | `listSystemStatus` | +| POST | `/contact_center/system_statuses` | Create a system status | `createSystemStatus` | +| DELETE | `/contact_center/system_statuses/{statusId}` | Delete a system status | `deleteSystemStatus` | +| GET | `/contact_center/system_statuses/{statusId}` | Get a system status | `getAStatus` | +| PATCH | `/contact_center/system_statuses/{statusId}` | Update a system status | `updateSystemStatus` | + +### Asset Library + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/asset_library/assets` | List assets | `listAssets` | +| POST | `/contact_center/asset_library/assets` | Create an asset | `createAnAsset` | +| DELETE | `/contact_center/asset_library/assets/{assetId}` | Delete an asset | `deleteAnAsset` | +| GET | `/contact_center/asset_library/assets/{assetId}` | Get an asset | `getAnAsset` | +| PATCH | `/contact_center/asset_library/assets/{assetId}` | Update an asset | `updateAnAsset` | +| POST | `/contact_center/asset_library/assets/{assetId}/duplicate` | Duplicate an asset | `duplicateAnAsset` | +| DELETE | `/contact_center/asset_library/assets/{assetId}/items` | Delete asset items | `Deleteassetitems` | +| GET | `/contact_center/asset_library/categories` | List asset categories | `listAssetCategories` | +| POST | `/contact_center/asset_library/categories` | Create an asset category | `createAnAssetCategory` | +| DELETE | `/contact_center/asset_library/categories/{categoryId}` | Delete an asset category | `deleteAnAssetCategory` | +| GET | `/contact_center/asset_library/categories/{categoryId}` | Get an asset category | `getAnAssetCategory` | +| PATCH | `/contact_center/asset_library/categories/{categoryId}` | Update an asset category | `updateAnAssetCategory` | + +### Call Control + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PUT | `/contact_center/engagements/{engagementId}/recording/{command}` | Control an engagement's recording | `engagementRecordingControl` | +| POST | `/contact_center/users/{userId}/commands` | Command control of a user | `userControl` | +| GET | `/contact_center/users/{userId}/devices` | List user's devices | `Listuserdevices` | + +### Campaigns + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/outbound_campaign/campaigns` | List outbound campaigns | `listOutboundCampaigns` | +| POST | `/contact_center/outbound_campaign/campaigns` | Create an outbound campaign | `createOutboundCampaign` | +| DELETE | `/contact_center/outbound_campaign/campaigns/{campaignId}` | Delete an outbound campaign | `deleteOutboundCampaign` | +| GET | `/contact_center/outbound_campaign/campaigns/{campaignId}` | Get an outbound campaign | `getOutboundCampaign` | +| PATCH | `/contact_center/outbound_campaign/campaigns/{campaignId}` | Update an outbound campaign | `updateOutboundCampaign` | +| PATCH | `/contact_center/outbound_campaign/campaigns/{campaignId}/status` | Update an outbound campaign status | `Updateanoutboundcampaignstatus` | +| GET | `/contact_center/outbound_campaign/contact_lists` | List campaign contact lists | `listCampaignContactLists` | +| PATCH | `/contact_center/outbound_campaign/contact_lists` | Batch update campaign contact lists | `Batchupdatecampaigncontactlists` | +| POST | `/contact_center/outbound_campaign/contact_lists` | Create a campaign contact list | `createCampaignContactList` | +| DELETE | `/contact_center/outbound_campaign/contact_lists/{contactListId}` | Remove a campaign contact list | `deleteCampaignContactList` | +| GET | `/contact_center/outbound_campaign/contact_lists/{contactListId}` | Get a campaign contact list | `getCampaignContactList` | +| PATCH | `/contact_center/outbound_campaign/contact_lists/{contactListId}` | Update a campaign contact list | `updateCampaignContactList` | +| GET | `/contact_center/outbound_campaign/contact_lists/{contactListId}/contacts` | List a campaign contact list's contacts | `listCampaignContactListContacts` | +| PATCH | `/contact_center/outbound_campaign/contact_lists/{contactListId}/contacts` | Update a batch of contacts on a campaign contact list | `Updateabatchofcontactsonacampaigncontactlist` | +| POST | `/contact_center/outbound_campaign/contact_lists/{contactListId}/contacts` | Create a campaign contact list's contact | `createCampaignContactListContact` | +| DELETE | `/contact_center/outbound_campaign/contact_lists/{contactListId}/contacts/{contactId}` | Remove campaign contact list's contact | `deleteCampaigncontactListContact` | +| GET | `/contact_center/outbound_campaign/contact_lists/{contactListId}/contacts/{contactId}` | Get a campaign contact list's contact | `getCampaignContactListContact` | +| PATCH | `/contact_center/outbound_campaign/contact_lists/{contactListId}/contacts/{contactId}` | Update contact on a campaign contact list | `updateCampaignContactListContact` | +| DELETE | `/contact_center/outbound_campaign/dnc_lists/{dncListId}/phones` | Batch delete a campaign DNC list's phones | `batchDeleteCampaignDncListPhone` | +| GET | `/contact_center/outbound_campaign/dnc_lists/{dncListId}/phones` | List campaign DNC phone numbers | `listCampaignDncListPhones` | +| POST | `/contact_center/outbound_campaign/dnc_lists/{dncListId}/phones` | Batch create a campaign DNC list's phones | `batchCreateCampaignDncListPhones` | + +### Dispositions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/dispositions` | List dispositions | `listDispositions` | +| POST | `/contact_center/dispositions` | Create a disposition | `createDisposition` | +| GET | `/contact_center/dispositions/sets` | List disposition sets | `listSets` | +| POST | `/contact_center/dispositions/sets` | Create a disposition set | `createSet` | +| DELETE | `/contact_center/dispositions/sets/{dispositionSetId}` | Delete a disposition set | `deleteSet` | +| GET | `/contact_center/dispositions/sets/{dispositionSetId}` | Get a disposition set | `getSet` | +| PATCH | `/contact_center/dispositions/sets/{dispositionSetId}` | Update a disposition set | `updateSet` | +| DELETE | `/contact_center/dispositions/{dispositionId}` | Delete a disposition | `deleteDisposition` | +| GET | `/contact_center/dispositions/{dispositionId}` | Get a disposition | `getDisposition` | +| PATCH | `/contact_center/dispositions/{dispositionId}` | Update a disposition | `updateDisposition` | + +### Engagements + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/contact_center/engagement` | Start an engagement | `Startworkitemengagement` | +| GET | `/contact_center/engagements` | List engagements | `listEngagements` | +| GET | `/contact_center/engagements/{engagementId}` | Get an engagement | `getEngagement` | +| PATCH | `/contact_center/engagements/{engagementId}` | Update an engagement | `updateEngagement` | +| GET | `/contact_center/engagements/{engagementId}/attachments` | Get an engagement's attachments | `ListAttachments` | +| GET | `/contact_center/engagements/{engagementId}/events` | Get an engagement's events | `getEngagementEvents` | +| GET | `/contact_center/engagements/{engagementId}/recordings/status` | Poll an engagement recording's status | `EngagementRecordingStatus` | +| GET | `/contact_center/engagements/{engagementId}/survey` | Get an engagement's survey | `getEngagementSurvey` | + +### Flows + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/flows` | List flows | `listFlows` | +| POST | `/contact_center/flows` | Import a flow | `ImportFlow` | +| DELETE | `/contact_center/flows/{flowId}` | Delete a flow | `DeleteFlow` | +| GET | `/contact_center/flows/{flowId}` | Get a flow | `getAFlow` | +| PATCH | `/contact_center/flows/{flowId}` | Edit a flow | `EditFlow` | +| DELETE | `/contact_center/flows/{flowId}/entry_points` | Remove flow entry points | `RemoveFlowEntryPoints` | +| GET | `/contact_center/flows/{flowId}/entry_points` | List flow's entry points | `ListFlowEntryPoints` | +| POST | `/contact_center/flows/{flowId}/entry_points` | Add flow entry points | `AddFlowEntryPoints` | +| GET | `/contact_center/flows/{flowId}/export` | Export a flow | `ExportFlow` | +| PUT | `/contact_center/flows/{flowId}/publish` | Publish a flow | `PublishFlow` | +| GET | `/contact_center/flows_entry_points` | List entry points | `ListentryPoints` | + +### Inboxes + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/contact_center/inboxes` | Delete inboxes | `inboxesDelete` | +| GET | `/contact_center/inboxes` | List inboxes | `listInbox` | +| POST | `/contact_center/inboxes` | Create an inbox | `inboxCreate` | +| DELETE | `/contact_center/inboxes/messages` | Delete inbox messages | `inboxesMessagesDelete` | +| GET | `/contact_center/inboxes/messages` | List an account's inbox messages | `listInboxesMessages` | +| GET | `/contact_center/inboxes/{inboxId}` | Get an inbox | `getInbox` | +| PATCH | `/contact_center/inboxes/{inboxId}` | Update an inbox | `inboxUpdate` | +| PATCH | `/contact_center/inboxes/{inboxId}/email_notification` | Update an inbox email notification | `Updateaninboxemailnotification` | +| GET | `/contact_center/inboxes/{inboxId}/email_notifications` | Get inbox email notification list | `Getinboxemailnotificationlist` | +| DELETE | `/contact_center/inboxes/{inboxId}/messages` | Delete an inbox's messages | `inboxMessagesDelete` | +| GET | `/contact_center/inboxes/{inboxId}/messages` | List an inbox's messages | `listInboxMessages` | +| DELETE | `/contact_center/inboxes/{inboxId}/messages/{messageId}` | Delete an inbox message | `inboxMessageDelete` | +| DELETE | `/contact_center/inboxes/{inboxId}/queues` | Remove inbox access queues | `unassignInboxQueues` | +| GET | `/contact_center/inboxes/{inboxId}/queues` | Get inbox access queues | `listInboxQueues` | +| POST | `/contact_center/inboxes/{inboxId}/queues` | Assign inbox access queues | `assignInboxQueues` | +| DELETE | `/contact_center/inboxes/{inboxId}/users` | Unassign inbox access users | `unassignInboxUsers` | +| GET | `/contact_center/inboxes/{inboxId}/users` | Get an inbox's users | `listInboxUsers` | +| POST | `/contact_center/inboxes/{inboxId}/users` | Assign inbox access users | `assignInboxUsers` | + +### Logs + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/email/messages` | List email message history | `getEmailMessageHistory` | +| GET | `/contact_center/messaging/messages` | List message history | `getMessageHistory` | +| GET | `/contact_center/sms` | List SMS logs | `listSMS` | +| GET | `/contact_center/voice_calls` | List voice call logs | `listVoiceCall` | +| GET | `/contact_center/work_item/messages` | List work item message history | `getWorkItemMessageHistory` | + +### Messaging + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/contact_center/messages` | Send a message | `SendaMessage` | + +### Notes + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/engagements/notes` | List notes | `notes` | +| GET | `/contact_center/engagements/{engagementId}/notes` | List engagement notes | `engagementNotes` | +| GET | `/contact_center/engagements/{engagementId}/notes/{noteId}` | Get a note | `getNote` | +| PATCH | `/contact_center/engagements/{engagementId}/notes/{noteId}` | Update a note | `noteUpdate` | + +### Operating Hours + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/business_hours` | List business hours | `listBusinessHours` | +| POST | `/contact_center/business_hours` | Create business hours | `businessHourCreate` | +| DELETE | `/contact_center/business_hours/{businessHourId}` | Delete business hours | `businessHourDelete` | +| GET | `/contact_center/business_hours/{businessHourId}` | Get business hours | `getABusinessHour` | +| PATCH | `/contact_center/business_hours/{businessHourId}` | Update business hours | `businessHourUpdate` | +| GET | `/contact_center/business_hours/{businessHourId}/flows` | List the business hours' flows | `listBusinessHourFlows` | +| GET | `/contact_center/business_hours/{businessHourId}/queues` | List the business hours' queues | `listBusinessHourQueues` | +| GET | `/contact_center/closures` | List closures | `listClosures` | +| POST | `/contact_center/closures` | Create a closure set | `closuresSetCreate` | +| DELETE | `/contact_center/closures/{closureSetId}` | Delete a closure set | `closureSetDelete` | +| GET | `/contact_center/closures/{closureSetId}` | Get a closure set | `getAClosureSet` | +| PATCH | `/contact_center/closures/{closureSetId}` | Update a closure set | `closureSetUpdate` | +| GET | `/contact_center/closures/{closureSetId}/flows` | List the closures' flows | `listClosureSetFlows` | +| GET | `/contact_center/closures/{closureSetId}/queues` | List the closures' queues | `listClosureSetQueues` | + +### Queues + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/queue_templates` | List queue templates | `Listqueuetemplates` | +| GET | `/contact_center/queue_templates/{queueTemplateId}` | Get a queue template | `getQueueTemplate` | +| GET | `/contact_center/queues` | List queues | `listQueues` | +| POST | `/contact_center/queues` | Create a queue | `queueCreate` | +| DELETE | `/contact_center/queues/batch` | Batch delete queues | `Batchdeletequeues` | +| POST | `/contact_center/queues/batch` | Batch create queues with a template | `Batchcreatequeueswithatemplate` | +| DELETE | `/contact_center/queues/{queueId}` | Delete a queue | `queueDelete` | +| GET | `/contact_center/queues/{queueId}` | Get a queue | `getAQueue` | +| PATCH | `/contact_center/queues/{queueId}` | Update a queue | `queueUpdate` | +| GET | `/contact_center/queues/{queueId}/agents` | List queue agents | `getQueueAgents` | +| POST | `/contact_center/queues/{queueId}/agents` | Assign queue agents | `assignQueueAgents` | +| DELETE | `/contact_center/queues/{queueId}/agents/{userId}` | Unassign a queue agent | `deleteQueueAgent` | +| PATCH | `/contact_center/queues/{queueId}/agents/{userId}` | Update a queue agent | `updateQueueAgent` | +| GET | `/contact_center/queues/{queueId}/dispositions` | List queue dispositions | `getQueueDispositions` | +| POST | `/contact_center/queues/{queueId}/dispositions` | Assign queue dispositions | `assignQueueDispositions` | +| GET | `/contact_center/queues/{queueId}/dispositions/sets` | List queue disposition sets | `getQueueDispositionSets` | +| POST | `/contact_center/queues/{queueId}/dispositions/sets` | Assign queue disposition sets | `assignQueueDispositionSets` | +| DELETE | `/contact_center/queues/{queueId}/dispositions/sets/{dispositionSetId}` | Unassign a queue disposition set | `deleteQueueDispositionSet` | +| DELETE | `/contact_center/queues/{queueId}/dispositions/{dispositionId}` | Unassign a queue disposition | `deleteQueueDisposition` | +| PATCH | `/contact_center/queues/{queueId}/interrupt` | Update a queue's interrupt settings | `updateQueueInterrupts` | +| DELETE | `/contact_center/queues/{queueId}/interrupt_menu` | Delete a queue's interrupt menu configuration | `deleteQueueInterruptMenu` | +| POST | `/contact_center/queues/{queueId}/interrupt_menu` | Assign a queue menu based interrupt | `assignQueueMenuBasedInterrupt` | +| GET | `/contact_center/queues/{queueId}/operating_hours` | Get a queue's operating hours | `getAQueueOperatingHours` | +| PATCH | `/contact_center/queues/{queueId}/operating_hours` | Update a queue's operating hours | `QueueOperatingHoursUpdate` | +| DELETE | `/contact_center/queues/{queueId}/recordings` | Delete queue recordings | `deleteQueueRecordings` | +| GET | `/contact_center/queues/{queueId}/recordings` | List queue recordings | `listQueueRecordings` | +| DELETE | `/contact_center/queues/{queueId}/scheduled_callbacks/attendees/{attendeeId}` | Delete an attendee from a scheduled callback event | `Deleteascheduledcallbackforanattendee` | +| POST | `/contact_center/queues/{queueId}/scheduled_callbacks/events` | Schedule a callback on a queue | `Scheduleacallbackonaqueue` | +| GET | `/contact_center/queues/{queueId}/scheduled_callbacks/supportive_slots` | List a queue's scheduled callbacks availability | `Listqueuescheduledcallbacksavailability` | +| GET | `/contact_center/queues/{queueId}/supervisors` | List queue supervisors | `getQueueSupervisors` | +| POST | `/contact_center/queues/{queueId}/supervisors` | Assign queue supervisors | `assignQueueSupervisors` | +| DELETE | `/contact_center/queues/{queueId}/supervisors/{userId}` | Unassign a queue supervisor | `deleteQueueSupervisor` | +| DELETE | `/contact_center/queues/{queueId}/teams` | Unassign multiple teams in a queue | `batchDeleteQueueTeams` | +| POST | `/contact_center/queues/{queueId}/teams` | Assign queue teams | `assignQueueTeams` | +| DELETE | `/contact_center/queues/{queueId}/teams/{teamId}` | Unassign a queue team | `deleteQueueTeam` | + +### Recordings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/contact_center/engagements/{engagementId}/recordings` | Delete engagement recordings | `deleteEngagementRecordings` | +| GET | `/contact_center/engagements/{engagementId}/recordings` | List engagement recordings | `listEngagementRecordings` | +| GET | `/contact_center/recordings` | List recordings | `listRecordings` | +| DELETE | `/contact_center/recordings/{recordingId}` | Delete a recording | `deleteRecording` | + +### Regions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/regions` | List regions | `ListRegions` | +| POST | `/contact_center/regions` | Create a region | `CreateARegion` | +| DELETE | `/contact_center/regions/{regionId}` | Delete a region | `DeleteARegion` | +| GET | `/contact_center/regions/{regionId}` | Get a region | `GetARegion` | +| PATCH | `/contact_center/regions/{regionId}` | Update a region | `UpdateARegion` | +| GET | `/contact_center/regions/{regionId}/users` | List a region's users | `ListRegion'sUsers` | +| POST | `/contact_center/regions/{regionId}/users` | Assign users to a region | `AssignUsersToARegion` | + +### Reports V2(CX Analytics) + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/analytics/dataset/historical/agent_performance` | List historical agent performance dataset data | `Listhistoricalagentperformancedatasetdata` | +| GET | `/contact_center/analytics/dataset/historical/agent_timecard` | List historical agent timecard dataset data | `Listhistoricalagenttimecarddatasetdata` | +| GET | `/contact_center/analytics/dataset/historical/disposition` | List historical disposition dataset data | `Listhistoricaldispositiondatasetdata` | +| GET | `/contact_center/analytics/dataset/historical/engagement` | List historical engagement dataset data | `Listengagementdatasetdata` | +| GET | `/contact_center/analytics/dataset/historical/expert_assist` | List historical expert assist dataset data | `Listexpertassistdatasetdata` | +| GET | `/contact_center/analytics/dataset/historical/flow_performance` | List historical flow performance dataset data | `Listhistoricalflowperformancedatasetdata` | +| GET | `/contact_center/analytics/dataset/historical/outbound_dialer_performance` | List historical outbound dialer performance dataset data | `Listhistoricaloutbounddialerperformancedatasetdata` | +| GET | `/contact_center/analytics/dataset/historical/queue_performance` | List historical queue performance dataset data | `Listhistoricalqueueperformancedatasetdata` | +| GET | `/contact_center/analytics/log/historical/engagement` | List historical engagement log data | `Listhistoricalengagementlogs` | +| GET | `/contact_center/analytics/log/historical/journey` | List historical Zoom Phone to Contact Center call journey data | `ListhistoricalZoomphonetozcccalljourneydata` | +| GET | `/contact_center/reports/operation_logs` | List operation logs | `listOperationLogs` | + +### Reports(Legacy Reports) + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/analytics/agents/leg_metrics` | List agent leg reports | `listAgentLegMetric` | +| GET | `/contact_center/analytics/agents/status_history` | List agent's status history reports | `listAgentStatusHistory` | +| GET | `/contact_center/analytics/agents/time_sheets` | List agent's time sheet reports | `listAgentTimeSheet` | +| GET | `/contact_center/analytics/historical/details/metrics` | List historical detail reports | `listHistoricalDetailMetric` | +| GET | `/contact_center/analytics/historical/queues/agents/metrics` | List historical agent reports by queue | `listQueueAgentsMetrics` | +| GET | `/contact_center/analytics/historical/queues/metrics` | List historical queue reports | `listHistoricalQueueMetric` | +| GET | `/contact_center/analytics/historical/queues/{queueId}/agents/metrics` | List historical queue's agents reports | `listQueueAgentMetric` | + +### Roles + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/roles` | List roles | `listRoles` | +| POST | `/contact_center/roles` | Create a role | `createRole` | +| POST | `/contact_center/roles/duplicate` | Duplicate a role | `Duplicatearole` | +| DELETE | `/contact_center/roles/{roleId}` | Delete a role | `deleteRole` | +| GET | `/contact_center/roles/{roleId}` | Get a role | `getRole` | +| PATCH | `/contact_center/roles/{roleId}` | Update a role | `updateRole` | +| DELETE | `/contact_center/roles/{roleId}/privileges` | Delete role privileges | `Deleteroleprivileges` | +| GET | `/contact_center/roles/{roleId}/users` | List users of a role | `getRoleUsers` | +| POST | `/contact_center/roles/{roleId}/users` | Assign a role | `assignRoleUsers` | +| DELETE | `/contact_center/roles/{roleId}/users/{userId}` | Unassign a role | `deleteRoleUser` | + +### Routing Profiles + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/agent_routing_profiles` | List agent routing profiles | `Listagentroutingprofiles` | +| POST | `/contact_center/agent_routing_profiles` | Create an agent routing profile | `Createanagentroutingprofile` | +| DELETE | `/contact_center/agent_routing_profiles/{agentRoutingProfileId}` | Delete an agent routing profile | `Deleteanagentroutingprofile` | +| GET | `/contact_center/agent_routing_profiles/{agentRoutingProfileId}` | Get an agent routing profile | `getAgentRoutingProfile` | +| PATCH | `/contact_center/agent_routing_profiles/{agentRoutingProfileId}` | Update an agent routing profile's details | `Updateanagentroutingprofile'sdetails` | +| GET | `/contact_center/consumer_routing_profiles` | List consumer routing profiles | `Listconsumerroutingprofiles` | +| POST | `/contact_center/consumer_routing_profiles` | Create a consumer routing profile | `Createaconsumerroutingprofile` | +| DELETE | `/contact_center/consumer_routing_profiles/{consumerRoutingProfileId}` | Delete a consumer routing profile | `Deleteaconsumerroutingprofile` | +| GET | `/contact_center/consumer_routing_profiles/{consumerRoutingProfileId}` | Get a consumer routing profile | `Getaconsumerroutingprofile` | +| PATCH | `/contact_center/consumer_routing_profiles/{consumerRoutingProfileId}` | Update a consumer routing profile's details | `Updateaconsumerroutingprofile'sdetails` | + +### Skills + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/skills` | List skills | `listSkills` | +| POST | `/contact_center/skills` | Create a skill | `skillCreate` | +| GET | `/contact_center/skills/categories` | List skill categories | `listSkillCategory` | +| POST | `/contact_center/skills/categories` | Create a skill category | `SkillCategoryCreate` | +| DELETE | `/contact_center/skills/categories/{skillCategoryId}` | Delete a skill category | `SkillCategoryDelete` | +| GET | `/contact_center/skills/categories/{skillCategoryId}` | Get a skill category | `getSkillCategory` | +| PATCH | `/contact_center/skills/categories/{skillCategoryId}` | Update a skill category | `SkillCategoryUpdate` | +| DELETE | `/contact_center/skills/{skillId}` | Delete a skill | `skillDelete` | +| GET | `/contact_center/skills/{skillId}` | Get a skill | `getSkill` | +| PATCH | `/contact_center/skills/{skillId}` | Update a skill | `skillNameUpdate` | +| GET | `/contact_center/skills/{skillId}/users` | List users of a skill | `listSkillUsers` | + +### SMS + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/contact_center/sms` | Send an SMS | `contactCenterSMS` | + +### Teams + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/teams` | List teams | `listTeams` | +| POST | `/contact_center/teams` | Create a team | `CreateTeam` | +| DELETE | `/contact_center/teams/{teamId}` | Delete a team | `deleteTeam` | +| GET | `/contact_center/teams/{teamId}` | Get a team | `getTeamDetail` | +| PATCH | `/contact_center/teams/{teamId}` | Update a team | `Updateateam` | +| DELETE | `/contact_center/teams/{teamId}/agents` | Unassign team agents | `unassignTeamAgents` | +| GET | `/contact_center/teams/{teamId}/agents` | List team agents | `listTeamAgents` | +| POST | `/contact_center/teams/{teamId}/agents` | Assign team agents | `assignTeamAgents` | +| GET | `/contact_center/teams/{teamId}/children` | List a team's child teams | `getTeamChildTeams` | +| PATCH | `/contact_center/teams/{teamId}/move` | Move a team | `moveTeam` | +| GET | `/contact_center/teams/{teamId}/parents` | List team's parent teams | `getTeamParentTeams` | +| DELETE | `/contact_center/teams/{teamId}/supervisors` | Unassign team supervisors | `unassignTeamSupervisors` | +| GET | `/contact_center/teams/{teamId}/supervisors` | List team supervisors | `listTeamSupervisors` | +| POST | `/contact_center/teams/{teamId}/supervisors` | Assign team supervisors | `assignTeamSupervisors` | + +### Users + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/contact_center/users` | Batch delete user profiles | `batchDeleteUsers` | +| GET | `/contact_center/users` | List users' profiles | `users` | +| PATCH | `/contact_center/users` | Batch update user profiles | `BatchUpdateUsers` | +| POST | `/contact_center/users` | Create a user's profile | `createUser` | +| POST | `/contact_center/users/batch` | Batch create user profiles | `BatchCreateUsers` | +| PATCH | `/contact_center/users/status` | Batch update user status | `Batchupdateuserstatus` | +| GET | `/contact_center/users/templates` | List user templates | `ListUserTemplates` | +| POST | `/contact_center/users/templates` | Create a user template | `createAUserTemplate` | +| DELETE | `/contact_center/users/templates/{templateId}` | Delete a user template | `deleteAUserTemplate` | +| GET | `/contact_center/users/templates/{templateId}` | Get a user template | `Getanusertemplate` | +| PATCH | `/contact_center/users/templates/{templateId}` | Update a user template | `updateAUserTemplate` | +| DELETE | `/contact_center/users/{userId}` | Delete a user's profile | `userDelete` | +| GET | `/contact_center/users/{userId}` | Get a user's profile | `userGet` | +| PATCH | `/contact_center/users/{userId}` | Update a user's profile | `userUpdate` | +| PATCH | `/contact_center/users/{userId}/opt_in_out_queues` | Batch opt in or opt out a user's queues | `BatchOptInOrOutUserQueues` | +| GET | `/contact_center/users/{userId}/queues` | List user's queues | `listUserQueues` | +| DELETE | `/contact_center/users/{userId}/recordings` | Delete a user's recordings | `deleteUserRecordings` | +| GET | `/contact_center/users/{userId}/recordings` | List a user's recordings | `listUserRecordings` | +| GET | `/contact_center/users/{userId}/skills` | List user's skills | `ListAUserSkills` | +| POST | `/contact_center/users/{userId}/skills` | Assign user's skills | `assignSkills` | +| DELETE | `/contact_center/users/{userId}/skills/{skillId}` | Unassign user's skill | `deleteASkill` | +| PATCH | `/contact_center/users/{userId}/status` | Update a user's status | `Updateauser'sstatus` | + +### Variables + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contact_center/variable_logs` | List variable logs | `listVariableLogs` | +| DELETE | `/contact_center/variable_logs/{variableLogId}` | Delete a variable log | `deleteVariableLog` | +| GET | `/contact_center/variable_logs/{variableLogId}` | Get a variable log | `getVariableLog` | +| GET | `/contact_center/variables` | List variables | `variables` | +| POST | `/contact_center/variables` | Create a variable | `createVariable` | +| GET | `/contact_center/variables/groups` | List variable groups | `listVariableGroups` | +| POST | `/contact_center/variables/groups` | Create a variable group | `createVariableGroup` | +| DELETE | `/contact_center/variables/groups/{variableGroupId}` | Delete a variable group | `DeleteGroup` | +| GET | `/contact_center/variables/groups/{variableGroupId}` | Get a variable group | `getAVariableGroup` | +| PATCH | `/contact_center/variables/groups/{variableGroupId}` | Update a variable group | `updateVariableGroup` | +| DELETE | `/contact_center/variables/{variableId}` | Delete a variable | `variableDelete` | +| GET | `/contact_center/variables/{variableId}` | Get a variable | `variableGet` | +| PATCH | `/contact_center/variables/{variableId}` | Update a variable | `variableUpdate` | diff --git a/plugins/zoom-developers/skills/rest-api/references/crc.md b/plugins/zoom-developers/skills/rest-api/references/crc.md new file mode 100644 index 00000000..7a2a8a69 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/crc.md @@ -0,0 +1,80 @@ +# Zoom Conference Room Connector API + +Authoritative endpoint inventory for Conference Room Connector. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/crc/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 20 | +| Path templates | 9 | +| Tags | 5 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Account | 2 | +| Api Connector | 7 | +| Cisco/Polycom Rooms | 5 | +| Participant | 1 | +| Room Template | 5 | + +## Endpoints by Tag + +### Account + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/crc/managed_rooms/account_setting` | Get Cisco/Polycom Room Account Setting | `getCiscoPolycomRoomAccountSetting` | +| PATCH | `/crc/managed_rooms/account_setting` | Update Cisco/Polycom Room Account Setting | `UpdateCiscoPolycomRoomAccountSetting` | + +### Api Connector + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/crc/api_connectors` | List API Connectors | `GetListAPIConnectors` | +| POST | `/crc/api_connectors` | Create an API Connector | `CreateAPIConnector` | +| DELETE | `/crc/api_connectors/{connectorId}` | Delete an API Connector | `DeleteAPIConnector` | +| GET | `/crc/api_connectors/{connectorId}` | Get an API Connector | `GetAPIConnector` | +| PATCH | `/crc/api_connectors/{connectorId}` | Update an API Connector | `UpdateAPIConnector` | +| GET | `/crc/api_connectors/{connectorId}/private_key` | Get an API Connector's private key | `GetanAPIConnector'sprivatekey` | +| PATCH | `/crc/api_connectors/{connectorId}/private_key` | Update an API Connector's private key | `UpdateAPIConnectorPrivateKey` | + +### Cisco/Polycom Rooms + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/crc/managed_rooms` | List Managed Rooms | `ListManagedRooms` | +| POST | `/crc/managed_rooms` | Create a Managed Room | `CreateaManagedRoom` | +| DELETE | `/crc/managed_rooms/{deviceId}` | Delete a managed room | `Deleteamanagedroom` | +| GET | `/crc/managed_rooms/{deviceId}` | Get a Managed Room | `GetaManagedRoom` | +| PATCH | `/crc/managed_rooms/{deviceId}` | Update a Managed Room | `UpdateaManagedRoom` | + +### Participant + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/crc/participant_identifier_code` | Get participant identifier code | `get_participant_identifier_code` | + +### Room Template + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/crc/room_templates` | List Room Templates | `ListRoomTemplates` | +| POST | `/crc/room_templates` | Create a Room Template | `CreateaRoomTemplate` | +| DELETE | `/crc/room_templates/{templateId}` | Delete a room template | `Deletearoomtemplate` | +| GET | `/crc/room_templates/{templateId}` | Get a Room Template | `GetaRoomTemplate` | +| PATCH | `/crc/room_templates/{templateId}` | Update a Room Template | `UpdateaRoomTemplate` | diff --git a/plugins/zoom-developers/skills/rest-api/references/environment-variables.md b/plugins/zoom-developers/skills/rest-api/references/environment-variables.md new file mode 100644 index 00000000..409282b1 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/environment-variables.md @@ -0,0 +1,21 @@ +# Zoom REST API Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_CLIENT_ID` | Yes | OAuth app identity for API access | Zoom Marketplace -> OAuth app -> App Credentials | +| `ZOOM_CLIENT_SECRET` | Yes | OAuth app secret | Zoom Marketplace -> OAuth app -> App Credentials | +| `ZOOM_ACCOUNT_ID` | S2S OAuth mode | Account token grant | Zoom Marketplace -> Server-to-Server OAuth app credentials | +| `ZOOM_REDIRECT_URI` | User OAuth mode | Authorization callback URL | Zoom Marketplace -> OAuth redirect/allow list | +| `ZOOM_WEBHOOK_SECRET` | If receiving events | Signature validation for webhook events | Zoom Marketplace -> Event Subscriptions -> Secret Token | + +## Runtime-only values + +- `ZOOM_ACCESS_TOKEN` +- `ZOOM_REFRESH_TOKEN` + +## Notes + +- Use `ZOOM_ACCOUNT_ID` for server-to-server service integrations. +- User-level integrations require authorization code flow and `ZOOM_REDIRECT_URI`. diff --git a/plugins/zoom-developers/skills/rest-api/references/events.md b/plugins/zoom-developers/skills/rest-api/references/events.md new file mode 100644 index 00000000..35d8850b --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/events.md @@ -0,0 +1,222 @@ +# Zoom Events API + +Authoritative endpoint inventory for Events. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/events/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 90 | +| Path templates | 52 | +| Tags | 17 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Attendee Actions | 4 | +| Co Editors | 2 | +| Emails | 2 | +| Event Access | 5 | +| Events | 6 | +| Exhibitors | 6 | +| Files | 3 | +| Hubs | 5 | +| Registrants | 2 | +| Reports | 7 | +| Sessions | 15 | +| Speakers | 5 | +| Ticket Types | 8 | +| Tickets | 5 | +| Video On-Demand | 9 | +| Video On-Demand Registrations | 4 | +| Videos | 2 | + +## Endpoints by Tag + +### Attendee Actions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/attendee_action` | List event attendee actions | `ListEventAttendeeActions` | +| PATCH | `/zoom_events/events/{eventId}/attendee_action` | Update event attendee actions | `UpdateEventAttendeeActions` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}/attendee_action` | List session attendee actions | `ListSessionAttendeeActions` | +| PATCH | `/zoom_events/events/{eventId}/sessions/{sessionId}/attendee_action` | Update session attendee actions | `UpdateSessionAttendeeActions` | + +### Co Editors + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/coeditors` | List coeditors | `getCoEditors` | +| PATCH | `/zoom_events/events/{eventId}/coeditors` | Add or remove event co-editors | `coeditoractions` | + +### Emails + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/email_types` | List event email types | `listEmailTypes` | +| GET | `/zoom_events/events/{eventId}/email_types/{emailTypeId}/send_status` | List event emails sent status | `listEmailSentStatuses` | + +### Event Access + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/access_links` | List event access links | `getEventAccessLinks` | +| POST | `/zoom_events/events/{eventId}/access_links` | Create event access link | `createEventAccessLink` | +| DELETE | `/zoom_events/events/{eventId}/access_links/{accessLinkId}` | Delete event access link | `deleteEventAccessLink` | +| GET | `/zoom_events/events/{eventId}/access_links/{accessLinkId}` | Get event access link | `GetEventAccessLink` | +| PATCH | `/zoom_events/events/{eventId}/access_links/{accessLinkId}` | Update event access | `updateEventAccess` | + +### Events + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events` | List events | `getEvents` | +| POST | `/zoom_events/events` | Create an event | `createEvent` | +| DELETE | `/zoom_events/events/{eventId}` | Delete an event | `deleteEvent` | +| GET | `/zoom_events/events/{eventId}` | Get an event | `getEventInfo` | +| PATCH | `/zoom_events/events/{eventId}` | Update an event | `updateEvent` | +| POST | `/zoom_events/events/{eventId}/event_actions` | Event actions | `EventActions` | + +### Exhibitors + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/exhibitors` | List exhibitors | `getExhibitors` | +| POST | `/zoom_events/events/{eventId}/exhibitors` | Create an exhibitor | `createExhibitor` | +| DELETE | `/zoom_events/events/{eventId}/exhibitors/{exhibitorId}` | Delete an exhibitor | `deleteExhibitor` | +| GET | `/zoom_events/events/{eventId}/exhibitors/{exhibitorId}` | Get an exhibitor | `getExhibitorInfo` | +| PATCH | `/zoom_events/events/{eventId}/exhibitors/{exhibitorId}` | Update exhibitor for an event | `updateExhibitor` | +| GET | `/zoom_events/events/{eventId}/sponsor_tiers` | List sponsor tiers | `ListSponsorTiers` | + +### Files + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/zoom_events/files` | Upload events file | `uploadEventFile` | +| POST | `/zoom_events/files/multipart` | Upload events multipart files | `uploadMultipartEventFile` | +| POST | `/zoom_events/files/multipart/upload` | Initiate and complete the multipart file upload | `initiateAndCompleteAEventMultipartUpload.` | + +### Hubs + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/hubs` | List hubs | `getHubList` | +| GET | `/zoom_events/hubs/{hubId}/hosts` | List hub Hosts | `gethubhostList` | +| POST | `/zoom_events/hubs/{hubId}/hosts` | Creates a new hub host | `createHubHost` | +| DELETE | `/zoom_events/hubs/{hubId}/hosts/{hostUserId}` | Remove hub host | `deleteHubHost` | +| GET | `/zoom_events/hubs/{hubId}/videos` | List hub videos | `ListHubVideos` | + +### Registrants + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/registrants` | List registrants | `getRegistrants` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}/attendees` | List session attendees | `getSessionAttendeeList` | + +### Reports + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/reports/chat_transcripts` | Get chat transcripts report | `ChatTranscriptsReport` | +| GET | `/zoom_events/events/{eventId}/reports/custom_reports/{customReportId}` | Get custom report | `getCustomEventReport` | +| GET | `/zoom_events/events/{eventId}/reports/event_attendance` | Get event attendance (Live or Lobby) report | `EventAttendanceReport` | +| GET | `/zoom_events/events/{eventId}/reports/sessions/{sessionId}/attendance` | Get session attendance report | `SessionAttendanceReport` | +| GET | `/zoom_events/events/{eventId}/reports/survey` | Get event survey report | `EventSurveyReportApi` | +| GET | `/zoom_events/events/{eventId}/reports/ticket_registration` | Get event registrations report | `EventRegistrationsReport` | +| GET | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/reports/registrations` | Get VOD channel registration report | `VodChannelReigistration` | + +### Sessions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/sessions` | List sessions | `getEventSessionList` | +| POST | `/zoom_events/events/{eventId}/sessions` | Create a session | `createEventSession` | +| DELETE | `/zoom_events/events/{eventId}/sessions/{sessionId}` | Delete a session | `deleteEventSession` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}` | Get the session information | `getEventSessionInfo` | +| PATCH | `/zoom_events/events/{eventId}/sessions/{sessionId}` | Update a session | `updateEventSession` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}/interpreters` | List session interpreters | `getSessionInterpreterList` | +| PUT | `/zoom_events/events/{eventId}/sessions/{sessionId}/interpreters` | Create or update session interpreters | `updateSessionInterpreters` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}/join_token` | Get ticket session join token by Event ID and Session ID | `getSessionJoinToken` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}/livestream` | Get session livestream configuration | `getSessionLivestreamConfiguration` | +| PATCH | `/zoom_events/events/{eventId}/sessions/{sessionId}/livestream` | Update session livestream configuration | `UpdateSessionLivestreamConfiguration` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}/polls` | List session polls | `getSessionPolls` | +| PUT | `/zoom_events/events/{eventId}/sessions/{sessionId}/polls` | Create or update session polls | `updateSessionPolls` | +| DELETE | `/zoom_events/events/{eventId}/sessions/{sessionId}/reservations` | Delete session reservations | `DeleteSessionReservations` | +| GET | `/zoom_events/events/{eventId}/sessions/{sessionId}/reservations` | List session reservations | `ListSessionReservations` | +| POST | `/zoom_events/events/{eventId}/sessions/{sessionId}/reservations` | Add session reservations | `AddSessionReservations` | + +### Speakers + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/speakers` | List speakers | `getSpeakers` | +| POST | `/zoom_events/events/{eventId}/speakers` | Create a speaker | `createSpeaker` | +| DELETE | `/zoom_events/events/{eventId}/speakers/{speakerId}` | Delete a speaker | `deleteSpeaker` | +| GET | `/zoom_events/events/{eventId}/speakers/{speakerId}` | Get a speaker | `getSpeaker` | +| PATCH | `/zoom_events/events/{eventId}/speakers/{speakerId}` | Update a speaker | `updateSpeaker` | + +### Ticket Types + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/questions` | List registration questions for an event | `getRegistrationQuestionsForEvent` | +| PUT | `/zoom_events/events/{eventId}/questions` | Update registration questions for an event | `updateRegistrationQuestionsForEvent` | +| GET | `/zoom_events/events/{eventId}/ticket_types` | List ticket types | `getEventTicketTypes` | +| POST | `/zoom_events/events/{eventId}/ticket_types` | Create an event ticket type | `createTicketType` | +| DELETE | `/zoom_events/events/{eventId}/ticket_types/{ticketTypeId}` | Delete a ticket type | `deleteEventTicketType` | +| PATCH | `/zoom_events/events/{eventId}/ticket_types/{ticketTypeId}` | Update ticket type for an event | `updateTicketType` | +| GET | `/zoom_events/events/{eventId}/ticket_types/{ticketTypeId}/questions` | List registration questions for ticket type | `getRegistrationQuestionsForTicketType` | +| PUT | `/zoom_events/events/{eventId}/ticket_types/{ticketTypeId}/questions` | Update registration questions for ticket type | `updateRegistrationQuestionsForTicketType` | + +### Tickets + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/events/{eventId}/tickets` | List tickets | `getTickets` | +| POST | `/zoom_events/events/{eventId}/tickets` | Create tickets | `createTickets` | +| DELETE | `/zoom_events/events/{eventId}/tickets/{ticketId}` | Delete a ticket | `deleteTicket` | +| GET | `/zoom_events/events/{eventId}/tickets/{ticketId}` | Get a ticket | `getTicketDetails` | +| PATCH | `/zoom_events/events/{eventId}/tickets/{ticketId}` | Update ticket | `Updateticket` | + +### Video On-Demand + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/hubs/{hubId}/vod_channels` | List channels | `getVODChannels` | +| POST | `/zoom_events/hubs/{hubId}/vod_channels` | Create VOD channel | `createVodChannel` | +| DELETE | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}` | Delete VOD Channel | `DeleteVODChannel` | +| GET | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}` | Get VOD channel details | `getVODChannelDetail` | +| PATCH | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}` | Update VOD channel | `UpdateVideoChannel` | +| POST | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/actions` | VOD channel actions | `vodChannelActions` | +| GET | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/videos` | List VOD channel videos | `ListVODChannelVideos` | +| POST | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/videos` | Add VOD channel videos | `AddVODChannelVideos` | +| DELETE | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/videos/{videoId}` | Delete VOD channel video | `DeleteVODChannelVideo` | + +### Video On-Demand Registrations + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/registration_questions` | Get VOD Registration Questions | `getRegistrationQuestionsForVODChannel` | +| PUT | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/registration_questions` | update VOD channel registration questions | `updateRegistrationQuestionsForVODchannel` | +| GET | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/registrations` | List VOD Registration | `ListVODRegistration` | +| POST | `/zoom_events/hubs/{hubId}/vod_channels/{channelId}/registrations` | VOD channel registration | `VODTicketRegistration` | + +### Videos + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zoom_events/videos/{videoId}/metadata` | Get metadata for a specific video | `getVideoMetadata` | +| PATCH | `/zoom_events/videos/{videoId}/metadata` | Update metadata for a specific video. | `updateVideoMetadata` | diff --git a/plugins/zoom-developers/skills/rest-api/references/full-guide.md b/plugins/zoom-developers/skills/rest-api/references/full-guide.md new file mode 100644 index 00000000..aa85c81b --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/full-guide.md @@ -0,0 +1,564 @@ +# /build-zoom-rest-api-app + +Background reference for deterministic server-side Zoom automation and resource management. Prefer `plan-zoom-product`, `plan-zoom-integration`, or `debug-zoom` first, then route here for endpoint-level detail. + +# Zoom REST API + +Expert guidance for building server-side integrations with the Zoom REST API. This API provides 600+ endpoints for managing meetings, users, webinars, recordings, reports, and all Zoom platform resources programmatically. + +**Official Documentation**: https://developers.zoom.us/api-hub/ +**API Hub Reference**: https://developers.zoom.us/api-hub/meetings/ +**OpenAPI Inventories**: `https://developers.zoom.us/api-hub//methods/endpoints.json` + +## Quick Links + +**New to Zoom REST API? Follow this path:** + +1. **[API Architecture](../concepts/api-architecture.md)** - Base URLs, regional URLs, `me` keyword, ID vs UUID, time formats +2. **[Authentication Flows](../concepts/authentication-flows.md)** - OAuth setup (S2S, User, PKCE, Device Code) +3. **[Meeting URLs vs Meeting SDK](../concepts/meeting-urls-and-sdk-joining.md)** - Stop mixing `join_url` with Meeting SDK +3. **[Meeting Lifecycle](../examples/meeting-lifecycle.md)** - Create → Update → Start → End → Delete with webhooks +4. **[Rate Limiting Strategy](../concepts/rate-limiting-strategy.md)** - Plan tiers, per-user limits, retry patterns + +**Reference:** +- **[Meetings](../references/meetings.md)** - Meeting CRUD, types, settings +- **[Users](../references/users.md)** - User provisioning and management +- **[Recordings](../references/recordings.md)** - Cloud recording access and download +- **[AI Services](../references/ai-services.md)** - Scribe endpoint inventory and current AI Services path surface +- **[GraphQL Queries](../examples/graphql-queries.md)** - Alternative query API (beta) +- **Integrated Index** - see the section below in this file + +Most domain files under `references/` are aligned to the official API Hub `endpoints.json` inventories. Treat those files as the local source of truth for method/path discovery. + +**Having issues?** +- Start with preflight checks → [5-Minute Runbook](../RUNBOOK.md) +- 401 Unauthorized → [Authentication Flows](../concepts/authentication-flows.md) (check token expiry, scopes) +- 429 Too Many Requests → [Rate Limiting Strategy](../concepts/rate-limiting-strategy.md) +- Error codes → [Common Errors](../troubleshooting/common-errors.md) +- Pagination confusion → [Common Issues](../troubleshooting/common-issues.md) +- Webhooks not arriving → [Webhook Server](../examples/webhook-server.md) +- Forum-derived FAQs → [Forum Top Questions](../troubleshooting/forum-top-questions.md) +- Token/scope failures → [Token + Scope Playbook](../troubleshooting/token-scope-playbook.md) + +**Building event-driven integrations?** +- [Webhook Server](../examples/webhook-server.md) - Express.js server with CRC validation +- [Recording Pipeline](../examples/recording-pipeline.md) - Auto-download via webhook events + +## Quick Start + +### Get an Access Token (Server-to-Server OAuth) + +```bash +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=account_credentials&account_id=ACCOUNT_ID" +``` + +Response: +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiJ9...", + "token_type": "bearer", + "expires_in": 3600, + "scope": "meeting:read meeting:write user:read" +} +``` + +### Create a Meeting + +```bash +curl -X POST "https://api.zoom.us/v2/users/HOST_USER_ID/meetings" \ + -H "Authorization: Bearer ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "topic": "Team Standup", + "type": 2, + "start_time": "2025-03-15T10:00:00Z", + "duration": 30, + "settings": { + "join_before_host": false, + "waiting_room": true + } + }' +``` + +For S2S OAuth, use an explicit host user ID or email in the path. Do not use `me`. + +### List Users with Pagination + +```bash +curl "https://api.zoom.us/v2/users?page_size=300&status=active" \ + -H "Authorization: Bearer ACCESS_TOKEN" +``` + +## Base URL + +``` +https://api.zoom.us/v2 +``` + +### Regional Base URLs + +The `api_url` field in OAuth token responses indicates the user's region. Use regional URLs for data residency compliance: + +| Region | URL | +|--------|-----| +| Global (default) | `https://api.zoom.us/v2` | +| Australia | `https://api-au.zoom.us/v2` | +| Canada | `https://api-ca.zoom.us/v2` | +| European Union | `https://api-eu.zoom.us/v2` | +| India | `https://api-in.zoom.us/v2` | +| Saudi Arabia | `https://api-sa.zoom.us/v2` | +| Singapore | `https://api-sg.zoom.us/v2` | +| United Kingdom | `https://api-uk.zoom.us/v2` | +| United States | `https://api-us.zoom.us/v2` | + +**Note:** You can always use the global URL `https://api.zoom.us` regardless of the `api_url` value. + +## Key Features + +| Feature | Description | +|---------|-------------| +| **Meeting Management** | Create, read, update, delete meetings with full scheduling control | +| **User Provisioning** | Automated user lifecycle (create, update, deactivate, delete) | +| **Webinar Operations** | Webinar CRUD, registrant management, panelist control | +| **Cloud Recordings** | List, download, delete recordings with file-type filtering | +| **Reports & Analytics** | Usage reports, participant data, daily statistics | +| **Team Chat** | Channel management, messaging, chatbot integration | +| **Zoom Phone** | Call management, voicemail, call routing | +| **Zoom Rooms** | Room management, device control, scheduling | +| **Webhooks** | Real-time event notifications for 100+ event types | +| **WebSockets** | Persistent event streaming without public endpoints | +| **GraphQL (Beta)** | Single-endpoint flexible queries at `v3/graphql` | +| **AI Companion** | Meeting summaries, transcripts, AI-generated content | +| **AI Services / Scribe** | File and archive transcription via Build-platform JWT-authenticated endpoints | + +## Prerequisites + +- Zoom account (Free tier has API access with lower rate limits) +- App registered on [Zoom App Marketplace](https://marketplace.zoom.us/) +- OAuth credentials (Server-to-Server OAuth or User OAuth) +- Appropriate scopes for target endpoints + +> **Need help with authentication?** See the **[zoom-oauth](../../oauth/SKILL.md)** skill for complete OAuth flow implementation. + +## Critical Gotchas and Best Practices + +### ⚠️ JWT App Type is Deprecated + +The JWT app type is deprecated. Migrate to **Server-to-Server OAuth**. This does NOT affect JWT token signatures used in Video SDK — only the Marketplace "JWT" app type for REST API access. + +```javascript +// OLD (JWT app type - DEPRECATED) +const token = jwt.sign({ iss: apiKey, exp: expiry }, apiSecret); + +// NEW (Server-to-Server OAuth) +const token = await getServerToServerToken(accountId, clientId, clientSecret); +``` + +### ⚠️ The `me` Keyword Rules + +- **User-level OAuth apps**: MUST use `me` instead of `userId` (otherwise: invalid token error) +- **Server-to-Server OAuth apps**: MUST NOT use `me` — provide the actual `userId` or email +- **Account-level OAuth apps**: Can use either `me` or `userId` + +### ⚠️ Meeting ID vs UUID — Double Encoding + +UUIDs that begin with `/` or contain `//` must be **double URL-encoded**: + +```javascript +// UUID: /abc== +// Single encode: %2Fabc%3D%3D +// Double encode: %252Fabc%253D%253D ← USE THIS + +const uuid = '/abc=='; +const encoded = encodeURIComponent(encodeURIComponent(uuid)); +const url = `https://api.zoom.us/v2/meetings/${encoded}`; +``` + +### ⚠️ Time Formats + +- `yyyy-MM-ddTHH:mm:ssZ` — **UTC time** (note the `Z` suffix) +- `yyyy-MM-ddTHH:mm:ss` — **Local time** (no `Z`, uses `timezone` field) +- Some report APIs only accept UTC. Check the API reference for each endpoint. + +### ⚠️ Rate Limits Are Per-Account, Not Per-App + +All apps on the same Zoom account **share** rate limits. One heavy app can impact others. Monitor `X-RateLimit-Remaining` headers proactively. + +### ⚠️ Per-User Daily Limits + +Meeting/Webinar create/update operations are limited to **100 per day per user** (resets at 00:00 UTC). Distribute operations across different host users when doing bulk operations. + +### ⚠️ Download URLs Require Auth and Follow Redirects + +Recording `download_url` values require Bearer token authentication and may redirect. Always follow redirects: + +```bash +curl -L -H "Authorization: Bearer ACCESS_TOKEN" "https://zoom.us/rec/download/..." +``` + +### Use Webhooks Instead of Polling + +```javascript +// DON'T: Poll every minute (wastes API quota) +setInterval(() => getMeetings(), 60000); + +// DO: Receive webhook events in real-time +app.post('/webhook', (req, res) => { + if (req.body.event === 'meeting.started') { + handleMeetingStarted(req.body.payload); + } + res.status(200).send(); +}); +``` + +> **Webhook setup details:** See the **[zoom-webhooks](../../webhooks/SKILL.md)** skill for comprehensive webhook implementation. + +## Complete Documentation Library + +This skill includes comprehensive guides organized by category: + +### Core Concepts +- **[API Architecture](../concepts/api-architecture.md)** - REST design, base URLs, regional routing, `me` keyword, ID vs UUID, time formats +- **[Authentication Flows](../concepts/authentication-flows.md)** - All OAuth flows (S2S, User, PKCE, Device Code) +- **[Rate Limiting Strategy](../concepts/rate-limiting-strategy.md)** - Limits by plan, retry patterns, request queuing + +### Complete Examples +- **[Meeting Lifecycle](../examples/meeting-lifecycle.md)** - Full Create → Update → Start → End → Delete flow with webhook events +- **[User Management](../examples/user-management.md)** - CRUD users, list with pagination, bulk operations +- **[Recording Pipeline](../examples/recording-pipeline.md)** - Download recordings via webhooks + API +- **[Webhook Server](../examples/webhook-server.md)** - Express.js server with CRC validation and signature verification +- **[GraphQL Queries](../examples/graphql-queries.md)** - GraphQL queries, mutations, cursor pagination + +### Troubleshooting +- **[Common Errors](../troubleshooting/common-errors.md)** - HTTP status codes, Zoom error codes, error response formats +- **[Common Issues](../troubleshooting/common-issues.md)** - Rate limits, token refresh, pagination pitfalls, gotchas + +### References (39 files covering all Zoom API domains) + +#### Core APIs +- **[references/meetings.md](../references/meetings.md)** - Meeting CRUD, types, settings +- **[references/users.md](../references/users.md)** - User provisioning, types, scopes +- **[references/webinars.md](../references/webinars.md)** - Webinar management, registrants +- **[references/recordings.md](../references/recordings.md)** - Cloud recording access +- **[references/reports.md](../references/reports.md)** - Usage reports, analytics +- **[references/accounts.md](../references/accounts.md)** - Account management + +#### Communication +- **[references/team-chat.md](../references/team-chat.md)** - Team Chat messaging +- **[references/chatbot.md](../references/chatbot.md)** - Interactive chatbots +- **[references/phone.md](../references/phone.md)** - Zoom Phone +- **[references/mail.md](../references/mail.md)** - Zoom Mail +- **[references/calendar.md](../references/calendar.md)** - Zoom Calendar + +#### Infrastructure +- **[references/rooms.md](../references/rooms.md)** - Zoom Rooms +- **[references/scim2.md](../references/scim2.md)** - SCIM 2.0 provisioning APIs +- **[references/rate-limits.md](../references/rate-limits.md)** - Rate limit details +- **[references/qss.md](../references/qss.md)** - Quality of Service Subscription + +#### Advanced +- **[references/graphql.md](../references/graphql.md)** - GraphQL API (beta) +- **[references/ai-companion.md](../references/ai-companion.md)** - AI features +- **[references/authentication.md](../references/authentication.md)** - Auth reference +- **[references/openapi.md](../references/openapi.md)** - OpenAPI specs, Postman, code generation + +#### Additional API Domains +- **[references/events.md](../references/events.md)** - Events and event platform APIs +- **[references/scheduler.md](../references/scheduler.md)** - Zoom Scheduler APIs +- **[references/tasks.md](../references/tasks.md)** - Tasks APIs +- **[references/whiteboard.md](../references/whiteboard.md)** - Whiteboard APIs +- **[references/video-management.md](../references/video-management.md)** - Video management APIs +- **[references/video-sdk-api.md](../references/video-sdk-api.md)** - Video SDK REST APIs +- **[references/marketplace-apps.md](../references/marketplace-apps.md)** - Marketplace app management +- **[references/commerce.md](../references/commerce.md)** - Commerce and billing APIs +- **[references/contact-center.md](../references/contact-center.md)** - Contact Center APIs +- **[references/quality-management.md](../references/quality-management.md)** - Quality management APIs +- **[references/workforce-management.md](../references/workforce-management.md)** - Workforce management APIs +- **[references/healthcare.md](../references/healthcare.md)** - Healthcare APIs +- **[references/auto-dialer.md](../references/auto-dialer.md)** - Auto dialer APIs +- **[references/number-management.md](../references/number-management.md)** - Number management APIs +- **[references/revenue-accelerator.md](../references/revenue-accelerator.md)** - Revenue Accelerator APIs +- **[references/virtual-agent.md](../references/virtual-agent.md)** - Virtual Agent APIs +- **[references/cobrowse-sdk-api.md](../references/cobrowse-sdk-api.md)** - Cobrowse SDK APIs +- **[references/crc.md](../references/crc.md)** - Cloud Room Connector APIs +- **[references/clips.md](../references/clips.md)** - Clips APIs +- **[references/zoom-docs.md](../references/zoom-docs.md)** - Zoom docs and source references + +## Sample Repositories + +### Official (by Zoom) + +| Type | Repository | +|------|------------| +| OAuth Sample | [oauth-sample-app](https://github.com/zoom/oauth-sample-app) | +| S2S OAuth Starter | [server-to-server-oauth-starter-api](https://github.com/zoom/server-to-server-oauth-starter-api) | +| User OAuth | [user-level-oauth-starter](https://github.com/zoom/user-level-oauth-starter) | +| S2S Token | [server-to-server-oauth-token](https://github.com/zoom/server-to-server-oauth-token) | +| Rivet Library | [rivet-javascript](https://github.com/zoom/rivet-javascript) | +| WebSocket Sample | [websocket-js-sample](https://github.com/zoom/websocket-js-sample) | +| Webhook Sample | [webhook-sample-node.js](https://github.com/zoom/webhook-sample-node.js) | +| Python S2S | [server-to-server-python-sample](https://github.com/zoom/server-to-server-python-sample) | + +## Resources + +- **API Reference**: https://developers.zoom.us/api-hub/ +- **GraphQL Playground**: https://nws.zoom.us/graphql/playground +- **Postman Collection**: https://marketplace.zoom.us/docs/api-reference/postman +- **Developer Forum**: https://devforum.zoom.us/ +- **Changelog**: https://developers.zoom.us/changelog/ +- **Status Page**: https://status.zoom.us/ + +--- + +**Need help?** Start with Integrated Index section below for complete navigation. + +--- + +## Integrated Index + +_This section was migrated from `SKILL.md`._ + +## Quick Start Path + +**If you're new to the Zoom REST API, follow this order:** + +1. **Run preflight checks first** → [RUNBOOK.md](../RUNBOOK.md) + +2. **Understand the API design** → [concepts/api-architecture.md](../concepts/api-architecture.md) + - Base URLs, regional endpoints, `me` keyword rules + - Meeting ID vs UUID, double-encoding, time formats + +3. **Set up authentication** → [concepts/authentication-flows.md](../concepts/authentication-flows.md) + - Server-to-Server OAuth (backend automation) + - User OAuth with PKCE (user-facing apps) + - Cross-reference: [zoom-oauth](../../oauth/SKILL.md) + +4. **Create your first meeting** → [examples/meeting-lifecycle.md](../examples/meeting-lifecycle.md) + - Full CRUD with curl and Node.js examples + - Webhook event integration + +5. **Handle rate limits** → [concepts/rate-limiting-strategy.md](../concepts/rate-limiting-strategy.md) + - Plan-based limits, retry patterns, request queuing + +6. **Set up webhooks** → [examples/webhook-server.md](../examples/webhook-server.md) + - CRC validation, signature verification, event handling + +7. **Troubleshoot issues** → [troubleshooting/common-issues.md](../troubleshooting/common-issues.md) + - Token refresh, pagination pitfalls, common gotchas + +--- + +## Documentation Structure + +``` +rest-api/ +├── SKILL.md # Main skill overview + quick start +├── SKILL.md # This file - navigation guide +│ +├── concepts/ # Core architectural concepts +│ ├── api-architecture.md # REST design, URLs, IDs, time formats +│ ├── authentication-flows.md # OAuth flows (S2S, User, PKCE, Device) +│ └── rate-limiting-strategy.md # Limits by plan, retry, queuing +│ +├── examples/ # Complete working code +│ ├── meeting-lifecycle.md # Create→Update→Start→End→Delete +│ ├── user-management.md # CRUD users, pagination, bulk ops +│ ├── recording-pipeline.md # Download recordings via webhooks +│ ├── webhook-server.md # Express.js CRC + signature verification +│ └── graphql-queries.md # GraphQL queries, mutations, pagination +│ +├── troubleshooting/ # Problem solving +│ ├── common-errors.md # HTTP codes, Zoom error codes table +│ └── common-issues.md # Rate limits, tokens, pagination pitfalls +│ +└── references/ # 39 domain-specific reference files + ├── authentication.md # Auth methods reference + ├── meetings.md # Meeting endpoints + ├── users.md # User management endpoints + ├── webinars.md # Webinar endpoints + ├── recordings.md # Cloud recording endpoints + ├── reports.md # Reports & analytics + ├── accounts.md # Account management + ├── rate-limits.md # Rate limit details + ├── graphql.md # GraphQL API (beta) + ├── zoom-team-chat.md # Team Chat messaging + ├── chatbot.md # Chatbot integration + ├── phone.md # Zoom Phone + ├── rooms.md # Zoom Rooms + ├── calendar.md # Zoom Calendar + ├── mail.md # Zoom Mail + ├── ai-companion.md # AI features + ├── openapi.md # OpenAPI specs + ├── qss.md # Quality of Service + ├── contact-center.md # Contact Center + ├── events.md # Zoom Events + ├── whiteboard.md # Whiteboard + ├── clips.md # Zoom Clips + ├── scheduler.md # Scheduler + ├── scim2.md # SCIM 2.0 + ├── marketplace-apps.md # App management + ├── zoom-video-sdk-api.md # Video SDK REST + └── ... (39 total files) +``` + +--- + +## By Use Case + +### I want to create and manage meetings +1. [API Architecture](../concepts/api-architecture.md) - Base URL, time formats +2. [Meeting Lifecycle](../examples/meeting-lifecycle.md) - Full CRUD + webhook events +3. [Meetings Reference](../references/meetings.md) - All endpoints, types, settings + +### I want to manage users programmatically +1. [User Management](../examples/user-management.md) - CRUD, pagination, bulk ops +2. [Users Reference](../references/users.md) - Endpoints, user types, scopes + +### I want to download recordings automatically +1. [Recording Pipeline](../examples/recording-pipeline.md) - Webhook-triggered downloads +2. [Recordings Reference](../references/recordings.md) - File types, download auth + +### I want to receive real-time events +1. [Webhook Server](../examples/webhook-server.md) - CRC validation, signature check +2. Cross-reference: [zoom-webhooks](../../webhooks/SKILL.md) for comprehensive webhook docs +3. Cross-reference: [zoom-websockets](../../websockets/SKILL.md) for WebSocket events + +### I want to use GraphQL instead of REST +1. [GraphQL Queries](../examples/graphql-queries.md) - Queries, mutations, pagination +2. [GraphQL Reference](../references/graphql.md) - Available entities, scopes, rate limits + +### I want to set up authentication +1. [Authentication Flows](../concepts/authentication-flows.md) - All OAuth methods +2. Cross-reference: [zoom-oauth](../../oauth/SKILL.md) for full OAuth implementation + +### I'm hitting rate limits +1. [Rate Limiting Strategy](../concepts/rate-limiting-strategy.md) - Limits by plan, strategies +2. [Rate Limits Reference](../references/rate-limits.md) - Detailed tables +3. [Common Issues](../troubleshooting/common-issues.md) - Practical solutions + +### I'm getting errors +1. [Common Errors](../troubleshooting/common-errors.md) - Error code tables +2. [Common Issues](../troubleshooting/common-issues.md) - Diagnostic workflow + +### I want to build webinars +1. [Webinars Reference](../references/webinars.md) - Endpoints, types, registrants +2. [Meeting Lifecycle](../examples/meeting-lifecycle.md) - Similar patterns apply + +### I want to integrate Zoom Phone +1. [Phone Reference](../references/phone.md) - Phone API endpoints +2. [Rate Limiting Strategy](../concepts/rate-limiting-strategy.md) - Separate Phone rate limits + +--- + +## Most Critical Documents + +### 1. API Architecture (FOUNDATION) +**[concepts/api-architecture.md](../concepts/api-architecture.md)** + +Essential knowledge before making any API call: +- Base URLs and regional endpoints +- The `me` keyword rules (different per app type!) +- Meeting ID vs UUID double-encoding +- ISO 8601 time formats (UTC vs local) +- Download URL authentication + +### 2. Rate Limiting Strategy (MOST COMMON PRODUCTION ISSUE) +**[concepts/rate-limiting-strategy.md](../concepts/rate-limiting-strategy.md)** + +Rate limits are per-account, shared across all apps: +- Free: 4/sec Light, 2/sec Medium, 1/sec Heavy +- Pro: 30/sec Light, 20/sec Medium, 10/sec Heavy +- Business+: 80/sec Light, 60/sec Medium, 40/sec Heavy +- Per-user: 100 meeting create/update per day + +### 3. Meeting Lifecycle (MOST COMMON TASK) +**[examples/meeting-lifecycle.md](../examples/meeting-lifecycle.md)** + +Complete CRUD with webhook integration — the pattern most developers need first. + +--- + +## Key Learnings + +### Critical Discoveries: + +1. **JWT app type is deprecated** — use Server-to-Server OAuth + - The JWT *app type* on Marketplace is deprecated, NOT JWT token signatures + - See: [Authentication Flows](../concepts/authentication-flows.md) + +2. **`me` keyword behaves differently by app type** + - User OAuth: MUST use `me` + - S2S OAuth: MUST NOT use `me` + - See: [API Architecture](../concepts/api-architecture.md) + +3. **Rate limiting is nuanced (don’t assume a single global rule)** + - Limits can vary by endpoint and may be enforced at account/app/user levels + - Treat quotas as potentially shared across your account and implement backoff + - Monitor rate limit response headers (for example `X-RateLimit-Remaining`) + - See: [Rate Limiting Strategy](../concepts/rate-limiting-strategy.md) + +4. **100 meeting creates per user per day** + - This is a hard per-user limit, not related to rate limits + - Distribute across host users for bulk operations + - See: [Rate Limiting Strategy](../concepts/rate-limiting-strategy.md) + +5. **UUID double-encoding is required for certain UUIDs** + - UUIDs starting with `/` or containing `//` must be double-encoded + - See: [API Architecture](../concepts/api-architecture.md) + +6. **Pagination: use `next_page_token`, not `page_number`** + - `page_number` is legacy and being phased out + - `next_page_token` is the recommended approach + - See: [Common Issues](../troubleshooting/common-issues.md) + +7. **GraphQL is at `/v3/graphql`, not `/v2/`** + - Single endpoint, cursor-based pagination + - Rate limits apply per-field (each field = one REST equivalent) + - See: [GraphQL Queries](../examples/graphql-queries.md) + +--- + +## Quick Reference + +### "401 Unauthorized" +→ [Authentication Flows](../concepts/authentication-flows.md) - Token expired or wrong scopes + +### "429 Too Many Requests" +→ [Rate Limiting Strategy](../concepts/rate-limiting-strategy.md) - Check headers for reset time + +### "Invalid token" when using userId +→ [API Architecture](../concepts/api-architecture.md) - User OAuth apps must use `me` + +### "How do I paginate results?" +→ [Common Issues](../troubleshooting/common-issues.md) - Use `next_page_token` + +### "Webhooks not arriving" +→ [Webhook Server](../examples/webhook-server.md) - CRC validation required + +### "Recording download fails" +→ [Recording Pipeline](../examples/recording-pipeline.md) - Bearer auth + follow redirects + +### "How do I create a meeting?" +→ [Meeting Lifecycle](../examples/meeting-lifecycle.md) - Full working examples + +--- + +## Related Skills + +| Skill | Use When | +|-------|----------| +| **[zoom-oauth](../../oauth/SKILL.md)** | Implementing OAuth flows, token management | +| **[zoom-webhooks](../../webhooks/SKILL.md)** | Deep webhook implementation, event catalog | +| **[zoom-websockets](../../websockets/SKILL.md)** | WebSocket event streaming | +| **[zoom-general](../../general/SKILL.md)** | Cross-product patterns, community repos | + +--- + +**Based on Zoom REST API v2 (current) and GraphQL v3 (beta)** + +## Environment Variables + +- See [references/environment-variables.md](../references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/plugins/zoom-developers/skills/rest-api/references/graphql.md b/plugins/zoom-developers/skills/rest-api/references/graphql.md new file mode 100644 index 00000000..9bb8efb6 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/graphql.md @@ -0,0 +1,484 @@ +# Zoom GraphQL API (Beta) + +Flexible data queries with GraphQL as an alternative to REST. + +## Overview + +Zoom GraphQL API provides a single endpoint for querying and mutating Zoom data. Unlike REST APIs with fixed endpoints, GraphQL allows you to request exactly the data you need. + +**Status**: Beta (requires signup) + +## Key URLs + +| Resource | URL | +|----------|-----| +| **Playground** | https://nws.zoom.us/graphql/playground | +| **Beta Signup** | https://beta.zoom.us/key/GRAPHQL | +| **Endpoint** | `https://api.zoom.us/graphql` | + +## GraphQL vs REST + +| Feature | REST API | GraphQL API | +|---------|----------|-------------| +| Endpoints | 600+ endpoints | Single endpoint | +| Data fetching | Fixed data per endpoint | Client specifies exact fields | +| Multiple resources | Multiple requests | Single request | +| Over-fetching | Common | Eliminated | +| Schema | Optional (OpenAPI) | Mandatory, strongly typed | +| Learning curve | Lower | Higher | + +## Authentication + +Same OAuth 2.0 as REST API: + +```javascript +const headers = { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' +}; +``` + +- Server-to-Server OAuth supported +- User-authorized OAuth supported +- Access tokens valid for 1 hour + +## Available Entities + +| Entity | Query | Mutation | +|--------|-------|----------| +| Users | ✅ | ✅ | +| Meetings | ✅ | Partial | +| Webinars | ✅ | Partial | +| Chat Channels | Partial | - | +| Cloud Recordings | ✅ | - | +| Dashboards | ✅ | - | +| Groups | Partial | - | +| Reports | Partial | - | + +**Note**: Not all REST endpoints are available in GraphQL yet (beta). + +## Query Examples + +### List Users + +```graphql +query ListUsers { + users(first: 100) { + edges { + node { + id + firstName + lastName + email + department + type + status + } + } + pageInfo { + hasNextPage + endCursor + } + } +} +``` + +### Get User Details + +```graphql +query GetUser($userId: ID!) { + user(id: $userId) { + id + firstName + lastName + email + department + timezone + type + createdAt + lastLoginTime + } +} +``` + +Variables: +```json +{ + "userId": "user_abc123" +} +``` + +### List Meetings + +```graphql +query ListMeetings($userId: ID!) { + user(id: $userId) { + meetings(first: 50) { + edges { + node { + id + topic + type + startTime + duration + timezone + joinUrl + } + } + } + } +} +``` + +### Get Meeting with Participants + +```graphql +query GetMeetingDetails($meetingId: ID!) { + meeting(id: $meetingId) { + id + topic + type + startTime + duration + host { + id + firstName + lastName + email + } + participants { + edges { + node { + id + name + email + joinTime + leaveTime + } + } + } + } +} +``` + +### Get Recordings + +```graphql +query GetRecordings($userId: ID!, $from: DateTime!, $to: DateTime!) { + user(id: $userId) { + recordings(from: $from, to: $to, first: 50) { + edges { + node { + id + topic + startTime + duration + totalSize + recordingFiles { + id + fileType + fileSize + downloadUrl + } + } + } + } + } +} +``` + +## Mutation Examples + +### Create User + +```graphql +mutation CreateUser($input: UserCreateInput!) { + createUser(input: $input) { + id + email + firstName + lastName + type + } +} +``` + +Variables: +```json +{ + "input": { + "action": "CUST_CREATE", + "userInfo": { + "email": "newuser@example.com", + "firstName": "John", + "lastName": "Doe", + "type": "BASIC" + } + } +} +``` + +### Update User + +```graphql +mutation UpdateUser($userId: ID!, $input: UserUpdateInput!) { + updateUser(id: $userId, input: $input) { + id + firstName + lastName + department + } +} +``` + +Variables: +```json +{ + "userId": "user_abc123", + "input": { + "firstName": "Jonathan", + "department": "Engineering" + } +} +``` + +## Making Requests + +### JavaScript/Node.js + +```javascript +async function graphqlQuery(query, variables = {}) { + const response = await fetch('https://api.zoom.us/graphql', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + query: query, + variables: variables + }) + }); + + const result = await response.json(); + + if (result.errors) { + throw new Error(result.errors[0].message); + } + + return result.data; +} + +// Usage +const users = await graphqlQuery(` + query { + users(first: 10) { + edges { + node { + id + email + firstName + lastName + } + } + } + } +`); + +console.log(users.users.edges); +``` + +### Python + +```python +import requests + +def graphql_query(query, variables=None): + response = requests.post( + 'https://api.zoom.us/graphql', + headers={ + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json' + }, + json={ + 'query': query, + 'variables': variables or {} + } + ) + + result = response.json() + + if 'errors' in result: + raise Exception(result['errors'][0]['message']) + + return result['data'] + +# Usage +users = graphql_query(''' + query { + users(first: 10) { + edges { + node { + id + email + } + } + } + } +''') +``` + +### cURL + +```bash +curl -X POST https://api.zoom.us/graphql \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "query { users(first: 10) { edges { node { id email } } } }" + }' +``` + +## Pagination + +GraphQL uses cursor-based pagination: + +```graphql +query PaginatedUsers($cursor: String) { + users(first: 100, after: $cursor) { + edges { + node { + id + email + } + cursor + } + pageInfo { + hasNextPage + endCursor + } + } +} +``` + +Fetching all pages: + +```javascript +async function fetchAllUsers() { + let allUsers = []; + let cursor = null; + let hasNextPage = true; + + while (hasNextPage) { + const result = await graphqlQuery( + `query($cursor: String) { + users(first: 100, after: $cursor) { + edges { + node { id email firstName lastName } + } + pageInfo { + hasNextPage + endCursor + } + } + }`, + { cursor } + ); + + allUsers.push(...result.users.edges.map(e => e.node)); + hasNextPage = result.users.pageInfo.hasNextPage; + cursor = result.users.pageInfo.endCursor; + } + + return allUsers; +} +``` + +## Error Handling + +GraphQL always returns HTTP 200. Errors are in the response body: + +```json +{ + "data": null, + "errors": [ + { + "message": "User not found", + "locations": [{ "line": 2, "column": 3 }], + "path": ["user"], + "extensions": { + "code": "NOT_FOUND" + } + } + ] +} +``` + +Handle errors: + +```javascript +async function safeQuery(query, variables) { + const response = await fetch('https://api.zoom.us/graphql', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ query, variables }) + }); + + const result = await response.json(); + + if (result.errors && result.errors.length > 0) { + const error = result.errors[0]; + console.error(`GraphQL Error: ${error.message}`); + console.error(`Code: ${error.extensions?.code}`); + throw new Error(error.message); + } + + return result.data; +} +``` + +## When to Use GraphQL vs REST + +### Use GraphQL When: +- Fetching specific fields from complex nested data +- Making multiple related queries in one request +- Building mobile apps where bandwidth matters +- Working with interconnected user, meeting, webinar data +- Prototyping and exploring the API + +### Use REST When: +- Need access to all 600+ endpoints (GraphQL coverage incomplete) +- Building simple CRUD applications +- Require HTTP caching +- Team is unfamiliar with GraphQL +- Production stability is critical (GraphQL is beta) + +## Playground + +The GraphQL Playground at https://nws.zoom.us/graphql/playground provides: +- Interactive query editor +- Auto-complete and syntax highlighting +- Query history +- Embedded documentation +- Schema explorer + +**Note**: Requires beta access. + +## Limitations (Beta) + +| Limitation | Notes | +|------------|-------| +| Coverage | Not all REST endpoints available | +| Stability | Beta - features may change | +| Access | Requires explicit beta signup | +| Documentation | Less comprehensive than REST | + +## Resources + +- **Playground**: https://nws.zoom.us/graphql/playground +- **Beta Signup**: https://beta.zoom.us/key/GRAPHQL +- **Postman Collection**: https://www.postman.com/zoom-developer/zoom-public-workspace/collection/2ub5ygf/zoom-graphql-collection-beta +- **Blog Post**: https://dev.to/zoom/the-zoom-graphql-api-playground-your-new-favorite-development-tool-59n5 diff --git a/plugins/zoom-developers/skills/rest-api/references/healthcare.md b/plugins/zoom-developers/skills/rest-api/references/healthcare.md new file mode 100644 index 00000000..d97f9612 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/healthcare.md @@ -0,0 +1,39 @@ +# Zoom Healthcare API + +Authoritative endpoint inventory for Healthcare. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/healthcare/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 3 | +| Path templates | 2 | +| Tags | 1 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| clinicalnotes | 3 | + +## Endpoints by Tag + +### clinicalnotes + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/clinical_notes/notes` | List clinical notes | `GetClinicalNote` | +| GET | `/clinical_notes/notes/{noteId}` | Get a Clinical Note | `GetaClinicalNote` | +| PATCH | `/clinical_notes/notes/{noteId}` | Update a Clinical Note | `UpdateClinicalNote` | diff --git a/plugins/zoom-developers/skills/rest-api/references/mail.md b/plugins/zoom-developers/skills/rest-api/references/mail.md new file mode 100644 index 00000000..5a7e819d --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/mail.md @@ -0,0 +1,131 @@ +# Zoom Mail API + +Authoritative endpoint inventory for Mail. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/mail/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 41 | +| Path templates | 26 | +| Tags | 10 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Drafts | 6 | +| History | 1 | +| Labels | 6 | +| Mailbox | 1 | +| Messages | 10 | +| Messages.Attachments | 1 | +| Settings | 2 | +| Settings.Delegates | 4 | +| Settings.Filters | 4 | +| Threads | 6 | + +## Endpoints by Tag + +### Drafts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/drafts` | List emails from draft folder | `list_draft_emails` | +| POST | `/emails/mailboxes/{email}/drafts` | Create a new draft email | `create_draft_email` | +| POST | `/emails/mailboxes/{email}/drafts/send` | Send out a draft email | `send_draft_email` | +| DELETE | `/emails/mailboxes/{email}/drafts/{draftId}` | Delete an existing draft email | `delete_draft_email` | +| GET | `/emails/mailboxes/{email}/drafts/{draftId}` | Get the specified draft email | `get_draft_email` | +| PUT | `/emails/mailboxes/{email}/drafts/{draftId}` | Update the specified draft email | `update_draft_email` | + +### History + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/history` | List history of events for mailbox | `list_mailbox_history` | + +### Labels + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/labels` | List labels in the mailbox | `list_labels_in_mailbox` | +| POST | `/emails/mailboxes/{email}/labels` | Create a new label in mailbox | `create_label_in_mailbox` | +| DELETE | `/emails/mailboxes/{email}/labels/{labelId}` | Delete an existing label from mailbox | `delete_label_from_mailbox` | +| GET | `/emails/mailboxes/{email}/labels/{labelId}` | Get the specified label in mailbox | `get_label_in_mailbox` | +| PATCH | `/emails/mailboxes/{email}/labels/{labelId}` | Patch the specified label in mailbox | `patch_label_in_mailbox` | +| PUT | `/emails/mailboxes/{email}/labels/{labelId}` | Update the specified label in mailbox | `update_label_in_mailbox` | + +### Mailbox + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/profile` | Get the mailbox profile | `get_mailbox_profile` | + +### Messages + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/messages` | List emails from the mailbox | `list_emails` | +| POST | `/emails/mailboxes/{email}/messages` | Create a new email | `create_email` | +| POST | `/emails/mailboxes/{email}/messages/batchDelete` | Batch delete the specified emails | `batch_delete_emails` | +| POST | `/emails/mailboxes/{email}/messages/batchModify` | Batch modify the specified emails | `batch_modify_emails` | +| POST | `/emails/mailboxes/{email}/messages/send` | Send out an email | `send_email` | +| DELETE | `/emails/mailboxes/{email}/messages/{messageId}` | Delete an existing email | `delete_email` | +| GET | `/emails/mailboxes/{email}/messages/{messageId}` | Get the specified email | `get_email` | +| POST | `/emails/mailboxes/{email}/messages/{messageId}/modify` | Update the specified email | `update_email` | +| POST | `/emails/mailboxes/{email}/messages/{messageId}/trash` | Move the specified email to TRASH folder | `trash_email` | +| POST | `/emails/mailboxes/{email}/messages/{messageId}/untrash` | Move the specified email out of TRASH folder | `untrash_email` | + +### Messages.Attachments + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/messages/{messageId}/attachments/{attachmentId}` | Get the specified attachment for an email | `get_email_attachment` | + +### Settings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/settings/vacation` | Get mailbox vacation response setting | `get_mail_vacation_response_setting` | +| PUT | `/emails/mailboxes/{email}/settings/vacation` | Update mailbox vacation response setting | `update_mailbox_vacation_response_setting` | + +### Settings.Delegates + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/settings/delegates` | List delegates on the mailbox | `list_mailbox_delegates` | +| POST | `/emails/mailboxes/{email}/settings/delegates` | Grant a new delegate access on the mailbox | `grant_mailbox_delegate` | +| DELETE | `/emails/mailboxes/{email}/settings/delegates/{delegateEmail}` | Revoke an existing delegate access from the mailbox | `revoke_mailbox_delegate` | +| GET | `/emails/mailboxes/{email}/settings/delegates/{delegateEmail}` | Get the specified delegate on the mailbox | `get_mailbox_delegate` | + +### Settings.Filters + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/settings/filters` | List email filters | `list_email_filters` | +| POST | `/emails/mailboxes/{email}/settings/filters` | Create an email filter | `create_email_filter` | +| DELETE | `/emails/mailboxes/{email}/settings/filters/{filterId}` | Delete the specified email filter | `delete_email_filter` | +| GET | `/emails/mailboxes/{email}/settings/filters/{filterId}` | Get the specified email filter | `get_email_filter` | + +### Threads + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/emails/mailboxes/{email}/threads` | List email threads from the mailbox | `list_email_threads` | +| DELETE | `/emails/mailboxes/{email}/threads/{threadId}` | Delete an existing email thread | `delete_email_thread` | +| GET | `/emails/mailboxes/{email}/threads/{threadId}` | Get the specified email thread | `get_email_thread` | +| POST | `/emails/mailboxes/{email}/threads/{threadId}/modify` | Update the specified thread | `update_email_thread` | +| POST | `/emails/mailboxes/{email}/threads/{threadId}/trash` | Move the specified thread to TRASH folder | `trash_email_thread` | +| POST | `/emails/mailboxes/{email}/threads/{threadId}/untrash` | Move the specified thread out of TRASH folder | `untrash_email_thread` | diff --git a/plugins/zoom-developers/skills/rest-api/references/marketplace-apps.md b/plugins/zoom-developers/skills/rest-api/references/marketplace-apps.md new file mode 100644 index 00000000..21512150 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/marketplace-apps.md @@ -0,0 +1,75 @@ +# Zoom Marketplace API + +Authoritative endpoint inventory for Marketplace. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/marketplace/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 27 | +| Path templates | 19 | +| Tags | 3 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| App | 23 | +| Apps | 1 | +| Manifest | 3 | + +## Endpoints by Tag + +### App + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/app/notifications` | Send app notifications | `Sendappnotifications` | +| GET | `/marketplace/app/custom_fields` | Get user's custom field values | `getCustomFieldValues` | +| DELETE | `/marketplace/app/event_subscription` | Unsubscribe app event subscription | `unSubscribeEventSubscription` | +| GET | `/marketplace/app/event_subscription` | Get user or account event subscription | `getUserEventSubscriptions` | +| POST | `/marketplace/app/event_subscription` | Create an event subscription | `createEventSubscription` | +| DELETE | `/marketplace/app/event_subscription/{eventSubscriptionId}` | Delete an event subscription | `deleteEventSubscription` | +| PATCH | `/marketplace/app/event_subscription/{eventSubscriptionId}` | Subscribe an event subscription | `subscribeEventSubscription` | +| GET | `/marketplace/apps` | List apps | `ListApps` | +| POST | `/marketplace/apps` | Create apps | `CreateApps` | +| DELETE | `/marketplace/apps/{appId}` | Deletes an app | `deleteApp` | +| GET | `/marketplace/apps/{appId}` | Get information about an app | `getAppInfo` | +| GET | `/marketplace/apps/{appId}/api_call_logs` | Get API call logs | `Getapicalllogs` | +| POST | `/marketplace/apps/{appId}/deeplink` | Generate Zoom App Deeplink | `GenerateZoomAppDeeplink` | +| POST | `/marketplace/apps/{appId}/preApprove` | Update app pre approval setting | `Updateapppreapprovalsetting` | +| GET | `/marketplace/apps/{appId}/requests` | Get an app's user requests | `getAppUserRequests` | +| PATCH | `/marketplace/apps/{appId}/requests` | Update app's request status | `updateAppRequestStatus` | +| POST | `/marketplace/apps/{appId}/requests` | Add app allow requests for users | `AddAppAllowRequestsForUsers` | +| POST | `/marketplace/apps/{appId}/rotate_client_secret` | Rotate client secret | `RotateClientSecret` | +| GET | `/marketplace/apps/{appId}/webhook_logs` | Get webhook logs | `getWebhookLogs` | +| GET | `/marketplace/monetization/entitlements` | Get app user entitlements | `getAppUserEntitlementRequests` | +| GET | `/marketplace/users/{userId}/apps` | Get a user's app requests | `getUserAppRequests` | +| PATCH | `/marketplace/users/{userId}/apps/{appId}/subscription` | Enable or disable user app subscription | `Enable/Disableuserappsubscription` | +| GET | `/marketplace/users/{userId}/entitlements` | Get a user's entitlements | `getUserEntitlementRequests` | + +### Apps + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/zoomapp/deeplink` | Generate an app deeplink | `generateAppDeeplink` | + +### Manifest + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/marketplace/apps/manifest/validate` | Validate an app manifest | `validatingManifest` | +| GET | `/marketplace/apps/{appId}/manifest` | Export an app manifest from an existing app | `getAppManifest` | +| PUT | `/marketplace/apps/{appId}/manifest` | Update an app by manifest | `updateAppByManifest` | diff --git a/plugins/zoom-developers/skills/rest-api/references/meetings.md b/plugins/zoom-developers/skills/rest-api/references/meetings.md new file mode 100644 index 00000000..d29279cb --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/meetings.md @@ -0,0 +1,327 @@ +# Zoom Meetings API + +Authoritative endpoint inventory for Meetings. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/meetings/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 183 | +| Path templates | 128 | +| Tags | 19 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Archiving | 6 | +| Cloud Recording | 17 | +| Devices | 13 | +| H323 Devices | 4 | +| In-Meeting Apps | 2 | +| In-Meeting Features | 5 | +| Invitation & Registration | 10 | +| Live streaming | 4 | +| Meetings | 13 | +| PAC | 1 | +| Polls | 7 | +| Reports | 23 | +| SIP Phone | 4 | +| Summaries | 3 | +| Surveys | 3 | +| Templates | 2 | +| Tracking Field | 5 | +| TSP | 8 | +| Webinars | 53 | + +## Endpoints by Tag + +### Archiving + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/archive_files` | List archived files | `listArchivedFiles` | +| GET | `/archive_files/statistics` | Get archived file statistics | `getArchivedFileStatistics` | +| PATCH | `/archive_files/{fileId}` | Update an archived file's auto-delete status | `updateArchivedFile` | +| GET | `/meetings/{meetingId}/jointoken/local_archiving` | Get a meeting's archive token for local archiving | `meetingLocalArchivingArchiveToken` | +| DELETE | `/past_meetings/{meetingUUID}/archive_files` | Delete a meeting's archived files | `deleteArchivedFiles` | +| GET | `/past_meetings/{meetingUUID}/archive_files` | Get a meeting's archived files | `getArchivedFiles` | + +### Cloud Recording + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/meetings/{meetingId}/recordings` | Delete meeting or webinar recordings | `recordingDelete` | +| GET | `/meetings/{meetingId}/recordings` | Get meeting recordings | `recordingGet` | +| GET | `/meetings/{meetingId}/recordings/analytics_details` | Get a meeting or webinar recording's analytics details | `analytics_details` | +| GET | `/meetings/{meetingId}/recordings/analytics_summary` | Get a meeting or webinar recording's analytics summary | `analytics_summary` | +| GET | `/meetings/{meetingId}/recordings/registrants` | List recording registrants | `meetingRecordingRegistrants` | +| POST | `/meetings/{meetingId}/recordings/registrants` | Create a recording registrant | `meetingRecordingRegistrantCreate` | +| GET | `/meetings/{meetingId}/recordings/registrants/questions` | Get registration questions | `recordingRegistrantsQuestionsGet` | +| PATCH | `/meetings/{meetingId}/recordings/registrants/questions` | Update registration questions | `recordingRegistrantQuestionUpdate` | +| PUT | `/meetings/{meetingId}/recordings/registrants/status` | Update a registrant's status | `meetingRecordingRegistrantStatus` | +| GET | `/meetings/{meetingId}/recordings/settings` | Get meeting recording settings | `recordingSettingUpdate` | +| PATCH | `/meetings/{meetingId}/recordings/settings` | Update meeting recording settings | `recordingSettingsUpdate` | +| DELETE | `/meetings/{meetingId}/recordings/{recordingId}` | Delete a recording file for a meeting or webinar | `recordingDeleteOne` | +| PUT | `/meetings/{meetingId}/recordings/{recordingId}/status` | Recover a single recording | `recordingStatusUpdateOne` | +| DELETE | `/meetings/{meetingId}/transcript` | Delete a meeting or webinar transcript | `DeleteMeetingTranscript` | +| GET | `/meetings/{meetingId}/transcript` | Get a meeting transcript | `GetMeetingTranscript` | +| PUT | `/meetings/{meetingUUID}/recordings/status` | Recover meeting recordings | `recordingStatusUpdate` | +| GET | `/users/{userId}/recordings` | List all recordings | `recordingsList` | + +### Devices + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/devices` | List devices | `listDevices` | +| POST | `/devices` | Add a new device | `addDevice` | +| GET | `/devices/groups` | Get ZDM group info | `Getzdmgroupinfo` | +| POST | `/devices/zpa/assignment` | Assign a device to a user or commonarea | `Assigndevicetoauser/commonarea` | +| GET | `/devices/zpa/settings` | Get Zoom Phone Appliance settings by user ID | `GetZpaDeviceListProfileSettingOfaUser` | +| POST | `/devices/zpa/upgrade` | Upgrade ZPA firmware or app | `UpgradeZpas/app` | +| DELETE | `/devices/zpa/vendors/{vendor}/mac_addresses/{macAddress}` | Delete ZPA device by vendor and mac address | `DeleteZpaDeviceByVendorAndMacAddress` | +| GET | `/devices/zpa/zdm_groups/{zdmGroupId}/versions` | Get ZPA version info | `GetZpaVersioninfo` | +| DELETE | `/devices/{deviceId}` | Delete device | `deleteDevice` | +| GET | `/devices/{deviceId}` | Get device detail | `getDevice` | +| PATCH | `/devices/{deviceId}` | Change device | `updateDevice` | +| PATCH | `/devices/{deviceId}/assign_group` | Assign a device to a group | `assginGroup` | +| PATCH | `/devices/{deviceId}/assignment` | Change device association | `changeDeviceAssociation` | + +### H323 Devices + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/h323/devices` | List H.323/SIP devices | `deviceList` | +| POST | `/h323/devices` | Create a H.323/SIP device | `deviceCreate` | +| DELETE | `/h323/devices/{deviceId}` | Delete a H.323/SIP device | `deviceDelete` | +| PATCH | `/h323/devices/{deviceId}` | Update a H.323/SIP device | `deviceUpdate` | + +### In-Meeting Apps + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/meetings/{meetingId}/open_apps` | Delete a meeting app | `meetingAppDelete` | +| POST | `/meetings/{meetingId}/open_apps` | Add a meeting app | `meetingAppAdd` | + +### In-Meeting Features + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/live_meetings/{meetingId}/chat/messages/{messageId}` | Delete a live meeting message | `deleteMeetingChatMessageById` | +| PATCH | `/live_meetings/{meetingId}/chat/messages/{messageId}` | Update a live meeting message | `updateMeetingChatMessageById` | +| PATCH | `/live_meetings/{meetingId}/events` | In-meeting controls | `inMeetingControl` | +| GET | `/meetings/{meetingId}/jointoken/local_recording` | Get a meeting's join token for local recording | `meetingLocalRecordingJoinToken` | +| GET | `/meetings/{meetingId}/token` | Get meeting's token | `meetingToken` | + +### Invitation & Registration + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/meetings/{meetingId}/batch_registrants` | Perform batch registration | `addBatchRegistrants` | +| GET | `/meetings/{meetingId}/invitation` | Get meeting invitation | `meetingInvitation` | +| POST | `/meetings/{meetingId}/invite_links` | Create a meeting's invite links | `meetingInviteLinksCreate` | +| GET | `/meetings/{meetingId}/registrants` | List meeting registrants | `meetingRegistrants` | +| POST | `/meetings/{meetingId}/registrants` | Add a meeting registrant | `meetingRegistrantCreate` | +| GET | `/meetings/{meetingId}/registrants/questions` | List registration questions | `meetingRegistrantsQuestionsGet` | +| PATCH | `/meetings/{meetingId}/registrants/questions` | Update registration questions | `meetingRegistrantQuestionUpdate` | +| PUT | `/meetings/{meetingId}/registrants/status` | Update registrant's status | `meetingRegistrantStatus` | +| DELETE | `/meetings/{meetingId}/registrants/{registrantId}` | Delete a meeting registrant | `meetingregistrantdelete` | +| GET | `/meetings/{meetingId}/registrants/{registrantId}` | Get a meeting registrant | `meetingRegistrantGet` | + +### Live streaming + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/meetings/{meetingId}/jointoken/live_streaming` | Get a meeting's join token for live streaming | `meetingLiveStreamingJoinToken` | +| GET | `/meetings/{meetingId}/livestream` | Get livestream details | `getMeetingLiveStreamDetails` | +| PATCH | `/meetings/{meetingId}/livestream` | Update a livestream | `meetingLiveStreamUpdate` | +| PATCH | `/meetings/{meetingId}/livestream/status` | Update livestream status | `meetingLiveStreamStatusUpdate` | + +### Meetings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PATCH | `/live_meetings/{meetingId}/rtms_app/status` | Update participant Real-Time Media Streams (RTMS) app status | `meetingRTMSStatusUpdate` | +| DELETE | `/meetings/{meetingId}` | Delete a meeting | `meetingDelete` | +| GET | `/meetings/{meetingId}` | Get a meeting | `meeting` | +| PATCH | `/meetings/{meetingId}` | Update a meeting | `meetingUpdate` | +| POST | `/meetings/{meetingId}/sip_dialing` | Get a meeting SIP URI with passcode | `getSipDialingWithPasscode` | +| PUT | `/meetings/{meetingId}/status` | Update meeting status | `meetingStatus` | +| GET | `/past_meetings/{meetingId}` | Get past meeting details | `pastMeetingDetails` | +| GET | `/past_meetings/{meetingId}/instances` | List past meeting instances | `pastMeetings` | +| GET | `/past_meetings/{meetingId}/participants` | Get past meeting participants | `pastMeetingParticipants` | +| GET | `/past_meetings/{meetingId}/qa` | List past meetings' Q&A | `listPastMeetingQA` | +| GET | `/users/{userId}/meetings` | List meetings | `meetings` | +| POST | `/users/{userId}/meetings` | Create a meeting | `meetingCreate` | +| GET | `/users/{userId}/upcoming_meetings` | List upcoming meetings | `listUpcomingMeeting` | + +### PAC + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/users/{userId}/pac` | List a user's PAC accounts | `userPACs` | + +### Polls + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/meetings/{meetingId}/batch_polls` | Perform batch poll creation | `createBatchPolls` | +| GET | `/meetings/{meetingId}/polls` | List meeting polls | `meetingPolls` | +| POST | `/meetings/{meetingId}/polls` | Create a meeting poll | `meetingPollCreate` | +| DELETE | `/meetings/{meetingId}/polls/{pollId}` | Delete a meeting poll | `meetingPollDelete` | +| GET | `/meetings/{meetingId}/polls/{pollId}` | Get a meeting poll | `meetingPollGet` | +| PUT | `/meetings/{meetingId}/polls/{pollId}` | Update a meeting poll | `meetingPollUpdate` | +| GET | `/past_meetings/{meetingId}/polls` | List past meeting's poll results | `listPastMeetingPolls` | + +### Reports + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/report/activities` | Get sign In / sign out activity report | `reportSignInSignOutActivities` | +| GET | `/report/billing` | Get billing reports | `getBillingReport` | +| GET | `/report/billing/invoices` | Get billing invoice reports | `getBillingInvoicesReports` | +| GET | `/report/cloud_recording` | Get cloud recording usage report | `reportCloudRecording` | +| GET | `/report/daily` | Get daily usage report | `reportDaily` | +| GET | `/report/history_meetings` | Get history meeting and webinar list | `Gethistorymeetingandwebinarlist` | +| GET | `/report/meeting_activities` | Get a meeting activities report | `reportMeetingactivitylogs` | +| GET | `/report/meetings/{meetingId}` | Get meeting detail reports | `reportMeetingDetails` | +| GET | `/report/meetings/{meetingId}/participants` | Get meeting participant reports | `reportMeetingParticipants` | +| GET | `/report/meetings/{meetingId}/polls` | Get meeting poll reports | `reportMeetingPolls` | +| GET | `/report/meetings/{meetingId}/qa` | Get meeting Q&A report | `reportMeetingQA` | +| GET | `/report/meetings/{meetingId}/survey` | Get meeting survey report | `reportMeetingSurvey` | +| GET | `/report/operationlogs` | Get operation logs report | `reportOperationLogs` | +| GET | `/report/remote_support` | Get remote support report | `Getremotesupportreport` | +| GET | `/report/telephone` | Get telephone reports | `reportTelephone` | +| GET | `/report/upcoming_events` | Get upcoming events report | `reportUpcomingEvents` | +| GET | `/report/users` | Get active or inactive host reports | `reportUsers` | +| GET | `/report/users/{userId}/meetings` | Get meeting reports | `reportMeetings` | +| GET | `/report/webinars/{webinarId}` | Get webinar detail reports | `reportWebinarDetails` | +| GET | `/report/webinars/{webinarId}/participants` | Get webinar participant reports | `reportWebinarParticipants` | +| GET | `/report/webinars/{webinarId}/polls` | Get webinar poll reports | `reportWebinarPolls` | +| GET | `/report/webinars/{webinarId}/qa` | Get webinar Q&A report | `reportWebinarQA` | +| GET | `/report/webinars/{webinarId}/survey` | Get webinar survey report | `reportWebinarSurvey` | + +### SIP Phone + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/sip_phones/phones` | List SIP phones | `ListSIPPhonePhones` | +| POST | `/sip_phones/phones` | Enable SIP phone | `EnableSIPPhonePhones` | +| DELETE | `/sip_phones/phones/{phoneId}` | Delete SIP phone | `deleteSIPPhonePhones` | +| PATCH | `/sip_phones/phones/{phoneId}` | Update SIP phone | `UpdateSIPPhonePhones` | + +### Summaries + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/meetings/meeting_summaries` | List an account's meeting or webinar summaries | `Listmeetingsummaries` | +| DELETE | `/meetings/{meetingId}/meeting_summary` | Delete a meeting or webinar summary | `Deletemeetingorwebinarsummary` | +| GET | `/meetings/{meetingId}/meeting_summary` | Get a meeting or webinar summary | `Getameetingsummary` | + +### Surveys + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/meetings/{meetingId}/survey` | Delete a meeting survey | `meetingSurveyDelete` | +| GET | `/meetings/{meetingId}/survey` | Get a meeting survey | `meetingSurveyGet` | +| PATCH | `/meetings/{meetingId}/survey` | Update a meeting survey | `meetingSurveyUpdate` | + +### Templates + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/users/{userId}/meeting_templates` | List meeting templates | `listMeetingTemplates` | +| POST | `/users/{userId}/meeting_templates` | Create a meeting template from an existing meeting | `meetingTemplateCreate` | + +### Tracking Field + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/tracking_fields` | List tracking fields | `trackingfieldList` | +| POST | `/tracking_fields` | Create a tracking field | `trackingfieldCreate` | +| DELETE | `/tracking_fields/{fieldId}` | Delete a tracking field | `trackingfieldDelete` | +| GET | `/tracking_fields/{fieldId}` | Get a tracking field | `trackingfieldGet` | +| PATCH | `/tracking_fields/{fieldId}` | Update a tracking field | `trackingfieldUpdate` | + +### TSP + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/tsp` | Get account's TSP information | `tsp` | +| PATCH | `/tsp` | Update an account's TSP information | `tspUpdate` | +| GET | `/users/{userId}/tsp` | List user's TSP accounts | `userTSPs` | +| POST | `/users/{userId}/tsp` | Add a user's TSP account | `userTSPCreate` | +| PATCH | `/users/{userId}/tsp/settings` | Set global dial-in URL for a TSP user | `tspUrlUpdate` | +| DELETE | `/users/{userId}/tsp/{tspId}` | Delete a user's TSP account | `userTSPDelete` | +| GET | `/users/{userId}/tsp/{tspId}` | Get a user's TSP account | `userTSP` | +| PATCH | `/users/{userId}/tsp/{tspId}` | Update a TSP account | `userTSPUpdate` | + +### Webinars + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/live_webinars/{webinarId}/chat/messages/{messageId}` | Delete a live webinar message | `deleteWebinarChatMessageById` | +| GET | `/past_webinars/{webinarId}/absentees` | Get webinar absentees | `webinarAbsentees` | +| GET | `/past_webinars/{webinarId}/instances` | List past webinar instances | `pastWebinars` | +| GET | `/past_webinars/{webinarId}/participants` | List webinar participants | `listWebinarParticipants` | +| GET | `/past_webinars/{webinarId}/polls` | List past webinar poll results | `listPastWebinarPollResults` | +| GET | `/past_webinars/{webinarId}/qa` | List Q&As of a past webinar | `listPastWebinarQA` | +| GET | `/users/{userId}/webinar_templates` | List webinar templates | `listWebinarTemplates` | +| POST | `/users/{userId}/webinar_templates` | Create a webinar template | `webinarTemplateCreate` | +| GET | `/users/{userId}/webinars` | List webinars | `webinars` | +| POST | `/users/{userId}/webinars` | Create a webinar | `webinarCreate` | +| DELETE | `/webinars/{webinarId}` | Delete a webinar | `webinarDelete` | +| GET | `/webinars/{webinarId}` | Get a webinar | `webinar` | +| PATCH | `/webinars/{webinarId}` | Update a webinar | `webinarUpdate` | +| POST | `/webinars/{webinarId}/batch_registrants` | Perform batch registration | `addBatchWebinarRegistrants` | +| GET | `/webinars/{webinarId}/branding` | Get webinar's session branding | `getWebinarBranding` | +| DELETE | `/webinars/{webinarId}/branding/name_tags` | Delete a webinar's branding name tag | `deleteWebinarBrandingNameTag` | +| POST | `/webinars/{webinarId}/branding/name_tags` | Create a webinar's branding name tag | `createWebinarBrandingNameTag` | +| PATCH | `/webinars/{webinarId}/branding/name_tags/{nameTagId}` | Update a webinar's branding name tag | `updateWebinarBrandingNameTag` | +| DELETE | `/webinars/{webinarId}/branding/virtual_backgrounds` | Delete a webinar's branding virtual backgrounds | `deleteWebinarBrandingVB` | +| PATCH | `/webinars/{webinarId}/branding/virtual_backgrounds` | Set webinar's default branding virtual background | `setWebinarBrandingVB` | +| POST | `/webinars/{webinarId}/branding/virtual_backgrounds` | Upload a webinar's branding virtual background | `uploadWebinarBrandingVB` | +| DELETE | `/webinars/{webinarId}/branding/wallpaper` | Delete a webinar's branding wallpaper | `deleteWebinarBrandingWallpaper` | +| POST | `/webinars/{webinarId}/branding/wallpaper` | Upload a webinar's branding wallpaper | `uploadWebinarBrandingWallpaper` | +| POST | `/webinars/{webinarId}/invite_links` | Create webinar's invite links | `webinarInviteLinksCreate` | +| GET | `/webinars/{webinarId}/jointoken/live_streaming` | Get a webinar's join token for live streaming | `webinarLiveStreamingJoinToken` | +| GET | `/webinars/{webinarId}/jointoken/local_archiving` | Get a webinar's archive token for local archiving | `webinarLocalArchivingArchiveToken` | +| GET | `/webinars/{webinarId}/jointoken/local_recording` | Get a webinar's join token for local recording | `webinarLocalRecordingJoinToken` | +| GET | `/webinars/{webinarId}/livestream` | Get live stream details | `getWebinarLiveStreamDetails` | +| PATCH | `/webinars/{webinarId}/livestream` | Update a live stream | `webinarLiveStreamUpdate` | +| PATCH | `/webinars/{webinarId}/livestream/status` | Update live stream status | `webinarLiveStreamStatusUpdate` | +| DELETE | `/webinars/{webinarId}/panelists` | Remove all panelists | `webinarPanelistsDelete` | +| GET | `/webinars/{webinarId}/panelists` | List panelists | `webinarPanelists` | +| POST | `/webinars/{webinarId}/panelists` | Add panelists | `webinarPanelistCreate` | +| DELETE | `/webinars/{webinarId}/panelists/{panelistId}` | Remove a panelist | `webinarPanelistDelete` | +| GET | `/webinars/{webinarId}/polls` | List a webinar's polls | `webinarPolls` | +| POST | `/webinars/{webinarId}/polls` | Create a webinar's poll | `webinarPollCreate` | +| DELETE | `/webinars/{webinarId}/polls/{pollId}` | Delete a webinar poll | `webinarPollDelete` | +| GET | `/webinars/{webinarId}/polls/{pollId}` | Get a webinar poll | `webinarPollGet` | +| PUT | `/webinars/{webinarId}/polls/{pollId}` | Update a webinar poll | `webinarPollUpdate` | +| GET | `/webinars/{webinarId}/registrants` | List webinar registrants | `webinarRegistrants` | +| POST | `/webinars/{webinarId}/registrants` | Add a webinar registrant | `webinarRegistrantCreate` | +| GET | `/webinars/{webinarId}/registrants/questions` | List registration questions | `webinarRegistrantsQuestionsGet` | +| PATCH | `/webinars/{webinarId}/registrants/questions` | Update registration questions | `webinarRegistrantQuestionUpdate` | +| PUT | `/webinars/{webinarId}/registrants/status` | Update registrant's status | `webinarRegistrantStatus` | +| DELETE | `/webinars/{webinarId}/registrants/{registrantId}` | Delete a webinar registrant | `deleteWebinarRegistrant` | +| GET | `/webinars/{webinarId}/registrants/{registrantId}` | Get a webinar registrant | `webinarRegistrantGet` | +| POST | `/webinars/{webinarId}/sip_dialing` | Get a webinar SIP URI with passcode | `getWebinarSipDialingWithPasscode` | +| PUT | `/webinars/{webinarId}/status` | Update webinar status | `webinarStatus` | +| DELETE | `/webinars/{webinarId}/survey` | Delete a webinar survey | `webinarSurveyDelete` | +| GET | `/webinars/{webinarId}/survey` | Get a webinar survey | `webinarSurveyGet` | +| PATCH | `/webinars/{webinarId}/survey` | Update a webinar survey | `webinarSurveyUpdate` | +| GET | `/webinars/{webinarId}/token` | Get webinar's token | `webinarToken` | +| GET | `/webinars/{webinarId}/tracking_sources` | Get webinar tracking sources | `getTrackingSources` | diff --git a/plugins/zoom-developers/skills/rest-api/references/number-management.md b/plugins/zoom-developers/skills/rest-api/references/number-management.md new file mode 100644 index 00000000..b1461158 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/number-management.md @@ -0,0 +1,94 @@ +# Zoom Number Management API + +Authoritative endpoint inventory for Number Management. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/number-management/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 28 | +| Path templates | 17 | +| Tags | 6 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Cloud Peering Provider Exchange | 5 | +| Phone Numbers | 6 | +| Phone Plan | 1 | +| Setting | 4 | +| SMS Campaigns | 4 | +| SMS Consent | 8 | + +## Endpoints by Tag + +### Cloud Peering Provider Exchange + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/number_management/carrier/peering_numbers` | List peering phone numbers for provider | `Listpeeringphonenumbersforprovider` | +| DELETE | `/number_management/peering_numbers` | Remove peering phone numbers | `deletePeeringPhoneNumbers` | +| GET | `/number_management/peering_numbers` | List peering phone numbers | `listPeeringPhoneNumbers` | +| PATCH | `/number_management/peering_numbers` | Update peering phone numbers | `updatePeeringPhoneNumbers` | +| POST | `/number_management/peering_numbers` | Add peering phone numbers | `addPeeringPhoneNumbers` | + +### Phone Numbers + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PATCH | `/number_management/allocation` | Allocate/Unallocate phone numbers | `AllocateNumber` | +| POST | `/number_management/byoc_numbers` | Add BYOC phone numbers | `AddBYOCPhoneNumber` | +| DELETE | `/number_management/numbers` | Delete phone numbers | `DeleteNumbers` | +| GET | `/number_management/numbers` | List phone numbers | `listPhoneNumbers` | +| GET | `/number_management/numbers/{phoneNumberId}` | Get a phone number | `getPhoneNumber` | +| PATCH | `/number_management/numbers/{phoneNumberId}` | Update a phone number | `updatePhoneNumberDetail` | + +### Phone Plan + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/number_management/plan` | List phone number plan information | `Listphonenumberplaninformation` | + +### Setting + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/number_management/ported_numbers/orders` | List ported numbers | `Listportednumbers` | +| GET | `/number_management/ported_numbers/orders/{orderId}` | Get ported numbers details | `Getportednumbersdetails` | +| GET | `/number_management/sip_groups` | List SIP groups | `ListSIPgroups` | +| GET | `/number_management/sip_trunks` | List BYOC SIP trunks | `ListBYOCSIPtrunks` | + +### SMS Campaigns + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/number_management/sms_campaigns` | List SMS campaigns | `listAccountSMSCampaigns` | +| GET | `/number_management/sms_campaigns/{smsCampaignId}` | Get an SMS campaign | `GetSMSCampaign` | +| DELETE | `/number_management/sms_campaigns/{smsCampaignId}/phone_numbers` | Unassign phone number from SMS campaign | `unassignCampaignPhoneNumber` | +| POST | `/number_management/sms_campaigns/{smsCampaignId}/phone_numbers` | Assign a phone number to SMS campaign | `assignCampaignPhoneNumbers` | + +### SMS Consent + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/number_management/sms_consent` | Delete SMS consent policies | `DeleteSMSConsents` | +| GET | `/number_management/sms_consent` | List SMS consent policies | `ListSMSConsents` | +| POST | `/number_management/sms_consent` | Create an SMS consent policy | `CreateSMSConsent` | +| GET | `/number_management/sms_consent/{consentId}` | Get an SMS consent policy | `GetSMSConsent` | +| PATCH | `/number_management/sms_consent/{consentId}` | Update an SMS consent policy | `UpdateSMSConsent` | +| DELETE | `/number_management/sms_consent/{consentId}/phone_numbers` | Unassign phone numbers from SMS consent policy | `UnassignPhoneNumbersFromConsent` | +| GET | `/number_management/sms_consent/{consentId}/phone_numbers` | List phone numbers assigned to SMS consent policy | `ListConsentPhoneNumbers` | +| POST | `/number_management/sms_consent/{consentId}/phone_numbers` | Assign phone numbers to SMS consent policy | `AssignPhoneNumbersToConsent` | diff --git a/plugins/zoom-developers/skills/rest-api/references/openapi.md b/plugins/zoom-developers/skills/rest-api/references/openapi.md new file mode 100644 index 00000000..1005525b --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/openapi.md @@ -0,0 +1,166 @@ +# OpenAPI / Swagger Specifications + +Zoom provides OpenAPI specifications for API client generation and tooling integration. + +## Official Specifications + +### Zoom API v2 (Current) + +| Property | Value | +|----------|-------| +| Format | Swagger 2.0 (JSON) | +| Status | **Deprecated** - Last updated November 2018 | +| Coverage | ~103 endpoints (subset of full API) | +| Download | [openapi.v2.json](https://raw.githubusercontent.com/zoom/api/442998230a148f403c3d1de1fe7aa54937354fa9/openapi.v2.json) | +| Repository | [github.com/zoom/api](https://github.com/zoom/api) | + +### Zoom API v1 (Legacy) + +| Property | Value | +|----------|-------| +| Format | Swagger 2.0 (JSON) | +| Status | **Deprecated** | +| Coverage | ~93 endpoints | +| Download | [openapi.v2.json](https://raw.githubusercontent.com/zoom/api-v1/5478bfe304a827f97acfed9aa5a0ba840b8d1aa9/openapi.v2.json) | +| Repository | [github.com/zoom/api-v1](https://github.com/zoom/api-v1) | + +## Coverage Limitations + +The official OpenAPI specs only cover a **small subset** of Zoom's 600+ endpoints: + +| Covered | NOT Covered | +|---------|-------------| +| Meetings | Phone API | +| Users | Team Chat API | +| Accounts | Mail API | +| Groups | Calendar API | +| Reports | Rooms API | +| Webinars | Clips API | +| H.323 Devices | Whiteboard API | +| IM Groups | Contact Center API | +| | AI Companion API | +| | 20+ other modern APIs | + +## Recommended Alternative: Zoom Rivet + +Instead of using the outdated OpenAPI specs, Zoom recommends using **Zoom Rivet** - their official API client library. + +### Installation + +```bash +npm install @zoom/rivet +``` + +### Usage + +```typescript +import Zoom from '@zoom/rivet'; + +const zoom = new Zoom({ + accountId: process.env.ZOOM_ACCOUNT_ID, + clientId: process.env.ZOOM_CLIENT_ID, + clientSecret: process.env.ZOOM_CLIENT_SECRET, +}); + +// Create a meeting +const meeting = await zoom.meetings.create({ + userId: 'me', + body: { + topic: 'My Meeting', + type: 2, + duration: 60, + }, +}); + +// List users +const users = await zoom.users.list(); + +// Get recordings +const recordings = await zoom.cloudRecordings.list({ + userId: 'me', + from: '2024-01-01', + to: '2024-01-31', +}); +``` + +### Why Rivet Over OpenAPI? + +| Aspect | OpenAPI Specs | Zoom Rivet | +|--------|---------------|------------| +| Coverage | ~103 endpoints | Full API | +| Maintenance | Deprecated (2018) | Actively maintained | +| Type Safety | Requires codegen | Built-in TypeScript | +| Auth Handling | Manual | Automatic token management | +| Pagination | Manual | Built-in helpers | +| Rate Limiting | Manual | Built-in retry logic | + +## Using OpenAPI Specs Anyway + +If you still need the OpenAPI specs (e.g., for custom tooling), here's how to use them: + +### Download the Spec + +```bash +# Download Zoom API v2 spec +curl -o zoom-api-v2.json \ + https://raw.githubusercontent.com/zoom/api/442998230a148f403c3d1de1fe7aa54937354fa9/openapi.v2.json +``` + +### Generate TypeScript Client + +```bash +# Using openapi-generator +npm install @openapitools/openapi-generator-cli -g + +openapi-generator-cli generate \ + -i zoom-api-v2.json \ + -g typescript-fetch \ + -o ./zoom-client +``` + +### Generate Python Client + +```bash +openapi-generator-cli generate \ + -i zoom-api-v2.json \ + -g python \ + -o ./zoom-client-python +``` + +### Known Issues with Code Generation + +The Zoom OpenAPI specs have known issues that may cause errors during code generation: + +| Issue | Workaround | +|-------|------------| +| Enum type mismatches | Manually fix integer/string enum definitions | +| Missing required fields | Add required fields to generated models | +| Invalid syntax | Validate and fix JSON before generation | +| Outdated endpoints | Supplement with manual API calls for new endpoints | + +## Postman Collection + +For interactive API exploration, Zoom provides a Postman collection: + +- **Postman Collection**: https://marketplace.zoom.us/docs/api-reference/postman + +```bash +# Import to Postman +# 1. Open Postman +# 2. Click Import +# 3. Enter URL: https://www.postman.com/zoom-developer/zoom-developer-api +``` + +## API Reference Documentation + +For the most up-to-date API documentation, use the official reference: + +- **REST API Reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/ +- **API Changelog**: https://developers.zoom.us/changelog/ + +## Resources + +- [Zoom Rivet (Official SDK)](https://github.com/zoom/rivet-javascript) +- [OpenAPI Generator](https://openapi-generator.tech/) +- [Swagger Editor](https://editor.swagger.io/) +- [Postman Collection](https://marketplace.zoom.us/docs/api-reference/postman) diff --git a/plugins/zoom-developers/skills/rest-api/references/phone.md b/plugins/zoom-developers/skills/rest-api/references/phone.md new file mode 100644 index 00000000..80868dc1 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/phone.md @@ -0,0 +1,666 @@ +# Zoom Phone API + +Authoritative endpoint inventory for Phone. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/phone/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 354 | +| Path templates | 213 | +| Tags | 47 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Accounts | 4 | +| Alerts | 5 | +| Audio Library | 6 | +| Auto Receptionists | 13 | +| Billing Account | 2 | +| Blocked List | 5 | +| Call Handling | 4 | +| Call Logs | 15 | +| Call Queues | 17 | +| Carrier Reseller | 4 | +| Common Areas | 17 | +| Dashboard | 11 | +| Device Line Keys | 2 | +| Dial by Name Directory | 6 | +| Emergency Addresses | 5 | +| Emergency Service Locations | 6 | +| External Contacts | 5 | +| Fax | 8 | +| Firmware Update Rules | 6 | +| Group Call Pickup | 8 | +| Groups | 3 | +| Inbound Blocked List | 10 | +| IVR | 2 | +| Line Keys | 3 | +| Monitoring Groups | 9 | +| Outbound Calling | 24 | +| Phone Devices | 11 | +| Phone Numbers | 8 | +| Phone Plans | 2 | +| Phone Roles | 11 | +| Private Directory | 4 | +| Provider Exchange | 5 | +| Provision Templates | 5 | +| Recordings | 8 | +| Reports | 4 | +| Routing Rules | 5 | +| Setting Templates | 4 | +| Settings | 8 | +| Shared Line Appearance | 1 | +| Shared Line Group | 16 | +| Sites | 12 | +| SMS | 7 | +| SMS Campaign | 7 | +| SMS Consent | 1 | +| Users | 18 | +| Voicemails | 7 | +| Zoom Rooms | 10 | + +## Endpoints by Tag + +### Accounts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/account_settings` | List an account's Zoom Phone settings | `listZoomPhoneAccountSettings` | +| DELETE | `/phone/outbound_caller_id/customized_numbers` | Delete phone numbers for an account's customized outbound caller ID | `deleteOutboundCallerNumbers` | +| GET | `/phone/outbound_caller_id/customized_numbers` | List an account's customized outbound caller ID phone numbers | `listCustomizeOutboundCallerNumbers` | +| POST | `/phone/outbound_caller_id/customized_numbers` | Add phone numbers for an account's customized outbound caller ID | `addOutboundCallerNumbers` | + +### Alerts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/alert_settings` | List alert settings with paging query | `ListAlertSettingsWithPagingQuery` | +| POST | `/phone/alert_settings` | Add an alert setting | `AddAnAlertSetting` | +| DELETE | `/phone/alert_settings/{alertSettingId}` | Delete an alert setting | `DeleteAnAlertSetting` | +| GET | `/phone/alert_settings/{alertSettingId}` | Get alert setting details | `GetAlertSettingDetails` | +| PATCH | `/phone/alert_settings/{alertSettingId}` | Update an alert setting | `UpdateAnAlertSetting` | + +### Audio Library + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/phone/audios/{audioId}` | Delete an audio item | `DeleteAudioItem` | +| GET | `/phone/audios/{audioId}` | Get an audio item | `GetAudioItem` | +| PATCH | `/phone/audios/{audioId}` | Update an audio item | `UpdateAudioItem` | +| GET | `/phone/users/{userId}/audios` | List audio items | `ListAudioItems` | +| POST | `/phone/users/{userId}/audios` | Add an audio item for text-to-speech conversion | `AddAnAudio` | +| POST | `/phone/users/{userId}/audios/batch` | Add audio items | `AddAudioItem` | + +### Auto Receptionists + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/auto_receptionists` | List auto receptionists | `listAutoReceptionists` | +| POST | `/phone/auto_receptionists` | Add an auto receptionist | `addAutoReceptionist` | +| DELETE | `/phone/auto_receptionists/{autoReceptionistId}` | Delete a non-primary auto receptionist | `deleteAutoReceptionist` | +| GET | `/phone/auto_receptionists/{autoReceptionistId}` | Get an auto receptionist | `getAutoReceptionistDetail` | +| PATCH | `/phone/auto_receptionists/{autoReceptionistId}` | Update an auto receptionist | `updateAutoReceptionist` | +| DELETE | `/phone/auto_receptionists/{autoReceptionistId}/phone_numbers` | Unassign all phone numbers | `unassignAllPhoneNumsAutoReceptionist` | +| POST | `/phone/auto_receptionists/{autoReceptionistId}/phone_numbers` | Assign phone numbers | `assignPhoneNumbersAutoReceptionist` | +| DELETE | `/phone/auto_receptionists/{autoReceptionistId}/phone_numbers/{phoneNumberId}` | Unassign a phone number | `unassignAPhoneNumAutoReceptionist` | +| GET | `/phone/auto_receptionists/{autoReceptionistId}/policies` | Get an auto receptionist policy | `getAutoReceptionistsPolicy` | +| PATCH | `/phone/auto_receptionists/{autoReceptionistId}/policies` | Update an auto receptionist policy | `updateAutoReceptionistPolicy` | +| DELETE | `/phone/auto_receptionists/{autoReceptionistId}/policies/{policyType}` | Delete a policy subsetting | `DeletePolicy` | +| PATCH | `/phone/auto_receptionists/{autoReceptionistId}/policies/{policyType}` | Update a policy subsetting | `updatePolicy` | +| POST | `/phone/auto_receptionists/{autoReceptionistId}/policies/{policyType}` | Add a policy subsetting | `AddPolicy` | + +### Billing Account + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/billing_accounts` | List billing accounts | `listBillingAccount` | +| GET | `/phone/billing_accounts/{billingAccountId}` | Get billing account details | `GetABillingAccount` | + +### Blocked List + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/blocked_list` | List blocked lists | `listBlockedList` | +| POST | `/phone/blocked_list` | Create a blocked list | `addAnumberToBlockedList` | +| DELETE | `/phone/blocked_list/{blockedListId}` | Delete a blocked list | `deleteABlockedList` | +| GET | `/phone/blocked_list/{blockedListId}` | Get blocked list details | `getABlockedList` | +| PATCH | `/phone/blocked_list/{blockedListId}` | Update a blocked list | `updateBlockedList` | + +### Call Handling + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/extension/{extensionId}/call_handling/settings` | Get call handling settings | `getCallHandling` | +| DELETE | `/phone/extension/{extensionId}/call_handling/settings/{settingType}` | Delete a call handling setting | `deleteCallHandling` | +| PATCH | `/phone/extension/{extensionId}/call_handling/settings/{settingType}` | Update a call handling setting | `updateCallHandling` | +| POST | `/phone/extension/{extensionId}/call_handling/settings/{settingType}` | Add a call handling setting | `addCallHandling` | + +### Call Logs + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/call_element/{callElementId}` | Get call element | `getCallElement` | +| GET | `/phone/call_history` | Get account's call history | `accountCallHistory` | +| GET | `/phone/call_history/{callHistoryUuid}` | Get call history | `getCallPath` | +| PATCH | `/phone/call_history/{callLogId}/client_code` | Add a client code to a call history | `addClientCodeToCallHistory` | +| GET | `/phone/call_history_detail/{callHistoryId}` | Get call history detail | `getCallHistoryDetail` | +| GET | `/phone/call_logs` | Get account's call logs | `accountCallLogs` | +| GET | `/phone/call_logs/{callLogId}` | Get call log details | `getCallLogDetails` | +| PUT | `/phone/call_logs/{callLogId}/client_code` | Add a client code to a call log | `addClientCodeToCallLog` | +| GET | `/phone/user/{userId}/ai_call_summary/{aiCallSummaryId}` | Get User AI Call Summary Detail | `getUserAICallSummary` | +| GET | `/phone/users/{userId}/call_history` | Get user's call history | `phoneUserCallHistory` | +| GET | `/phone/users/{userId}/call_history/sync` | Sync user's call history | `syncUserCallHistory` | +| DELETE | `/phone/users/{userId}/call_history/{callLogId}` | Delete a user's call history | `deleteUserCallHistory` | +| GET | `/phone/users/{userId}/call_logs` | Get user's call logs | `phoneUserCallLogs` | +| GET | `/phone/users/{userId}/call_logs/sync` | Sync user's call logs | `syncUserCallLogs` | +| DELETE | `/phone/users/{userId}/call_logs/{callLogId}` | Delete a user's call log | `deleteCallLog` | + +### Call Queues + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/call_queue_analytics` | List call queue analytics | `callqueueanalytics` | +| GET | `/phone/call_queues` | List call queues | `listCallQueues` | +| POST | `/phone/call_queues` | Create a call queue | `createCallQueue` | +| DELETE | `/phone/call_queues/{callQueueId}` | Delete a call queue | `deleteACallQueue` | +| GET | `/phone/call_queues/{callQueueId}` | Get call queue details | `getACallQueue` | +| PATCH | `/phone/call_queues/{callQueueId}` | Update call queue details | `updateCallQueue` | +| DELETE | `/phone/call_queues/{callQueueId}/members` | Unassign all members | `unassignAllMembers` | +| GET | `/phone/call_queues/{callQueueId}/members` | List call queue members | `listCallQueueMembers` | +| POST | `/phone/call_queues/{callQueueId}/members` | Add members to a call queue | `addMembersToCallQueue` | +| DELETE | `/phone/call_queues/{callQueueId}/members/{memberId}` | Unassign a member | `unassignMemberFromCallQueue` | +| DELETE | `/phone/call_queues/{callQueueId}/phone_numbers` | Unassign all phone numbers | `unassignAPhoneNumCallQueue` | +| POST | `/phone/call_queues/{callQueueId}/phone_numbers` | Assign numbers to a call queue | `assignPhoneToCallQueue` | +| DELETE | `/phone/call_queues/{callQueueId}/phone_numbers/{phoneNumberId}` | Unassign a phone number | `unAssignPhoneNumCallQueue` | +| DELETE | `/phone/call_queues/{callQueueId}/policies/{policyType}` | Delete a CQ policy setting | `removeCQPolicySubSetting` | +| PATCH | `/phone/call_queues/{callQueueId}/policies/{policyType}` | Update a call queue's policy subsetting | `updateCQPolicySubSetting` | +| POST | `/phone/call_queues/{callQueueId}/policies/{policyType}` | Add a policy subsetting to a call queue | `addCQPolicySubSetting` | +| GET | `/phone/call_queues/{callQueueId}/recordings` | Get call queue recordings | `getCallQueueRecordings` | + +### Carrier Reseller + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/carrier_reseller/numbers` | List phone numbers | `listCRPhoneNumbers` | +| PATCH | `/phone/carrier_reseller/numbers` | Activate phone numbers | `activeCRPhoneNumbers` | +| POST | `/phone/carrier_reseller/numbers` | Create phone numbers | `createCRPhoneNumbers` | +| DELETE | `/phone/carrier_reseller/numbers/{number}` | Delete a phone number | `deleteCRPhoneNumber` | + +### Common Areas + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/common_areas` | List common areas | `listCommonAreas` | +| POST | `/phone/common_areas` | Add a common area | `addCommonArea` | +| POST | `/phone/common_areas/activation_code` | Generate activation codes for common areas | `Generateactivationcodesforcommonareas` | +| GET | `/phone/common_areas/activation_codes` | List activation codes | `listActivationCodes` | +| POST | `/phone/common_areas/template_id/{templateId}` | Apply template to common areas | `ApplyTemplatetoCommonAreas` | +| DELETE | `/phone/common_areas/{commonAreaId}` | Delete a common area | `deleteCommonArea` | +| GET | `/phone/common_areas/{commonAreaId}` | Get common area details | `getACommonArea` | +| PATCH | `/phone/common_areas/{commonAreaId}` | Update common area | `updateCommonArea` | +| POST | `/phone/common_areas/{commonAreaId}/calling_plans` | Assign calling plans to a common area | `assignCallingPlansToCommonArea` | +| DELETE | `/phone/common_areas/{commonAreaId}/calling_plans/{type}` | Unassign a calling plan from the common area | `unassignCallingPlansFromCommonArea` | +| POST | `/phone/common_areas/{commonAreaId}/phone_numbers` | Assign phone numbers to a common area | `assignPhoneNumbersToCommonArea` | +| DELETE | `/phone/common_areas/{commonAreaId}/phone_numbers/{phoneNumberId}` | Unassign phone numbers from common area | `unassignPhoneNumbersFromCommonArea` | +| PATCH | `/phone/common_areas/{commonAreaId}/pin_code` | Update common area pin code | `UpdateCommonAreaPinCode` | +| GET | `/phone/common_areas/{commonAreaId}/settings` | Get common area settings | `getCommonAreaSettings` | +| DELETE | `/phone/common_areas/{commonAreaId}/settings/{settingType}` | Delete common area setting | `deleteCommonAreaSetting` | +| PATCH | `/phone/common_areas/{commonAreaId}/settings/{settingType}` | Update common area setting | `UpdateCommonAreaSetting` | +| POST | `/phone/common_areas/{commonAreaId}/settings/{settingType}` | Add common area setting | `AddCommonAreaSetting` | + +### Dashboard + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/metrics/call_logs` | List call logs | `listCallLogsMetrics` | +| GET | `/phone/metrics/call_logs/{callId}/qos` | Get call QoS | `getCallQoS` | +| GET | `/phone/metrics/call_logs/{call_id}` | Get call details from call log | `getCallLogMetricsDetails` | +| GET | `/phone/metrics/emergency_services/default_emergency_address/users` | List default emergency address users | `listUserDefaultEmergencyAddress` | +| GET | `/phone/metrics/emergency_services/detectable_personal_location/users` | List detectable personal location users | `listUserDetectablePersonalLocation` | +| GET | `/phone/metrics/emergency_services/location_sharing_permission/users` | List users permission for location sharing | `listUserLocationSharingPermission` | +| GET | `/phone/metrics/emergency_services/nomadic_emergency_services/users` | List nomadic emergency services users | `listUserNomadicEmergencyServices` | +| GET | `/phone/metrics/emergency_services/realtime_location/devices` | List real time location for IP phones | `listPhoneRealtimelocation` | +| GET | `/phone/metrics/emergency_services/realtime_location/users` | List real time location for users | `listUserRealtimeLocation` | +| GET | `/phone/metrics/location_tracking` | List tracked locations | `listTrackedLocations` | +| GET | `/phone/metrics/past_calls` | List past call metrics | `listPastCallMetrics` | + +### Device Line Keys + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/devices/{deviceId}/line_keys` | Get device line keys information | `listDeviceLineKeySetting` | +| PATCH | `/phone/devices/{deviceId}/line_keys` | Batch update device line key position | `batchUpdateDeviceLineKeySetting` | + +### Dial by Name Directory + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/phone/dial_by_name_directory/extensions` | Delete users from a directory | `DeleteUsersFromDirectory` | +| GET | `/phone/dial_by_name_directory/extensions` | List users in directory | `ListUsersFromDirectory` | +| POST | `/phone/dial_by_name_directory/extensions` | Add users to a directory | `AddUsersToDirectory` | +| DELETE | `/phone/sites/{siteId}/dial_by_name_directory/extensions` | Delete users from a directory of a site | `DeleteUsersFromDirectoryBySite` | +| GET | `/phone/sites/{siteId}/dial_by_name_directory/extensions` | List users in a directory by site | `ListUsersFromDirectoryBySite` | +| POST | `/phone/sites/{siteId}/dial_by_name_directory/extensions` | Add users to a directory of a site | `AddUsersToDirectoryBySite` | + +### Emergency Addresses + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/emergency_addresses` | List emergency addresses | `listEmergencyAddresses` | +| POST | `/phone/emergency_addresses` | Add an emergency address | `addEmergencyAddress` | +| DELETE | `/phone/emergency_addresses/{emergencyAddressId}` | Delete an emergency address | `deleteEmergencyAddress` | +| GET | `/phone/emergency_addresses/{emergencyAddressId}` | Get emergency address details | `getEmergencyAddress` | +| PATCH | `/phone/emergency_addresses/{emergencyAddressId}` | Update an emergency address | `updateEmergencyAddress` | + +### Emergency Service Locations + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/phone/batch_locations` | Batch add emergency service locations | `batchAddLocations` | +| GET | `/phone/locations` | List emergency service locations | `listLocations` | +| POST | `/phone/locations` | Add an emergency service location | `addLocation` | +| DELETE | `/phone/locations/{locationId}` | Delete an emergency location | `deleteLocation` | +| GET | `/phone/locations/{locationId}` | Get emergency service location details | `getLocation` | +| PATCH | `/phone/locations/{locationId}` | Update emergency service location | `updateLocation` | + +### External Contacts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/external_contacts` | List external contacts | `listExternalContacts` | +| POST | `/phone/external_contacts` | Add an external contact | `addExternalContact` | +| DELETE | `/phone/external_contacts/{externalContactId}` | Delete an external contact | `deleteAExternalContact` | +| GET | `/phone/external_contacts/{externalContactId}` | Get external contact details | `getAExternalContact` | +| PATCH | `/phone/external_contacts/{externalContactId}` | Update external contact | `updateExternalContact` | + +### Fax + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/fax/files` | Upload fax file | `UploadFaxFiles` | +| GET | `/phone/extension/{extensionId}/fax/logs` | Get extension's fax logs | `Getuser'sfaxlogs` | +| POST | `/phone/fax/documents` | Send fax | `SendEFax` | +| GET | `/phone/fax/logs` | Get account's fax logs | `GetAccount'sFaxLogs` | +| DELETE | `/phone/fax/logs/{faxLogId}` | Delete fax log | `DeleteFaxLog` | +| GET | `/phone/fax/logs/{faxLogId}` | Get fax log details | `GetFaxLogDetails` | +| PATCH | `/phone/fax/logs/{faxLogId}` | Update fax log read status | `UpdateFaxLogReadStatus` | +| GET | `/phone/fax/logs/{faxLogId}/file/{fileId}` | Download fax file | `Downloadfaxfile` | + +### Firmware Update Rules + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/firmware_update_rules` | List firmware update rules | `ListFirmwareRules` | +| POST | `/phone/firmware_update_rules` | Add a firmware update rule | `AddFirmwareRule` | +| DELETE | `/phone/firmware_update_rules/{ruleId}` | Delete firmware update rule | `DeleteFirmwareUpdateRule` | +| GET | `/phone/firmware_update_rules/{ruleId}` | Get firmware update rule information | `GetFirmwareRuleDetail` | +| PATCH | `/phone/firmware_update_rules/{ruleId}` | Update firmware update rule | `UpdateFirmwareRule` | +| GET | `/phone/firmwares` | List updatable firmwares | `ListFirmwares` | + +### Group Call Pickup + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/group_call_pickup` | List group call pickup objects | `listGCP` | +| POST | `/phone/group_call_pickup` | Add a group call pickup object | `addGCP` | +| DELETE | `/phone/group_call_pickup/{groupId}` | Delete group call pickup objects | `deleteGCP` | +| GET | `/phone/group_call_pickup/{groupId}` | Get call pickup group by ID | `GetGCP` | +| PATCH | `/phone/group_call_pickup/{groupId}` | Update the group call pickup information | `updateGCP` | +| GET | `/phone/group_call_pickup/{groupId}/members` | List call pickup group members | `listGCPMembers` | +| POST | `/phone/group_call_pickup/{groupId}/members` | Add members to a call pickup group | `addGCPMembers` | +| DELETE | `/phone/group_call_pickup/{groupId}/members/{extensionId}` | Remove members from call pickup group | `removeGCPMembers` | + +### Groups + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/groups/{groupId}/policies/{policyType}` | Get group policy details | `GetGroupPolicyDetails` | +| PATCH | `/phone/groups/{groupId}/policies/{policyType}` | Update group policy | `updateGroupPolicy` | +| GET | `/phone/groups/{groupId}/settings` | Get group phone settings | `getGroupPhoneSettings` | + +### Inbound Blocked List + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/phone/extension/{extensionId}/inbound_blocked/rules` | Delete an extension's inbound block rule | `DeleteExtensiontLevelInboundBlockRules` | +| GET | `/phone/extension/{extensionId}/inbound_blocked/rules` | List an extension's inbound block rules | `ListExtensionLevelInboundBlockRules` | +| POST | `/phone/extension/{extensionId}/inbound_blocked/rules` | Add an extension's inbound block rule | `AddExtensiontLevelInboundBlockRules` | +| DELETE | `/phone/inbound_blocked/extension_rules/statistics` | Delete an account's inbound blocked statistics | `DeleteAccountLevelInboundBlockedStatistics` | +| GET | `/phone/inbound_blocked/extension_rules/statistics` | List an account's inbound blocked statistics | `ListAccountLevelInboundBlockedStatistics` | +| PATCH | `/phone/inbound_blocked/extension_rules/statistics/blocked_for_all` | Mark a phone number as blocked for all extensions | `MarkPhoneNumberAsBlockedForAllExtensions` | +| DELETE | `/phone/inbound_blocked/rules` | Delete an account's inbound block rule | `DeleteAccountLevelInboundBlockRules` | +| GET | `/phone/inbound_blocked/rules` | List an account's inbound block rules | `ListAccountLevelInboundBlockRules` | +| POST | `/phone/inbound_blocked/rules` | Add an account's inbound block rule | `AddAccountLevelInboundBlockRules` | +| PATCH | `/phone/inbound_blocked/rules/{blockedRuleId}` | Update an account's inbound block rule | `UpdateAccountLevelInboundBlockRule` | + +### IVR + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/auto_receptionists/{autoReceptionistId}/ivr` | Get auto receptionist IVR | `getAutoReceptionistIVR` | +| PATCH | `/phone/auto_receptionists/{autoReceptionistId}/ivr` | Update auto receptionist IVR | `updateAutoReceptionistIVR` | + +### Line Keys + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/extension/{extensionId}/line_keys` | Get line key position and settings information | `listLineKeySetting` | +| PATCH | `/phone/extension/{extensionId}/line_keys` | Batch update line key position and settings information | `BatchUpdateLineKeySetting` | +| DELETE | `/phone/extension/{extensionId}/line_keys/{lineKeyId}` | Delete a line key setting. | `DeleteLineKey` | + +### Monitoring Groups + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/monitoring_groups` | Get a list of monitoring groups on an account | `listMonitoringGroup` | +| POST | `/phone/monitoring_groups` | Create a monitoring group | `createMonitoringGroup` | +| DELETE | `/phone/monitoring_groups/{monitoringGroupId}` | Delete a monitoring group | `deleteMonitoringGroup` | +| GET | `/phone/monitoring_groups/{monitoringGroupId}` | Get monitoring group by ID | `getMonitoringGroupById` | +| PATCH | `/phone/monitoring_groups/{monitoringGroupId}` | Update a monitoring group | `updateMonitoringGroup` | +| DELETE | `/phone/monitoring_groups/{monitoringGroupId}/monitor_members` | Remove all monitors or monitored members from a monitoring group | `removeMembers` | +| GET | `/phone/monitoring_groups/{monitoringGroupId}/monitor_members` | Get members of a monitoring group | `listMembers` | +| POST | `/phone/monitoring_groups/{monitoringGroupId}/monitor_members` | Add members to a monitoring group | `addMembers` | +| DELETE | `/phone/monitoring_groups/{monitoringGroupId}/monitor_members/{memberExtensionId}` | Remove a member from a monitoring group | `removeMember` | + +### Outbound Calling + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/common_areas/{commonAreaId}/outbound_calling/countries_regions` | Get common area level outbound calling countries and regions | `GetCommonAreaOutboundCallingCountriesAndRegions` | +| PATCH | `/phone/common_areas/{commonAreaId}/outbound_calling/countries_regions` | Update common area level outbound calling countries or regions | `UpdateCommonAreaOutboundCallingCountriesOrRegions` | +| GET | `/phone/common_areas/{commonAreaId}/outbound_calling/exception_rules` | List common area level outbound calling exception rules | `listCommonAreaOutboundCallingExceptionRule` | +| POST | `/phone/common_areas/{commonAreaId}/outbound_calling/exception_rules` | Add common area level outbound calling exception rule | `AddCommonAreaOutboundCallingExceptionRule` | +| DELETE | `/phone/common_areas/{commonAreaId}/outbound_calling/exception_rules/{exceptionRuleId}` | Delete common area level outbound calling exception rule | `deleteCommonAreaOutboundCallingExceptionRule` | +| PATCH | `/phone/common_areas/{commonAreaId}/outbound_calling/exception_rules/{exceptionRuleId}` | Update common area level outbound calling exception rule | `UpdateCommonAreaOutboundCallingExceptionRule` | +| GET | `/phone/outbound_calling/countries_regions` | Get account level outbound calling countries and regions | `GetAccountOutboundCallingCountriesAndRegions` | +| PATCH | `/phone/outbound_calling/countries_regions` | Update account level outbound calling countries or regions | `UpdateAccountOutboundCallingCountriesOrRegions` | +| GET | `/phone/outbound_calling/exception_rules` | List account level outbound calling exception rules | `listAccountOutboundCallingExceptionRule` | +| POST | `/phone/outbound_calling/exception_rules` | Add account level outbound calling exception rule | `AddAccountOutboundCallingExceptionRule` | +| DELETE | `/phone/outbound_calling/exception_rules/{exceptionRuleId}` | Delete account level outbound calling exception rule | `deleteAccountOutboundCallingExceptionRule` | +| PATCH | `/phone/outbound_calling/exception_rules/{exceptionRuleId}` | Update account level outbound calling exception rule | `UpdateAccountOutboundCallingExceptionRule` | +| GET | `/phone/sites/{siteId}/outbound_calling/countries_regions` | Get site level outbound calling countries and regions | `GetSiteOutboundCallingCountriesAndRegions` | +| PATCH | `/phone/sites/{siteId}/outbound_calling/countries_regions` | Update site level outbound calling countries or regions | `UpdateSiteOutboundCallingCountriesOrRegions` | +| GET | `/phone/sites/{siteId}/outbound_calling/exception_rules` | List site level outbound calling exception rules | `listSiteOutboundCallingExceptionRule` | +| POST | `/phone/sites/{siteId}/outbound_calling/exception_rules` | Add site level outbound calling exception rule | `AddSiteOutboundCallingExceptionRule` | +| DELETE | `/phone/sites/{siteId}/outbound_calling/exception_rules/{exceptionRuleId}` | Delete site level outbound calling exception rule | `deleteSiteOutboundCallingExceptionRule` | +| PATCH | `/phone/sites/{siteId}/outbound_calling/exception_rules/{exceptionRuleId}` | Update site level outbound calling exception rule | `UpdateSiteOutboundCallingExceptionRule` | +| GET | `/phone/users/{userId}/outbound_calling/countries_regions` | Get user level outbound calling countries and regions | `GetUserOutboundCallingCountriesAndRegions` | +| PATCH | `/phone/users/{userId}/outbound_calling/countries_regions` | Update user level outbound calling countries or regions | `UpdateUserOutboundCallingCountriesOrRegions` | +| GET | `/phone/users/{userId}/outbound_calling/exception_rules` | List user level outbound calling exception rules | `listUserOutboundCallingExceptionRule` | +| POST | `/phone/users/{userId}/outbound_calling/exception_rules` | Add user level outbound calling exception rule | `AddUserOutboundCallingExceptionRule` | +| DELETE | `/phone/users/{userId}/outbound_calling/exception_rules/{exceptionRuleId}` | Delete user level outbound calling exception rule | `deleteUserOutboundCallingExceptionRule` | +| PATCH | `/phone/users/{userId}/outbound_calling/exception_rules/{exceptionRuleId}` | Update user level outbound calling exception rule | `UpdateUserOutboundCallingExceptionRule` | + +### Phone Devices + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/devices` | List devices | `listPhoneDevices` | +| POST | `/phone/devices` | Add a device | `addPhoneDevice` | +| POST | `/phone/devices/sync` | Sync deskphones | `syncPhoneDevice` | +| DELETE | `/phone/devices/{deviceId}` | Delete a device | `deleteADevice` | +| GET | `/phone/devices/{deviceId}` | Get device details | `getADevice` | +| PATCH | `/phone/devices/{deviceId}` | Update a device | `updateADevice` | +| POST | `/phone/devices/{deviceId}/extensions` | Assign an entity to a device | `addExtensionsToADevice` | +| DELETE | `/phone/devices/{deviceId}/extensions/{extensionId}` | Unassign an entity from the device | `deleteExtensionFromADevice` | +| PUT | `/phone/devices/{deviceId}/provision_templates` | Update provision template of a device | `updateProvisionTemplateToDevice` | +| POST | `/phone/devices/{deviceId}/reboot` | Reboot a desk phone | `rebootPhoneDevice` | +| GET | `/phone/smartphones` | List Smartphones | `ListSmartphones` | + +### Phone Numbers + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/phone/byoc_numbers` | Add BYOC phone numbers | `addBYOCNumber` | +| DELETE | `/phone/numbers` | Delete unassigned phone numbers | `deleteUnassignedPhoneNumbers` | +| GET | `/phone/numbers` | List phone numbers | `listAccountPhoneNumbers` | +| PATCH | `/phone/numbers/sites/{siteId}` | Update a site's unassigned phone numbers | `updateSiteForUnassignedPhoneNumbers` | +| GET | `/phone/numbers/{phoneNumberId}` | Get a phone number | `getPhoneNumberDetails` | +| PATCH | `/phone/numbers/{phoneNumberId}` | Update a phone number | `updatePhoneNumberDetails` | +| POST | `/phone/users/{userId}/phone_numbers` | Assign a phone number to a user | `assignPhoneNumber` | +| DELETE | `/phone/users/{userId}/phone_numbers/{phoneNumberId}` | Unassign a phone number | `UnassignPhoneNumber` | + +### Phone Plans + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/calling_plans` | List calling plans | `listCallingPlans` | +| GET | `/phone/plans` | List plan information | `listPhonePlans` | + +### Phone Roles + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/roles` | List phone roles | `ListPhoneRoles` | +| POST | `/phone/roles` | Duplicate a phone role | `DuplicatePhoneRole` | +| DELETE | `/phone/roles/{roleId}` | Delete a phone role | `DeletePhoneRole` | +| GET | `/phone/roles/{roleId}` | Get role information | `getRoleInformation` | +| PATCH | `/phone/roles/{roleId}` | Update a phone role | `UpdatePhoneRole` | +| DELETE | `/phone/roles/{roleId}/members` | Delete members in a role | `DelRoleMembers` | +| GET | `/phone/roles/{roleId}/members` | List members in a role | `ListRoleMembers` | +| POST | `/phone/roles/{roleId}/members` | Add members to roles | `AddRoleMembers` | +| DELETE | `/phone/roles/{roleId}/targets` | Delete phone role targets | `DeletePhoneRoleTargets` | +| GET | `/phone/roles/{roleId}/targets` | List phone role targets | `ListPhoneRoleTargets` | +| POST | `/phone/roles/{roleId}/targets` | Add phone role targets | `AddPhoneRoleTargets` | + +### Private Directory + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/private_directory/members` | List private directory members | `listPrivateDirectoryMembers` | +| POST | `/phone/private_directory/members` | Add members to a private directory | `addMembersToAPrivateDirectory` | +| DELETE | `/phone/private_directory/members/{extensionId}` | Remove a member from a private directory | `removeAMemberFromAPrivateDirectory` | +| PATCH | `/phone/private_directory/members/{extensionId}` | Update a private directory member | `updateAPrivateDirectoryMember` | + +### Provider Exchange + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/carrier_peering/numbers` | List carrier peering phone numbers. | `listCarrierPeeringPhoneNumbers` | +| DELETE | `/phone/peering/numbers` | Remove peering phone numbers | `deletePeeringPhoneNumbers` | +| GET | `/phone/peering/numbers` | List peering phone numbers | `listPeeringPhoneNumbers` | +| PATCH | `/phone/peering/numbers` | Update peering phone numbers | `updatePeeringPhoneNumbers` | +| POST | `/phone/peering/numbers` | Add peering phone numbers | `addPeeringPhoneNumbers` | + +### Provision Templates + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/provision_templates` | List provision templates | `listAccountProvisionTemplate` | +| POST | `/phone/provision_templates` | Add a provision template | `addProvisionTemplate` | +| DELETE | `/phone/provision_templates/{templateId}` | Delete a provision template | `deleteProvisionTemplate` | +| GET | `/phone/provision_templates/{templateId}` | Get a provision template | `GetProvisionTemplate` | +| PATCH | `/phone/provision_templates/{templateId}` | Update a provision template | `updateProvisionTemplate` | + +### Recordings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/call_logs/{id}/recordings` | Get recording by call ID | `getPhoneRecordingsByCallIdOrCallLogId` | +| GET | `/phone/recording/download/{fileId}` | Download a phone recording | `phoneDownloadRecordingFile` | +| GET | `/phone/recording_transcript/download/{recordingId}` | Download a phone recording transcript | `phoneDownloadRecordingTranscript` | +| GET | `/phone/recordings` | Get call recordings | `getPhoneRecordings` | +| DELETE | `/phone/recordings/{recordingId}` | Delete a call recording | `deleteCallRecording` | +| PATCH | `/phone/recordings/{recordingId}` | Update auto delete field | `UpdateAutoDeleteField` | +| PUT | `/phone/recordings/{recordingId}/status` | Update Recording Status | `UpdateRecordingStatus` | +| GET | `/phone/users/{userId}/recordings` | Get user's recordings | `phoneUserRecordings` | + +### Reports + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/reports/call_charges` | Get call charges usage report | `GetCallChargesUsageReport` | +| GET | `/phone/reports/fax_charges` | Get fax charges usage report | `Getfaxchargesusagereport` | +| GET | `/phone/reports/operationlogs` | Get operation logs report | `getPSOperationLogs` | +| GET | `/phone/reports/sms_charges` | Get SMS/MMS charges usage report | `GetSMSChargesUsageReport` | + +### Routing Rules + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/routing_rules` | List directory backup routing rules | `listRoutingRule` | +| POST | `/phone/routing_rules` | Add directory backup routing rule | `addRoutingRule` | +| DELETE | `/phone/routing_rules/{routingRuleId}` | Delete directory backup routing rule | `deleteRoutingRule` | +| GET | `/phone/routing_rules/{routingRuleId}` | Get directory backup routing rule | `getRoutingRule` | +| PATCH | `/phone/routing_rules/{routingRuleId}` | Update directory backup routing rule | `updateRoutingRule` | + +### Setting Templates + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/setting_templates` | List setting templates | `listSettingTemplates` | +| POST | `/phone/setting_templates` | Add a setting template | `addSettingTemplate` | +| GET | `/phone/setting_templates/{templateId}` | Get setting template details | `getSettingTemplate` | +| PATCH | `/phone/setting_templates/{templateId}` | Update a setting template | `updateSettingTemplate` | + +### Settings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/policies/{policyType}` | Get account policy details | `GetAccountPolicyDetails` | +| PATCH | `/phone/policies/{policyType}` | Update account policy | `updateAccountPolicy` | +| GET | `/phone/ported_numbers/orders` | List ported numbers | `listPortedNumbers` | +| GET | `/phone/ported_numbers/orders/{orderId}` | Get ported numbers details | `getPortedNumbersDetails` | +| GET | `/phone/settings` | Get phone account settings | `phoneSetting` | +| PATCH | `/phone/settings` | Update phone account settings | `updatePhoneSettings` | +| GET | `/phone/sip_groups` | List SIP groups | `listSipGroups` | +| GET | `/phone/sip_trunk/trunks` | List BYOC SIP trunks | `listBYOCSIPTrunk` | + +### Shared Line Appearance + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/shared_line_appearances` | List shared line appearances | `listSharedLineAppearances` | + +### Shared Line Group + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/shared_line_groups` | List shared line groups | `listSharedLineGroups` | +| POST | `/phone/shared_line_groups` | Create a shared line group | `createASharedLineGroup` | +| GET | `/phone/shared_line_groups/{sharedLineGroupId}` | Get a shared line group | `getASharedLineGroup` | +| GET | `/phone/shared_line_groups/{sharedLineGroupId}/policies` | Get a shared line group policy | `getSharedLineGroupPolicy` | +| PATCH | `/phone/shared_line_groups/{sharedLineGroupId}/policies` | Update a shared line group policy | `updateSharedLineGroupPolicy` | +| DELETE | `/phone/shared_line_groups/{slgId}` | Delete a shared line group | `deleteASharedLineGroup` | +| PATCH | `/phone/shared_line_groups/{slgId}` | Update a shared line group | `updateASharedLineGroup` | +| DELETE | `/phone/shared_line_groups/{slgId}/members` | Unassign members from a shared line group | `deleteMembersOfSLG` | +| POST | `/phone/shared_line_groups/{slgId}/members` | Add members to a shared line group | `addMembersToSharedLineGroup` | +| DELETE | `/phone/shared_line_groups/{slgId}/members/{memberId}` | Unassign a member from a shared line group | `deleteAMemberSLG` | +| DELETE | `/phone/shared_line_groups/{slgId}/phone_numbers` | Unassign all phone numbers | `deletePhoneNumbersSLG` | +| POST | `/phone/shared_line_groups/{slgId}/phone_numbers` | Assign phone numbers | `assignPhoneNumbersSLG` | +| DELETE | `/phone/shared_line_groups/{slgId}/phone_numbers/{phoneNumberId}` | Unassign a phone number | `deleteAPhoneNumberSLG` | +| DELETE | `/phone/shared_line_groups/{slgId}/policies/{policyType}` | Delete an SLG policy setting | `removeSLGPolicySubSetting` | +| PATCH | `/phone/shared_line_groups/{slgId}/policies/{policyType}` | Update an SLG policy setting | `updateSLGPolicySubSetting` | +| POST | `/phone/shared_line_groups/{slgId}/policies/{policyType}` | Add a policy setting to a shared line group | `addSLGPolicySubSetting` | + +### Sites + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/sites` | List phone sites | `listPhoneSites` | +| POST | `/phone/sites` | Create a phone site | `createPhoneSite` | +| DELETE | `/phone/sites/{siteId}` | Delete a phone site | `deletePhoneSite` | +| GET | `/phone/sites/{siteId}` | Get phone site details | `getASite` | +| PATCH | `/phone/sites/{siteId}` | Update phone site details | `updateSiteDetails` | +| DELETE | `/phone/sites/{siteId}/outbound_caller_id/customized_numbers` | Remove customized outbound caller ID phone numbers | `deleteSiteOutboundCallerNumbers` | +| GET | `/phone/sites/{siteId}/outbound_caller_id/customized_numbers` | List customized outbound caller ID phone numbers | `listSiteCustomizeOutboundCallerNumbers` | +| POST | `/phone/sites/{siteId}/outbound_caller_id/customized_numbers` | Add customized outbound caller ID phone numbers | `addSiteOutboundCallerNumbers` | +| DELETE | `/phone/sites/{siteId}/settings/{settingType}` | Delete a site setting | `deleteSiteSetting` | +| GET | `/phone/sites/{siteId}/settings/{settingType}` | Get a phone site setting | `getSiteSettingForType` | +| PATCH | `/phone/sites/{siteId}/settings/{settingType}` | Update the site setting | `updateSiteSetting` | +| POST | `/phone/sites/{siteId}/settings/{settingType}` | Add a site setting | `addSiteSetting` | + +### SMS + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/phone/sms/messages` | Post SMS message | `postSmsMessage` | +| GET | `/phone/sms/sessions` | Get account's SMS sessions | `accountSmsSession` | +| GET | `/phone/sms/sessions/{sessionId}` | Get SMS session details | `smsSessionDetails` | +| GET | `/phone/sms/sessions/{sessionId}/messages/{messageId}` | Get SMS by message ID | `smsByMessageId` | +| GET | `/phone/sms/sessions/{sessionId}/sync` | Sync SMS by session ID | `smsSessionSync` | +| GET | `/phone/users/{userId}/sms/sessions` | Get user's SMS sessions | `userSmsSession` | +| GET | `/phone/users/{userId}/sms/sessions/sync` | List user's SMS sessions in descending order | `GetSmsSessions` | + +### SMS Campaign + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/sms_campaigns` | List SMS campaigns | `listAccountSMSCampaigns` | +| GET | `/phone/sms_campaigns/{smsCampaignId}` | Get an SMS campaign | `GetSMSCampaign` | +| POST | `/phone/sms_campaigns/{smsCampaignId}/phone_numbers` | Assign a phone number to SMS campaign | `assignCampaignPhoneNumbers` | +| GET | `/phone/sms_campaigns/{smsCampaignId}/phone_numbers/opt_status` | List opt statuses of phone numbers assigned to SMS campaign | `getNumberCampaignOptStatus` | +| PATCH | `/phone/sms_campaigns/{smsCampaignId}/phone_numbers/opt_status` | Update opt statuses of phone numbers assigned to SMS campaign | `updateNumberCampaignOptStatus` | +| DELETE | `/phone/sms_campaigns/{smsCampaignId}/phone_numbers/{phoneNumberId}` | Unassign a phone number | `unassignCampaignPhoneNumber` | +| GET | `/phone/user/{userId}/sms_campaigns/phone_numbers/opt_status` | List user's opt statuses of phone numbers | `getUserNumberCampaignOptStatus` | + +### SMS Consent + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/sms_consent/{consentId}/phone_numbers/opt_status` | List opt statuses of phone numbers assigned to SMS consent | `getNumberConsentOptStatus` | + +### Users + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/users` | List phone users | `listPhoneUsers` | +| POST | `/phone/users/batch` | Batch add users | `batchAddUsers` | +| PUT | `/phone/users/batch` | Update multiple users' properties in batch | `updateUsersPropertiesInBatch` | +| GET | `/phone/users/{userId}` | Get a user's profile | `phoneUser` | +| PATCH | `/phone/users/{userId}` | Update a user's profile | `updateUserProfile` | +| POST | `/phone/users/{userId}/calling_plans` | Assign calling plan to a user | `assignCallingPlan` | +| PUT | `/phone/users/{userId}/calling_plans` | Update user's calling plan | `updateCallingPlan` | +| DELETE | `/phone/users/{userId}/calling_plans/{planType}` | Unassign user's calling plan | `unassignCallingPlan` | +| DELETE | `/phone/users/{userId}/outbound_caller_id/customized_numbers` | Remove users' customized outbound caller ID phone numbers | `deleteUserOutboundCallerNumbers` | +| GET | `/phone/users/{userId}/outbound_caller_id/customized_numbers` | List users' phone numbers for a customized outbound caller ID | `listUserCustomizeOutboundCallerNumbers` | +| POST | `/phone/users/{userId}/outbound_caller_id/customized_numbers` | Add phone numbers for users' customized outbound caller ID | `addUserOutboundCallerNumbers` | +| GET | `/phone/users/{userId}/policies/{policyType}` | Get user policy details | `GetUserPolicyDetails` | +| PATCH | `/phone/users/{userId}/policies/{policyType}` | Update user policy | `updateUserPolicy` | +| GET | `/phone/users/{userId}/settings` | Get a user's profile settings | `phoneUserSettings` | +| PATCH | `/phone/users/{userId}/settings` | Update a user's profile settings | `updateUserSettings` | +| DELETE | `/phone/users/{userId}/settings/{settingType}` | Delete a user's shared access setting | `deleteUserSetting` | +| PATCH | `/phone/users/{userId}/settings/{settingType}` | Update a user's shared access setting | `updateUserSetting` | +| POST | `/phone/users/{userId}/settings/{settingType}` | Add a user's shared access setting | `addUserSetting` | + +### Voicemails + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/users/{userId}/call_logs/{id}/voice_mail` | Get user voicemail details from a call log | `getVoicemailDetailsByCallIdOrCallLogId` | +| GET | `/phone/users/{userId}/voice_mails` | Get user's voicemails | `phoneUserVoiceMails` | +| GET | `/phone/voice_mails` | Get account voicemails | `accountVoiceMails` | +| GET | `/phone/voice_mails/download/{fileId}` | Download a phone voicemail | `phoneDownloadVoicemailFile` | +| DELETE | `/phone/voice_mails/{voicemailId}` | Delete a voicemail | `deleteVoicemail` | +| GET | `/phone/voice_mails/{voicemailId}` | Get voicemail details | `getVoicemailDetails` | +| PATCH | `/phone/voice_mails/{voicemailId}` | Update Voicemail Read Status | `updateVoicemailReadStatus` | + +### Zoom Rooms + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/phone/rooms` | List Zoom Rooms under Zoom Phone license | `listZoomRooms` | +| POST | `/phone/rooms` | Add a Zoom Room to a Zoom Phone | `addZoomRoom` | +| GET | `/phone/rooms/unassigned` | List Zoom Rooms without Zoom Phone assignment | `listUnassignedZoomRooms` | +| DELETE | `/phone/rooms/{roomId}` | Remove a Zoom Room from a ZP account | `RemoveZoomRoom` | +| GET | `/phone/rooms/{roomId}` | Get a Zoom Room under Zoom Phone license | `getZoomRoom` | +| PATCH | `/phone/rooms/{roomId}` | Update a Zoom Room under Zoom Phone license | `updateZoomRoom` | +| POST | `/phone/rooms/{roomId}/calling_plans` | Assign calling plans to a Zoom Room | `assignCallingPlanToRoom` | +| DELETE | `/phone/rooms/{roomId}/calling_plans/{type}` | Remove a calling plan from a Zoom Room | `unassignCallingPlanFromRoom` | +| POST | `/phone/rooms/{roomId}/phone_numbers` | Assign phone numbers to a Zoom Room | `assignPhoneNumberToZoomRoom` | +| DELETE | `/phone/rooms/{roomId}/phone_numbers/{phoneNumberId}` | Remove a phone number from a Zoom Room | `UnassignPhoneNumberFromZoomRoom` | diff --git a/plugins/zoom-developers/skills/rest-api/references/qss.md b/plugins/zoom-developers/skills/rest-api/references/qss.md new file mode 100644 index 00000000..62eae230 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/qss.md @@ -0,0 +1,45 @@ +# Zoom QSS API + +Authoritative endpoint inventory for QSS. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/qss/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 3 | +| Path templates | 3 | +| Tags | 2 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Dashboards | 2 | +| Video SDK Sessions | 1 | + +## Endpoints by Tag + +### Dashboards + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/metrics/meetings/{meetingId}/participants/qos_summary` | List meeting participants QoS Summary | `dashboardMeetingParticipantsQOSSummary` | +| GET | `/metrics/webinars/{webinarId}/participants/qos_summary` | List webinar participants QoS Summary | `dashboardWebinarParticipantsQOSSummary` | + +### Video SDK Sessions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/videosdk/sessions/{sessionId}/users/qos_summary` | List session users QoS Summary | `sessionUsersQOSSummary` | diff --git a/plugins/zoom-developers/skills/rest-api/references/quality-management.md b/plugins/zoom-developers/skills/rest-api/references/quality-management.md new file mode 100644 index 00000000..c214e6cc --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/quality-management.md @@ -0,0 +1,48 @@ +# Zoom Quality Management API + +Authoritative endpoint inventory for Quality Management. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/quality-management/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 6 | +| Path templates | 5 | +| Tags | 2 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Evaluations | 3 | +| Interactions | 3 | + +## Endpoints by Tag + +### Evaluations + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/qm/automated_evaluations` | List automated evaluations | `ListAutomatedEvaluations` | +| GET | `/qm/evaluation` | List evaluations | `Listevaluations` | +| GET | `/qm/evaluation/{evaluationId}` | View evaluation detail | `EvaluationDetail` | + +### Interactions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/qm/interactions` | List interactions | `ListInteractions` | +| POST | `/qm/interactions` | Add an interaction | `Addinteraction` | +| GET | `/qm/interactions/{interactionId}` | View interaction detail | `InteractionDetail` | diff --git a/plugins/zoom-developers/skills/rest-api/references/rate-limits.md b/plugins/zoom-developers/skills/rest-api/references/rate-limits.md new file mode 100644 index 00000000..fb18b1ab --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/rate-limits.md @@ -0,0 +1,386 @@ +# REST API - Rate Limits + +Understanding and handling Zoom API rate limits for reliable integrations. + +## Overview + +Zoom APIs enforce rate limits to ensure fair usage. Limits vary by endpoint category, account type, and are shared across all apps on an account. + +## Rate Limit Categories + +### Main REST API + +| Category | Free | Pro | Business+ | +|----------|------|-----|-----------| +| **Light** | 4/sec, 6,000/day | 30/sec | 80/sec | +| **Medium** | 2/sec, 2,000/day | 20/sec | 60/sec | +| **Heavy** | 1/sec, 1,000/day | 10/sec* | 40/sec* | +| **Resource-Intensive** | 10/min, 30,000/day | 10/min* | 20/min* | + +**\* Daily limits (shared):** +- **Pro**: 30,000/day (Heavy + Resource-Intensive combined) +- **Business+**: 60,000/day (Heavy + Resource-Intensive combined) + +### Zoom Phone API + +| Category | Pro | Business+ | +|----------|-----|-----------| +| **Light** | 20/sec | 40/sec | +| **Medium** | 10/sec | 20/sec | +| **Heavy** | 5/sec, 15,000/day* | 10/sec, 30,000/day* | +| **Resource-Intensive** | 5/min, 15,000/day* | 10/min, 30,000/day* | + +### Zoom Contact Center API + +| Category | Pro | Business+ | +|----------|-----|-----------| +| **Light** | 20/sec | 40/sec | +| **Medium** | 10/sec | 20/sec | +| **Heavy** | 5/sec, 15,000/day* | 10/sec, 30,000/day* | + +**\* Daily limit shared with Resource-Intensive APIs** + +## Endpoint Category Examples + +| Light | Medium | Heavy | +|-------|--------|-------| +| Add Meeting Registrant | Create Meeting | Get Daily Usage Report | +| Get A Meeting | Get Past Meeting Participants | List Devices | +| Get Meeting Recordings | List All Recordings | | +| Update A Meeting | List Meetings | | +| Delete Meeting Recordings | List Webinars | | + +## Special Per-User Limits + +### Meeting/Webinar Operations + +| Operation | Limit | Reset | +|-----------|-------|-------| +| Meeting/Webinar Create/Update | **100/day per user** | 00:00 UTC | +| Registrant Addition | **3/day per registrant** | 00:00 UTC | +| Registrant Status Updates | **10/day per registrant** | 00:00 UTC | + +**Note:** The 100/day limit applies to all Meeting/Webinar IDs hosted by a specific user. + +### Lock-Key Limits (Concurrent Operations) + +Zoom enforces lock-key limits for user resource operations: + +| Scenario | Behavior | +|----------|----------| +| Multiple DELETE on same userId | Only 1 concurrent DELETE allowed | +| POST to `/v2/users` | Blocks GET/PATCH/PUT/DELETE until complete | + +**Error Response:** +```json +{ + "code": 429, + "message": "Too many concurrent requests. A request to disassociate this user has already been made." +} +``` + +## Response Headers + +Every API response includes rate limit headers: + +| Header | Description | +|--------|-------------| +| `X-RateLimit-Category` | Light, Medium, Heavy, or Resource-intensive | +| `X-RateLimit-Type` | `QPS` (per-second) or `Daily-limit` | +| `X-RateLimit-Limit` | Maximum requests in current window | +| `X-RateLimit-Remaining` | Requests remaining in current window | + +### On Per-Second/Minute Limit Hit + +| Header | Description | +|--------|-------------| +| `X-RateLimit-Reset` | Unix timestamp when limit resets | + +### On Daily Limit Hit + +| Header | Description | +|--------|-------------| +| `Retry-After` | ISO8601 datetime when you can retry | + +### Example Headers + +**Normal response:** +``` +X-RateLimit-Category: Medium +X-RateLimit-Type: QPS +X-RateLimit-Limit: 60 +X-RateLimit-Remaining: 55 +``` + +**Rate limited (per-second):** +``` +HTTP/1.1 429 Too Many Requests +X-RateLimit-Category: Light +X-RateLimit-Type: QPS +X-RateLimit-Limit: 80 +X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 1705312800 +``` + +**Rate limited (daily):** +``` +HTTP/1.1 429 Too Many Requests +X-RateLimit-Category: Heavy +X-RateLimit-Type: Daily-limit +X-RateLimit-Limit: 60000 +X-RateLimit-Remaining: 0 +Retry-After: 2025-01-20T00:00:00Z +``` + +## Error Responses + +### Per-Second Limit + +```json +{ + "code": 429, + "message": "You have reached the maximum per-second rate limit for this API. Try again later." +} +``` + +### Daily Limit + +```json +{ + "code": 429, + "message": "You have reached the maximum daily rate limit for this API. Refer to the response header for details on when you can make another request." +} +``` + +## Handling Rate Limits + +### Basic Retry with Backoff + +```javascript +async function callZoomAPI(url, options, maxRetries = 5) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + const response = await fetch(url, options); + + if (response.status === 429) { + // Check for Retry-After (daily limit) + const retryAfter = response.headers.get('Retry-After'); + if (retryAfter) { + const retryDate = new Date(retryAfter); + const waitMs = retryDate - Date.now(); + console.log(`Daily limit hit. Retry after: ${retryAfter}`); + await sleep(waitMs); + continue; + } + + // Per-second limit - use exponential backoff + const delay = Math.pow(2, attempt) * 1000; + const jitter = delay * 0.2 * Math.random(); // 20% jitter + console.log(`Rate limited. Retrying in ${delay + jitter}ms`); + await sleep(delay + jitter); + continue; + } + + return response; + } + + throw new Error('Max retries exceeded'); +} +``` + +### Monitor Rate Limit Headers + +```javascript +async function callAPIWithMonitoring(url, options) { + const response = await fetch(url, options); + + const remaining = parseInt(response.headers.get('X-RateLimit-Remaining')); + const limit = parseInt(response.headers.get('X-RateLimit-Limit')); + const category = response.headers.get('X-RateLimit-Category'); + const type = response.headers.get('X-RateLimit-Type'); + + console.log(`[${category}/${type}] ${remaining}/${limit} remaining`); + + // Proactive throttling + if (remaining < limit * 0.1) { // Less than 10% remaining + console.warn('Approaching rate limit - throttling requests'); + await sleep(1000); // Slow down + } + + return response; +} +``` + +### Request Queue Pattern + +For high-volume applications: + +```javascript +class RateLimitedQueue { + constructor(maxConcurrent = 10, minDelayMs = 100) { + this.queue = []; + this.running = 0; + this.maxConcurrent = maxConcurrent; + this.minDelayMs = minDelayMs; + } + + async add(requestFn) { + return new Promise((resolve, reject) => { + this.queue.push({ requestFn, resolve, reject }); + this.process(); + }); + } + + async process() { + if (this.running >= this.maxConcurrent || this.queue.length === 0) { + return; + } + + const { requestFn, resolve, reject } = this.queue.shift(); + this.running++; + + try { + const result = await requestFn(); + resolve(result); + } catch (error) { + reject(error); + } finally { + this.running--; + await sleep(this.minDelayMs); + this.process(); + } + } +} + +// Usage +const queue = new RateLimitedQueue(10, 100); // 10 concurrent, 100ms min delay + +const results = await Promise.all([ + queue.add(() => fetch('/api/users/1')), + queue.add(() => fetch('/api/users/2')), + queue.add(() => fetch('/api/users/3')), + // ... more requests +]); +``` + +## Best Practices + +### 1. Cache GET Responses + +```javascript +const cache = new Map(); +const CACHE_TTL = 60000; // 1 minute + +async function cachedGet(url, options) { + const cached = cache.get(url); + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + return cached.data; + } + + const response = await fetch(url, options); + const data = await response.json(); + + cache.set(url, { data, timestamp: Date.now() }); + return data; +} +``` + +### 2. Use Webhooks Instead of Polling + +Instead of polling for changes: +```javascript +// DON'T: Poll every minute +setInterval(async () => { + const meetings = await getMeetings(); // Uses API quota +}, 60000); +``` + +Use webhooks: +```javascript +// DO: Receive webhook events +app.post('/webhook', (req, res) => { + const event = req.body; + if (event.event === 'meeting.started') { + handleMeetingStarted(event.payload); + } + res.status(200).send(); +}); +``` + +### 3. Batch Operations + +Use list endpoints with pagination instead of individual fetches: +```javascript +// DON'T: Fetch users one by one +for (const userId of userIds) { + const user = await getUser(userId); // N API calls +} + +// DO: Fetch users in batches +const users = await listUsers({ page_size: 300 }); // 1 API call +``` + +### 4. Use QSS for Quality Data + +For Quality of Service data, use QSS (push-based) instead of polling: +- QSS streams telemetry via webhooks/WebSocket +- Pushes data 4-6 times per minute +- Reduces need for Reports API polling + +### 5. Distribute Requests Over Time + +```javascript +// DON'T: Burst all requests at once +await Promise.all(users.map(u => updateUser(u))); // May hit rate limit + +// DO: Distribute over time +for (const user of users) { + await updateUser(user); + await sleep(100); // 100ms between requests +} +``` + +## Account Type Notes + +### Rate Limits Are Per-Account + +- Limits are shared by ALL users and ALL apps on the account +- Upgrading account increases limits for everyone +- Not per-app - one heavy app can impact others + +### Business+ Includes + +- Business +- Education +- Enterprise +- Partners + +### Video SDK Accounts + +| Plan | Uses Limits | +|------|-------------| +| Pay As You Go (Deprecated) | Pro | +| Annual Prepay Monthly Usage | Pro | +| All other plans | Business+ | + +## Event Subscription Limits + +- **Maximum 10 event subscriptions** per application +- **No limit** on events per subscription +- Event subscription API has **Heavy** rate limit +- WebSocket: Only 1 subscription connection at a time (new connection closes previous) + +## Common Gotchas + +| Issue | Solution | +|-------|----------| +| 429 on first request of the day | Another app on account used quota | +| Different limits than documented | Check account type (Free/Pro/Business+) | +| Meeting create fails at 100/day | Per-user limit - use different host | +| Concurrent DELETE errors | Serialize DELETE operations on same user | +| Daily limit hit unexpectedly | Heavy + Resource-Intensive share quota | + +## Resources + +- **Rate limits docs**: https://developers.zoom.us/docs/api/rest/rate-limits/ +- **QSS docs**: https://developers.zoom.us/docs/api/rest/qss-api/ +- **Webhooks docs**: https://developers.zoom.us/docs/api/rest/webhook-reference/ diff --git a/plugins/zoom-developers/skills/rest-api/references/recordings.md b/plugins/zoom-developers/skills/rest-api/references/recordings.md new file mode 100644 index 00000000..19c8ce18 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/recordings.md @@ -0,0 +1,70 @@ +# REST API - Recordings + +Cloud recording management endpoints. + +## Overview + +Access and manage Zoom cloud recordings. + +## Endpoints + +### List User Recordings + +```bash +GET /users/{userId}/recordings +``` + +Query parameters: +- `from` - Start date (YYYY-MM-DD) +- `to` - End date (YYYY-MM-DD) + +### Get Meeting Recordings + +```bash +GET /meetings/{meetingId}/recordings +``` + +### Delete Recordings + +```bash +DELETE /meetings/{meetingId}/recordings +``` + +### Delete Single Recording File + +```bash +DELETE /meetings/{meetingId}/recordings/{recordingId} +``` + +## Recording Files + +Response includes multiple file types: + +| Type | Description | +|------|-------------| +| `shared_screen_with_speaker_view` | Screen share + speaker | +| `shared_screen_with_gallery_view` | Screen share + gallery | +| `active_speaker` | Speaker view only | +| `gallery_view` | Gallery view only | +| `audio_only` | Audio file (M4A) | +| `chat_file` | Chat transcript | +| `timeline` | Meeting timeline | +| `audio_transcript` | VTT transcript | + +## Download Recordings + +```bash +# Get download URL from recording files +GET {download_url}?access_token={token} +``` + +**Note:** Download URLs require authentication. + +## Required Scopes + +- `recording:read` - View/download recordings +- `recording:write` - Delete recordings + +## Resources + +- **API Reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Cloud-Recording diff --git a/plugins/zoom-developers/skills/rest-api/references/reports.md b/plugins/zoom-developers/skills/rest-api/references/reports.md new file mode 100644 index 00000000..013127a8 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/reports.md @@ -0,0 +1,86 @@ +# REST API - Reports + +Usage reports and analytics endpoints. + +## Overview + +Access meeting statistics, usage reports, and analytics data. + +## Endpoints + +### Daily Usage Report + +```bash +GET /report/daily +``` + +Query parameters: +- `year` - Year (required) +- `month` - Month (required) + +### Meeting Participants Report + +```bash +GET /report/meetings/{meetingId}/participants +``` + +### Meeting Details Report + +```bash +GET /report/meetings/{meetingId} +``` + +### Webinar Participants Report + +```bash +GET /report/webinars/{webinarId}/participants +``` + +### Active/Inactive Host Report + +```bash +GET /report/users +``` + +## Response Data + +### Daily Usage + +```json +{ + "dates": [ + { + "date": "2024-01-15", + "new_users": 5, + "meetings": 25, + "participants": 150, + "meeting_minutes": 3600 + } + ] +} +``` + +### Meeting Participants + +```json +{ + "participants": [ + { + "id": "user_id", + "name": "User Name", + "user_email": "user@example.com", + "join_time": "2024-01-15T10:00:00Z", + "leave_time": "2024-01-15T11:00:00Z", + "duration": 3600 + } + ] +} +``` + +## Required Scopes + +- `report:read` - View reports + +## Resources + +- **API Reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Reports diff --git a/plugins/zoom-developers/skills/rest-api/references/revenue-accelerator.md b/plugins/zoom-developers/skills/rest-api/references/revenue-accelerator.md new file mode 100644 index 00000000..1d0d913c --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/revenue-accelerator.md @@ -0,0 +1,141 @@ +# Zoom Revenue Accelerator API + +Authoritative endpoint inventory for Revenue Accelerator. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/iq/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 75 | +| Path templates | 51 | +| Tags | 6 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Accounts | 2 | +| Conversations | 34 | +| CRM | 12 | +| Deals | 8 | +| ScheduleMeetings | 1 | +| Teams | 18 | + +## Endpoints by Tag + +### Accounts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/iq/settings/indicators` | Get indicators settings [Deprecated] | `accountSettingsIndicatorsDeprecated` | +| GET | `/zra/settings/indicators` | Get indicators settings | `accountIndicatorsSettings` | + +### Conversations + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/iq/conversations` | List conversations [Deprecated] | `listConversationsDeprecated` | +| POST | `/iq/conversations` | Add conversation by file id or download url. [Deprecated] | `AddConversationByFileIdOrDownloadUrlDeprecated` | +| DELETE | `/iq/conversations/{conversationId}` | Delete conversation by conversation ID [Deprecated] | `deleteConversationDeprecated` | +| GET | `/iq/conversations/{conversationId}` | Get conversation information [Deprecated] | `getConversationInfoDeprecated` | +| GET | `/iq/conversations/{conversationId}/comments` | Get conversation comments [Deprecated] | `getConversationCommentsDeprecated` | +| POST | `/iq/conversations/{conversationId}/comments` | Add new comments to the conversation [Deprecated] | `addConversationCommentDeprecated` | +| DELETE | `/iq/conversations/{conversationId}/comments/{commentId}` | Delete conversation's comment [Deprecated] | `deleteConversationCommentDeprecated` | +| PATCH | `/iq/conversations/{conversationId}/comments/{commentId}` | Edit conversation comment [Deprecated] | `editConversationCommentDeprecated` | +| GET | `/iq/conversations/{conversationId}/content_analysis` | Get conversation content analysis [Deprecated] | `getConversationContentAnalysisDeprecated` | +| GET | `/iq/conversations/{conversationId}/interactions` | Get conversation interactions [Deprecated] | `getConversationInteractionsDeprecated` | +| GET | `/iq/conversations/{conversationId}/scorecards` | Get conversation scorecards [Deprecated] | `getConversationScorecardsDeprecated` | +| PATCH | `/iq/conversations/{conversationId}/update_host` | Update conversation host id to new host id by conversation id [Deprecated] | `UpdateconversationhostidtonewhostidbyconversationidDeprecated` | +| POST | `/iq/files` | Upload IQ file [Deprecated] | `UploadIQFileDeprecated` | +| POST | `/iq/files/multipart` | Upload iq multipart file. [Deprecated] | `UploadIqMultipartFileDeprecated` | +| POST | `/iq/files/multipart/upload_events` | Initiate and complete a multipart upload. [Deprecated] | `InitiateAndCompleteAMultipartUpload. [Deprecated]` | +| POST | `/iq/users/{userId}/conversations` | Add conversation by meeting record url or meeting UUID. [Deprecated] | `addConversationDeprecated` | +| GET | `/iq/users/{userId}/conversations/playlists` | Get a user's playlist [Deprecated] | `getUserPlaylistDeprecated` | +| GET | `/zra/conversations` | List conversations | `listAllConversations` | +| POST | `/zra/conversations` | Add conversation by file id or download url. | `AddConversationByFileIdOrDownloadUrl` | +| DELETE | `/zra/conversations/{conversationId}` | Delete conversation by conversation ID | `deleteConversationById` | +| GET | `/zra/conversations/{conversationId}` | Get conversation information | `getConversationDetail` | +| GET | `/zra/conversations/{conversationId}/comments` | Get conversation comments | `getConversationCommentsById` | +| POST | `/zra/conversations/{conversationId}/comments` | Add new comments to the conversation | `addConversationComments` | +| DELETE | `/zra/conversations/{conversationId}/comments/{commentId}` | Delete conversation's comment | `deleteConversationCommentById` | +| PATCH | `/zra/conversations/{conversationId}/comments/{commentId}` | Edit conversation comment | `editConversationCommentById` | +| GET | `/zra/conversations/{conversationId}/content_analysis` | Get conversation content analysis | `getConversationContentAnalysisById` | +| GET | `/zra/conversations/{conversationId}/interactions` | Get conversation interactions | `getConversationInteraction` | +| GET | `/zra/conversations/{conversationId}/scorecards` | Get conversation scorecards | `getConversationScorecardsById` | +| PATCH | `/zra/conversations/{conversationId}/update_host` | Update conversation host id | `UpdateConversationhostid2NewHostidByConversationId` | +| POST | `/zra/files` | Upload IQ file | `UploadZraFile` | +| POST | `/zra/files/multipart` | Upload iq multipart file. | `UploadZraMultipartFile.` | +| POST | `/zra/files/multipart/upload_events` | Initiate and complete a multipart upload. | `InitiateAndCompleteAMultipartUpload` | +| POST | `/zra/users/{userId}/conversations` | Add conversation by meeting record url or meeting UUID. | `addConversationByRecord` | +| GET | `/zra/users/{userId}/conversations/playlists` | Get a user's playlist | `getUserPlaylists` | + +### CRM + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zra/crm/accounts` | Get CRM accounts | `getCrmAccounts` | +| POST | `/zra/crm/accounts` | Bulk import CRM accounts | `bulkImportAccounts` | +| GET | `/zra/crm/contacts` | Get CRM contacts | `getCrmContacts` | +| POST | `/zra/crm/contacts` | Bulk import CRM contacts | `bulkImportContacts` | +| GET | `/zra/crm/deals` | Get CRM deals | `getCrmDeals` | +| POST | `/zra/crm/deals` | Bulk import CRM deals | `bulkImportDeals` | +| GET | `/zra/crm/leads` | Get CRM leads | `getCrmLeads` | +| POST | `/zra/crm/leads` | Bulk import CRM leads | `bulkImportLeads` | +| DELETE | `/zra/crm/settings` | Unregister CRM API connection | `unregisterCrm` | +| GET | `/zra/crm/settings` | Get current CRM API registration | `getCrmRegistration` | +| POST | `/zra/crm/settings` | Register CRM API connection | `registerCrm` | +| GET | `/zra/crm/tasks/{taskId}` | Poll async task result | `getAsyncTask` | + +### Deals + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/iq/deals` | List deals [Deprecated] | `listDealsDeprecated` | +| GET | `/iq/deals/{dealId}` | Get deal information [Deprecated] | `getDealInfoDeprecated` | +| DELETE | `/iq/deals/{dealId}/activities` | Delete activity from the deal [Deprecated] | `DeleteActivityFromTheDealDeprecated` | +| GET | `/iq/deals/{dealId}/activities` | Get deal activities [Deprecated] | `getDealActivitiesDeprecated` | +| GET | `/zra/deals` | List deals | `listAllDeals` | +| GET | `/zra/deals/{dealId}` | Get deal information | `getDealDetail` | +| DELETE | `/zra/deals/{dealId}/activities` | Delete activity from the deal | `DeleteActivityFromDeal` | +| GET | `/zra/deals/{dealId}/activities` | Get deal activities | `geAllActivitiesFromDeal` | + +### ScheduleMeetings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/zra/scheduled` | List scheduled meetings | `Listallscheduledmeetings` | + +### Teams + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/iq/teams` | List Account Teams [Deprecated] | `ListAccountTeamsDeprecated` | +| POST | `/zra/team` | Create Team | `CreateTeam` | +| GET | `/zra/team/unassigned_team_users` | List Unassigned Team Users | `ListUnassignedTeamUsers` | +| DELETE | `/zra/team/{teamId}` | Delete Team | `DeleteTeam` | +| GET | `/zra/team/{teamId}` | Get Team Detail | `GetTeamDetail` | +| PATCH | `/zra/team/{teamId}` | Update Team name | `UpdateTeam` | +| DELETE | `/zra/team/{teamId}/access/granted-from` | Remove additional access from current team | `DeleteSharedFromTeams` | +| POST | `/zra/team/{teamId}/access/granted-from` | Grant additional access to current team | `AddSharedFromTeams` | +| DELETE | `/zra/team/{teamId}/access/granted-to` | Remove additional access from target teams | `DeleteSharedToTeams` | +| POST | `/zra/team/{teamId}/access/granted-to` | Grant additional access to target teams | `AddSharedToTeams` | +| PATCH | `/zra/team/{teamId}/parent_team` | Move team to new parent | `MoveTeam` | +| DELETE | `/zra/team/{teamId}/team_managers` | Unassign Team Managers | `UnassignTeamManagers` | +| GET | `/zra/team/{teamId}/team_managers` | List Team Managers | `ListTeamManagers` | +| POST | `/zra/team/{teamId}/team_managers` | Assign Team Managers | `AssignTeamManagers` | +| DELETE | `/zra/team/{teamId}/team_members` | Unassign Team Members | `UnassignTeamMembers` | +| GET | `/zra/team/{teamId}/team_members` | List Team Members | `ListTeamMembers` | +| POST | `/zra/team/{teamId}/team_members` | Assign Team Members | `AssignTeamMembers` | +| GET | `/zra/teams` | List Account Teams | `ListTeams` | diff --git a/plugins/zoom-developers/skills/rest-api/references/rooms.md b/plugins/zoom-developers/skills/rest-api/references/rooms.md new file mode 100644 index 00000000..e2eedbcb --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/rooms.md @@ -0,0 +1,211 @@ +# Zoom Rooms API + +Authoritative endpoint inventory for Rooms. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/rooms/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 121 | +| Path templates | 70 | +| Tags | 10 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Room Apps | 1 | +| Visitor Management | 6 | +| Workspaces | 33 | +| Zoom Rooms | 20 | +| Zoom Rooms Account | 4 | +| Zoom Rooms Calendar | 7 | +| Zoom Rooms Content | 31 | +| Zoom Rooms Devices | 2 | +| Zoom Rooms Location | 10 | +| Zoom Rooms Tags | 7 | + +## Endpoints by Tag + +### Room Apps + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/rooms/controller/apps/config` | Config Zoom Room Controller Apps | `ConfigZoomRoomControllerApps` | + +### Visitor Management + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/visitor/invitation` | Get a list of visitors by location | `invitationList` | +| POST | `/visitor/invitation` | Send an invitation | `createInvitation` | +| DELETE | `/visitor/invitation/{invitationId}` | Delete an Invitation | `deleteInvitation` | +| GET | `/visitor/invitation/{invitationId}` | Invitation details by invitationID | `getInvitation` | +| PATCH | `/visitor/invitation/{invitationId}` | Update an invitation | `updateInvitation` | +| POST | `/visitor/invitation/{invitationId}/checkin` | Check in a visitor | `checkinVisitor` | + +### Workspaces + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/workspaces` | List workspaces | `listWorkspaces` | +| POST | `/workspaces` | Create a workspace | `createWorkspace` | +| GET | `/workspaces/additional_informations` | List workspace additional information with time range | `Getaworkspaceadditionalenhancements` | +| GET | `/workspaces/assets` | Get all workspace assets | `Getallworkspaceassets` | +| POST | `/workspaces/assets` | Create a workspace asset | `AddaWorkspaceasset` | +| DELETE | `/workspaces/assets/{assetId}` | Delete a workspace asset | `DeleteaWorkspaceasset` | +| GET | `/workspaces/assets/{assetId}` | Get a workspace asset | `get_workspace_asset` | +| PATCH | `/workspaces/assets/{assetId}` | Edit a workspace asset | `PatchWorkspaceasset` | +| POST | `/workspaces/events` | Check in/out of a reservation | `reservationEvent` | +| POST | `/workspaces/floormap/files` | Add or Update a Workspace floor map | `AddOrUpdateAWorkspaceFloorMap` | +| GET | `/workspaces/locations/{locationId}/floor_map` | Get a workspace location floor map | `Getlocationfloormap` | +| GET | `/workspaces/released_workspaces_by_timeout` | List released workspaces by timeout | `getWorksapceReservationReleaseInof` | +| PATCH | `/workspaces/settings` | Update workspace settings | `updateWorkspaceSettings` | +| DELETE | `/workspaces/settings/photos` | Delete workspace photos | `DeleteaWorkspacephoto` | +| POST | `/workspaces/settings/photos` | Add a workspace photo | `AddaWorkspacephoto` | +| GET | `/workspaces/usage` | Get a location's hot desk usage | `getHotDeskUsage` | +| GET | `/workspaces/users/{userId}/calendar/settings` | Get Workspace Calendar Free/Busy Event | `GetWorkspaceCalendarFree/BusyEvent` | +| POST | `/workspaces/users/{userId}/calendar/settings` | Set Workspace Calendar Free/Busy Event | `SetCalendarFree/BusyEvent` | +| GET | `/workspaces/users/{userId}/reservations` | Get a user's workspace's reservations | `userListReservations` | +| DELETE | `/workspaces/{locationId}/background` | Delete Workspace floor map | `DeleteWorkspaceFloorMap` | +| DELETE | `/workspaces/{workspaceId}` | Delete a workspace | `deleteWorkspace` | +| GET | `/workspaces/{workspaceId}` | Get a workspace | `getWorkspace` | +| PATCH | `/workspaces/{workspaceId}` | Update a workspace | `updateWorkspace` | +| DELETE | `/workspaces/{workspaceId}/assignment` | Delete a desk assignment | `Deleteadeskassignment` | +| GET | `/workspaces/{workspaceId}/assignment` | Get a desk assignment | `Getadeskassignment` | +| PUT | `/workspaces/{workspaceId}/assignment` | Set a desk assignment | `setADeskAssignment` | +| GET | `/workspaces/{workspaceId}/qr_code` | Get a workspace QR code | `getWorkspaceQRCode` | +| GET | `/workspaces/{workspaceId}/reservations` | Get a workspace's reservations | `listReservations` | +| POST | `/workspaces/{workspaceId}/reservations` | Create a reservation | `createReservation` | +| DELETE | `/workspaces/{workspaceId}/reservations/{reservationId}` | Delete a reservation | `deleteReservation` | +| GET | `/workspaces/{workspaceId}/reservations/{reservationId}` | Get a workspace reservation by reservationId | `GETGetaworkspacereservationbyreservationID` | +| PATCH | `/workspaces/{workspaceId}/reservations/{reservationId}` | Update a reservation | `updateReservation` | +| GET | `/workspaces/{workspaceId}/reservations/{reservationId}/questionnaires` | List workspace reservation questionnaires | `Listworkspacereservationquestionnaires` | + +### Zoom Rooms + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/rooms` | List Zoom Rooms | `listZoomRooms` | +| POST | `/rooms` | Add a Zoom Room | `addARoom` | +| GET | `/rooms/digital_signage` | List digital signage contents | `listDigitalSignageContent` | +| PATCH | `/rooms/events` | Update E911 digital signage | `manageE911signage` | +| PATCH | `/rooms/{id}/events` | Use Zoom Room controls | `ZoomRoomsControls` | +| GET | `/rooms/{id}/settings` | Get Zoom Room settings | `getZRSettings` | +| PATCH | `/rooms/{id}/settings` | Update Zoom Room settings | `updateZRSettings` | +| DELETE | `/rooms/{roomId}` | Delete a Zoom Room | `deleteAZoomRoom` | +| GET | `/rooms/{roomId}` | Get Zoom Room profile | `getZRProfile` | +| PATCH | `/rooms/{roomId}` | Update a Zoom Room profile | `updateRoomProfile` | +| GET | `/rooms/{roomId}/device_profiles` | List device profiles | `getRoomProfiles` | +| POST | `/rooms/{roomId}/device_profiles` | Create a device profile | `createRoomDeviceProfile` | +| GET | `/rooms/{roomId}/device_profiles/devices` | Get device information | `getRoomDevices` | +| DELETE | `/rooms/{roomId}/device_profiles/{deviceProfileId}` | Delete a device profile | `deleteRoomProfile` | +| GET | `/rooms/{roomId}/device_profiles/{deviceProfileId}` | Get a device profile | `getRoomProfile` | +| PATCH | `/rooms/{roomId}/device_profiles/{deviceProfileId}` | Update a device profile | `updateDeviceProfile` | +| GET | `/rooms/{roomId}/devices` | List Zoom Room devices | `listZRDevices` | +| PUT | `/rooms/{roomId}/location` | Change a Zoom Room's location | `changeZRLocation` | +| GET | `/rooms/{roomId}/sensor_data` | Get Zoom Room sensor data | `getZRSensorData` | +| GET | `/rooms/{roomId}/virtual_controller` | Get Zoom Rooms virtual controller URL | `getWebzrcUrl` | + +### Zoom Rooms Account + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/rooms/account_profile` | Get Zoom Room account profile | `getZRAccountProfile` | +| PATCH | `/rooms/account_profile` | Update Zoom Room account profile | `updateZRAccProfile` | +| GET | `/rooms/account_settings` | Get Zoom Room account settings | `getZRAccountSettings` | +| PATCH | `/rooms/account_settings` | Update Zoom Room account settings | `updateZoomRoomAccSettings` | + +### Zoom Rooms Calendar + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/rooms/calendar/services` | List calendar services | `getCalendarServices` | +| DELETE | `/rooms/calendar/services/{serviceId}` | Delete a calendar service | `deleteACalendarService` | +| GET | `/rooms/calendar/services/{serviceId}/resources` | List calendar resources by calendar service | `getCalendarResourcesByServiceId` | +| POST | `/rooms/calendar/services/{serviceId}/resources` | Add a calendar resource to a calendar service | `addACalendarResourceToCalendarService` | +| DELETE | `/rooms/calendar/services/{serviceId}/resources/{resourceId}` | Delete a calendar resource | `deleteACalendarResource` | +| GET | `/rooms/calendar/services/{serviceId}/resources/{resourceId}` | Get a calendar resource by ID | `getCalendarResourceById` | +| PUT | `/rooms/calendar/services/{serviceId}/sync` | Start calendar service sync process | `syncACalendarService` | + +### Zoom Rooms Content + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/rooms/content/background/contents` | List Zoom Rooms background image library contents | `ListZoomRoomsbackgroundimagelibrarycontents` | +| DELETE | `/rooms/content/background/contents/{contentId}` | Delete Zoom Rooms Background Image Library Content | `DeleteZoomRoomsBackgroundImageLibraryContent` | +| GET | `/rooms/content/background/contents/{contentId}` | Get Zoom Rooms background image library content | `GetZoomRoomsBackgroundImageLibraryContent` | +| GET | `/rooms/content/background/defaults` | List default Zoom Rooms background image library contents | `ListDefaultZoomRoomsBackgroundImageLibrarycontents` | +| GET | `/rooms/content/background/folders` | List Zoom Rooms Background Image Library Folders | `ListZoomRoomsBackgroundLibraryFolders` | +| POST | `/rooms/content/background/folders` | Add Zoom Rooms Background Image Library Folder | `AddZoomRoomsBackgroundLibraryFolder` | +| DELETE | `/rooms/content/background/folders/{folderId}` | Delete Zoom Rooms Background Image Library Folder | `DeleteZoomRoomsBackgroundLibraryFolder` | +| GET | `/rooms/content/background/folders/{folderId}` | Get Zoom Rooms Background Image Library Folder | `GetZoomRoomsBackgroundLibraryFolder` | +| PATCH | `/rooms/content/background/folders/{folderId}` | Update Zoom Rooms Background Image Library Folder | `UpdateaZoomRoomsBackgroundLibraryFolderName` | +| GET | `/rooms/content/digital_signage/contents` | List Digital Signage content items | `GETListdigitalsignagecontentitems` | +| POST | `/rooms/content/digital_signage/contents` | Add a digital signage URL | `AddadigitalsignageURL` | +| DELETE | `/rooms/content/digital_signage/contents/{contentId}` | Delete a Digital Signage content item | `Deleteadigitalsignagecontentitem` | +| GET | `/rooms/content/digital_signage/contents/{contentId}` | Get Digital Signage content item | `Getdigitalsignagecontentitem` | +| PATCH | `/rooms/content/digital_signage/contents/{contentId}` | Update a Digital Signage content item attributes | `Updateadigitalsignagecontentitemattributes` | +| POST | `/rooms/content/digital_signage/folders` | Add a digital signage content folder | `Addadigitalsignagecontentfolder` | +| DELETE | `/rooms/content/digital_signage/folders/{folderId}` | Delete a Digital Signage content folder | `Deleteadigitalsignagecontentfolder` | +| GET | `/rooms/content/digital_signage/folders/{folderId}` | Get Digital Signage content folder | `Getdigitalsignagecontentfolderdetails` | +| PATCH | `/rooms/content/digital_signage/folders/{folderId}` | Update a digital signage content folder | `Updateadigitalsignagecontentfolder` | +| GET | `/rooms/content/digital_signage/playlists` | List Digital Signage library playlists | `ListDigitalSignagelibraryplaylists` | +| POST | `/rooms/content/digital_signage/playlists` | Add a Digital Signage library playlist | `AddaDigitalSignagelibraryplaylist` | +| DELETE | `/rooms/content/digital_signage/playlists/{playlistId}` | Delete Digital Signage library playlist | `DeleteDigitalSignagelibraryplaylist` | +| GET | `/rooms/content/digital_signage/playlists/{playlistId}` | Get Digital Signage library playlist | `GetDigitalSignagelibraryplaylist` | +| PATCH | `/rooms/content/digital_signage/playlists/{playlistId}` | Update a Digital Signage library playlist | `UpdateaDigitalSignagelibraryplaylist` | +| GET | `/rooms/content/digital_signage/playlists/{playlistId}/contents` | Get Digital Signage library playlist content items | `GetDigitalSignagelibraryplaylistcontentitems` | +| PUT | `/rooms/content/digital_signage/playlists/{playlistId}/contents` | Update Digital Signage library playlist content items | `UpdateDigitalSignagelibraryplaylistcontentitems` | +| GET | `/rooms/content/digital_signage/playlists/{playlistId}/rooms` | List Digital Signage library playlist published rooms | `ListDigitalSignagelibraryplaylistpublishedrooms` | +| PUT | `/rooms/content/digital_signage/playlists/{playlistId}/rooms` | Update Digital Signage library playlist published rooms | `UpdateDigitalSignagelibraryplaylistpublishedrooms` | +| POST | `/zrbackground/files` | Add Zoom Rooms background image library content | `AddZoomRoomsBackgroundImageLibraryContent` | +| PUT | `/zrbackground/files` | Update Zoom Rooms background image library content | `UpdateZoomRoomsBackgroundImageLibraryContent` | +| POST | `/zrdigitalsignage/files` | Add a digital signage image or video | `Adddigitalsignageimageorvideo` | +| PUT | `/zrdigitalsignage/files` | Update a Digital Signage image or video file | `Updateadigitalsignageimageorvideofile` | + +### Zoom Rooms Devices + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/rooms/{roomId}/devices/{deviceId}` | Delete a Zoom Room user device | `deleteDevice` | +| PUT | `/rooms/{roomId}/devices/{deviceId}/app_version` | Change Zoom Rooms app version | `changeZoomRoomsAppVersion` | + +### Zoom Rooms Location + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/rooms/locations` | List Zoom Room locations | `listZRLocations` | +| POST | `/rooms/locations` | Add a location | `addAZRLocation` | +| GET | `/rooms/locations/structure` | Get Zoom Room location structure | `getZRLocationStructure` | +| PUT | `/rooms/locations/structure` | Update Zoom Rooms location structure | `updateZoomRoomsLocationStructure` | +| DELETE | `/rooms/locations/{locationId}` | Delete a location | `deleteAZRLocation` | +| GET | `/rooms/locations/{locationId}` | Get Zoom Room location profile | `getZRLocationProfile` | +| PATCH | `/rooms/locations/{locationId}` | Update Zoom Room location profile | `updateZRLocationProfile` | +| PUT | `/rooms/locations/{locationId}/location` | Change the assigned parent location | `changeParentLocation` | +| GET | `/rooms/locations/{locationId}/settings` | Get location settings | `getZRLocationSettings` | +| PATCH | `/rooms/locations/{locationId}/settings` | Update location settings | `updateZRLocationSettings` | + +### Zoom Rooms Tags + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PATCH | `/rooms/locations/{locationId}/tags` | Assign Tags to Zoom Rooms By Location ID | `AssignTagsToZoomRoomsByLocationID` | +| GET | `/rooms/tags` | List all Zoom Room Tags | `listZoomRoomTags` | +| POST | `/rooms/tags` | Create a new Zoom Rooms Tag | `createZoomRoomTag` | +| DELETE | `/rooms/tags/{tagId}` | Delete Tag | `deleteZoomRoomTag` | +| PATCH | `/rooms/tags/{tagId}` | Edit Tag | `editZoomRoomTag` | +| DELETE | `/rooms/{roomId}/tags` | Un-assign Tags from a Zoom Room | `unassignZoomRoomTag` | +| PATCH | `/rooms/{roomId}/tags` | Assign Tags to a Zoom Room | `AssignTagsToAZoomRoom` | diff --git a/plugins/zoom-developers/skills/rest-api/references/scheduler.md b/plugins/zoom-developers/skills/rest-api/references/scheduler.md new file mode 100644 index 00000000..b7a165ef --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/scheduler.md @@ -0,0 +1,105 @@ +# Zoom Scheduler API + +Authoritative endpoint inventory for Scheduler. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/scheduler/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 21 | +| Path templates | 13 | +| Tags | 9 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| analytics | 1 | +| availability | 5 | +| Routing Forms | 1 | +| scheduled events | 5 | +| schedules | 5 | +| scheduling links | 1 | +| shares | 1 | +| team | 1 | +| users | 1 | + +## Endpoints by Tag + +### analytics + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scheduler/analytics` | Report analytics | `report_analytics` | + +### availability + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scheduler/availability` | List availability | `list_availability` | +| POST | `/scheduler/availability` | Insert availability | `insert_availability` | +| DELETE | `/scheduler/availability/{availabilityId}` | Delete availability | `delete_availability` | +| GET | `/scheduler/availability/{availabilityId}` | Get availability | `get_availability` | +| PATCH | `/scheduler/availability/{availabilityId}` | Patch availability | `patch_availability` | + +### Routing Forms + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scheduler/routing/forms/{formId}/response/{responseId}` | get routing response | `Getroutingresponse` | + +### scheduled events + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scheduler/events` | List scheduled events | `list_scheduled_events` | +| DELETE | `/scheduler/events/{eventId}` | Delete scheduled events | `delete_scheduled_events` | +| GET | `/scheduler/events/{eventId}` | Get scheduled events | `get_scheduled_events` | +| PATCH | `/scheduler/events/{eventId}` | Patch scheduled events | `patch_scheduled_events` | +| GET | `/scheduler/events/{eventId}/attendees/{attendeeId}` | Get scheduled event attendee | `get_scheduled_event_attendee` | + +### schedules + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scheduler/schedules` | List schedules | `list_schedules` | +| POST | `/scheduler/schedules` | Insert schedules | `insert_schedule` | +| DELETE | `/scheduler/schedules/{scheduleId}` | Delete schedules | `delete_schedules` | +| GET | `/scheduler/schedules/{scheduleId}` | Get schedules | `get_schedule` | +| PATCH | `/scheduler/schedules/{scheduleId}` | Patch schedules | `patch_schedule` | + +### scheduling links + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/scheduler/schedules/single_use_link` | Single use link | `single_use_link` | + +### shares + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/scheduler/shares` | Create shares | `create_shares` | + +### team + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scheduler/teams` | List team | `Listteam` | + +### users + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scheduler/users/{userId}` | Get user | `get_user` | diff --git a/plugins/zoom-developers/skills/rest-api/references/scim2.md b/plugins/zoom-developers/skills/rest-api/references/scim2.md new file mode 100644 index 00000000..09fe3236 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/scim2.md @@ -0,0 +1,53 @@ +# Zoom SCIM2 API + +Authoritative endpoint inventory for SCIM2. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/scim2/methods/endpoints.json +- Base URL: `https://api.zoom.us` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 11 | +| Path templates | 4 | +| Tags | 2 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Group | 5 | +| User | 6 | + +## Endpoints by Tag + +### Group + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scim2/Groups` | List groups | `groupSCIM2List` | +| POST | `/scim2/Groups` | Create a group | `groupScim2Create` | +| DELETE | `/scim2/Groups/{groupId}` | Delete a group | `groupSCIM2Delete` | +| GET | `/scim2/Groups/{groupId}` | Get a group | `groupSCIM2Get` | +| PATCH | `/scim2/Groups/{groupId}` | Update a group | `groupSCIM2Update` | + +### User + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/scim2/Users` | List users | `userSCIM2List` | +| POST | `/scim2/Users` | Create a user | `userScim2Create` | +| DELETE | `/scim2/Users/{userId}` | Delete a user | `userSCIM2Delete` | +| GET | `/scim2/Users/{userId}` | Get a user | `userSCIM2Get` | +| PATCH | `/scim2/Users/{userId}` | Deactivate a user | `userADSCIM2Deactivate` | +| PUT | `/scim2/Users/{userId}` | Update a user | `userSCIM2Update` | diff --git a/plugins/zoom-developers/skills/rest-api/references/tasks.md b/plugins/zoom-developers/skills/rest-api/references/tasks.md new file mode 100644 index 00000000..6f939448 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/tasks.md @@ -0,0 +1,68 @@ +# Zoom Tasks API + +Authoritative endpoint inventory for Tasks. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/tasks/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 14 | +| Path templates | 8 | +| Tags | 4 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Assignee | 3 | +| Collaborator | 3 | +| Comment | 3 | +| Tasks | 5 | + +## Endpoints by Tag + +### Assignee + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/tasks/items/{taskId}/assignees` | Get assignees of a task | `GetAssigneesOfATask` | +| POST | `/tasks/items/{taskId}/assignees` | Add assignees to a task | `addTasksAssignees` | +| DELETE | `/tasks/items/{taskId}/assignees/{userId}` | Remove Assignee from task | `removeTaskAssignee` | + +### Collaborator + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/tasks/items/{taskId}/collaborators` | Get collaborators of a task | `Getcollaboratorsofatask` | +| POST | `/tasks/items/{taskId}/collaborators` | Add collaborators to a task | `addTasksCollaborators` | +| DELETE | `/tasks/items/{taskId}/collaborators/{userId}` | Remove collaborator from task | `removeTaskCollaborator` | + +### Comment + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/tasks/items/{taskId}/comments` | Get a task's comments | `GetAV1TasksComment` | +| POST | `/tasks/items/{taskId}/comments` | Add a comment to task | `addComment` | +| DELETE | `/tasks/items/{taskId}/comments/{commentId}` | Delete a task's comment | `DeleteTaskComment` | + +### Tasks + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/tasks/items` | List tasks | `getMyTasks` | +| POST | `/tasks/items` | Create a new task | `createTask` | +| DELETE | `/tasks/items/{taskId}` | Delete a task | `deleteTask` | +| GET | `/tasks/items/{taskId}` | Get task details | `getTaskDetail` | +| PATCH | `/tasks/items/{taskId}` | Update task fields | `updateTask` | diff --git a/plugins/zoom-developers/skills/rest-api/references/team-chat.md b/plugins/zoom-developers/skills/rest-api/references/team-chat.md new file mode 100644 index 00000000..16bafa90 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/team-chat.md @@ -0,0 +1,233 @@ +# Zoom Team Chat API + +Authoritative endpoint inventory for Team Chat. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/team-chat/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 107 | +| Path templates | 69 | +| Tags | 16 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Chat Channel Mention Group | 7 | +| Chat Channels | 16 | +| Chat Channels (Account-level) | 16 | +| Chat Emoji | 3 | +| Chat Files | 4 | +| Chat Messages | 15 | +| Chat Migration | 7 | +| Chat Reminder | 3 | +| Chat Sessions | 2 | +| Contacts | 3 | +| IM Chat | 1 | +| IM Groups | 8 | +| Invitations | 1 | +| Legal Hold | 6 | +| Reports | 2 | +| Shared Spaces | 13 | + +## Endpoints by Tag + +### Chat Channel Mention Group + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/chat/channels/{channelId}/mention_group` | List channel mention groups | `getChannelMentionGroup` | +| POST | `/chat/channels/{channelId}/mention_group` | Create a channel mention group | `createChannelMentionGroup` | +| DELETE | `/chat/channels/{channelId}/mention_group/{mentionGroupId}` | Delete a channel mention group | `deleteAChannelMentionGroup` | +| PATCH | `/chat/channels/{channelId}/mention_group/{mentionGroupId}` | Update a channel mention group information | `updateChannelMentionGroup` | +| DELETE | `/chat/channels/{channelId}/mention_group/{mentionGroupId}/members` | Remove channel mention group members | `removeChannelMentionGroupMembers` | +| GET | `/chat/channels/{channelId}/mention_group/{mentionGroupId}/members` | List the members of a mention group | `listTheMembersOfMentionGroup` | +| POST | `/chat/channels/{channelId}/mention_group/{mentionGroupId}/members` | Add channel members to a mention group | `addAChannelMembersToMentionGroup` | + +### Chat Channels + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/chat/activities/channels` | List channel activity logs | `listAllChannelActivityLogs` | +| PATCH | `/chat/channels/events` | Perform operations on channels | `PerformOperationsOnChannels` | +| DELETE | `/chat/channels/{channelId}` | Delete a channel | `deleteUserLevelChannel` | +| GET | `/chat/channels/{channelId}` | Get a channel | `getUserLevelChannel` | +| PATCH | `/chat/channels/{channelId}` | Update a channel | `updateUserLevelChannel` | +| DELETE | `/chat/channels/{channelId}/members` | Batch remove members from a channel | `batchRemoveChannelMembers` | +| GET | `/chat/channels/{channelId}/members` | List channel members | `listUserLevelChannelMembers` | +| POST | `/chat/channels/{channelId}/members` | Invite channel members | `InviteUserLevelChannelMembers` | +| GET | `/chat/channels/{channelId}/members/groups` | List channel members (Groups) | `listChannelMembersGroups` | +| POST | `/chat/channels/{channelId}/members/groups` | Invite channel members (Groups) | `inviteChannelMembersGroups` | +| DELETE | `/chat/channels/{channelId}/members/groups/{groupId}` | Remove a member (group) | `removeAMemberGroup` | +| DELETE | `/chat/channels/{channelId}/members/me` | Leave a channel | `leaveChannel` | +| POST | `/chat/channels/{channelId}/members/me` | Join a channel | `joinChannel` | +| DELETE | `/chat/channels/{channelId}/members/{identifier}` | Remove a member | `removeAUserLevelChannelMember` | +| GET | `/chat/users/{userId}/channels` | List user's channels | `getChannels` | +| POST | `/chat/users/{userId}/channels` | Create a channel | `createChannel` | + +### Chat Channels (Account-level) + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/chat/channels` | List account's public channels | `getAccountChannels` | +| POST | `/chat/channels/search` | Search user's or account's channels | `searchChannels` | +| GET | `/chat/channels/{channelId}/activities` | List channel activity logs | `listChannelActivityLogs` | +| GET | `/chat/channels/{channelId}/retention` | Get retention policy of a channel | `getChannelRetention` | +| PATCH | `/chat/channels/{channelId}/retention` | Update retention policy of a channel | `updateChannelRetention` | +| DELETE | `/chat/users/{userId}/channels` | Batch delete channels | `batchDeleteChannelsAccountLevel` | +| DELETE | `/chat/users/{userId}/channels/{channelId}` | Delete a channel | `deleteChannel` | +| GET | `/chat/users/{userId}/channels/{channelId}` | Get a channel | `getChannel` | +| PATCH | `/chat/users/{userId}/channels/{channelId}` | Update a channel | `updateChannel` | +| DELETE | `/chat/users/{userId}/channels/{channelId}/admins` | Batch demote channel administrators | `batchDemoteChannelAdministrators` | +| GET | `/chat/users/{userId}/channels/{channelId}/admins` | List channel administrators | `listChannelAdministrators` | +| POST | `/chat/users/{userId}/channels/{channelId}/admins` | Promote channel members to administrators | `promoteChannelMembersAsAdmin` | +| DELETE | `/chat/users/{userId}/channels/{channelId}/members` | Batch remove members from a user's channel | `batchRemoveUserChannelMembers` | +| GET | `/chat/users/{userId}/channels/{channelId}/members` | List channel members | `listChannelMembers` | +| POST | `/chat/users/{userId}/channels/{channelId}/members` | Invite channel members | `inviteChannelMembers` | +| DELETE | `/chat/users/{userId}/channels/{channelId}/members/{identifier}` | Remove a member | `removeAChannelMember` | + +### Chat Emoji + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/chat/emoji` | List custom emojis | `listCustomEmojis` | +| POST | `/chat/emoji/files` | Add a custom emoji | `addACustomEmoji` | +| DELETE | `/chat/emoji/{fileId}` | Delete a custom emoji | `DeleteCustomEmoji` | + +### Chat Files + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/chat/files/{fileId}` | Delete a chat file | `deleteChatFile` | +| GET | `/chat/files/{fileId}` | Get file info | `getFileInfo` | +| POST | `/chat/users/{userId}/files` | Upload a chat file | `uploadAChatFile` | +| POST | `/chat/users/{userId}/messages/files` | Send a chat file | `sendChatFile` | + +### Chat Messages + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PATCH | `/chat/channel/message/events` | Perform operations on the message of channel | `PerformMessageOfChannel` | +| GET | `/chat/channels/{channelId}/pinned` | List pinned history messages of channel | `listChannelPinnedMessages` | +| GET | `/chat/forwarded_message/{forwardId}` | Get a forwarded message | `getForwardedMessage` | +| GET | `/chat/messages/bookmarks` | List bookmarks | `fetchBookmarks` | +| PATCH | `/chat/messages/bookmarks` | Add or remove a bookmark | `addOrRemoveABookmark` | +| GET | `/chat/messages/schedule` | List scheduled messages | `listScheduledMessages` | +| DELETE | `/chat/messages/schedule/{draftId}` | Delete a scheduled message | `deleteScheduleMessage` | +| GET | `/chat/users/{userId}/messages` | List user's chat messages | `getChatMessages` | +| POST | `/chat/users/{userId}/messages` | Send a chat message | `sendaChatMessage` | +| DELETE | `/chat/users/{userId}/messages/{messageId}` | Delete a message | `deleteChatMessage` | +| GET | `/chat/users/{userId}/messages/{messageId}` | Get a message | `getChatMessage` | +| PUT | `/chat/users/{userId}/messages/{messageId}` | Update a message | `editMessage` | +| PATCH | `/chat/users/{userId}/messages/{messageId}/emoji_reactions` | React to a chat message | `reactMessage` | +| PATCH | `/chat/users/{userId}/messages/{messageId}/status` | Mark message read or unread | `markMessage` | +| GET | `/chat/users/{userId}/messages/{messageId}/thread` | Retrieve a thread | `retrieveThread` | + +### Chat Migration + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/chat/migration/channels/{channelId}/members` | Migrate channel members | `MigrateChannelMembers` | +| POST | `/chat/migration/emoji_reactions` | Migrate chat message reactions | `MigrateChatMessageReactions` | +| GET | `/chat/migration/mappings/channels` | Get migrated Zoom channel IDs | `getMigrationChannelsMapping` | +| GET | `/chat/migration/mappings/users` | Get migrated Zoom user IDs | `getMigrationUsersMapping` | +| POST | `/chat/migration/messages` | Migrate chat messages | `MigrateChatMessages` | +| POST | `/chat/migration/users/{identifier}/channels` | Migrate a chat channel | `MigrateAChatChannel` | +| POST | `/chat/migration/users/{identifier}/events` | Migrate 1:1 conversation or channel operations | `Migrate1:1ConversationOrChannelOperations` | + +### Chat Reminder + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/chat/messages/{messageId}/reminder` | Delete a reminder for a message | `deleteReminderForMessage` | +| POST | `/chat/messages/{messageId}/reminder` | Create a reminder message | `createReminderForMessage` | +| GET | `/chat/reminder` | List reminders | `listReminders` | + +### Chat Sessions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PATCH | `/chat/users/{userId}/events` | Star or unstar a channel or contact user | `starUnstarChannelContact` | +| GET | `/chat/users/{userId}/sessions` | List a user's chat sessions | `getChatSessions` | + +### Contacts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/chat/users/me/contacts` | List user's contacts | `getUserContacts` | +| GET | `/chat/users/me/contacts/{identifier}` | Get user's contact details | `getUserContact` | +| GET | `/contacts` | Search company contacts | `searchCompanyContacts` | + +### IM Chat + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/im/users/me/chat/messages` | Send IM messages | `sendimmessages` | + +### IM Groups + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/im/groups` | List IM directory groups | `imGroups` | +| POST | `/im/groups` | Create an IM directory group | `imGroupCreate` | +| DELETE | `/im/groups/{groupId}` | Delete an IM directory group | `imGroupDelete` | +| GET | `/im/groups/{groupId}` | Retrieve an IM directory group | `imGroup` | +| PATCH | `/im/groups/{groupId}` | Update an IM directory group | `imGroupUpdate` | +| GET | `/im/groups/{groupId}/members` | List IM directory group members | `imGroupMembers` | +| POST | `/im/groups/{groupId}/members` | Add IM directory group members | `imGroupMembersCreate` | +| DELETE | `/im/groups/{groupId}/members/{memberId}` | Delete IM directory group member | `imGroupMembersDelete` | + +### Invitations + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/chat/users/{userId}/invitations` | Send new contact invitation | `sendNewContactInvitation` | + +### Legal Hold + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/chat/legalhold/matters` | List legal hold matters | `listLegalHoldMatters` | +| POST | `/chat/legalhold/matters` | Add a legal hold matter | `addLegalHoldMatter` | +| DELETE | `/chat/legalhold/matters/{matterId}` | Delete legal hold matters | `deleteLegalHoldMatters` | +| PATCH | `/chat/legalhold/matters/{matterId}` | Update legal hold matter | `updateLegalHoldMatter` | +| GET | `/chat/legalhold/matters/{matterId}/files` | List legal hold files by given matter | `listLegalHoldFiles` | +| GET | `/chat/legalhold/matters/{matterId}/files/download` | Download legal hold files for given matter | `downloadLegalHoldFiles` | + +### Reports + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/report/chat/sessions` | Get chat sessions reports | `reportChatSessions` | +| GET | `/report/chat/sessions/{sessionId}` | Get chat message reports | `reportChatMessages` | + +### Shared Spaces + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/chat/spaces` | List shared spaces | `listSharedSpaces` | +| POST | `/chat/spaces` | Create a shared space | `createSpace` | +| DELETE | `/chat/spaces/{spaceId}` | Delete a shared space | `deleteSpace` | +| GET | `/chat/spaces/{spaceId}` | Get a shared space | `getASharedSpace` | +| PATCH | `/chat/spaces/{spaceId}` | Update shared space settings | `updateSharedSpaceSettings` | +| DELETE | `/chat/spaces/{spaceId}/admins` | Demote shared space administrators to members | `demoteSpaceAdmins` | +| POST | `/chat/spaces/{spaceId}/admins` | Promote shared space members to administrators | `promoteSpaceMembers` | +| GET | `/chat/spaces/{spaceId}/channels` | List shared space channels | `listSharedSpaceChannels` | +| PATCH | `/chat/spaces/{spaceId}/channels` | Move shared space channels | `updateSharedSpaceChannels` | +| DELETE | `/chat/spaces/{spaceId}/members` | Remove members from a shared space | `deleteSpaceMembers` | +| GET | `/chat/spaces/{spaceId}/members` | List shared space members | `listSharedSpaceMembers` | +| POST | `/chat/spaces/{spaceId}/members` | Add members to a shared space | `addSpaceMembers` | +| PATCH | `/chat/spaces/{spaceId}/owner` | Transfer shared space ownership | `transferSpaceOwner` | diff --git a/plugins/zoom-developers/skills/rest-api/references/users.md b/plugins/zoom-developers/skills/rest-api/references/users.md new file mode 100644 index 00000000..3424b891 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/users.md @@ -0,0 +1,125 @@ +# Zoom Users API + +Authoritative endpoint inventory for Users. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/users/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 71 | +| Path templates | 41 | +| Tags | 4 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Contact Groups | 8 | +| Divisions | 7 | +| Groups | 21 | +| Users | 35 | + +## Endpoints by Tag + +### Contact Groups + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/contacts/groups` | List contact groups | `contactGroups` | +| POST | `/contacts/groups` | Create a contact group | `contactGroupCreate` | +| DELETE | `/contacts/groups/{groupId}` | Delete a contact group | `contactGroupDelete` | +| GET | `/contacts/groups/{groupId}` | Get a contact group | `contactGroup` | +| PATCH | `/contacts/groups/{groupId}` | Update a contact group | `contactGroupUpdate` | +| DELETE | `/contacts/groups/{groupId}/members` | Remove members in a contact group | `contactGroupMemberRemove` | +| GET | `/contacts/groups/{groupId}/members` | List contact group members | `contactGroupMembers` | +| POST | `/contacts/groups/{groupId}/members` | Add contact group members | `contactGroupMemberAdd` | + +### Divisions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/divisions` | List divisions | `listDivisions` | +| POST | `/divisions` | Create a division | `Createadivision` | +| DELETE | `/divisions/{divisionId}` | Delete a division | `Deletedivision` | +| GET | `/divisions/{divisionId}` | Get a division | `Getdivision` | +| PATCH | `/divisions/{divisionId}` | Update a division | `Updateadivision` | +| GET | `/divisions/{divisionId}/users` | List division members | `listDivisionMembers` | +| POST | `/divisions/{divisionId}/users` | Assign a division | `assigndivisionMember` | + +### Groups + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/groups` | List groups | `groups` | +| POST | `/groups` | Create a group | `groupCreate` | +| DELETE | `/groups/{groupId}` | Delete a group | `groupDelete` | +| GET | `/groups/{groupId}` | Get a group | `group` | +| PATCH | `/groups/{groupId}` | Update a group | `groupUpdate` | +| GET | `/groups/{groupId}/admins` | List group admins | `groupAdmins` | +| POST | `/groups/{groupId}/admins` | Add group admins | `groupAdminsCreate` | +| DELETE | `/groups/{groupId}/admins/{userId}` | Delete a group admin | `groupAdminsDelete` | +| GET | `/groups/{groupId}/channels` | List group channels | `groupChannels` | +| GET | `/groups/{groupId}/lock_settings` | Get locked settings | `getGroupLockSettings` | +| PATCH | `/groups/{groupId}/lock_settings` | Update locked settings | `groupLockedSettings` | +| GET | `/groups/{groupId}/members` | List group members | `groupMembers` | +| POST | `/groups/{groupId}/members` | Add group members | `groupMembersCreate` | +| DELETE | `/groups/{groupId}/members/{memberId}` | Delete a group member | `groupMembersDelete` | +| PATCH | `/groups/{groupId}/members/{memberId}` | Update a group member | `updateAGroupMember` | +| GET | `/groups/{groupId}/settings` | Get a group's settings | `getGroupSettings` | +| PATCH | `/groups/{groupId}/settings` | Update a group's settings | `updateGroupSettings` | +| GET | `/groups/{groupId}/settings/registration` | Get a group's webinar registration settings | `groupSettingsRegistration` | +| PATCH | `/groups/{groupId}/settings/registration` | Update a group's webinar registration settings | `groupSettingsRegistrationUpdate` | +| DELETE | `/groups/{groupId}/settings/virtual_backgrounds` | Delete Virtual Background files | `delGroupVB` | +| POST | `/groups/{groupId}/settings/virtual_backgrounds` | Upload Virtual Background files | `uploadGroupVB` | + +### Users + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/users` | List users | `users` | +| POST | `/users` | Create users | `userCreate` | +| GET | `/users/email` | Check a user email | `userEmail` | +| POST | `/users/features` | Bulk update features for users | `bulkUpdateFeature` | +| GET | `/users/me/zak` | Get the user's ZAK | `userZak` | +| GET | `/users/summary` | Get user summary | `userSummary` | +| GET | `/users/vanity_name` | Check a user's PM room | `userVanityName` | +| DELETE | `/users/{userId}` | Delete a user | `userDelete` | +| GET | `/users/{userId}` | Get a user | `user` | +| PATCH | `/users/{userId}` | Update a user | `userUpdate` | +| DELETE | `/users/{userId}/assistants` | Delete user assistants | `userAssistantsDelete` | +| GET | `/users/{userId}/assistants` | List user assistants | `userAssistants` | +| POST | `/users/{userId}/assistants` | Add assistants | `userAssistantCreate` | +| DELETE | `/users/{userId}/assistants/{assistantId}` | Delete a user assistant | `userAssistantDelete` | +| GET | `/users/{userId}/collaboration_devices` | List a user's collaboration devices | `listCollaborationDevices` | +| GET | `/users/{userId}/collaboration_devices/{collaborationDeviceId}` | Get collaboration device detail | `getCollaborationDevice` | +| PUT | `/users/{userId}/email` | Update a user's email | `userEmailUpdate` | +| GET | `/users/{userId}/meeting_summary_templates` | Get meeting summary templates | `Getmeetingsummarytemplates` | +| GET | `/users/{userId}/meeting_templates/{meetingTemplateId}` | Get meeting template detail | `getUserMeetingTemplates` | +| PUT | `/users/{userId}/password` | Update a user's password | `userPassword` | +| GET | `/users/{userId}/permissions` | Get user permissions | `userPermission` | +| DELETE | `/users/{userId}/picture` | Delete a user's profile picture | `userPictureDelete` | +| POST | `/users/{userId}/picture` | Upload a user's profile picture | `userPicture` | +| GET | `/users/{userId}/presence_status` | Get a user presence status | `getUserPresenceStatus` | +| PUT | `/users/{userId}/presence_status` | Update a user's presence status | `updatePresenceStatus` | +| DELETE | `/users/{userId}/schedulers` | Delete user schedulers | `userSchedulersDelete` | +| GET | `/users/{userId}/schedulers` | List user schedulers | `userSchedulers` | +| DELETE | `/users/{userId}/schedulers/{schedulerId}` | Delete a scheduler | `userSchedulerDelete` | +| GET | `/users/{userId}/settings` | Get user settings | `userSettings` | +| PATCH | `/users/{userId}/settings` | Update user settings | `userSettingsUpdate` | +| DELETE | `/users/{userId}/settings/virtual_backgrounds` | Delete Virtual Background files | `delUserVB` | +| POST | `/users/{userId}/settings/virtual_backgrounds` | Upload Virtual Background files | `uploadVBuser` | +| PUT | `/users/{userId}/status` | Update user status | `userStatus` | +| DELETE | `/users/{userId}/token` | Revoke a user's SSO token | `userSSOTokenDelete` | +| GET | `/users/{userId}/token` | Get a user's token | `userToken` | diff --git a/plugins/zoom-developers/skills/rest-api/references/video-management.md b/plugins/zoom-developers/skills/rest-api/references/video-management.md new file mode 100644 index 00000000..64affe94 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/video-management.md @@ -0,0 +1,85 @@ +# Zoom Video Management API + +Authoritative endpoint inventory for Video Management. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/video-management/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 25 | +| Path templates | 11 | +| Tags | 5 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Channels | 6 | +| files | 1 | +| Permissions | 4 | +| Playlists | 7 | +| Videos | 7 | + +## Endpoints by Tag + +### Channels + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/video_management/channels` | List channels | `listVideoChannels` | +| POST | `/video_management/channels` | Create a channel | `createChannel` | +| DELETE | `/video_management/channels/{channelId}` | Delete channel | `DeleteChannel` | +| GET | `/video_management/channels/{channelId}` | Get channel details | `getChannelDetail` | +| PATCH | `/video_management/channels/{channelId}` | Update channel | `UpdateVideoChannel` | +| PATCH | `/video_management/channels/{channelId}/actions` | Channel actions | `channelActions` | + +### files + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/video_management/files` | Upload file for video management | `uploadVODtFile` | + +### Permissions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/video_management/channels/{channelId}/permissions` | Delete channel permissions | `DeleteChannelPermissions` | +| GET | `/video_management/channels/{channelId}/permissions` | List channel permissions | `listChannelPermissions` | +| PATCH | `/video_management/channels/{channelId}/permissions` | Update channel permissions | `updateChannelPermissions` | +| POST | `/video_management/channels/{channelId}/permissions` | Create channel permissions | `createChannelPermissions` | + +### Playlists + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/video_management/channels/{channelId}/playlists` | Delete channel playlists | `DeleteChannelPlaylists` | +| GET | `/video_management/channels/{channelId}/playlists` | List channel playlists | `ListChannelPlaylists` | +| POST | `/video_management/channels/{channelId}/playlists` | Add channel playlists | `AddChannelPlaylists` | +| GET | `/video_management/playlists` | List playlists | `ListPlaylists` | +| POST | `/video_management/playlists` | Create a playlist | `createPlaylist` | +| DELETE | `/video_management/playlists/{playlistId}` | Delete playlist | `DeletePlaylist` | +| PATCH | `/video_management/playlists/{playlistId}` | Update playlist | `UpdatePlaylist` | + +### Videos + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| DELETE | `/video_management/channels/{channelId}/videos` | Delete channel videos | `DeleteChannelVideos` | +| GET | `/video_management/channels/{channelId}/videos` | List channel videos | `ListChannelVideos` | +| POST | `/video_management/channels/{channelId}/videos` | Add channel videos | `AddChannelVideos` | +| DELETE | `/video_management/playlists/{playlistId}/videos` | Delete playlist videos | `DeletePlaylistVideos` | +| GET | `/video_management/playlists/{playlistId}/videos` | List playlist videos | `ListPlaylistVideos` | +| POST | `/video_management/playlists/{playlistId}/videos` | Add playlist videos | `AddPlaylistVideos` | +| GET | `/video_management/videos` | List all videos | `ListAllVideos` | diff --git a/plugins/zoom-developers/skills/rest-api/references/video-sdk-api.md b/plugins/zoom-developers/skills/rest-api/references/video-sdk-api.md new file mode 100644 index 00000000..5484f974 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/video-sdk-api.md @@ -0,0 +1,92 @@ +# Zoom Video SDK API + +Authoritative endpoint inventory for Video SDK. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/video-sdk/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 38 | +| Path templates | 28 | +| Tags | 4 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Byos Storage | 6 | +| Cloud Recording | 6 | +| Sessions | 21 | +| Video SDK Reports | 5 | + +## Endpoints by Tag + +### Byos Storage + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PATCH | `/videosdk/settings/storage` | Update Bring Your Own Storage settings | `byosStorageSwitchPatch` | +| GET | `/videosdk/settings/storage/location` | List storage location | `byosStorageGet` | +| POST | `/videosdk/settings/storage/location` | Add storage location | `byosStoragePost` | +| DELETE | `/videosdk/settings/storage/location/{storageLocationId}` | Delete storage location detail | `byosStorageDetailDelete` | +| GET | `/videosdk/settings/storage/location/{storageLocationId}` | Storage location detail | `byosStorageDetailGet` | +| PATCH | `/videosdk/settings/storage/location/{storageLocationId}` | Change storage location detail | `byosStorageDetailPatch` | + +### Cloud Recording + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/videosdk/recordings` | List recordings of an account | `recordingsList` | +| DELETE | `/videosdk/sessions/{sessionId}/recordings` | Delete session's recordings | `recordingDelete` | +| GET | `/videosdk/sessions/{sessionId}/recordings` | List session's recordings | `recordingGet` | +| PUT | `/videosdk/sessions/{sessionId}/recordings/status` | Recover session's recordings | `recordingStatusUpdate` | +| DELETE | `/videosdk/sessions/{sessionId}/recordings/{recordingId}` | Delete session's recording file | `recordingDeleteOne` | +| PUT | `/videosdk/sessions/{sessionId}/recordings/{recordingId}/status` | Recover a single recording | `recordingStatusUpdateOne` | + +### Sessions + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/videosdk/sessions` | List sessions | `sessions` | +| POST | `/videosdk/sessions` | Create a session | `Createasession` | +| DELETE | `/videosdk/sessions/{sessionId}` | Delete a session | `sessionDelete` | +| GET | `/videosdk/sessions/{sessionId}` | Get session details | `sessionDetail` | +| PATCH | `/videosdk/sessions/{sessionId}/events` | Use in-session events controls | `inSessionEventsControl` | +| GET | `/videosdk/sessions/{sessionId}/livestream` | Get session live stream details | `getLiveStreamDetails` | +| PATCH | `/videosdk/sessions/{sessionId}/livestream` | Update a session live stream | `sessionLiveStreamUpdate` | +| PATCH | `/videosdk/sessions/{sessionId}/livestream/status` | Update session livestream status | `sessionLiveStreamStatusUpdate` | +| PATCH | `/videosdk/sessions/{sessionId}/rtms_app/status` | Update Realtime Media Streams (RTMS) app status | `updateSessionRtmsAppStatus` | +| POST | `/videosdk/sessions/{sessionId}/sip_dialing` | Get a session SIP URI with passcode | `GetASessionSIPURIWithPasscode` | +| PUT | `/videosdk/sessions/{sessionId}/status` | Update session status | `sessionStatus` | +| GET | `/videosdk/sessions/{sessionId}/stream_ingestions` | List session streaming ingestions | `Listsessionstreamingingestions` | +| GET | `/videosdk/sessions/{sessionId}/users` | List session users | `sessionUsers` | +| GET | `/videosdk/sessions/{sessionId}/users/qos` | List session users QoS | `sessionUsersQOS` | +| GET | `/videosdk/sessions/{sessionId}/users/sharing` | Get sharing/recording details | `sessionParticipantShare` | +| GET | `/videosdk/sessions/{sessionId}/users/{userId}/qos` | Get session user QoS | `sessionUserQOS` | +| GET | `/videosdk/stream_ingestions` | List stream ingestions | `Liststreamingestions` | +| POST | `/videosdk/stream_ingestions` | Create a stream ingestion | `Createastreamingestion` | +| DELETE | `/videosdk/stream_ingestions/{streamId}` | Delete a stream ingestion | `Deleteastreamingestion` | +| GET | `/videosdk/stream_ingestions/{streamId}` | Get a stream ingestion | `Getastreamingestion` | +| PATCH | `/videosdk/stream_ingestions/{streamId}` | Update a stream ingestion | `Updateastreamingestion` | + +### Video SDK Reports + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/videosdk/report/cloud_recording` | Get cloud recording usage report | `vsdkReportCloudRecording` | +| GET | `/videosdk/report/daily` | Get daily usage report | `vsdkReportDaily` | +| GET | `/videosdk/report/operationlogs` | Get operation logs report | `vsdkReportOperationLogs` | +| GET | `/videosdk/report/telephone` | Get telephone report | `vsdkReportTelephone` | +| GET | `/videosdk/report/webhook_logs` | Get webhook logs | `getWebhookLogs` | diff --git a/plugins/zoom-developers/skills/rest-api/references/virtual-agent.md b/plugins/zoom-developers/skills/rest-api/references/virtual-agent.md new file mode 100644 index 00000000..ca42126e --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/virtual-agent.md @@ -0,0 +1,54 @@ +# Zoom Virtual Agent API + +Authoritative endpoint inventory for Virtual Agent. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/virtual-agent/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 12 | +| Path templates | 9 | +| Tags | 2 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Knowledge Management | 7 | +| Report | 5 | + +## Endpoints by Tag + +### Knowledge Management + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/km/kbs/{kbId}/articles` | Get articles | `GetArticles` | +| POST | `/km/kbs/{kbId}/articles` | Create article | `CreateArticle` | +| DELETE | `/km/kbs/{kbId}/articles/{articleId}` | Delete article | `DeleteArticle` | +| GET | `/km/kbs/{kbId}/articles/{articleId}` | Get article | `GetArticle` | +| PUT | `/km/kbs/{kbId}/articles/{articleId}` | Update article | `UpdateArticle` | +| POST | `/km/kbs/{kbId}/sync` | Create sync request | `CreateSyncRequest` | +| GET | `/km/kbs/{kbId}/sync/{syncId}` | Get sync | `GetSync` | + +### Report + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/virtual_agent/report/engagements` | Get ZVA engagements | `GetZVAEngagements` | +| GET | `/virtual_agent/report/engagements/query_details` | Get ZVA query details | `GetZVAQueryDetails` | +| GET | `/virtual_agent/report/engagements/variables` | Get ZVA variable details | `GetZVAengagementvariabledetails` | +| GET | `/virtual_agent/report/surveys` | Get ZVA Surveys | `GetZVASurveys` | +| GET | `/virtual_agent/report/transcripts` | Get ZVA transcripts | `GetZVATranscripts` | diff --git a/plugins/zoom-developers/skills/rest-api/references/webinars.md b/plugins/zoom-developers/skills/rest-api/references/webinars.md new file mode 100644 index 00000000..e1dd1240 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/webinars.md @@ -0,0 +1,76 @@ +# REST API - Webinars + +Webinar management endpoints. + +## Overview + +Create and manage Zoom webinars programmatically. + +## Endpoints + +### Create Webinar + +```bash +POST /users/{userId}/webinars +``` + +```json +{ + "topic": "My Webinar", + "type": 5, + "start_time": "2024-01-15T10:00:00Z", + "duration": 60, + "settings": { + "host_video": true, + "panelists_video": true, + "registration_type": 1 + } +} +``` + +### Get Webinar + +```bash +GET /webinars/{webinarId} +``` + +### Update Webinar + +```bash +PATCH /webinars/{webinarId} +``` + +### Delete Webinar + +```bash +DELETE /webinars/{webinarId} +``` + +### List Webinar Registrants + +```bash +GET /webinars/{webinarId}/registrants +``` + +### Add Registrant + +```bash +POST /webinars/{webinarId}/registrants +``` + +## Webinar Types + +| Type | Value | Description | +|------|-------|-------------| +| Webinar | 5 | Single webinar | +| Recurring (no fixed time) | 6 | Recurring, no schedule | +| Recurring (fixed time) | 9 | Recurring with schedule | + +## Required Scopes + +- `webinar:read` - View webinars +- `webinar:write` - Create/update/delete webinars + +## Resources + +- **API Reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#tag/Webinars diff --git a/plugins/zoom-developers/skills/rest-api/references/whiteboard.md b/plugins/zoom-developers/skills/rest-api/references/whiteboard.md new file mode 100644 index 00000000..671b456c --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/whiteboard.md @@ -0,0 +1,110 @@ +# Zoom Whiteboard API + +Authoritative endpoint inventory for Whiteboard. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/whiteboard/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 32 | +| Path templates | 21 | +| Tags | 8 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Archiving | 3 | +| Collaborator | 4 | +| Document | 5 | +| Export | 3 | +| File | 2 | +| Import | 2 | +| Project | 12 | +| Settings | 1 | + +## Endpoints by Tag + +### Archiving + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/whiteboards/sessions` | List whiteboards sessions | `Createwhiteboardsarchivefiles` | +| GET | `/whiteboards/sessions/activity/download/{path}` | Download Whiteboards activity file | `Downloadwhiteboardsactivityfile` | +| GET | `/whiteboards/sessions/{seesionId}` | List whiteboard sessions activities | `Listwhiteboardsessionsarchivedfiles` | + +### Collaborator + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/whiteboards/{whiteboardId}/collaborator` | Get collaborators of a whiteboard | `GetAWhiteboardCollaborator` | +| PATCH | `/whiteboards/{whiteboardId}/collaborator` | Update whiteboard collaborators | `UpdateAWhiteboardCollaborator` | +| POST | `/whiteboards/{whiteboardId}/collaborator` | Share a whiteboard to new users or team chat channels. | `AddAWhiteboardCollaborator` | +| DELETE | `/whiteboards/{whiteboardId}/collaborator/{collaboratorId}` | Remove the collaborator from a whiteboard | `DeleteAWhiteboardCollaborator` | + +### Document + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/whiteboards` | List all whiteboards | `ListWhiteboards` | +| POST | `/whiteboards` | Create a new whiteboard | `newWhiteboardCreate` | +| DELETE | `/whiteboards/{whiteboardId}` | Delete a whiteboard | `DeleteAWhiteboard` | +| GET | `/whiteboards/{whiteboardId}` | Get a whiteboard | `GetAWhiteboard` | +| PUT | `/whiteboards/{whiteboardId}` | Update whiteboard basic information | `UpdateAWhiteboardMetadata` | + +### Export + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/whiteboards/export` | Create whiteboard export | `Createwhiteboardsexport` | +| GET | `/whiteboards/export/task/{taskId}` | Download whiteboard export | `Downloadwhiteboardexport` | +| GET | `/whiteboards/export/task/{taskId}/status` | Get whiteboard export generation status | `Getwhiteboardexportdatagenerationstatus` | + +### File + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/whiteboards/files` | Upload file for whiteboard import | `Uploadfileforwhiteboardimport` | +| GET | `/whiteboards/{whiteboardId}/files/{fileId}` | Download Imported Whiteboard File | `Downloadembeddedwhiteboardfile` | + +### Import + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/whiteboards/import` | Create a new whiteboard by import | `CreateWhiteboardImport` | +| GET | `/whiteboards/import/{taskId}/status` | Get whiteboard import status | `GetWhiteboardimportstatus` | + +### Project + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/whiteboards/projects` | List all projects | `Listallprojects` | +| POST | `/whiteboards/projects` | Create a new project | `Createproject` | +| DELETE | `/whiteboards/projects/{projectId}` | Delete a project | `Deleteproject` | +| GET | `/whiteboards/projects/{projectId}` | Get a project | `Getaproject` | +| PATCH | `/whiteboards/projects/{projectId}` | Update project basic information | `Updateproject` | +| GET | `/whiteboards/projects/{projectId}/collaborators` | Get collaborators of a project | `Getcollaboratorsofaproject` | +| PATCH | `/whiteboards/projects/{projectId}/collaborators` | Update project collaborators | `Updateprojectcollaborators` | +| POST | `/whiteboards/projects/{projectId}/collaborators` | Share a project to new users | `Shareaprojecttonewusers` | +| DELETE | `/whiteboards/projects/{projectId}/collaborators/{collaboratorId}` | Remove the collaborator from a project | `Removethecollaboratorfromaproject` | +| GET | `/whiteboards/projects/{projectId}/subprojects` | List subprojects | `listSubProjects` | +| DELETE | `/whiteboards/projects/{projectId}/whiteboards` | Remove whiteboards from a project | `Removewhiteboardsfromaproject` | +| POST | `/whiteboards/projects/{projectId}/whiteboards` | Move whiteboards to a project | `Movewhiteboardstoproject` | + +### Settings + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| PATCH | `/whiteboards/{whiteboardId}/share_setting` | Update whiteboard share setting | `UpdateAWhiteboardShareSetting` | diff --git a/plugins/zoom-developers/skills/rest-api/references/workforce-management.md b/plugins/zoom-developers/skills/rest-api/references/workforce-management.md new file mode 100644 index 00000000..50c74a63 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/workforce-management.md @@ -0,0 +1,72 @@ +# Zoom Workforce Management API + +Authoritative endpoint inventory for Workforce Management. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/workforce-management/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 12 | +| Path templates | 9 | +| Tags | 5 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Forecasts | 2 | +| Imports | 2 | +| Organizational Groups | 5 | +| Reports | 2 | +| Schedules | 1 | + +## Endpoints by Tag + +### Forecasts + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/workforce-management/forecasts` | List forecasts | `Listforecasts` | +| GET | `/workforce-management/forecasts/{forecastId}/scheduling-groups/{schedulingGroupId}` | Get a forecast for a scheduling group | `Getforecastofschedulinggroup` | + +### Imports + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/workforce-management/imports/historical-queue-metrics` | Upload historical queue metrics | `Uploadhistoricalqueuemetrics` | +| GET | `/workforce-management/imports/{importId}/historical-queue-metrics` | Get historical queue metrics import metadata | `Viewhistoricalqueuemetricimoprtmeta` | + +### Organizational Groups + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/workforce-management/organizational-groups` | Get multiple organizational groups | `getMultipleOrganizationalGroups` | +| POST | `/workforce-management/organizational-groups` | Create an organizational group | `createOrganizationalGroup` | +| DELETE | `/workforce-management/organizational-groups/{organizationalGroupId}` | Delete organizational group | `deleteOrganizationalGroup` | +| GET | `/workforce-management/organizational-groups/{organizationalGroupId}` | Get a single organizational group | `getOrganizationalGroup` | +| PATCH | `/workforce-management/organizational-groups/{organizationalGroupId}` | Update an organizational group | `updateOrganizationalGroup` | + +### Reports + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/workforce-management/reports/adherence/agents` | List agents' adherence data | `listAdherenceAgents` | +| GET | `/workforce-management/reports/schedules/agents` | List all schedule agents | `listScheduleAgents` | + +### Schedules + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/workforce-management/schedules/agents` | List all schedule agents | `listSchedules` | diff --git a/plugins/zoom-developers/skills/rest-api/references/zoom-docs.md b/plugins/zoom-developers/skills/rest-api/references/zoom-docs.md new file mode 100644 index 00000000..92c80d55 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/references/zoom-docs.md @@ -0,0 +1,82 @@ +# Zoom Docs API + +Authoritative endpoint inventory for Zoom Docs. This file mirrors the official Zoom API Hub OpenAPI document for this product area. + +## Canonical Source + +- OpenAPI JSON: https://developers.zoom.us/api-hub/zoom-docs/methods/endpoints.json +- Base URL: `https://api.zoom.us/v2` +- Authentication details: [authentication.md](authentication.md) + +## Notes + +- Endpoint methods and paths below are generated from the official Zoom API Hub `paths` object. +- Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation. +- Use this file for endpoint discovery and inventory. Use `../examples/` for orchestration patterns, not as the canonical source of path names. + +## Coverage + +| Metric | Value | +|--------|-------| +| Endpoint operations | 16 | +| Path templates | 11 | +| Tags | 6 | + +## Tag Index + +| Tag | Operations | +|-----|------------| +| Collaborator | 4 | +| Export | 2 | +| File Management | 5 | +| File Uploads | 1 | +| General Access | 2 | +| Import | 2 | + +## Endpoints by Tag + +### Collaborator + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/docs/files/{fileId}/collaborators` | List collaborators of a file | `ListCollaborators` | +| POST | `/docs/files/{fileId}/collaborators` | Add collaborators for a file | `AddCollaborators` | +| DELETE | `/docs/files/{fileId}/collaborators/{collaboratorId}` | Remove a collaborator from a file | `RemoveACollaborator` | +| PATCH | `/docs/files/{fileId}/collaborators/{collaboratorId}` | Modify a collaborator’s role on a file | `ModifyCollaboratorRole` | + +### Export + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/docs/exports` | Create a file export | `Createafileexport` | +| GET | `/docs/exports/{exportId}/status` | Get file export status | `Getfileexportstatus` | + +### File Management + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/docs/files` | Create a new file | `CreateDoc` | +| DELETE | `/docs/files/{fileId}` | Delete a file | `DeleteFile` | +| GET | `/docs/files/{fileId}` | Get metadata of a file | `QueryFileMetadata` | +| PATCH | `/docs/files/{fileId}` | Modify metadata of a file | `ModifyMetadata` | +| GET | `/docs/files/{fileId}/children` | List all children of a file | `ListAllChildren` | + +### File Uploads + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/docs/file_uploads` | Create file upload for docs import or attachments | `Uploadfilefordocsimportorattachments` | + +### General Access + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| GET | `/docs/files/{fileId}/general_access_setting` | Get the general access setting of a file | `GetFileGeneralAccess` | +| PATCH | `/docs/files/{fileId}/general_access_setting` | Modify the general access setting of a file | `ModifyFileGeneralAccess` | + +### Import + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/docs/imports` | Create a new file by import | `Createanewfilebyimport` | +| GET | `/docs/imports/{importId}/status` | Get file import status | `Getdocsfileimportstatus` | diff --git a/plugins/zoom-developers/skills/rest-api/troubleshooting/common-errors.md b/plugins/zoom-developers/skills/rest-api/troubleshooting/common-errors.md new file mode 100644 index 00000000..9a84728f --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/troubleshooting/common-errors.md @@ -0,0 +1,429 @@ +# Common Errors - HTTP Status Codes and Zoom Error Codes + +Complete reference for Zoom REST API error codes, HTTP status codes, and solutions. + +## HTTP Status Codes + +### 2XX - Success + +| Status | Description | Action | +|--------|-------------|--------| +| `200 OK` | Request succeeded | Parse response body | +| `201 Created` | Resource created | Check `Location` header for new resource URL | +| `204 No Content` | Request succeeded, no body | Common for UPDATE/DELETE operations | + +### 4XX - Client Errors + +| Status | Description | Common Causes | Solution | +|--------|-------------|---------------|----------| +| `400 Bad Request` | Invalid request | Missing required fields, invalid JSON, validation errors | Check request body format and required fields | +| `401 Unauthorized` | Authentication failed | Invalid/expired token, missing `Authorization` header | Refresh access token, check token format | +| `403 Forbidden` | Permission denied | Missing scopes, user lacks permission, shared access not granted | Add required scopes, check user role | +| `404 Not Found` | Resource doesn't exist | Invalid userId/meetingId, wrong `me` keyword usage | Verify resource ID, check `me` keyword rules | +| `409 Conflict` | Resource conflict | Email already exists, duplicate operation | Use unique identifiers | +| `429 Too Many Requests` | Rate limit exceeded | Too many requests per second/day | Implement exponential backoff, throttle requests | + +### 5XX - Server Errors + +| Status | Description | Action | +|--------|-------------|--------| +| `500 Internal Server Error` | Zoom server error | Retry with exponential backoff | +| `502 Bad Gateway` | Gateway error | Retry after delay | +| `503 Service Unavailable` | Zoom service down | Check [status.zoom.us](https://status.zoom.us), retry later | + +## Zoom Error Codes + +When an API call fails, Zoom returns an error response: + +```json +{ + "code": 300, + "message": "Request Body should be a valid JSON object." +} +``` + +### Common Zoom Error Codes + +| Code | HTTP | Message | Cause | Solution | +|------|------|---------|-------|----------| +| `300` | 400 | Invalid request | Bad JSON, validation error | Check request body structure | +| `124` | 400 | Invalid parameter | Wrong parameter type/value | Validate parameters against API docs | +| `200` | 401 | Invalid credentials | Incorrect OAuth token | Refresh access token | +| `201` | 401 | Access token expired | Token expired | Request new token | +| `1001` | 404 | User does not exist | Invalid userId or wrong `me` usage | Check userId, review `me` keyword rules | +| `300` | 404 | Meeting not found | Invalid meetingId | Verify meeting exists | +| `3000` | 404 | Cannot access webinar info | Webinar doesn't exist or no access | Check webinarId and scopes | +| `200` | 429 | Rate limit exceeded | Too many requests | Implement rate limiting | +| `4700` | 401 | Invalid access token | Token missing scopes | Add required scopes in app config | +| `3001` | 403 | Not allowed to access | Missing permission | Upgrade user role or add scope | + +## Detailed Error Scenarios + +### 401 Unauthorized + +#### Scenario 1: Token Expired + +```json +{ + "code": 201, + "message": "Access token is expired." +} +``` + +**Solution:** +```javascript +async function apiCallWithRetry(url, options) { + try { + const response = await fetch(url, options); + + if (response.status === 401) { + const error = await response.json(); + + if (error.code === 201) { + // Token expired - refresh and retry + const newToken = await refreshAccessToken(); + options.headers['Authorization'] = `Bearer ${newToken}`; + return await fetch(url, options); + } + } + + return response; + } catch (error) { + console.error('API call failed:', error); + throw error; + } +} +``` + +#### Scenario 2: Missing/Invalid Token + +```json +{ + "code": 200, + "message": "Invalid credentials." +} +``` + +**Solution:** +- Check `Authorization` header is present: `Authorization: Bearer ACCESS_TOKEN` +- Verify token format (should be JWT) +- Ensure token is for the correct account + +#### Scenario 3: Missing Scopes + +```json +{ + "code": 4700, + "message": "Invalid access token, does not contain scopes." +} +``` + +**Solution:** +1. Go to your app on Zoom Marketplace +2. Add required scopes (e.g., `meeting:write:admin`) +3. Request new access token with updated scopes + +### 403 Forbidden + +#### Scenario 1: Missing Permission + +```json +{ + "code": 3001, + "message": "This user is not allowed to access this resource." +} +``` + +**Solution:** +- Verify user has appropriate role (Admin, Owner) +- Check if endpoint requires admin-level scopes +- Use account-level scope (e.g., `meeting:write:admin` instead of `meeting:write`) + +#### Scenario 2: Shared Access Permissions + +```json +{ + "code": 3001, + "message": "Authenticated user has not permitted access to the targeted resource." +} +``` + +**Cause:** User hasn't authorized shared access permissions for your app. + +**Solution:** Direct user to: [Allowing Apps Access to Shared Access Permissions](https://support.zoom.us/hc/en-us/articles/4413265586189) + +### 404 Not Found + +#### Scenario 1: User Not Found (Wrong `me` Usage) + +```json +{ + "code": 1001, + "message": "User does not exist: user@example.com" +} +``` + +**Cause:** Using `userId` with User OAuth app (should use `me`). + +**Solution:** +```javascript +// WRONG (User OAuth app) +fetch('https://api.zoom.us/v2/users/user@example.com', { + headers: { 'Authorization': `Bearer ${userToken}` } +}); + +// CORRECT (User OAuth app) +fetch('https://api.zoom.us/v2/users/me', { + headers: { 'Authorization': `Bearer ${userToken}` } +}); +``` + +#### Scenario 2: Meeting/Resource Not Found + +```json +{ + "code": 300, + "message": "Meeting not found." +} +``` + +**Causes:** +- Meeting deleted +- Meeting expired (30 days after last use) +- Invalid meetingId +- Wrong UUID encoding + +**Solution for UUID:** +```javascript +// UUID starts with / or contains // → double-encode +const uuid = '/xyzAbC123=='; +const encoded = encodeURIComponent(encodeURIComponent(uuid)); +fetch(`https://api.zoom.us/v2/meetings/${encoded}`, { headers }); +``` + +### 429 Too Many Requests + +```json +{ + "code": 200, + "message": "You have reached the maximum per-second rate limit for this API." +} +``` + +**Solution:** Implement exponential backoff + +```javascript +async function apiCallWithBackoff(url, options, maxRetries = 3) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + const response = await fetch(url, options); + + if (response.status === 429) { + const retryAfter = response.headers.get('Retry-After') || Math.pow(2, attempt); + console.log(`Rate limited. Retrying after ${retryAfter}s`); + await sleep(retryAfter * 1000); + continue; + } + + return response; + } + + throw new Error('Max retries exceeded'); +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} +``` + +**Check rate limit headers:** +```javascript +const remaining = response.headers.get('X-RateLimit-Remaining'); +const type = response.headers.get('X-RateLimit-Type'); + +console.log(`Rate limit remaining: ${remaining} (${type})`); +``` + +## Validation Error Responses + +### Field Validation Errors + +```json +{ + "code": 300, + "message": "Validation Failed.", + "errors": [ + { + "field": "start_time", + "message": "Invalid field." + }, + { + "field": "type", + "message": "Invalid field." + } + ] +} +``` + +**Solution:** +- Check each field's format in API documentation +- Verify required fields are present +- Ensure data types match (number vs string) + +### Common Field Errors + +| Field | Error | Cause | Fix | +|-------|-------|-------|-----| +| `start_time` | Invalid field | Wrong format | Use `yyyy-MM-ddTHH:mm:ssZ` | +| `type` | Invalid field | Invalid meeting type | Use 1, 2, 3, or 8 | +| `email` | Invalid field | Invalid email format | Check email format | +| `duration` | Invalid field | Duration too long | Max duration varies by plan | + +## Error Response Structure + +### Standard Error + +```json +{ + "code": 300, + "message": "Descriptive error message" +} +``` + +### Validation Error + +```json +{ + "code": 300, + "message": "Validation Failed.", + "errors": [ + { + "field": "field_name", + "message": "Error description" + } + ] +} +``` + +## Error Handling Best Practices + +### 1. Parse Error Response + +```javascript +async function handleApiResponse(response) { + if (!response.ok) { + const error = await response.json(); + + console.error(`API Error ${response.status}:`, error); + + // Check for validation errors + if (error.errors && Array.isArray(error.errors)) { + error.errors.forEach(err => { + console.error(`- Field "${err.field}": ${err.message}`); + }); + } + + throw new Error(`${error.code}: ${error.message}`); + } + + return await response.json(); +} +``` + +### 2. Retry with Exponential Backoff + +```javascript +async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + const isRetryable = error.status >= 500 || error.status === 429; + + if (!isRetryable || attempt === maxRetries) { + throw error; + } + + const delay = baseDelay * Math.pow(2, attempt - 1); + const jitter = Math.random() * 1000; // Add randomness + + console.log(`Retry attempt ${attempt} after ${delay + jitter}ms`); + await sleep(delay + jitter); + } + } +} +``` + +### 3. Log Errors with Context + +```javascript +function logApiError(error, context) { + const logEntry = { + timestamp: new Date().toISOString(), + status: error.status, + code: error.code, + message: error.message, + context: { + endpoint: context.endpoint, + method: context.method, + userId: context.userId + } + }; + + console.error('API Error:', JSON.stringify(logEntry, null, 2)); + + // Send to monitoring service + // monitoring.track(logEntry); +} +``` + +### 4. User-Friendly Error Messages + +```javascript +function getUserFriendlyError(error) { + const errorMap = { + 1001: 'User not found. Please check the email address.', + 300: 'Invalid request. Please check your input.', + 201: 'Your session has expired. Please sign in again.', + 4700: 'Permission denied. Please contact your administrator.', + 200: 'Too many requests. Please try again in a few moments.' + }; + + return errorMap[error.code] || 'An unexpected error occurred. Please try again.'; +} +``` + +## Troubleshooting Checklist + +### Quick Diagnostic Steps + +1. **Check HTTP status code** - Indicates error category (auth, validation, server) +2. **Read error message** - Provides specific details +3. **Check Zoom error code** - Maps to specific issue +4. **Verify authentication** - Token valid, scopes present +5. **Check rate limits** - Monitor `X-RateLimit-Remaining` header +6. **Test with Postman** - Isolate code vs API issue +7. **Check Zoom Status** - Visit [status.zoom.us](https://status.zoom.us) + +### Common Fixes + +| Symptom | Check | Fix | +|---------|-------|-----| +| 401 Unauthorized | Token expiry | Refresh access token | +| 404 User not found | `me` keyword usage | Use `me` for User OAuth | +| 404 Meeting not found | UUID encoding | Double-encode UUIDs starting with `/` | +| 403 Forbidden | Scopes | Add required scopes in app config | +| 429 Rate limit | Request rate | Implement throttling | +| 400 Validation error | Request body | Check field formats in docs | + +## Related Documentation + +- **[API Architecture](../concepts/api-architecture.md)** - `me` keyword, UUID encoding, time formats +- **[Rate Limiting Strategy](../concepts/rate-limiting-strategy.md)** - Handle 429 errors +- **[Authentication Flows](../concepts/authentication-flows.md)** - Token refresh patterns +- **[Common Issues](common-issues.md)** - Practical troubleshooting + +## Resources + +- [Error Codes Documentation](https://developers.zoom.us/docs/api/errors/) +- [Zoom Status Page](https://status.zoom.us/) +- [Developer Forum](https://devforum.zoom.us/) diff --git a/plugins/zoom-developers/skills/rest-api/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/rest-api/troubleshooting/common-issues.md new file mode 100644 index 00000000..e454ae27 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/troubleshooting/common-issues.md @@ -0,0 +1,25 @@ +# Common Issues (REST API) + +This complements `common-errors.md` (HTTP codes and Zoom error codes). + +## Pagination + +- Some endpoints use `next_page_token`. +- Others use `page_number` + `page_size`. +- For large accounts, always code defensively for partial results. + +## Token Expiry / Scopes + +- `Access token is expired`: refresh or request a new S2S token. +- `does not contain scopes`: add scopes in Marketplace and re-authorize users (User OAuth). + +## Webhooks + +- Webhooks are at-least-once delivery: design idempotent handlers. +- Verify signatures and handle `endpoint.url_validation`. + +## Also See + +- `common-errors.md` +- `token-scope-playbook.md` + diff --git a/plugins/zoom-developers/skills/rest-api/troubleshooting/forum-top-questions.md b/plugins/zoom-developers/skills/rest-api/troubleshooting/forum-top-questions.md new file mode 100644 index 00000000..ea59d7dd --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/troubleshooting/forum-top-questions.md @@ -0,0 +1,56 @@ +--- +title: "Forum-Derived Top Questions (REST API)" +--- + +# Forum-Derived Top Questions (REST API) + +This is a checklist of the most common forum questions for **Zoom REST API** integrations. + +## Fast Routing Questions (Ask First) + +- App type: **Server-to-Server OAuth** vs **User OAuth (authorization_code / PKCE)** +- Target endpoints: meetings, webinars, recordings, users, reports, team chat +- Who are you acting as: account-level automation vs per-user actions? +- Exact error: HTTP status + Zoom error `code` + `message` +- Scopes: list current scopes on the token (and whether they match the endpoint) + +## “Invalid access token” / “does not contain scopes” + +Common causes to cover: +- wrong OAuth app type for the use case +- token from the wrong account/app +- scopes not granted or not activated +- using `me` incorrectly (User OAuth vs S2S OAuth rules) + +## Server-to-Server OAuth Migration + +Common asks: +- “How do I switch from JWT app type to S2S OAuth?” +- “What is `grant_type=account_credentials` and `account_id`?” + +Answer pattern: +- explain that Marketplace JWT app type is deprecated (REST API) +- show token request + where to store/refresh tokens server-side + +## Webhooks + +Common asks: +- verification (CRC or event verification) +- “which event proves the user actually joined?” +- limiting events to subsets of users + +Answer pattern: +- recommend event-driven architecture over polling +- show signature/verification steps and typical pitfalls + +## Recordings + Transcripts + +Common asks: +- downloading recordings (`download_url` auth + redirects) +- bulk download across account/users +- getting transcripts and transcription availability + +Answer pattern: +- use webhooks to trigger downloads +- follow redirects and keep auth headers + diff --git a/plugins/zoom-developers/skills/rest-api/troubleshooting/token-scope-playbook.md b/plugins/zoom-developers/skills/rest-api/troubleshooting/token-scope-playbook.md new file mode 100644 index 00000000..3a8c8be6 --- /dev/null +++ b/plugins/zoom-developers/skills/rest-api/troubleshooting/token-scope-playbook.md @@ -0,0 +1,48 @@ +--- +title: "Token + Scope Playbook (Invalid Access Token / Missing Scopes)" +--- + +# Token + Scope Playbook (Invalid Access Token / Missing Scopes) + +This covers the most common REST API forum failures: + +- `401` / `403` +- `"Invalid access token"` +- `"does not contain scopes"` +- `"Access token is expired"` + +## Step 1: Identify the OAuth App Type + +- **Server-to-Server OAuth**: backend automation, no user consent screen +- **User OAuth (authorization_code / PKCE)**: per-user actions, user consent + +Wrong app type is the #1 cause of “token works for endpoint A but not endpoint B”. + +## Step 2: Verify `me` Keyword Rules + +- User OAuth: use `users/me/...` +- S2S OAuth: do **not** use `me` (use a real userId/email where required) + +If they get `1001 user does not exist` or “invalid access token” on `users/{id}` calls, this is often the reason. + +## Step 3: Confirm Scopes Match the Endpoint + +If the error says missing scopes, you must: + +1. enable the scopes in Marketplace for the app +2. obtain a **new** token (tokens won’t gain scopes retroactively) + +## Step 4: Token Expiry and Refresh + +- S2S OAuth access tokens expire quickly; refresh on the server and cache with a buffer. +- User OAuth: refresh tokens and retry on `code=201` “Access token is expired.” + +## Step 5: Confirm the Token’s Account/App + +If tokens are created from multiple environments (dev/stage/prod) or accounts, mixups happen. + +Ask for: +- the app type +- the account id (human-readable) +- the exact endpoint being called + diff --git a/plugins/zoom-developers/skills/rivet-sdk/RUNBOOK.md b/plugins/zoom-developers/skills/rivet-sdk/RUNBOOK.md new file mode 100644 index 00000000..e2fc5def --- /dev/null +++ b/plugins/zoom-developers/skills/rivet-sdk/RUNBOOK.md @@ -0,0 +1,70 @@ +# Rivet SDK 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 names against changelog and TypeDoc before release. + +## 1) Confirm Integration Surface + +- Confirm this is a server-side Node.js integration path using `@zoom/rivet`. +- Confirm module boundaries (Chatbot, Team Chat, Meetings, Phone, Accounts, Users, Video SDK). +- Confirm whether the app needs event receiver mode, API-only mode, or both. + +## 2) Confirm Required Credentials + +- `clientId`, `clientSecret` for each module auth flow. +- `webhooksSecretToken` for webhook verification (required for receiver flows). +- `accountId` for S2S modules. +- `redirectUri` and `stateStore` for User OAuth modules. + +## 3) Confirm Lifecycle Order + +1. Instantiate module client(s) with auth/receiver options. +2. Register webhook listeners before traffic. +3. Start client server(s) and note per-module port(s). +4. Expose `/zoom/events` for each active receiver endpoint. +5. Execute API calls through `client.endpoints.*`. + +## 4) Confirm Event/State Handling + +- Match event names exactly (`bot_notification`, `interactive_message_fields_editable`, etc.). +- Keep module-specific webhook endpoints aligned with Marketplace subscription settings. +- Persist OAuth tokens/state for long-lived integrations. +- Keep idempotency in event handlers for retries/duplicates. + +## 5) Confirm Cleanup + Upgrade Posture + +- Revalidate all module constructors and options on each upgrade. +- Keep compatibility shims when module/type names change. +- Review changelog version-by-version from current deployment to target. + +## 6) Quick Probes + +- Client starts and binds to expected port(s). +- Webhook verification succeeds for subscribed events. +- One API call and one event callback complete successfully for each active module. +- User OAuth install/callback flow works end-to-end when applicable. + +## 7) Fast Decision Tree + +- API works but events fail -> wrong endpoint URL/port/path (`/zoom/events`) or bad webhook token. +- OAuth install fails -> redirect URI/state store mismatch or unsupported receiver mode. +- Multi-module collisions -> duplicate ports or shared env keys incorrectly mapped. +- Lambda flow issues -> receiver type mismatch or missing `webhooksSecretToken`. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/rivet/ +- https://developers.zoom.us/docs/rivet/javascript/ +- https://zoom.github.io/rivet-javascript/ + +### Raw docs in repo + +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/` +- `tools/zoom-crawler/raw-docs/zoom.github.io/rivet-javascript/` diff --git a/plugins/zoom-developers/skills/rivet-sdk/SKILL.md b/plugins/zoom-developers/skills/rivet-sdk/SKILL.md new file mode 100644 index 00000000..9d858736 --- /dev/null +++ b/plugins/zoom-developers/skills/rivet-sdk/SKILL.md @@ -0,0 +1,25 @@ +--- +name: rivet-sdk +description: Use when using Rivet SDK. +--- + +# Zoom Rivet SDK + +Use this skill when building a server-side Zoom integration with Rivet rather than hand-rolled API and webhook plumbing. + +## Workflow + +1. Confirm Rivet is the right abstraction for the integration’s REST, webhook, auth, and deployment needs. +2. Model the modules: app configuration, OAuth, API clients, webhook handlers, and business workflow handlers. +3. Implement the smallest authenticated API call and webhook receiver before composing multi-module flows. +4. Keep deployment constraints explicit, especially Lambda-style receivers and environment variable handling. +5. Debug by checking app credentials, token refresh, webhook signature handling, module wiring, and framework version drift. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- Rivet overview: [rivet-sdk.md](rivet-sdk.md) +- Architecture and lifecycle: [concepts/architecture-and-lifecycle.md](concepts/architecture-and-lifecycle.md) +- Getting started pattern: [examples/getting-started-pattern.md](examples/getting-started-pattern.md) +- Multi-client pattern: [examples/multi-client-pattern.md](examples/multi-client-pattern.md) +- Common issues: [troubleshooting/common-issues.md](troubleshooting/common-issues.md) diff --git a/plugins/zoom-developers/skills/rivet-sdk/concepts/architecture-and-lifecycle.md b/plugins/zoom-developers/skills/rivet-sdk/concepts/architecture-and-lifecycle.md new file mode 100644 index 00000000..4559fb5a --- /dev/null +++ b/plugins/zoom-developers/skills/rivet-sdk/concepts/architecture-and-lifecycle.md @@ -0,0 +1,82 @@ +# Rivet Architecture and Lifecycle + +## What Rivet Provides + +Rivet wraps three concerns into one module client: +- Auth/token orchestration +- Webhook receiver + event dispatch +- Typed REST API endpoint wrappers + +## Architecture Model + +```text ++--------------------+ +------------------------------+ +| Zoom Marketplace | | Your Rivet App | +| App Config | | (Node.js/TypeScript) | ++----------+---------+ +---------------+--------------+ + | | + | OAuth install / token exchange | + |--------------------------------->| + | | + | Webhooks (POST /zoom/events) | + |--------------------------------->| + | v + | +------------------------+ + | | Rivet Module Clients | + | | - ChatbotClient | + | | - TeamChatClient | + | | - Meetings*Client | + | | - Users*Client | + | | - Phone*Client | + | | - VideoSdkClient | + | +-----+------------+-----+ + | | | + | | +--> webEventConsumer + | | + | +--> endpoints.* (REST wrappers) + | | + | v + | +------------------+ + +--------------------------------> | Zoom APIs | + +------------------+ +``` + +## Lifecycle Workflow + +1. Select module(s): +- Example: `ChatbotClient` + `TeamChatClient` for bot + channel lookup. +- Example: `UsersS2SAuthClient` + `MeetingsS2SAuthClient` for admin automation. + +2. Pick auth model by module: +- Chatbot: Client Credentials +- Team Chat/Meetings/Phone/Accounts/Users: User OAuth or S2S OAuth +- Video SDK: JWT auth for Video SDK API + +3. Configure client options: +- Required: `clientId`, `clientSecret` +- Often required: `webhooksSecretToken` +- Conditional: `accountId`, `installerOptions`, `receiver`, `port`, `tokenStore` + +4. Register listeners: +- Generic: `webEventConsumer.event("event_name", handler)` +- Shortcuts where available: `onSlashCommand`, `onButtonClick`, `onChannelMessagePosted` + +5. Start server(s): +- `await client.start()` returns server handler/address +- For multi-module apps, assign unique ports + +6. Wire Marketplace subscriptions: +- Endpoint URL must target each module's receiver port +- Endpoint path should include `/zoom/events` + +7. Process API + events: +- API calls via `client.endpoints.*` +- Event-driven actions via callback handlers + +8. Operate and upgrade: +- Persist OAuth tokens/state for stable restarts +- Use changelog + TypeDoc workflow for version upgrades + +## Why Multi-Client Port Strategy Matters + +In sample patterns, each module runs its own receiver port. If multiple modules share one port by mistake, webhook routing and verification behavior can break in non-obvious ways. diff --git a/plugins/zoom-developers/skills/rivet-sdk/examples/getting-started-pattern.md b/plugins/zoom-developers/skills/rivet-sdk/examples/getting-started-pattern.md new file mode 100644 index 00000000..3446896b --- /dev/null +++ b/plugins/zoom-developers/skills/rivet-sdk/examples/getting-started-pattern.md @@ -0,0 +1,43 @@ +# Getting Started Pattern (Single Module) + +This pattern shows a minimal Rivet bootstrap for Team Chat with OAuth installation support. + +```javascript +import { TeamChatClient } from "@zoom/rivet/teamchat"; + +(async () => { + const teamchatClient = new TeamChatClient({ + clientId: process.env.RIVET_CLIENT_ID, + clientSecret: process.env.RIVET_CLIENT_SECRET, + webhooksSecretToken: process.env.RIVET_WEBHOOK_SECRET_TOKEN, + installerOptions: { + redirectUri: process.env.RIVET_REDIRECT_URI, + stateStore: process.env.RIVET_STATE_STORE_SECRET, + }, + port: Number(process.env.RIVET_PORT || 8080), + }); + + teamchatClient.webEventConsumer.event("chat_message.sent", ({ payload }) => { + console.log("event", payload); + }); + + const server = await teamchatClient.start(); + console.log("rivet server", server.address()); +})(); +``` + +## Required Marketplace Wiring + +- Add endpoint URL to event subscription with `/zoom/events` suffix. +- Ensure OAuth redirect URI exactly matches `installerOptions.redirectUri` + callback path behavior. +- Include all scopes needed by `endpoints.*` calls. + +## Common Extension + +Add API calls with typed wrappers: + +```javascript +const channels = await teamchatClient.endpoints.chatChannels.listUsersChannels({ + path: { userId: "me" }, +}); +``` diff --git a/plugins/zoom-developers/skills/rivet-sdk/examples/multi-client-pattern.md b/plugins/zoom-developers/skills/rivet-sdk/examples/multi-client-pattern.md new file mode 100644 index 00000000..d2dfaf6c --- /dev/null +++ b/plugins/zoom-developers/skills/rivet-sdk/examples/multi-client-pattern.md @@ -0,0 +1,53 @@ +# Multi-Client Pattern (Two Modules, Two Ports) + +Use this when combining chatbot command handling with Team Chat API lookups. + +```javascript +import { ChatbotClient } from "@zoom/rivet/chatbot"; +import { TeamChatClient } from "@zoom/rivet/teamchat"; + +const CHATBOT_PORT = Number(process.env.RIVET_CHATBOT_PORT || 4001); +const TEAMCHAT_PORT = Number(process.env.RIVET_TEAMCHAT_PORT || 4002); + +(async () => { + const chatbotClient = new ChatbotClient({ + clientId: process.env.RIVET_CLIENT_ID, + clientSecret: process.env.RIVET_CLIENT_SECRET, + webhooksSecretToken: process.env.RIVET_WEBHOOK_SECRET_TOKEN, + port: CHATBOT_PORT, + }); + + const teamchatClient = new TeamChatClient({ + clientId: process.env.RIVET_CLIENT_ID, + clientSecret: process.env.RIVET_CLIENT_SECRET, + webhooksSecretToken: process.env.RIVET_WEBHOOK_SECRET_TOKEN, + installerOptions: { + redirectUri: process.env.RIVET_REDIRECT_URI, + stateStore: process.env.RIVET_STATE_STORE_SECRET, + }, + port: TEAMCHAT_PORT, + }); + + chatbotClient.webEventConsumer.onSlashCommand("help", async ({ say }) => { + await say("Rivet bot ready."); + }); + + chatbotClient.webEventConsumer.onSlashCommand("channels", async ({ say, payload }) => { + const result = await teamchatClient.endpoints.chatChannels.listUsersChannels({ + path: { userId: payload.userId }, + }); + + const names = (result.data?.channels || []).map((x) => x.name).join(", "); + await say(`Channels: ${names || "none"}`); + }); + + await teamchatClient.start(); + await chatbotClient.start(); +})(); +``` + +## Operational Notes + +- Subscribe chatbot events to `CHATBOT_PORT` endpoint. +- Complete Team Chat OAuth install flow against `TEAMCHAT_PORT`. +- Keep ports unique and explicit in documentation and deployment configs. diff --git a/plugins/zoom-developers/skills/rivet-sdk/references/environment-variables.md b/plugins/zoom-developers/skills/rivet-sdk/references/environment-variables.md new file mode 100644 index 00000000..e4a2afc5 --- /dev/null +++ b/plugins/zoom-developers/skills/rivet-sdk/references/environment-variables.md @@ -0,0 +1,44 @@ +# Rivet SDK Environment Variables + +Standardized `.env` keys for Rivet-based integrations. + +## Core Keys + +| Key | Required | Description | Where to find it | +|-----|----------|-------------|------------------| +| `RIVET_CLIENT_ID` | Yes | OAuth or product client ID used by module | Zoom App Marketplace -> your app -> Basic Information -> Client ID | +| `RIVET_CLIENT_SECRET` | Yes | OAuth or product client secret used by module | Zoom App Marketplace -> your app -> Basic Information -> Client Secret | +| `RIVET_WEBHOOK_SECRET_TOKEN` | Receiver flows | Secret used to verify webhook requests | Zoom App Marketplace -> Feature / Event Subscriptions / Access page -> Secret Token | +| `RIVET_ACCOUNT_ID` | S2S only | Account ID for Server-to-Server OAuth module clients | Zoom App Marketplace -> Server-to-Server OAuth app -> App Credentials | +| `RIVET_REDIRECT_URI` | User OAuth only | Redirect URI for install/callback flow | Zoom App Marketplace -> OAuth settings -> Redirect URL | +| `RIVET_STATE_STORE_SECRET` | User OAuth only | State store signing secret | Generated by you (store in secret manager) | + +## Port Keys + +| Key | Required | Description | +|-----|----------|-------------| +| `RIVET_PORT` | Optional | Single-module HTTP receiver port (default often 8080) | +| `RIVET_CHATBOT_PORT` | Multi-module | Chatbot module port | +| `RIVET_TEAMCHAT_PORT` | Multi-module | Team Chat module port | +| `RIVET_USERS_PORT` | Multi-module | Users module port | +| `RIVET_MEETINGS_PORT` | Multi-module | Meetings module port | +| `RIVET_PHONE_PORT` | Multi-module | Phone module port | +| `RIVET_ACCOUNTS_PORT` | Multi-module | Accounts module port | +| `RIVET_VIDEOSDK_PORT` | Multi-module | Video SDK module port | + +## Video SDK-Specific Note + +Rivet's `videosdk` module constructor uses `clientId`/`clientSecret` fields, but those map to Video SDK credentials for the app type you configure. Validate with current Video SDK credential model before release. + +## Example `.env` + +```env +RIVET_CLIENT_ID="..." +RIVET_CLIENT_SECRET="..." +RIVET_WEBHOOK_SECRET_TOKEN="..." +RIVET_ACCOUNT_ID="..." +RIVET_REDIRECT_URI="http://YOUR_DEV_HOST:4002" +RIVET_STATE_STORE_SECRET="replace-me" +RIVET_CHATBOT_PORT=4001 +RIVET_TEAMCHAT_PORT=4002 +``` diff --git a/plugins/zoom-developers/skills/rivet-sdk/references/full-guide.md b/plugins/zoom-developers/skills/rivet-sdk/references/full-guide.md new file mode 100644 index 00000000..5f0ab0e1 --- /dev/null +++ b/plugins/zoom-developers/skills/rivet-sdk/references/full-guide.md @@ -0,0 +1,82 @@ +# Zoom Rivet SDK + +Background reference for Zoom Rivet as a JavaScript and TypeScript server framework for Zoom integrations. + +Implementation guidance for Zoom Rivet (JavaScript/TypeScript) as a server-side framework for: +- OAuth and token handling +- Webhook event consumption +- Typed REST API endpoint wrappers +- Multi-module server composition + +Official docs: +- https://developers.zoom.us/docs/rivet/ +- https://developers.zoom.us/docs/rivet/javascript/ +- https://zoom.github.io/rivet-javascript/ + +Reference samples: +- https://github.com/zoom/rivet-javascript-sample +- https://github.com/zoom/isv-rivet-starter +- https://github.com/zoom/Rivet-Server-Sample +- https://github.com/zoom/rivet-javascript + +## Routing Guardrail + +- Rivet SDK is a Node.js framework that bundles Zoom auth handling, webhook receivers, and typed API wrappers. +- Rivet is recommended for faster server-side scaffolding, but it is not mandatory. +- At planning start, confirm preference: +- `Do you want Rivet SDK, or direct OAuth + REST without Rivet?` +- Use Rivet when the user wants a Node.js server that combines Zoom auth + webhooks + API calls with minimal glue code. +- If the user only needs direct API calls from an existing backend, chain with [../rest-api/SKILL.md](../../rest-api/SKILL.md). +- If the user is focused on Zoom Team Chat app cards/commands behavior, chain with [../team-chat/SKILL.md](../../team-chat/SKILL.md). +- If the user needs SDK embed (Meeting SDK/Video SDK client runtime), route to [../meeting-sdk/SKILL.md](../../meeting-sdk/SKILL.md) or [../video-sdk/SKILL.md](../../video-sdk/SKILL.md). + +## Quick Links + +Start here: +1. [concepts/architecture-and-lifecycle.md](../concepts/architecture-and-lifecycle.md) +2. [scenarios/high-level-scenarios.md](../scenarios/high-level-scenarios.md) +3. [examples/getting-started-pattern.md](../examples/getting-started-pattern.md) +4. [examples/multi-client-pattern.md](../examples/multi-client-pattern.md) +5. [references/rivet-reference-map.md](../references/rivet-reference-map.md) +6. [references/versioning-and-compatibility.md](../references/versioning-and-compatibility.md) +7. [references/samples-validation.md](../references/samples-validation.md) +8. [references/source-map.md](../references/source-map.md) +9. [references/environment-variables.md](../references/environment-variables.md) +10. [troubleshooting/common-issues.md](../troubleshooting/common-issues.md) +11. [RUNBOOK.md](../RUNBOOK.md) +12. [rivet-sdk.md](../rivet-sdk.md) + +## Common Lifecycle Pattern + +1. Choose modules and auth model per module (Client Credentials, User OAuth, S2S OAuth, Video SDK JWT). +2. Instantiate client(s) with credentials, webhook secret, and per-module port. +3. Register event handlers (`webEventConsumer.event(...)` or shortcuts). +4. Implement API calls through `client.endpoints.*`. +5. Start receiver(s) and expose webhook endpoint(s) (`/zoom/events`) to Zoom. +6. Persist tokens/state for OAuth workloads and enforce signature verification. +7. Monitor module-specific failures and rotate secrets/version with changelog cadence. + +## High-Level Scenarios + +- Team Chat slash-command bot + Team Chat data API enrichment. +- Multi-module backend (Users + Meetings + Team Chat + Phone) sharing one process. +- Video SDK telemetry backend using `videosdk` module event stream + API surfaces. +- ISV orchestration layer with tenant-aware token storage and per-module webhooks. +- AWS Lambda webhook processor with Rivet `AwsLambdaReceiver`. + +See [scenarios/high-level-scenarios.md](../scenarios/high-level-scenarios.md) for details. + +## Chaining + +- OAuth architecture and grant selection: [../oauth/SKILL.md](../../oauth/SKILL.md) +- API endpoint semantics and request payload details: [../rest-api/SKILL.md](../../rest-api/SKILL.md) +- Team Chat app cards, command and bot UX: [../team-chat/SKILL.md](../../team-chat/SKILL.md) +- Video SDK API-specific behavior and BYOS context: [../video-sdk/SKILL.md](../../video-sdk/SKILL.md) + +## Environment Variables + +- See [references/environment-variables.md](../references/environment-variables.md) for standardized `.env` keys and where to find each value. + +## Operations + +- [RUNBOOK.md](../RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/rivet-sdk/references/rivet-reference-map.md b/plugins/zoom-developers/skills/rivet-sdk/references/rivet-reference-map.md new file mode 100644 index 00000000..1ada5ef1 --- /dev/null +++ b/plugins/zoom-developers/skills/rivet-sdk/references/rivet-reference-map.md @@ -0,0 +1,41 @@ +# Rivet Reference Map + +## Canonical Documentation + +- Product docs: https://developers.zoom.us/docs/rivet/ +- JavaScript docs: https://developers.zoom.us/docs/rivet/javascript/ +- TypeDoc reference: https://zoom.github.io/rivet-javascript/modules.html + +## Modules (TypeDoc) + +From `@zoom/rivet` TypeDoc module index: +- Accounts +- Chatbot +- Meetings +- Phone +- Team Chat +- Users +- Video SDK + +Each module generally exposes: +- `*Client` class(es) +- `*Endpoints` wrapper class +- `*EventProcessor` +- `HttpReceiver` and `AwsLambdaReceiver` +- Shared option/types/error surfaces + +## Key API Shapes + +- Event subscription: +- `client.webEventConsumer.event(eventName, handler)` +- Event shortcut examples: +- `onSlashCommand`, `onButtonClick`, `onChannelMessagePosted` +- Endpoint wrappers: +- `client.endpoints..({ path, query, body })` + +## Important Links for Validation + +- Rivet sample app: https://github.com/zoom/rivet-javascript-sample +- ISV starter sample: https://github.com/zoom/isv-rivet-starter +- Rivet server sample: https://github.com/zoom/Rivet-Server-Sample +- Rivet package source: https://github.com/zoom/rivet-javascript diff --git a/plugins/zoom-developers/skills/rivet-sdk/references/samples-validation.md b/plugins/zoom-developers/skills/rivet-sdk/references/samples-validation.md new file mode 100644 index 00000000..c6ac9201 --- /dev/null +++ b/plugins/zoom-developers/skills/rivet-sdk/references/samples-validation.md @@ -0,0 +1,41 @@ +# Rivet Sample Validation and Observed Drift + +Validated sources: +- `zoom/rivet-javascript-sample` +- `zoom/isv-rivet-starter` +- `zoom/Rivet-Server-Sample` +- `zoom/rivet-javascript` + +## Lifecycle Patterns Confirmed + +- Module clients are instantiated with auth + receiver options. +- Handlers are registered before or near startup. +- `client.start()` bootstraps receiver/server. +- Multi-module samples use unique ports per module. +- `/zoom/events` endpoint suffix is required for webhook callbacks. + +## Architecture Patterns Confirmed + +- Rivet acts as an orchestration layer: +- Typed endpoint wrappers for API operations. +- Webhook consumer methods for events. +- OAuth helper behavior embedded in client lifecycle. + +## Useful Additions Incorporated into Skill + +- Multi-module port segregation and webhook endpoint mapping. +- Distinct auth patterns by module. +- Sample-derived operational gotchas for ngrok and OAuth install. + +## Contradictions and Drift Notes + +- Some docs/samples reference older Team Chat doc paths (`team-chat-apps`) while current docs may use updated routing. +- Sample env variable naming is inconsistent across repos (`StS_*`, `WEBHOOK_SECRET_TOKEN`, per-module keys). This skill standardizes names in `environment-variables.md`. +- Some sample README commands imply one port while module receivers may actually use `base+1` or per-module ports. +- User OAuth behavior depends on receiver choice; `AwsLambdaReceiver` limitations must be handled explicitly. + +## Recommendations + +- Keep a local compatibility table: `rivet_version` x `modules_used` x `auth_flows` x `receiver_type`. +- Treat sample repos as patterns, not strict source of truth. +- Re-check TypeDoc and changelog before each release. diff --git a/plugins/zoom-developers/skills/rivet-sdk/references/source-map.md b/plugins/zoom-developers/skills/rivet-sdk/references/source-map.md new file mode 100644 index 00000000..e9123959 --- /dev/null +++ b/plugins/zoom-developers/skills/rivet-sdk/references/source-map.md @@ -0,0 +1,19 @@ +# Rivet Source Map + +## Crawled Docs (local raw-docs) + +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/javascript.md` +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/javascript/get-started.md` +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/javascript/authorization.md` +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/javascript/apis-events.md` +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/javascript/config-options.md` +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/javascript/deployment.md` +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/rivet/javascript/event-shortcuts.md` +- `tools/zoom-crawler/raw-docs/zoom.github.io/rivet-javascript/` (TypeDoc crawl) + +## External Validation Repos + +- `zoom/rivet-javascript-sample` +- `zoom/isv-rivet-starter` +- `zoom/Rivet-Server-Sample` +- `zoom/rivet-javascript` diff --git a/plugins/zoom-developers/skills/rivet-sdk/references/versioning-and-compatibility.md b/plugins/zoom-developers/skills/rivet-sdk/references/versioning-and-compatibility.md new file mode 100644 index 00000000..42bc7f50 --- /dev/null +++ b/plugins/zoom-developers/skills/rivet-sdk/references/versioning-and-compatibility.md @@ -0,0 +1,32 @@ +# Rivet Versioning and Compatibility + +## Upgrade Strategy + +Use the standard upgrade workflow: +- [../../general/references/sdk-upgrade-workflow.md](../../general/references/sdk-upgrade-workflow.md) + +For Rivet, treat upgrades as three parallel checks: +1. `@zoom/rivet` package release changes +2. Underlying Zoom API/event payload changes +3. Marketplace app config and scope changes + +## Compatibility Risks + +- Module/auth behavior drift across versions. +- Type alias or endpoint wrapper signature changes. +- Event payload shape differences for webhook types. +- Receiver behavior changes in Node runtime/Lambda environments. + +## Version Signals + +- `@zoom/rivet` package version in `package.json`. +- TypeDoc module/classes/type aliases under `zoom.github.io/rivet-javascript`. +- Changelog updates under https://developers.zoom.us/changelog/. + +## Safe Upgrade Checklist + +- Pin current and target `@zoom/rivet` versions. +- Compare TypeDoc module pages for changed constructor options and endpoints. +- Validate event names and payload fields used by your handlers. +- Revalidate OAuth install/callback flow and token persistence. +- Revalidate per-module port and `/zoom/events` mapping. diff --git a/plugins/zoom-developers/skills/rivet-sdk/rivet-sdk.md b/plugins/zoom-developers/skills/rivet-sdk/rivet-sdk.md new file mode 100644 index 00000000..49a1e88a --- /dev/null +++ b/plugins/zoom-developers/skills/rivet-sdk/rivet-sdk.md @@ -0,0 +1,15 @@ +# Zoom Rivet SDK (Overview) + +Rivet is a server-side framework for Zoom integrations in JavaScript/TypeScript. + +For full documentation and navigation, start at [SKILL.md](SKILL.md). + +## Quick Links + +- [Architecture and Lifecycle](concepts/architecture-and-lifecycle.md) +- [High-Level Scenarios](scenarios/high-level-scenarios.md) +- [Getting Started Pattern](examples/getting-started-pattern.md) +- [Multi-Client Pattern](examples/multi-client-pattern.md) +- [Reference Map](references/rivet-reference-map.md) +- [Sample Validation](references/samples-validation.md) +- [Common Issues](troubleshooting/common-issues.md) diff --git a/plugins/zoom-developers/skills/rivet-sdk/scenarios/high-level-scenarios.md b/plugins/zoom-developers/skills/rivet-sdk/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..e1883dc4 --- /dev/null +++ b/plugins/zoom-developers/skills/rivet-sdk/scenarios/high-level-scenarios.md @@ -0,0 +1,60 @@ +# Rivet High-Level Scenarios + +## 1) Team Chat Standup Bot with Channel Intelligence + +- Modules: `ChatbotClient` + `TeamChatClient` +- Auth: Client Credentials (chatbot) + User OAuth or S2S for Team Chat APIs +- Flow: +1. Slash command enters chatbot webhook. +2. Bot queries channel and member lists via Team Chat endpoints. +3. Bot posts/updates interactive message cards. +- Risks: +- Misaligned scopes cause endpoint failures. +- Wrong port in Marketplace event subscription prevents callbacks. + +## 2) ISV Admin Automation Service + +- Modules: `UsersS2SAuthClient`, `MeetingsS2SAuthClient`, optionally `AccountsS2SAuthClient` +- Auth: S2S OAuth +- Flow: +1. Backend receives internal request to create/update Zoom resources. +2. Rivet endpoints wrap REST calls. +3. Webhooks confirm completion state. +- Risks: +- Missing `accountId` or stale S2S credentials. +- Event and API schema mismatch across versions. + +## 3) Video SDK API Operations and Recording Workflow + +- Module: `VideoSdkClient` +- Auth: Video SDK JWT +- Flow: +1. Create/list/manage sessions. +2. Manage recording/BYOS/report endpoints. +3. React to session/recording webhooks. +- Risks: +- Wrong credential type (OAuth client vs Video SDK key/secret). +- Ignoring recording and BYOS endpoint field changes after upgrades. + +## 4) AWS Lambda Event Receiver Deployment + +- Module: product-specific client + `AwsLambdaReceiver` +- Flow: +1. Instantiate client with `receiver: new AwsLambdaReceiver(...)`. +2. Export Lambda handler that delegates to `await client.start()` handler. +3. Use API Gateway/serverless-offline for local parity. +- Risks: +- User OAuth expectations with unsupported receiver flow. +- Secret token mismatch across Lambda environments. + +## 5) Multi-Tenant Event Router + +- Modules: one or more, with external token/state stores +- Flow: +1. Receive webhook. +2. Resolve tenant and token context. +3. Execute routed API action. +4. Persist audit and retry state. +- Risks: +- In-memory token store only (tokens lost on restart). +- Missing idempotency for retried webhook events. diff --git a/plugins/zoom-developers/skills/rivet-sdk/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/rivet-sdk/troubleshooting/common-issues.md new file mode 100644 index 00000000..71a47cdd --- /dev/null +++ b/plugins/zoom-developers/skills/rivet-sdk/troubleshooting/common-issues.md @@ -0,0 +1,51 @@ +# Rivet Common Issues + +## Webhooks never reach handlers + +Symptoms: +- `client.start()` succeeds, but no event callbacks fire. + +Checks: +- Marketplace endpoint URL points to correct module port. +- Endpoint includes `/zoom/events` suffix. +- `webhooksSecretToken` matches app configuration. +- Ngrok forward is active and mapped to the receiver port. + +## OAuth install/callback fails + +Symptoms: +- Install page redirects fail or callback errors. + +Checks: +- `installerOptions.redirectUri` exactly matches Marketplace OAuth redirect. +- `stateStore` value is configured and stable. +- Receiver mode supports User OAuth (Lambda receiver caveat). + +## Multi-module runtime conflicts + +Symptoms: +- One module works, another silently fails. + +Checks: +- Every module uses a unique port. +- Event subscriptions target the correct module endpoint. +- Env variables are not accidentally shared with wrong module. + +## API-only mode confusion + +Symptoms: +- OAuth expectations fail when receiver disabled. + +Checks: +- `disableReceiver: true` disables OAuth flow behavior. +- For OAuth + API-only behavior, use receiver-compatible configuration and relax webhook verification as documented. + +## Sample parity mismatches + +Symptoms: +- Following sample exactly still fails in your environment. + +Checks: +- Normalize env key names to your project standard. +- Reconcile sample README assumptions with current TypeDoc signatures. +- Verify module auth type alignment (Client Credentials vs User OAuth vs S2S). diff --git a/plugins/zoom-developers/skills/rtms/RUNBOOK.md b/plugins/zoom-developers/skills/rtms/RUNBOOK.md new file mode 100644 index 00000000..ee6314a5 --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/RUNBOOK.md @@ -0,0 +1,83 @@ +# RTMS 5-Minute Preflight Runbook + +Use this before deep debugging. It catches the highest-frequency RTMS issues fast. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Architecture Assumption + +- RTMS is backend-first media ingestion. +- Frontend is optional and should consume backend outputs (WebSocket/SSE/etc). + +If implementation assumes frontend-only RTMS behavior, redesign first. + +## 2) Confirm Event-Triggered Kickoff + +- Processing starts only after RTMS lifecycle start events: + - `meeting.rtms_started` + - `webinar.rtms_started` + - `session.rtms_started` +- Stop events should deactivate pipeline. + +If media handling starts before lifecycle start, session gating is wrong. + +## 3) Confirm Product-Specific IDs + +- Meetings/Webinars: use `meeting_uuid` +- Video SDK: use `session_id` +- Use `rtms_stream_id` from payload for stream context + +Using wrong ID field commonly breaks handshake/signature. + +## 4) Confirm Webhook Handling Pattern + +- Respond `200` immediately. +- Do heavy work asynchronously. +- Verify webhook signature if secret token is configured. + +Slow webhook responses can trigger retries and duplicate stream attempts. + +## 5) Confirm Connection and Heartbeat + +- Track one active connection per stream/session reference. +- Handle heartbeat ping/pong per protocol. +- Implement reconnection strategy explicitly. + +No heartbeat handling means unexpected disconnects. + +## 6) Confirm Media Subscription/Gating + +- Ensure requested media types match your processing path. +- Reject/ignore media packets for inactive sessions. +- Expose pipeline status endpoint for observability. + +This avoids silent packet handling when lifecycle is not active. + +## 7) Quick Probe Checklist + +- `GET /api/health` returns service alive. +- `GET /api/pipeline/status` shows expected active session count. +- Mock/media probes show: + - media before start -> rejected + - start event -> pipeline active + - media after start -> accepted + +### Copy/Paste Validation Commands + +```bash +curl -sS "$RTMS_BASE_URL/api/health" +curl -sS "$RTMS_BASE_URL/api/pipeline/status" +``` + +Expected: healthy service JSON and correct active pipeline visibility. + +## 8) Fast Decision Tree + +- **No media at all** -> lifecycle event not received or wrong webhook route. +- **Duplicate streams** -> delayed webhook response or no active-session guard. +- **Handshake/auth errors** -> wrong credential pair or wrong session ID field. +- **Frontend appears idle** -> backend bridge not connected, not an RTMS source issue. diff --git a/plugins/zoom-developers/skills/rtms/SKILL.md b/plugins/zoom-developers/skills/rtms/SKILL.md new file mode 100644 index 00000000..c8302690 --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/SKILL.md @@ -0,0 +1,27 @@ +--- +name: zoom-rtms +description: Use when using RTMS. +--- + +# Zoom Realtime Media Streams + +Use this skill when the integration needs real-time meeting or contact-center media as a backend stream. If the workflow needs a visible participant bot, compare against `build-zoom-bot` and `zoom-meeting-sdk-linux` before choosing RTMS. + +## Workflow + +1. Confirm the source surface: Meetings RTMS, Contact Center voice media, or another documented RTMS stream. +2. Decide whether RTMS is sufficient or whether a Meeting SDK bot is required for participant identity, meeting controls, or UI-visible behavior. +3. Implement the WebSocket lifecycle first: event subscription, connection validation, media start, heartbeat, reconnect, and shutdown. +4. Process media types intentionally: audio, video, screen share, chat, live transcript, and metadata have different payload and timing constraints. +5. Design downstream pipelines for buffering, transcription, AI analysis, recording, or tool invocation. +6. Debug by isolating app setup, webhook/event delivery, stream authorization, network connectivity, and media payload decoding. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- Connection architecture: [concepts/connection-architecture.md](concepts/connection-architecture.md) +- Lifecycle flow: [concepts/lifecycle-flow.md](concepts/lifecycle-flow.md) +- Media types: [references/media-types.md](references/media-types.md) +- Data types: [references/data-types.md](references/data-types.md) +- RTMS bot example: [examples/rtms-bot.md](examples/rtms-bot.md) +- Common issues: [troubleshooting/common-issues.md](troubleshooting/common-issues.md) diff --git a/plugins/zoom-developers/skills/rtms/concepts/connection-architecture.md b/plugins/zoom-developers/skills/rtms/concepts/connection-architecture.md new file mode 100644 index 00000000..87d5fe74 --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/concepts/connection-architecture.md @@ -0,0 +1,223 @@ +# RTMS Connection Architecture + +RTMS uses a **two-phase WebSocket design** to separate control plane from data plane. + +## Overview + +> **Multi-Product Note**: The two-phase WebSocket design described here is **identical** for all RTMS products (meetings, webinars, and Video SDK sessions). The only difference is the initial webhook event name and payload ID field. Once connected, the signaling and media protocols are the same. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Zoom Meeting │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Zoom RTMS Backend │ +│ ┌─────────────────────┐ ┌─────────────────────────────┐ │ +│ │ Signaling Server │ │ Media Server │ │ +│ │ (Control Plane) │ │ (Data Plane) │ │ +│ └──────────┬──────────┘ └──────────────┬──────────────┘ │ +└─────────────┼───────────────────────────────┼───────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Your Server │ +│ ┌─────────────────────┐ ┌─────────────────────────────┐ │ +│ │ Signaling Socket │ │ Media Socket │ │ +│ │ - Handshake │ │ - Audio data │ │ +│ │ - Start/Stop │ │ - Video data │ │ +│ │ - Heartbeat │ │ - Transcript │ │ +│ └─────────────────────┘ └─────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Two-Phase Design + +### Phase 1: Signaling WebSocket (Control Plane) + +**Purpose**: Authentication, session control, heartbeats + +| Responsibility | Description | +|----------------|-------------| +| Authentication | Validate signature, establish session | +| Media Server Discovery | Returns media server URL in handshake response | +| Stream Control | Start/stop streaming commands | +| Heartbeat | Keep connection alive (msg_type 12/13) | +| Event Notifications | Participant join/leave, sharing start/stop | + +**URL Source**: From `server_urls` in webhook payload + +**Message Flow**: +``` +Client Signaling Server + │ │ + │──── Handshake Request (1) ────────>│ + │<─── Handshake Response (2) ────────│ <- Contains media_server.server_urls + │ │ + │──── Client Ready (7) ─────────────>│ <- After media handshake complete + │ │ + │<─── Keep Alive Request (12) ───────│ + │──── Keep Alive Response (13) ─────>│ + │ │ +``` + +### Phase 2: Media WebSocket (Data Plane) + +**Purpose**: Actual audio, video, transcript, chat, screen share data + +| Responsibility | Description | +|----------------|-------------| +| Media Configuration | Set audio/video parameters (codec, resolution, fps) | +| Media Streaming | Receive binary media data | +| Heartbeat | Keep connection alive (msg_type 12/13) | + +**URL Source**: From signaling handshake response (`media_server.server_urls.all`) + +**Message Flow**: +``` +Client Media Server + │ │ + │──── Media Handshake Request (3) ──>│ <- With media_params + │<─── Media Handshake Response (4) ──│ + │ │ + │<─── Audio Data (14) ───────────────│ + │<─── Video Data (15) ───────────────│ + │<─── Screen Share Data (16) ────────│ + │<─── Transcript Data (17) ──────────│ + │<─── Chat Data (18) ────────────────│ + │ │ + │<─── Keep Alive Request (12) ───────│ + │──── Keep Alive Response (13) ─────>│ + │ │ +``` + +## Why Two Connections? + +| Benefit | Explanation | +|---------|-------------| +| **Separation of Concerns** | Control logic doesn't interfere with media streaming | +| **Independent Scaling** | Signaling and media servers scale differently | +| **Fault Isolation** | Media reconnection doesn't require re-auth | +| **Split Mode Support** | Each media type can have its own connection | + +## Connection Modes + +### Split Mode (Recommended) + +Each media type gets its own dedicated WebSocket connection: + +``` +Signaling WS ─────┬───> Audio WS + ├───> Video WS + ├───> Transcript WS + └───> Screen Share WS +``` + +**Advantages**: +- Independent reconnection per media type +- Better reliability +- Fault isolation + +### Unified Mode + +One media WebSocket for all media types: + +``` +Signaling WS ─────> Media WS (all types) +``` + +**When to use**: +- Real-time audio+video muxing where sync matters +- Simpler implementation for small projects + +## Signature Generation + +Both signaling and media handshakes require HMAC-SHA256 signature: + +```javascript +// For meetings and webinars: use meeting_uuid +const message = `${clientId},${meetingUuid},${streamId}`; +// For Video SDK: use session_id +const message = `${clientId},${sessionId},${streamId}`; + +// Generic approach: use whichever ID is present +const idValue = payload.meeting_uuid || payload.session_id; +const message = `${clientId},${idValue},${streamId}`; +const signature = crypto.createHmac('sha256', clientSecret) + .update(message) + .digest('hex'); +``` + +> **Important**: Webinars use `meeting_uuid` (not `webinar_uuid`). Video SDK uses `session_id`. + +**Components**: +- `clientId`: OAuth Client ID (General App) or SDK Key (Video SDK App) +- `meetingUuid` / `sessionId`: From webhook payload (`meeting_uuid` for meetings/webinars, `session_id` for Video SDK) +- `streamId`: From webhook payload (`rtms_stream_id`) +- `clientSecret`: OAuth Client Secret (General App) or SDK Secret (Video SDK App) + +## Heartbeat Protocol + +**CRITICAL**: Both connections require heartbeat responses. + +When you receive `msg_type: 12` (Keep Alive Request): + +```javascript +// Immediately respond with msg_type: 13 +ws.send(JSON.stringify({ + msg_type: 13, + timestamp: receivedMessage.timestamp +})); +``` + +**Timeout**: +- Signaling: ~60 seconds without heartbeat response +- Media: ~65 seconds without heartbeat response + +**Failure to respond = connection closed!** + +## Reconnection + +RTMS does **NOT** auto-reconnect. You must implement: + +```javascript +ws.on('close', (code, reason) => { + console.log(`Connection closed: ${code} ${reason}`); + + // Implement exponential backoff + setTimeout(() => { + reconnect(); + }, retryDelay); + + retryDelay = Math.min(retryDelay * 2, 30000); +}); +``` + +**Timeouts**: +| Connection | Reconnection Window | +|------------|---------------------| +| Signaling | 60 seconds | +| Media | 65 seconds | + +## Server URL Geo-Routing + +Server URLs contain region codes: + +| Code | Location | +|------|----------| +| `sjc` | San Jose, California | +| `iad` | Washington DC | +| `sin` | Singapore | +| `fra` | Frankfurt, Germany | +| `syd` | Sydney, Australia | + +**Example**: `wss://rtms-sjc1.zoom.us/...` + +For production, route to workers in the same region as the Zoom server for lower latency. + +## Next Steps + +- **[Lifecycle Flow](lifecycle-flow.md)** - Complete webhook-to-streaming sequence +- **[SDK Quickstart](../examples/sdk-quickstart.md)** - SDK handles all this for you +- **[Manual WebSocket](../examples/manual-websocket.md)** - Full protocol implementation diff --git a/plugins/zoom-developers/skills/rtms/concepts/lifecycle-flow.md b/plugins/zoom-developers/skills/rtms/concepts/lifecycle-flow.md new file mode 100644 index 00000000..1cc1c006 --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/concepts/lifecycle-flow.md @@ -0,0 +1,494 @@ +# RTMS Lifecycle Flow + +Complete flow from meeting/webinar/session start to media streaming. + +## High-Level Flow + +``` +┌─────────────────────────────┐ +│ Meeting/Webinar/Session │ +│ Starts │ +└────────────┬────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ Zoom sends webhook event │ +│ meeting.rtms_started OR │ +│ webinar.rtms_started OR │ +│ session.rtms_started │ +└────────────┬────────────────┘ + │ + ▼ +┌──────────────────┐ +│ Your server │ +│ receives │ +│ webhook │ +│ │ +│ RESPOND 200 │ +│ IMMEDIATELY! │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ Connect to │ +│ Signaling WS │ +│ │ +│ Send handshake │ +│ (msg_type: 1) │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ Receive │ +│ handshake resp │ +│ (msg_type: 2) │ +│ │ +│ Extract media │ +│ server URL │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ Connect to │ +│ Media WS │ +│ │ +│ Send handshake │ +│ (msg_type: 3) │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ Receive media │ +│ handshake resp │ +│ (msg_type: 4) │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ Send Client │ +│ Ready to │ +│ Signaling │ +│ (msg_type: 7) │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ Receive media │ +│ data: │ +│ - Audio (14) │ +│ - Video (15) │ +│ - Share (16) │ +│ - Transcript(17)│ +│ - Chat (18) │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ Respond to │ +│ heartbeats │ +│ (12 -> 13) │ +└────────┬─────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ Optional control-plane │ +│ actions during stream │ +│ - EVENT_SUBSCRIPTION │ +│ - VIDEO_SUBSCRIPTION_REQ │ +│ - STREAM_CLOSE_REQ │ +└────────────┬────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ meeting/webinar/session │ +│ .rtms_stopped │ +│ │ +│ Close sockets │ +│ Cleanup │ +└─────────────────────────────┘ +``` + +## Detailed Steps + +### Step 1: Receive Webhook + +When RTMS starts, Zoom sends a webhook. The event name and payload differ by product: + +**Meeting RTMS:** +```json +{ + "event": "meeting.rtms_started", + "payload": { + "account_id": "abc123", + "object": { + "meeting_id": "123456789", + "meeting_uuid": "AbC123...", + "host_id": "user123", + "rtms_stream_id": "stream123==", + "server_urls": "wss://rtms-sjc1.zoom.us/...", + "signature": "pre_computed_signature" + } + } +} +``` + +**Webinar RTMS:** +```json +{ + "event": "webinar.rtms_started", + "payload": { + "account_id": "abc123", + "object": { + "meeting_id": "123456789", + "meeting_uuid": "AbC123...", + "host_id": "user123", + "rtms_stream_id": "stream123==", + "server_urls": "wss://rtms-sjc1.zoom.us/...", + "signature": "pre_computed_signature" + } + } +} +``` + +> **Note**: Webinar payloads use `meeting_uuid`, NOT `webinar_uuid`. + +**Video SDK RTMS:** +```json +{ + "event": "session.rtms_started", + "payload": { + "account_id": "abc123", + "object": { + "session_id": "SessionABC...", + "rtms_stream_id": "stream123==", + "server_urls": "wss://rtms-sjc1.zoom.us/...", + "signature": "pre_computed_signature" + } + } +} +``` + +> **Note**: Video SDK payloads use `session_id` instead of `meeting_uuid`. + +### Product Differences + +| Aspect | Meetings | Webinars | Video SDK | +|--------|----------|----------|-----------| +| Webhook event | `meeting.rtms_started` | `webinar.rtms_started` | `session.rtms_started` | +| Payload ID field | `meeting_uuid` | `meeting_uuid` (same!) | `session_id` | +| App type | General App (OAuth) | General App (OAuth) | Video SDK App (SDK Key/Secret) | +| Participants | All participants | Panelists have full streams; attendees may not | All participants | +| Protocol after connect | Identical | Identical | Identical | + +**CRITICAL**: Respond with HTTP 200 **IMMEDIATELY** before any processing! + +```javascript +const RTMS_EVENTS = ['meeting.rtms_started', 'webinar.rtms_started', 'session.rtms_started']; + +app.post('/webhook', (req, res) => { + res.status(200).send(); // FIRST! + + const { event, payload } = req.body; + if (RTMS_EVENTS.includes(event)) { + handleRTMSStarted(payload); + } +}); +``` + +**Why?** If you delay, Zoom retries the webhook. The retry creates a second connection, which kicks out your first connection. + +### Step 2: Connect to Signaling WebSocket + +```javascript +const signalingWs = new WebSocket(payload.server_urls); + +// Use meeting_uuid for meetings/webinars, session_id for Video SDK +const idValue = payload.meeting_uuid || payload.session_id; + +signalingWs.on('open', () => { + const signature = generateSignature( + CLIENT_ID, + idValue, + payload.rtms_stream_id, + CLIENT_SECRET + ); + + signalingWs.send(JSON.stringify({ + msg_type: 1, // Handshake request + protocol_version: 1, + meeting_uuid: idValue, + rtms_stream_id: payload.rtms_stream_id, + signature: signature, + media_type: 9 // Audio(1) + Transcript(8) + })); +}); +``` + +### Step 3: Handle Signaling Response + +```javascript +signalingWs.on('message', (data) => { + const msg = JSON.parse(data); + + switch (msg.msg_type) { + case 2: // Handshake response + if (msg.status_code === 0) { + // Extract media server URL + const mediaUrl = msg.media_server.server_urls.all; + connectToMediaServer(mediaUrl); + } else { + console.error('Handshake failed:', msg.status_code); + } + break; + + case 12: // Keep alive request + signalingWs.send(JSON.stringify({ + msg_type: 13, + timestamp: msg.timestamp + })); + break; + } +}); +``` + +### Step 4: Connect to Media WebSocket + +```javascript +function connectToMediaServer(mediaUrl) { + const mediaWs = new WebSocket(mediaUrl); + + mediaWs.on('open', () => { + mediaWs.send(JSON.stringify({ + msg_type: 3, // Media handshake request + protocol_version: 1, + meeting_uuid: idValue, // meeting_uuid or session_id + rtms_stream_id: streamId, + signature: signature, + media_type: 9, // Audio + Transcript + payload_encryption: false, + media_params: { + audio: { + content_type: 2, // RAW_AUDIO + sample_rate: 1, // 16kHz + channel: 1, // Mono + codec: 1, // L16 (PCM) + data_opt: 1, // Mixed stream + send_rate: 20 // 20ms chunks + }, + transcript: { + content_type: 5, // TEXT + src_language: 9, // English + enable_lid: false // Fixed language, no auto-switch + } + } + })); + }); +} +``` + +### Step 5: Start Streaming + +After media handshake succeeds, tell signaling you're ready: + +```javascript +mediaWs.on('message', (data) => { + const msg = JSON.parse(data); + + if (msg.msg_type === 4 && msg.status_code === 0) { + // Media handshake success - tell signaling we're ready + signalingWs.send(JSON.stringify({ + msg_type: 7, // Client ready + rtms_stream_id: streamId + })); + } +}); +``` + +### Step 6: Receive Media Data + +```javascript +mediaWs.on('message', (data) => { + const msg = JSON.parse(data); + + switch (msg.msg_type) { + case 14: // Audio + const audioBuffer = Buffer.from(msg.content, 'base64'); + processAudio(audioBuffer, msg.user_name, msg.timestamp); + break; + + case 15: // Video + const videoBuffer = Buffer.from(msg.content, 'base64'); + processVideo(videoBuffer, msg.user_name, msg.timestamp); + break; + + case 16: // Screen share + const shareBuffer = Buffer.from(msg.content, 'base64'); + processScreenShare(shareBuffer, msg.user_name, msg.timestamp); + break; + + case 17: // Transcript + console.log(`${msg.user_name}: ${msg.content}`); + break; + + case 18: // Chat + console.log(`[Chat] ${msg.user_name}: ${msg.content}`); + break; + + case 12: // Keep alive + mediaWs.send(JSON.stringify({ + msg_type: 13, + timestamp: msg.timestamp + })); + break; + } +}); +``` + +### Step 6A: Track Available Participant Video Streams + +When using the new single-individual-video mode, the signaling socket tells you whose camera is currently available. + +```javascript +const activeVideoUsers = new Set(); + +function handleEventUpdate(msg) { + const eventType = msg.event?.event_type; + const participants = msg.event?.participants || []; + + if (eventType === 8) { // PARTICIPANT_VIDEO_ON + for (const participant of participants) activeVideoUsers.add(participant.user_id); + } + + if (eventType === 9) { // PARTICIPANT_VIDEO_OFF + for (const participant of participants) activeVideoUsers.delete(participant.user_id); + } +} +``` + +Use these events as the control-plane signal for which participant video streams are currently subscribable. + +### Step 6B: Select One Participant Video Stream + +```javascript +function subscribeToParticipantVideo(streamId, userId) { + const signalingWs = signalingConnections.get(streamId); + if (!signalingWs) return; + + signalingWs.send(JSON.stringify({ + msg_type: 28, // VIDEO_SUBSCRIPTION_REQ + user_id: userId, + subscribe: true, + timestamp: Date.now() + })); +} +``` + +Important constraint: + +- only one participant stream can be active at a time +- the newest successful subscription replaces the previous selection + +### Step 7: Handle Session End + +```javascript +const RTMS_STOP_EVENTS = ['meeting.rtms_stopped', 'webinar.rtms_stopped', 'session.rtms_stopped']; + +// Via webhook +app.post('/webhook', (req, res) => { + res.status(200).send(); + + const { event, payload } = req.body; + + if (RTMS_STOP_EVENTS.includes(event)) { + const streamId = payload.rtms_stream_id; + + // Close connections + signalingConnections.get(streamId)?.close(); + mediaConnections.get(streamId)?.close(); + + // Cleanup + signalingConnections.delete(streamId); + mediaConnections.delete(streamId); + } +}); + +// Also handle WebSocket close events +signalingWs.on('close', (code, reason) => { + console.log('Signaling closed:', code, reason); + // Implement reconnection if needed +}); +``` + +### Optional: Client-Initiated Graceful Close + +The backend can now ask RTMS to terminate the stream cleanly: + +```javascript +function closeStream(streamId) { + const signalingWs = signalingConnections.get(streamId); + if (!signalingWs) return; + + signalingWs.send(JSON.stringify({ + msg_type: 21, // STREAM_CLOSE_REQ + rtms_stream_id: streamId + })); +} +``` + +Expect a `STREAM_CLOSE_RESP` followed by normal socket teardown. + +## Session Tracking + +**CRITICAL**: Track active sessions to prevent duplicate connections! + +```javascript +const activeSessions = new Map(); + +function handleRTMSStarted(payload) { + const streamId = payload.rtms_stream_id; + + // Check for existing connection + if (activeSessions.has(streamId)) { + console.log('Already connected to this stream, ignoring'); + return; + } + + // Mark as active (meeting_uuid for meetings/webinars, session_id for Video SDK) + activeSessions.set(streamId, { + startTime: Date.now(), + idValue: payload.meeting_uuid || payload.session_id + }); + + // Connect + connectToRTMS(payload); +} + +function handleRTMSStopped(payload) { + const streamId = payload.rtms_stream_id; + activeSessions.delete(streamId); + // ... cleanup +} +``` + +## Error Handling + +```javascript +// SDK state management (from Arlo sample) +try { + client.join(payload); +} catch (error) { + if (error.message?.includes('Invalid status')) { + console.warn('SDK in invalid state, waiting to retry...'); + + setTimeout(() => { + handleRTMSStarted(payload); + }, 2000); + } +} +``` + +## Next Steps + +- **[SDK Quickstart](../examples/sdk-quickstart.md)** - SDK handles all this automatically +- **[Manual WebSocket](../examples/manual-websocket.md)** - Full implementation code +- **[Common Issues](../troubleshooting/common-issues.md)** - Debugging connection problems diff --git a/plugins/zoom-developers/skills/rtms/examples/ai-integration.md b/plugins/zoom-developers/skills/rtms/examples/ai-integration.md new file mode 100644 index 00000000..f85f7831 --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/examples/ai-integration.md @@ -0,0 +1,436 @@ +# AI Integration Patterns + +Patterns for integrating RTMS with AI services for transcription, analysis, and meeting assistants. These examples work with meetings, webinars, and Video SDK sessions. + +## Audio Transcription with External Services + +### Deepgram Integration + +```javascript +import rtms from "@zoom/rtms"; +import { createClient } from "@deepgram/sdk"; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; +const deepgram = createClient(process.env.DEEPGRAM_API_KEY); + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + // Configure for Deepgram-compatible audio + client.setAudioParams({ + codec: 1, // L16 (PCM) + sampleRate: 1, // 16kHz + channel: 1, // Mono + dataOpt: 1 // Mixed stream + }); + + // Create live transcription connection + const connection = deepgram.listen.live({ + model: "nova-2", + language: "en", + smart_format: true, + punctuate: true, + }); + + connection.on("Results", (data) => { + const transcript = data.channel.alternatives[0].transcript; + if (transcript) { + console.log(`[Deepgram]: ${transcript}`); + } + }); + + client.onAudioData((buffer, timestamp, metadata) => { + // Send audio to Deepgram + connection.send(buffer); + }); + + client.onLeave(() => { + connection.finish(); + }); + + client.join(payload); +}); +``` + +### AssemblyAI Integration + +```javascript +import rtms from "@zoom/rtms"; +import { AssemblyAI } from "assemblyai"; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; +const aai = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY }); + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + client.setAudioParams({ + codec: 1, // L16 (PCM) + sampleRate: 1, // 16kHz + channel: 1 // Mono + }); + + const transcriber = aai.realtime.createService({ + sampleRate: 16000, + }); + + transcriber.connect(); + + transcriber.on("transcript", (transcript) => { + if (transcript.text) { + console.log(`[AssemblyAI]: ${transcript.text}`); + } + }); + + client.onAudioData((buffer, timestamp, metadata) => { + transcriber.sendAudio(buffer); + }); + + client.onLeave(() => { + transcriber.close(); + }); + + client.join(payload); +}); +``` + +### Whisper (Local) Integration + +```javascript +import rtms from "@zoom/rtms"; +import { Whisper } from "whisper-node"; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; +const whisper = new Whisper("base.en"); + +let audioBuffer = Buffer.alloc(0); +const BUFFER_SIZE = 16000 * 10; // 10 seconds at 16kHz + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + client.setAudioParams({ + codec: 1, // L16 (PCM) + sampleRate: 1, // 16kHz + channel: 1 // Mono + }); + + client.onAudioData(async (buffer, timestamp, metadata) => { + // Accumulate audio + audioBuffer = Buffer.concat([audioBuffer, buffer]); + + // Transcribe when buffer is full + if (audioBuffer.length >= BUFFER_SIZE) { + const transcript = await whisper.transcribe(audioBuffer); + console.log(`[Whisper]: ${transcript}`); + audioBuffer = Buffer.alloc(0); + } + }); + + client.join(payload); +}); +``` + +## Meeting Summarization + +### OpenAI/GPT Integration + +```javascript +import rtms from "@zoom/rtms"; +import OpenAI from "openai"; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; +const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); + +const transcripts = []; +let summaryInterval; + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + client.onTranscriptData((buffer, timestamp, metadata) => { + const text = buffer.toString('utf8'); + transcripts.push({ + speaker: metadata.userName, + text: text, + time: new Date(timestamp) + }); + }); + + // Generate summary every 5 minutes + summaryInterval = setInterval(async () => { + if (transcripts.length === 0) return; + + const fullTranscript = transcripts + .map(t => `${t.speaker}: ${t.text}`) + .join('\n'); + + const summary = await openai.chat.completions.create({ + model: "gpt-4", + messages: [ + { + role: "system", + content: "Summarize this meeting transcript. Include key points, decisions, and action items." + }, + { + role: "user", + content: fullTranscript + } + ] + }); + + console.log("Meeting Summary:", summary.choices[0].message.content); + }, 5 * 60 * 1000); + + client.onLeave(async () => { + clearInterval(summaryInterval); + + // Generate final summary + const fullTranscript = transcripts + .map(t => `${t.speaker}: ${t.text}`) + .join('\n'); + + const summary = await openai.chat.completions.create({ + model: "gpt-4", + messages: [ + { + role: "system", + content: `Create a comprehensive meeting summary with: +- Key topics discussed +- Decisions made +- Action items with owners +- Follow-up items` + }, + { + role: "user", + content: fullTranscript + } + ] + }); + + console.log("Final Summary:", summary.choices[0].message.content); + }); + + client.join(payload); +}); +``` + +## Real-Time Sentiment Analysis + +```javascript +import rtms from "@zoom/rtms"; + +async function analyzeSentiment(text) { + // Use any sentiment API (OpenAI, HuggingFace, etc.) + const response = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'gpt-3.5-turbo', + messages: [{ + role: 'user', + content: `Analyze sentiment (positive/neutral/negative): "${text}"` + }] + }) + }); + + const data = await response.json(); + return data.choices[0].message.content; +} + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + let recentTranscripts = []; + + client.onTranscriptData(async (buffer, timestamp, metadata) => { + const text = buffer.toString('utf8'); + recentTranscripts.push(text); + + // Analyze every 10 segments + if (recentTranscripts.length >= 10) { + const combinedText = recentTranscripts.join(' '); + const sentiment = await analyzeSentiment(combinedText); + console.log(`Sentiment: ${sentiment}`); + recentTranscripts = []; + } + }); + + client.join(payload); +}); +``` + +## Audio Recording with Gap Filling + +For continuous playback, fill audio gaps with silence: + +```javascript +import rtms from "@zoom/rtms"; +import fs from 'fs'; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; +const SAMPLE_RATE = 16000; +const BYTES_PER_SAMPLE = 2; // 16-bit +const MS_PER_FRAME = 20; +const BYTES_PER_FRAME = SAMPLE_RATE * BYTES_PER_SAMPLE * MS_PER_FRAME / 1000; + +function generateSilentFrame(durationMs) { + const samples = SAMPLE_RATE * durationMs / 1000; + return Buffer.alloc(samples * BYTES_PER_SAMPLE); +} + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + const streamId = payload.rtms_stream_id; + + const audioStream = fs.createWriteStream(`recordings/${streamId}.pcm`); + let lastTimestamp = null; + + client.setAudioParams({ + codec: 1, // L16 (PCM) + sampleRate: 1, // 16kHz + channel: 1, // Mono + dataOpt: 1, // Mixed stream + duration: 20 // 20ms chunks + }); + + client.onAudioData((buffer, timestamp, metadata) => { + if (lastTimestamp !== null) { + const gap = timestamp - lastTimestamp; + + // Fill gaps >= 500ms with silence + if (gap >= 500) { + const silentFrames = Math.floor(gap / MS_PER_FRAME); + console.log(`Gap detected: ${gap}ms, filling ${silentFrames} frames`); + + for (let i = 0; i < silentFrames; i++) { + audioStream.write(generateSilentFrame(MS_PER_FRAME)); + } + } + } + + lastTimestamp = timestamp; + audioStream.write(buffer); + }); + + client.onLeave(() => { + audioStream.end(); + console.log(`Recording saved: recordings/${streamId}.pcm`); + }); + + client.join(payload); +}); +``` + +## Multi-Format Transcript Output + +Generate VTT, SRT, and TXT simultaneously: + +```javascript +import rtms from "@zoom/rtms"; +import fs from 'fs'; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; + +function formatVttTimestamp(ms) { + const s = Math.floor(ms / 1000); + const m = Math.floor(s / 60); + const h = Math.floor(m / 60); + const msec = ms % 1000; + return `${String(h).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}.${String(msec).padStart(3, '0')}`; +} + +function formatSrtTimestamp(ms) { + return formatVttTimestamp(ms).replace('.', ','); +} + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + const streamId = payload.rtms_stream_id; + + const baseDir = `recordings/${streamId}`; + fs.mkdirSync(baseDir, { recursive: true }); + + fs.writeFileSync(`${baseDir}/transcript.vtt`, 'WEBVTT\n\n'); + let srtIndex = 1; + let startTimestamp = null; + + client.onTranscriptData((buffer, timestamp, metadata) => { + const text = buffer.toString('utf8'); + const userName = metadata.userName; + + if (startTimestamp === null) { + startTimestamp = timestamp; + } + + const relative = timestamp - startTimestamp; + const endTime = relative + 2000; // 2 second duration + + // VTT format + const vttLine = `${formatVttTimestamp(relative)} --> ${formatVttTimestamp(endTime)}\n${userName}: ${text}\n\n`; + fs.appendFileSync(`${baseDir}/transcript.vtt`, vttLine); + + // SRT format + const srtLine = `${srtIndex++}\n${formatSrtTimestamp(relative)} --> ${formatSrtTimestamp(endTime)}\n${userName}: ${text}\n\n`; + fs.appendFileSync(`${baseDir}/transcript.srt`, srtLine); + + // Plain text + const txtLine = `[${new Date(timestamp).toISOString()}] ${userName}: ${text}\n`; + fs.appendFileSync(`${baseDir}/transcript.txt`, txtLine); + }); + + client.join(payload); +}); +``` + +## Environment Variables + +```bash +# Zoom RTMS +ZM_RTMS_CLIENT=your_client_id +ZM_RTMS_SECRET=your_client_secret + +# AI Services +OPENAI_API_KEY=sk-... +DEEPGRAM_API_KEY=... +ASSEMBLYAI_API_KEY=... + +# OpenRouter (free models) +OPENROUTER_API_KEY=sk-or-... +``` + +## Free AI Model Considerations + +When using free models (Gemma, Qwen, DeepSeek via OpenRouter): + +| Limitation | Impact | Solution | +|------------|--------|----------| +| No image support | Can't analyze screen shares | Use paid model or skip image analysis | +| Context limits | Long transcripts may fail | Chunk transcripts, summarize incrementally | +| Rate limiting | May get 429 errors | Implement retry with backoff, stagger requests | + +**Recommended for production**: OpenRouter with `google/gemini-2.5-pro` - supports vision + XML tagging. + +## Next Steps + +- **[SDK Quickstart](sdk-quickstart.md)** - Basic RTMS setup +- **[Manual WebSocket](manual-websocket.md)** - Protocol details +- **[Media Types](../references/media-types.md)** - Audio/video configuration diff --git a/plugins/zoom-developers/skills/rtms/examples/manual-websocket.md b/plugins/zoom-developers/skills/rtms/examples/manual-websocket.md new file mode 100644 index 00000000..e105e70a --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/examples/manual-websocket.md @@ -0,0 +1,580 @@ +# Manual WebSocket Implementation + +Full RTMS protocol implementation without the SDK. Use this for: +- Languages without SDK support +- Custom protocol requirements +- Learning the underlying protocol + +## Overview + +RTMS requires two WebSocket connections: +1. **Signaling WebSocket** - Control plane (handshake, heartbeat, start/stop) +2. **Media WebSocket** - Data plane (audio, video, transcript, chat, share) + +## Complete Implementation + +```javascript +const WebSocket = require('ws'); +const crypto = require('crypto'); +const express = require('express'); + +const app = express(); +app.use(express.json()); + +// Configuration +const CLIENT_ID = process.env.ZOOM_CLIENT_ID; +const CLIENT_SECRET = process.env.ZOOM_CLIENT_SECRET; +const SECRET_TOKEN = process.env.ZOOM_SECRET_TOKEN; + +// Active connections +const signalingConnections = new Map(); +const mediaConnections = new Map(); +const activeSessions = new Map(); +const activeVideoUsers = new Map(); + +// ============================================ +// SIGNATURE GENERATION +// Uses meeting_uuid for meetings/webinars, session_id for Video SDK +// ============================================ + +function generateSignature(clientId, idValue, streamId, clientSecret) { + const message = `${clientId},${idValue},${streamId}`; + return crypto.createHmac('sha256', clientSecret) + .update(message) + .digest('hex'); +} + +// ============================================ +// WEBHOOK HANDLER +// ============================================ + +const RTMS_EVENTS = ['meeting.rtms_started', 'webinar.rtms_started', 'session.rtms_started']; +const RTMS_STOP_EVENTS = ['meeting.rtms_stopped', 'webinar.rtms_stopped', 'session.rtms_stopped']; + +app.post('/webhook', (req, res) => { + // CRITICAL: Respond 200 IMMEDIATELY before any processing! + res.status(200).send(); + + const { event, payload } = req.body; + + // Handle URL validation challenge + if (event === 'endpoint.url_validation') { + const hash = crypto + .createHmac('sha256', SECRET_TOKEN) + .update(payload.plainToken) + .digest('hex'); + return res.json({ + plainToken: payload.plainToken, + encryptedToken: hash + }); + } + + // Handle RTMS events (meetings, webinars, and Video SDK) + if (RTMS_EVENTS.includes(event)) { + handleRTMSStarted(payload.object); + } else if (RTMS_STOP_EVENTS.includes(event)) { + handleRTMSStopped(payload.object); + } +}); + +// ============================================ +// RTMS START HANDLER +// ============================================ + +function handleRTMSStarted(payload) { + const { rtms_stream_id, server_urls } = payload; + // meeting_uuid for meetings/webinars, session_id for Video SDK + const idValue = payload.meeting_uuid || payload.session_id; + + // Prevent duplicate connections + if (activeSessions.has(rtms_stream_id)) { + console.log('Already connected to this stream, ignoring'); + return; + } + + activeSessions.set(rtms_stream_id, { + idValue: idValue, + startTime: Date.now() + }); + + connectToSignaling(idValue, rtms_stream_id, server_urls); +} + +// ============================================ +// SIGNALING WEBSOCKET +// ============================================ + +function connectToSignaling(idValue, streamId, serverUrl) { + console.log('Connecting to signaling:', serverUrl); + + const signature = generateSignature(CLIENT_ID, idValue, streamId, CLIENT_SECRET); + const ws = new WebSocket(serverUrl); + + signalingConnections.set(streamId, ws); + + ws.on('open', () => { + console.log('Signaling connected, sending handshake'); + + ws.send(JSON.stringify({ + msg_type: 1, // SIGNALING_HAND_SHAKE_REQ + protocol_version: 1, + meeting_uuid: idValue, // Works for both meeting_uuid and session_id + rtms_stream_id: streamId, + sequence: Math.floor(Math.random() * 1000000), + signature: signature, + media_type: 9 // AUDIO(1) | TRANSCRIPT(8) + })); + }); + + ws.on('message', (data) => { + const msg = JSON.parse(data.toString()); + handleSignalingMessage(msg, idValue, streamId); + }); + + ws.on('close', (code, reason) => { + console.log('Signaling closed:', code, reason.toString()); + signalingConnections.delete(streamId); + // Implement reconnection logic if needed + }); + + ws.on('error', (error) => { + console.error('Signaling error:', error); + }); +} + +function handleSignalingMessage(msg, idValue, streamId) { + switch (msg.msg_type) { + case 2: // SIGNALING_HAND_SHAKE_RESP + if (msg.status_code === 0) { + console.log('Signaling handshake success'); + + // Extract media server URL and connect + const mediaUrl = msg.media_server.server_urls.all; + connectToMedia(idValue, streamId, mediaUrl); + } else { + console.error('Signaling handshake failed:', msg.status_code); + } + break; + + case 6: // EVENT_UPDATE + handleEventUpdate(msg, streamId); + break; + + case 8: // STREAM_STATE_UPDATE + console.log('Stream state:', msg.state); + break; + + case 9: // SESSION_STATE_UPDATE + console.log('Session state:', msg.state); + break; + + case 12: // KEEP_ALIVE_REQ + const signalingWs = signalingConnections.get(streamId); + if (signalingWs) { + signalingWs.send(JSON.stringify({ + msg_type: 13, // KEEP_ALIVE_RESP + timestamp: msg.timestamp + })); + } + break; + } +} + +function handleEventUpdate(msg, streamId) { + const eventType = msg.event?.event_type ?? msg.event_type; + const participants = msg.event?.participants ?? []; + + switch (eventType) { + case 2: // ACTIVE_SPEAKER_CHANGE + console.log('Active speaker:', msg.user_name); + break; + case 3: // PARTICIPANT_JOIN + console.log('Participant joined:', msg.user_name); + break; + case 4: // PARTICIPANT_LEAVE + console.log('Participant left:', msg.user_name); + break; + case 5: // SHARING_START + console.log('Sharing started by:', msg.user_name); + break; + case 6: // SHARING_STOP + console.log('Sharing stopped'); + break; + case 8: // PARTICIPANT_VIDEO_ON + for (const participant of participants) { + const set = activeVideoUsers.get(streamId) || new Set(); + set.add(participant.user_id); + activeVideoUsers.set(streamId, set); + } + break; + case 9: // PARTICIPANT_VIDEO_OFF + for (const participant of participants) { + activeVideoUsers.get(streamId)?.delete(participant.user_id); + } + break; + } +} + +// ============================================ +// MEDIA WEBSOCKET +// ============================================ + +function connectToMedia(idValue, streamId, mediaUrl) { + console.log('Connecting to media:', mediaUrl); + + const signature = generateSignature(CLIENT_ID, idValue, streamId, CLIENT_SECRET); + const ws = new WebSocket(mediaUrl); + + mediaConnections.set(streamId, ws); + + ws.on('open', () => { + console.log('Media connected, sending handshake'); + + ws.send(JSON.stringify({ + msg_type: 3, // DATA_HAND_SHAKE_REQ + protocol_version: 1, + meeting_uuid: idValue, // Works for both meeting_uuid and session_id + rtms_stream_id: streamId, + signature: signature, + media_type: 9, // AUDIO(1) | TRANSCRIPT(8) + payload_encryption: false, + media_params: { + audio: { + content_type: 2, // RAW_AUDIO + sample_rate: 1, // 16kHz + channel: 1, // Mono + codec: 1, // L16 (PCM) + data_opt: 1, // Mixed stream + send_rate: 20 // 20ms chunks + }, + transcript: { + content_type: 5, // TEXT + src_language: 9, // English + enable_lid: false // Fixed language, no auto-switch + } + } + })); + }); + + ws.on('message', (data) => { + const msg = JSON.parse(data.toString()); + handleMediaMessage(msg, streamId); + }); + + ws.on('close', (code, reason) => { + console.log('Media closed:', code, reason.toString()); + mediaConnections.delete(streamId); + }); + + ws.on('error', (error) => { + console.error('Media error:', error); + }); +} + +function handleMediaMessage(msg, streamId) { + switch (msg.msg_type) { + case 4: // DATA_HAND_SHAKE_RESP + if (msg.status_code === 0) { + console.log('Media handshake success, sending client ready'); + + // Tell signaling we're ready to receive + const signalingWs = signalingConnections.get(streamId); + if (signalingWs) { + signalingWs.send(JSON.stringify({ + msg_type: 7, // CLIENT_READY_ACK + rtms_stream_id: streamId + })); + } + } else { + console.error('Media handshake failed:', msg.status_code); + } + break; + + case 12: // KEEP_ALIVE_REQ + const mediaWs = mediaConnections.get(streamId); + if (mediaWs) { + mediaWs.send(JSON.stringify({ + msg_type: 13, // KEEP_ALIVE_RESP + timestamp: msg.timestamp + })); + } + break; + + case 14: // MEDIA_DATA_AUDIO + handleAudioData(msg); + break; + + case 15: // MEDIA_DATA_VIDEO + handleVideoData(msg); + break; + + case 16: // MEDIA_DATA_SHARE + handleShareData(msg); + break; + + case 17: // MEDIA_DATA_TRANSCRIPT + handleTranscriptData(msg); + break; + + case 18: // MEDIA_DATA_CHAT + handleChatData(msg); + break; + } +} + +// ============================================ +// MEDIA DATA HANDLERS +// ============================================ + +function handleAudioData(msg) { + const audioBuffer = Buffer.from(msg.content, 'base64'); + console.log(`Audio: ${audioBuffer.length} bytes from ${msg.user_name || 'mixed'}`); + + // Process audio: + // - Send to transcription service + // - Save to file + // - Stream to output +} + +function handleVideoData(msg) { + const videoBuffer = Buffer.from(msg.content, 'base64'); + console.log(`Video: ${videoBuffer.length} bytes from ${msg.user_name}`); + + // Process video: + // - Decode H.264/JPG + // - Save frames + // - AI analysis +} + +function handleShareData(msg) { + const shareBuffer = Buffer.from(msg.content, 'base64'); + console.log(`Share: ${shareBuffer.length} bytes from ${msg.user_name}`); +} + +function handleTranscriptData(msg) { + console.log(`[${msg.user_name}]: ${msg.content}`); + + // Save transcript, process with AI, etc. +} + +function handleChatData(msg) { + console.log(`[Chat] ${msg.user_name}: ${msg.content}`); +} + +// ============================================ +// RTMS STOP HANDLER +// ============================================ + +function handleRTMSStopped(payload) { + const streamId = payload.rtms_stream_id; + + console.log('RTMS stopped:', streamId); + + // Close connections + const signalingWs = signalingConnections.get(streamId); + const mediaWs = mediaConnections.get(streamId); + + if (signalingWs) signalingWs.close(); + if (mediaWs) mediaWs.close(); + + // Cleanup + signalingConnections.delete(streamId); + mediaConnections.delete(streamId); + activeSessions.delete(streamId); +} + +// ============================================ +// START SERVER +// ============================================ + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`RTMS server running on port ${PORT}`); +}); +``` + +## Message Type Reference + +### Signaling Messages + +| msg_type | Name | Direction | Description | +|----------|------|-----------|-------------| +| 1 | SIGNALING_HAND_SHAKE_REQ | Client -> Server | Initial handshake | +| 2 | SIGNALING_HAND_SHAKE_RESP | Server -> Client | Handshake response with media URL | +| 5 | EVENT_SUBSCRIPTION | Client -> Server | Subscribe to events | +| 6 | EVENT_UPDATE | Server -> Client | Event notification | +| 7 | CLIENT_READY_ACK | Client -> Server | Ready to receive media | +| 8 | STREAM_STATE_UPDATE | Server -> Client | Stream state changed | +| 9 | SESSION_STATE_UPDATE | Server -> Client | Session state changed | +| 12 | KEEP_ALIVE_REQ | Server -> Client | Heartbeat ping | +| 13 | KEEP_ALIVE_RESP | Client -> Server | Heartbeat pong | + +### Media Messages + +| msg_type | Name | Direction | Description | +|----------|------|-----------|-------------| +| 3 | DATA_HAND_SHAKE_REQ | Client -> Server | Media handshake with params | +| 4 | DATA_HAND_SHAKE_RESP | Server -> Client | Media handshake response | +| 12 | KEEP_ALIVE_REQ | Server -> Client | Heartbeat ping | +| 13 | KEEP_ALIVE_RESP | Client -> Server | Heartbeat pong | +| 14 | MEDIA_DATA_AUDIO | Server -> Client | Audio data | +| 15 | MEDIA_DATA_VIDEO | Server -> Client | Video data | +| 16 | MEDIA_DATA_SHARE | Server -> Client | Screen share data | +| 17 | MEDIA_DATA_TRANSCRIPT | Server -> Client | Transcript data | +| 18 | MEDIA_DATA_CHAT | Server -> Client | Chat message | + +## Media Parameters + +### Audio Parameters + +```javascript +{ + content_type: 2, // 1=RTP, 2=RAW_AUDIO + sample_rate: 1, // 0=8kHz, 1=16kHz, 2=32kHz, 3=48kHz + channel: 1, // 1=Mono, 2=Stereo (OPUS only) + codec: 1, // 1=L16, 2=G.711, 3=G.722, 4=OPUS + data_opt: 1, // 1=Mixed, 2=Multi-streams + send_rate: 20 // Chunk size in ms (multiple of 20) +} + +function subscribeToParticipantVideo(streamId, userId) { + const signalingWs = signalingConnections.get(streamId); + if (!signalingWs) return; + + signalingWs.send(JSON.stringify({ + msg_type: 28, // VIDEO_SUBSCRIPTION_REQ + user_id: userId, + subscribe: true, + timestamp: Date.now() + })); +} + +function closeStream(streamId) { + const signalingWs = signalingConnections.get(streamId); + if (!signalingWs) return; + + signalingWs.send(JSON.stringify({ + msg_type: 21, // STREAM_CLOSE_REQ + rtms_stream_id: streamId + })); +} +``` + +## March 2026 Notes + +- The new `PARTICIPANT_VIDEO_ON` / `PARTICIPANT_VIDEO_OFF` events tell you which participants currently have subscribable camera streams. +- To receive one participant camera feed, use `VIDEO_SINGLE_INDIVIDUAL_STREAM` in the video media handshake and then send `VIDEO_SUBSCRIPTION_REQ`. +- RTMS currently supports only **one** individual participant video stream at a time. A new subscription replaces the previous one. +- `STREAM_CLOSE_REQ` / `STREAM_CLOSE_RESP` let the backend terminate a stream cleanly. +- Exact numeric values: + - `PARTICIPANT_VIDEO_ON = 8` + - `PARTICIPANT_VIDEO_OFF = 9` + - `STREAM_CLOSE_REQ = 21` + - `STREAM_CLOSE_RESP = 22` + - `VIDEO_SUBSCRIPTION_REQ = 28` + - `VIDEO_SUBSCRIPTION_RESP = 29` + +### Video Parameters + +```javascript +{ + content_type: 3, // 3=RAW_VIDEO + codec: 7, // 5=JPG, 6=PNG, 7=H.264 + resolution: 2, // 1=SD, 2=HD, 3=FHD, 4=QHD + fps: 25, // 1-30 (JPG/PNG max 5) + data_opt: 3 // 3=Single active speaker +} +``` + +### Screen Share Parameters + +```javascript +{ + content_type: 3, // 3=RAW_VIDEO + codec: 5, // 5=JPG, 6=PNG, 7=H.264 + resolution: 3, // 1=SD, 2=HD, 3=FHD, 4=QHD + fps: 1 // 1-30 (JPG/PNG max 1) +} +``` + +### Transcript Parameters + +```javascript +{ + content_type: 5, // 5=TEXT + src_language: 9, // 9=English + enable_lid: false // Fixed language, no auto-switch +} +``` + +## Status Codes + +| Code | Name | Description | +|------|------|-------------| +| 0 | STATUS_OK | Success | +| 3 | STATUS_INVALID_SIGNATURE | Invalid signature | +| 8 | STATUS_DUPLICATE_SIGNAL_REQUEST | Duplicate signaling connection | +| 16 | STATUS_DUPLICATE_MEDIA_DATA_CONNECTION | Duplicate media connection | +| 40 | STATUS_INVALID_RTMS_SESSION_ID | Invalid RTMS session ID | +| 43 | STATUS_INVALID_MEDIA_TRANSCRIPT_SROUCE_LANGUAGE | Invalid transcript source language | + +See [Data Types](../references/data-types.md) for complete list. + +## Error Handling + +```javascript +// Implement exponential backoff for reconnection +let retryDelay = 1000; + +ws.on('close', (code, reason) => { + console.log('Connection closed:', code, reason); + + // Don't reconnect if intentionally closed + if (code === 1000) return; + + setTimeout(() => { + reconnect(); + }, retryDelay); + + retryDelay = Math.min(retryDelay * 2, 30000); +}); + +ws.on('error', (error) => { + console.error('WebSocket error:', error); + // Connection will close, triggering reconnection +}); +``` + +## Gap-Filled Audio Recording + +Fill gaps with silence for continuous playback: + +```javascript +function handleAudioData(msg, streamId) { + const now = msg.timestamp; + const last = lastTimestamps.get(streamId) || now; + const gap = now - last; + + // Fill gaps >= 500ms with silence + if (gap >= 500) { + const silentFrames = Math.floor(gap / 20); + console.log(`Filling ${silentFrames} silent frames`); + + for (let i = 0; i < silentFrames; i++) { + const silentFrame = Buffer.alloc(640); // 20ms @ 16kHz mono + writeToFile(silentFrame); + } + } + + lastTimestamps.set(streamId, now); + + const audioBuffer = Buffer.from(msg.content, 'base64'); + writeToFile(audioBuffer); +} +``` + +## Next Steps + +- **[SDK Quickstart](sdk-quickstart.md)** - SDK handles all this complexity +- **[AI Integration](ai-integration.md)** - Transcription and analysis +- **[Data Types](../references/data-types.md)** - All enums and constants diff --git a/plugins/zoom-developers/skills/rtms/examples/rtms-bot.md b/plugins/zoom-developers/skills/rtms/examples/rtms-bot.md new file mode 100644 index 00000000..2b7fcc14 --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/examples/rtms-bot.md @@ -0,0 +1,696 @@ +# RTMS Bot (Real-Time Media Streams) + +Build resilient RTMS bots that access meeting audio, video, transcription, screen share, and chat without joining as a visible participant. + +## Overview + +RTMS bots are invisible read-only services that subscribe to meeting media streams via WebSockets. They do NOT appear in the participant list. + +**Use this approach when:** +- You only need to observe/transcribe (no interaction needed) +- You want invisible operation +- You're processing external meetings (with permission) +- You want minimal resource usage + +**Alternative:** See [Meeting SDK Bot (Linux)](../../meeting-sdk/linux/meeting-sdk-bot.md) for visible participant bots with full meeting control. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ RTMS BOT FLOW │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ 1. Trigger RTMS: REST API or In-Meeting Start │ +│ └── POST /meetings/{meetingId}/rtms │ +│ └── Or: Start RTMS manually from Zoom client │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 2. Wait for Webhook: meeting.rtms_started │ +│ └── Zoom sends signaling + media WebSocket URLs │ +│ └── No webhook = RTMS unavailable (no polling fallback) │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 3. Connect: Signaling WebSocket (Handshake with HMAC) │ +│ └── Generate HMAC-SHA256 signature │ +│ └── Send handshake message │ +│ └── Receive session confirmation │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 4. Connect: Media WebSocket (Subscribe to Streams) │ +│ └── Subscribe to: audio, video, transcription, share, chat │ +│ └── Send keep-alive pings │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 5. Process Media Data │ +│ └── Audio: Opus/PCM streams per speaker │ +│ └── Video: H.264 encoded frames │ +│ └── Transcription: Real-time text with speaker labels │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 6. Mid-Stream: Connection Monitoring │ +│ └── Detect WebSocket close → Exponential backoff retry │ +│ └── Stop after N reconnection attempts │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Skills Required + +| Skill | Purpose | +|-------|---------| +| **zoom-rest-api** | Trigger RTMS start (optional - can also start manually) | +| **rtms** | WebSocket connection, media processing | +| **webhooks** | Receive `meeting.rtms_started` event | + +## Prerequisites + +- Zoom app with RTMS features enabled +- Webhook endpoint (HTTPS, publicly accessible) +- Event subscriptions: `meeting.rtms_started`, `meeting.rtms_stopped` +- Scopes: `meeting:read:admin`, `meeting:write:admin` (if triggering via API) +- RTMS SDK or native WebSocket implementation + +## Configuration + +### Retry Parameters (Customizable) + +```javascript +// config.js or environment variables +const rtmsConfig = { + // WebSocket connection (initial) + connection_timeout_ms: 10000, // Handshake timeout (default: 10s) + connection_max_attempts: 5, // Max connection attempts (default: 5) + connection_retry_delay_ms: 5000, // Constant retry: 5s (default: 5s) + + // Mid-stream reconnection (network failures) + reconnect_max_attempts: 3, // Max reconnection attempts (default: 3) + reconnect_base_delay_ms: 2000, // Initial delay: 2s (default: 2s) + // Exponential backoff: 2s, 4s, 8s... + + // Keep-alive ping + keepalive_interval_ms: 5000, // Send ping every 5s (default: 5s, min: 3s) + keepalive_timeout_ms: 15000, // Expect pong within 15s (default: 15s) + + // Webhook wait timeout + webhook_wait_timeout_ms: 300000 // Wait 5min for webhook (default: 5min) +}; + +// Load from environment variables (recommended for production) +function loadConfig() { + return { + connection_timeout_ms: + parseInt(process.env.RTMS_CONNECTION_TIMEOUT_MS) || 10000, + connection_max_attempts: + parseInt(process.env.RTMS_CONNECTION_MAX_ATTEMPTS) || 5, + connection_retry_delay_ms: + parseInt(process.env.RTMS_CONNECTION_RETRY_DELAY_MS) || 5000, + reconnect_max_attempts: + parseInt(process.env.RTMS_RECONNECT_MAX_ATTEMPTS) || 3, + reconnect_base_delay_ms: + parseInt(process.env.RTMS_RECONNECT_BASE_DELAY_MS) || 2000, + keepalive_interval_ms: + Math.max(parseInt(process.env.RTMS_KEEPALIVE_INTERVAL_MS) || 5000, 3000), + keepalive_timeout_ms: + parseInt(process.env.RTMS_KEEPALIVE_TIMEOUT_MS) || 15000, + webhook_wait_timeout_ms: + parseInt(process.env.RTMS_WEBHOOK_WAIT_TIMEOUT_MS) || 300000 + }; +} +``` + +### Customization Guide + +| Parameter | Default | When to Increase | When to Decrease | +|-----------|---------|------------------|------------------| +| `connection_max_attempts` | 5 | Slow/congested networks | Fast failure detection needed | +| `connection_retry_delay_ms` | 5000 (5s) | High network latency | Local network, low latency | +| `reconnect_max_attempts` | 3 | Critical meetings, unstable network | Cost-sensitive, batch processing | +| `reconnect_base_delay_ms` | 2000 (2s) | International connections | Local network | +| `keepalive_interval_ms` | 5000 (5s) | Aggressive connection monitoring | Reduce bandwidth overhead | +| `webhook_wait_timeout_ms` | 300000 (5min) | Meetings may start late | Fast failure detection | + +**Recommended Ranges:** +- Connection attempts: 3-10 +- Connection retry delay: 2s-15s +- Reconnect attempts: 2-5 +- Reconnect base delay: 1s-5s +- Keep-alive interval: 3s-30s (min: 3s per Zoom docs) + +**Examples:** + +```bash +# High-priority production bot (aggressive) +export RTMS_CONNECTION_MAX_ATTEMPTS=10 +export RTMS_CONNECTION_RETRY_DELAY_MS=3000 # 3s +export RTMS_RECONNECT_MAX_ATTEMPTS=5 +export RTMS_RECONNECT_BASE_DELAY_MS=1000 # 1s +export RTMS_KEEPALIVE_INTERVAL_MS=3000 # 3s (minimum) + +# Cost-sensitive batch processing (conservative) +export RTMS_CONNECTION_MAX_ATTEMPTS=3 +export RTMS_CONNECTION_RETRY_DELAY_MS=10000 # 10s +export RTMS_RECONNECT_MAX_ATTEMPTS=2 +export RTMS_RECONNECT_BASE_DELAY_MS=5000 # 5s +export RTMS_KEEPALIVE_INTERVAL_MS=15000 # 15s + +# Development/testing (fail fast) +export RTMS_CONNECTION_MAX_ATTEMPTS=2 +export RTMS_CONNECTION_RETRY_DELAY_MS=2000 # 2s +export RTMS_RECONNECT_MAX_ATTEMPTS=1 +export RTMS_RECONNECT_BASE_DELAY_MS=1000 # 1s +export RTMS_WEBHOOK_WAIT_TIMEOUT_MS=60000 # 1min +``` + +## Step 1: Trigger RTMS (Optional - REST API) + +You can start RTMS programmatically or manually from the Zoom client. + +### Option A: REST API Trigger + +```bash +# Start RTMS for a meeting +curl -X POST "https://api.zoom.us/v2/meetings/{meetingId}/rtms" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "type": "meeting" + }' +``` + +**Response:** +```json +{ + "rtms_id": "abc123def456", + "status": "starting" +} +``` + +### Option B: Manual Start (In-Meeting) + +Host clicks **Apps** → Your RTMS App → **Start RTMS** + +**Both trigger the same webhook** → `meeting.rtms_started` + +## Step 2: Wait for Webhook (Required) + +**CRITICAL:** RTMS requires a webhook. There is NO polling alternative. If webhook doesn't arrive, RTMS is unavailable. + +### Webhook Handler + +```javascript +const express = require('express'); +const crypto = require('crypto'); +const app = express(); + +const config = loadConfig(); +const pendingConnections = new Map(); // Track webhook waiters + +app.post('/webhook', express.json(), async (req, res) => { + // 1. Verify webhook signature + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + + if (!verifyWebhookSignature(req.body, signature, timestamp)) { + return res.status(403).send('Invalid signature'); + } + + // 2. Respond immediately (Zoom expects 200 within 3s) + res.status(200).send(); + + // 3. Process webhook asynchronously + const event = req.body; + + if (event.event === 'meeting.rtms_started') { + console.log('[WEBHOOK] RTMS started for meeting:', event.payload.object.uuid); + + const rtmsInfo = { + meetingUuid: event.payload.object.uuid, + signalingUrl: event.payload.object.signaling_url, + mediaUrl: event.payload.object.media_url, + sessionKey: event.payload.object.session_key + }; + + // Notify waiting connection + const waiter = pendingConnections.get(rtmsInfo.meetingUuid); + if (waiter) { + waiter.resolve(rtmsInfo); + pendingConnections.delete(rtmsInfo.meetingUuid); + } else { + // No waiter - proactive start + connectToRTMS(rtmsInfo); + } + } +}); + +function verifyWebhookSignature(body, signature, timestamp) { + const message = `v0:${timestamp}:${JSON.stringify(body)}`; + const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET_TOKEN); + const computed = 'v0=' + hmac.update(message).digest('hex'); + return crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(computed) + ); +} +``` + +### Wait for Webhook (with Timeout) + +```javascript +async function waitForRTMSWebhook(meetingUuid) { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + pendingConnections.delete(meetingUuid); + reject(new Error( + `RTMS webhook not received within ${config.webhook_wait_timeout_ms}ms. ` + + `Possible causes: ` + + `(1) Meeting hasn't started, ` + + `(2) RTMS not enabled for this meeting, ` + + `(3) Webhook endpoint unreachable.` + )); + }, config.webhook_wait_timeout_ms); + + pendingConnections.set(meetingUuid, { + resolve: (rtmsInfo) => { + clearTimeout(timeoutId); + resolve(rtmsInfo); + }, + reject + }); + }); +} + +// Usage +try { + console.log('[RTMS] Waiting for webhook...'); + const rtmsInfo = await waitForRTMSWebhook(MEETING_UUID); + console.log('[RTMS] Webhook received, connecting...'); + await connectToRTMS(rtmsInfo); +} catch (error) { + console.error('[RTMS] ABORT:', error.message); +} +``` + +**Error if no webhook:** ABORT. No webhook = RTMS unavailable. No polling alternative. + +## Step 3: Connect to Signaling WebSocket + +### Connection with Retry + +```javascript +const WebSocket = require('ws'); + +async function connectSignalingWithRetry(signalingUrl, sessionKey) { + for (let attempt = 1; attempt <= config.connection_max_attempts; attempt++) { + console.log(`[SIGNALING] Attempt ${attempt}/${config.connection_max_attempts}`); + + try { + const ws = await connectSignalingSocket(signalingUrl, sessionKey); + console.log('[SIGNALING] Connected successfully'); + return ws; + } catch (error) { + console.error(`[SIGNALING] Attempt ${attempt} failed:`, error.message); + + if (attempt < config.connection_max_attempts) { + const delay = config.connection_retry_delay_ms; + console.log(`[SIGNALING] Retrying in ${delay}ms...`); + await sleep(delay); + } + } + } + + throw new Error( + `Failed to connect signaling WebSocket after ${config.connection_max_attempts} attempts` + ); +} + +function connectSignalingSocket(signalingUrl, sessionKey) { + return new Promise((resolve, reject) => { + const ws = new WebSocket(signalingUrl); + const timeoutId = setTimeout(() => { + ws.close(); + reject(new Error('Signaling connection timeout')); + }, config.connection_timeout_ms); + + ws.on('open', () => { + console.log('[SIGNALING] WebSocket opened, sending handshake...'); + + // Generate HMAC signature + const timestamp = Date.now(); + const message = `${timestamp}:${sessionKey}`; + const signature = crypto + .createHmac('sha256', WEBHOOK_SECRET_TOKEN) + .update(message) + .digest('hex'); + + // Send handshake + ws.send(JSON.stringify({ + type: 'handshake', + timestamp, + signature + })); + }); + + ws.on('message', (data) => { + const msg = JSON.parse(data); + + if (msg.type === 'handshake_response') { + clearTimeout(timeoutId); + + if (msg.status === 'success') { + console.log('[SIGNALING] Handshake successful'); + resolve(ws); + } else { + ws.close(); + reject(new Error(`Handshake failed: ${msg.error}`)); + } + } + }); + + ws.on('error', (error) => { + clearTimeout(timeoutId); + reject(error); + }); + + ws.on('close', (code, reason) => { + clearTimeout(timeoutId); + reject(new Error(`Connection closed: ${code} ${reason}`)); + }); + }); +} +``` + +## Step 4: Connect to Media WebSocket + +```javascript +async function connectMediaWithRetry(mediaUrl, signalingWs) { + for (let attempt = 1; attempt <= config.connection_max_attempts; attempt++) { + console.log(`[MEDIA] Attempt ${attempt}/${config.connection_max_attempts}`); + + try { + const ws = await connectMediaSocket(mediaUrl); + console.log('[MEDIA] Connected successfully'); + subscribeToStreams(ws); + setupKeepAlive(ws); + return ws; + } catch (error) { + console.error(`[MEDIA] Attempt ${attempt} failed:`, error.message); + + if (attempt < config.connection_max_attempts) { + const delay = config.connection_retry_delay_ms; + console.log(`[MEDIA] Retrying in ${delay}ms...`); + await sleep(delay); + } + } + } + + throw new Error( + `Failed to connect media WebSocket after ${config.connection_max_attempts} attempts` + ); +} + +function subscribeToStreams(mediaWs) { + // Subscribe to all available streams + mediaWs.send(JSON.stringify({ + type: 'subscribe', + streams: ['audio', 'video', 'transcription', 'share', 'chat'] + })); + + console.log('[MEDIA] Subscribed to: audio, video, transcription, share, chat'); +} +``` + +## Step 5: Keep-Alive Management + +```javascript +function setupKeepAlive(ws) { + let lastPongReceived = Date.now(); + let keepAliveInterval; + let timeoutCheck; + + // Send ping periodically + keepAliveInterval = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.ping(); + console.log('[KEEPALIVE] Ping sent'); + } + }, config.keepalive_interval_ms); + + // Check for pong timeout + timeoutCheck = setInterval(() => { + const timeSinceLastPong = Date.now() - lastPongReceived; + + if (timeSinceLastPong > config.keepalive_timeout_ms) { + console.error('[KEEPALIVE] Pong timeout, closing connection'); + clearInterval(keepAliveInterval); + clearInterval(timeoutCheck); + ws.close(1000, 'Keep-alive timeout'); + } + }, 1000); + + ws.on('pong', () => { + lastPongReceived = Date.now(); + console.log('[KEEPALIVE] Pong received'); + }); + + ws.on('close', () => { + clearInterval(keepAliveInterval); + clearInterval(timeoutCheck); + }); +} +``` + +## Step 6: Mid-Stream Reconnection + +```javascript +class ResilientRTMSConnection { + constructor(rtmsInfo, config) { + this.rtmsInfo = rtmsInfo; + this.config = config; + this.reconnectionAttempt = 0; + this.signalingWs = null; + this.mediaWs = null; + } + + async connect() { + try { + this.signalingWs = await connectSignalingWithRetry( + this.rtmsInfo.signalingUrl, + this.rtmsInfo.sessionKey + ); + + this.mediaWs = await connectMediaWithRetry( + this.rtmsInfo.mediaUrl, + this.signalingWs + ); + + this.setupReconnectionHandlers(); + + } catch (error) { + console.error('[RTMS] Initial connection failed:', error); + throw error; + } + } + + setupReconnectionHandlers() { + const handleDisconnection = (wsType) => async (code, reason) => { + console.error(`[${wsType}] Disconnected: ${code} ${reason}`); + + this.reconnectionAttempt++; + + if (this.reconnectionAttempt > this.config.reconnect_max_attempts) { + console.error( + `[RECONNECT] Giving up after ${this.reconnectionAttempt} attempts` + ); + this.cleanup(); + return; + } + + // Exponential backoff: 2s, 4s, 8s... + const delay = this.config.reconnect_base_delay_ms + * Math.pow(2, this.reconnectionAttempt - 1); + + console.log( + `[RECONNECT] Attempt ${this.reconnectionAttempt}/` + + `${this.config.reconnect_max_attempts} in ${delay}ms...` + ); + + await sleep(delay); + + try { + await this.connect(); + console.log('[RECONNECT] Successfully reconnected'); + this.reconnectionAttempt = 0; // Reset counter + } catch (error) { + console.error('[RECONNECT] Failed:', error.message); + // Handler will be called again if connection fails + } + }; + + this.signalingWs.on('close', handleDisconnection('SIGNALING')); + this.mediaWs.on('close', handleDisconnection('MEDIA')); + + this.signalingWs.on('error', (error) => { + console.error('[SIGNALING] Error:', error.message); + }); + + this.mediaWs.on('error', (error) => { + console.error('[MEDIA] Error:', error.message); + }); + } + + cleanup() { + if (this.signalingWs) this.signalingWs.close(); + if (this.mediaWs) this.mediaWs.close(); + } +} +``` + +### Customizing Reconnection Behavior + +```javascript +// Example: Capped exponential backoff (max 30s) +const delay = Math.min( + config.reconnect_base_delay_ms * Math.pow(2, reconnectionAttempt - 1), + 30000 // Cap at 30s +); + +// Example: Linear backoff instead of exponential +const delay = config.reconnect_base_delay_ms * reconnectionAttempt; + +// Example: Jittered backoff (avoid thundering herd) +const baseDelay = config.reconnect_base_delay_ms * Math.pow(2, reconnectionAttempt - 1); +const jitter = Math.random() * 1000; // Random 0-1000ms +const delay = baseDelay + jitter; +``` + +## Complete Resilient Bot Example + +```javascript +const config = loadConfig(); + +async function main() { + try { + // 1. Optional: Trigger RTMS via REST API + console.log('[RTMS] Triggering RTMS start...'); + await triggerRTMSStart(MEETING_ID); + + // 2. Wait for webhook + console.log('[RTMS] Waiting for meeting.rtms_started webhook...'); + const rtmsInfo = await waitForRTMSWebhook(MEETING_UUID); + + // 3. Connect with resilience + const rtms = new ResilientRTMSConnection(rtmsInfo, config); + await rtms.connect(); + + console.log('[RTMS] Bot is running, processing streams...'); + + // 4. Process media data + rtms.mediaWs.on('message', (data) => { + const frame = parseMediaFrame(data); + processMediaFrame(frame); + }); + + // 5. Handle graceful shutdown + process.on('SIGINT', () => { + console.log('[RTMS] Shutting down...'); + rtms.cleanup(); + process.exit(0); + }); + + } catch (error) { + console.error('[RTMS] ABORT:', error.message); + process.exit(1); + } +} + +main(); +``` + +## Comparison: RTMS Bot vs Meeting SDK Bot + +| Aspect | RTMS Bot | Meeting SDK Bot | +|--------|----------|-----------------| +| **Visibility** | Invisible (read-only service) | Visible participant | +| **Authentication** | REST API trigger + webhook | JWT + OBF token | +| **Join Dependency** | No dependency on participants | Owner must be present | +| **Retry Logic** | Not applicable (webhook-based) | Required (owner presence) | +| **Media Access** | Audio/video/text/share/chat via WebSocket | Raw audio/video/share via SDK | +| **Recording Control** | None (read-only) | Full (local, cloud, raw) | +| **Interaction** | Cannot interact | Can send chat, reactions | +| **Resource Usage** | Lower (WebSocket only) | Higher (full SDK) | +| **Use Case** | Passive transcription, analytics | Interactive bots, recording, moderation | + +**Choose RTMS Bot when:** +- You only need to observe/transcribe +- You want minimal resource usage +- You prefer invisible operation +- You're processing external meetings (with permission) + +**Choose Meeting SDK Bot when:** +- You need to interact with the meeting (chat, reactions) +- You need local recording control +- You want to be visible in participant list +- You're processing your own meetings + +## Troubleshooting + +### Webhook Never Arrives + +**Symptom:** `waitForRTMSWebhook()` times out + +**Solution:** +1. Verify webhook endpoint is HTTPS and publicly accessible +2. Check Event Subscriptions in Zoom Marketplace: `meeting.rtms_started` enabled +3. Verify RTMS was actually started (check Zoom client or REST API response) +4. Increase `webhook_wait_timeout_ms` if meeting starts later than expected +5. Test webhook delivery: `curl -X POST YOUR_WEBHOOK_URL` + +### Signaling Handshake Fails + +**Symptom:** Connection closes immediately after handshake + +**Solution:** +1. Verify HMAC signature generation matches Zoom docs +2. Check timestamp is current (not stale) +3. Verify `WEBHOOK_SECRET_TOKEN` matches Zoom Marketplace config +4. Check signaling URL hasn't expired (short TTL) + +### Keep-Alive Timeout + +**Symptom:** Connection closes with "Keep-alive timeout" + +**Solution:** +1. Network congestion - increase `keepalive_timeout_ms` +2. Server overloaded - increase `keepalive_interval_ms` +3. Verify ping/pong implementation is correct +4. Check firewall/proxy not blocking WebSocket pings + +### Frequent Reconnections + +**Symptom:** Bot reconnects multiple times, then gives up + +**Solution:** +1. Increase `reconnect_max_attempts` (e.g., 5 instead of 3) +2. Increase `reconnect_base_delay_ms` if network is slow +3. Monitor server resources (CPU/memory/network) +4. Check for rate limiting (too many connection attempts) + +## Resources + +- **RTMS Docs**: https://developers.zoom.us/docs/rtms/ +- **RTMS WebSocket Guide**: https://developers.zoom.us/docs/api/websockets/ +- **RTMS SDK**: https://github.com/zoom/rtms +- **Webhook Reference**: [../references/webhooks.md](../references/webhooks.md) +- **Connection Architecture**: [../concepts/connection-architecture.md](../concepts/connection-architecture.md) +- **Meeting SDK Bot Alternative**: [Meeting SDK Bot (Linux)](../../meeting-sdk/linux/meeting-sdk-bot.md) diff --git a/plugins/zoom-developers/skills/rtms/examples/sdk-quickstart.md b/plugins/zoom-developers/skills/rtms/examples/sdk-quickstart.md new file mode 100644 index 00000000..8518b109 --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/examples/sdk-quickstart.md @@ -0,0 +1,365 @@ +# SDK Quickstart + +The fastest way to receive RTMS media using the official `@zoom/rtms` SDK. + +## Installation + +```bash +# Requires Node.js 20.3.0+ (24 LTS recommended) +npm install @zoom/rtms express +``` + +## Environment Setup + +```bash +# .env +ZM_RTMS_CLIENT=your_client_id +ZM_RTMS_SECRET=your_client_secret +``` + +## Multi-Product Support + +The SDK accepts both `meeting_uuid` (meetings/webinars) and `session_id` (Video SDK) via `client.join(payload)` transparently. You only need to handle the different webhook event names -- the rest of the protocol is identical. + +```javascript +// These constants cover all RTMS products +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; +const RTMS_STOP_EVENTS = ["meeting.rtms_stopped", "webinar.rtms_stopped", "session.rtms_stopped"]; +``` + +## Minimal Example + +```javascript +import rtms from "@zoom/rtms"; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; + +// Handle webhook events - SDK starts webhook server automatically +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + client.onTranscriptData((data, timestamp, metadata) => { + const text = data.toString('utf8'); + console.log(`${metadata.userName}: ${text}`); + }); + + // SDK handles all WebSocket complexity + // Accepts both meeting_uuid and session_id transparently + client.join(payload); +}); +``` + +## Complete Example with All Media Types + +```javascript +import rtms from "@zoom/rtms"; +import fs from 'fs'; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; +const RTMS_STOP_EVENTS = ["meeting.rtms_stopped", "webinar.rtms_stopped", "session.rtms_stopped"]; + +const clients = new Map(); + +rtms.onWebhookEvent(({ event, payload }) => { + const streamId = payload?.rtms_stream_id; + + // Handle session end (meetings, webinars, and Video SDK) + if (RTMS_STOP_EVENTS.includes(event)) { + const client = clients.get(streamId); + if (client) { + client.leave(); + clients.delete(streamId); + } + return; + } + + if (!RTMS_EVENTS.includes(event)) return; + + // Prevent duplicate connections + if (clients.has(streamId)) { + console.log('Already connected to this stream'); + return; + } + + const client = new rtms.Client(); + clients.set(streamId, client); + + // Join confirmation + client.onJoinConfirm((reason) => { + console.log(`Joined meeting: ${reason}`); + }); + + // Audio data + client.onAudioData((buffer, timestamp, metadata) => { + console.log(`Audio from ${metadata.userName}: ${buffer.length} bytes`); + // Save to file, send to transcription service, etc. + }); + + // Video data + client.onVideoData((buffer, timestamp, trackId, metadata) => { + console.log(`Video from ${metadata.userName}: ${buffer.length} bytes`); + // H.264 NAL units or JPG/PNG frames + }); + + // Transcript (real-time speech-to-text from Zoom) + client.onTranscriptData((buffer, timestamp, metadata) => { + const text = buffer.toString('utf8'); + console.log(`[${metadata.userName}]: ${text}`); + }); + + // Chat messages + client.onChatData((buffer, timestamp, metadata) => { + const text = buffer.toString('utf8'); + console.log(`[Chat] ${metadata.userName}: ${text}`); + }); + + // Screen share + client.onShareData((buffer, timestamp, metadata) => { + console.log(`Screen share from ${metadata.userName}: ${buffer.length} bytes`); + }); + + // Participant events + client.onParticipantEvent((event, timestamp, participants) => { + participants.forEach(p => { + console.log(`Participant ${event}: ${p.userName}`); + }); + }); + + // Active speaker changed + client.onActiveSpeakerEvent((timestamp, userId, userName) => { + console.log(`Active speaker: ${userName}`); + }); + + // Screen sharing started/stopped + client.onSharingEvent((event, timestamp, userId, userName) => { + console.log(`Sharing ${event}: ${userName}`); + }); + + // Session ended + client.onLeave((reason) => { + console.log(`Left meeting: ${reason}`); + clients.delete(streamId); + }); + + // Join the meeting + client.join(payload); +}); +``` + +## Configuring Audio Parameters + +```javascript +import rtms from "@zoom/rtms"; + +const client = new rtms.Client(); + +// Set audio parameters before joining +client.setAudioParams({ + contentType: 2, // RAW_AUDIO + codec: 4, // OPUS (default) + sampleRate: 3, // 48kHz + channel: 2, // Stereo (only with OPUS) + dataOpt: 2, // AUDIO_MULTI_STREAMS (per-participant) + duration: 20, // 20ms chunks + frameSize: 960 // Samples per frame +}); + +client.join(payload); +``` + +### Audio Parameter Options + +| Parameter | Options | +|-----------|---------| +| `contentType` | 1=RTP, 2=RAW_AUDIO | +| `codec` | 1=L16 (PCM), 2=G.711, 3=G.722, 4=OPUS | +| `sampleRate` | 0=8kHz, 1=16kHz, 2=32kHz, 3=48kHz | +| `channel` | 1=Mono, 2=Stereo (OPUS only!) | +| `dataOpt` | 1=Mixed stream, 2=Multi-streams (per participant) | +| `duration` | Chunk size in ms (multiple of 20, max 1000) | + +## Configuring Video Parameters + +```javascript +client.setVideoParams({ + contentType: 3, // RAW_VIDEO + codec: 7, // H.264 + resolution: 2, // HD (720p) + fps: 25, + dataOpt: 3 // Single active speaker +}); +``` + +### Video Parameter Options + +| Parameter | Options | +|-----------|---------| +| `codec` | 5=JPG, 6=PNG, 7=H.264 | +| `resolution` | 1=SD (480p), 2=HD (720p), 3=FHD (1080p), 4=QHD (1440p) | +| `fps` | 1-30 (JPG/PNG max 5, H.264 max 30) | +| `dataOpt` | 3=Single active speaker | + +## With Express Webhook Handler + +```javascript +import rtms from "@zoom/rtms"; +import express from "express"; + +const app = express(); +app.use(express.json()); + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; + +// Use SDK's webhook handler +app.post('/webhook', rtms.createWebhookHandler(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + client.onTranscriptData((data, timestamp, metadata) => { + console.log(`${metadata.userName}: ${data.toString('utf8')}`); + }); + + client.join(payload); +}, '/webhook')); + +app.listen(3000, () => { + console.log('Server running on port 3000'); +}); +``` + +## Class-Based Approach (Multiple Connections) + +For applications needing multiple concurrent connections: + +```javascript +import rtms from "@zoom/rtms"; + +// Initialize SDK once +rtms.Client.initialize(); + +// Create multiple clients +const client1 = new rtms.Client(); +const client2 = new rtms.Client(); + +client1.onTranscriptData((data, ts, meta) => { + console.log(`[Meeting 1] ${meta.userName}: ${data.toString('utf8')}`); +}); + +client2.onTranscriptData((data, ts, meta) => { + console.log(`[Meeting 2] ${meta.userName}: ${data.toString('utf8')}`); +}); + +// Join different meetings +client1.join(meeting1Payload); +client2.join(meeting2Payload); +``` + +## Error Handling + +```javascript +client.onJoinConfirm((reason) => { + if (reason !== 0) { + console.error(`Join failed with reason: ${reason}`); + // Handle error + } +}); + +client.onLeave((reason) => { + console.log(`Left meeting with reason: ${reason}`); + + // Cleanup + clients.delete(streamId); + + // Optionally reconnect + if (reason === /* unexpected disconnect */) { + setTimeout(() => reconnect(), 2000); + } +}); +``` + +## Python SDK + +```python +import rtms +from dotenv import load_dotenv + +load_dotenv() + +RTMS_EVENTS = ['meeting.rtms_started', 'webinar.rtms_started', 'session.rtms_started'] +RTMS_STOP_EVENTS = ['meeting.rtms_stopped', 'webinar.rtms_stopped', 'session.rtms_stopped'] + +clients = {} + +@rtms.onWebhookEvent +def handle_webhook(webhook): + event = webhook.get('event') + payload = webhook.get('payload', {}) + stream_id = payload.get('rtms_stream_id') + + if event in RTMS_STOP_EVENTS: + if stream_id in clients: + clients[stream_id].leave() + del clients[stream_id] + return + + if event not in RTMS_EVENTS: + return + + client = rtms.Client() + clients[stream_id] = client + + @client.onTranscriptData + def on_transcript(data, size, timestamp, metadata): + text = data.decode('utf-8') + print(f'[{metadata.userName}]: {text}') + + @client.onJoinConfirm + def on_join(reason): + print(f'Joined: {reason}') + + @client.onLeave + def on_leave(reason): + print(f'Left: {reason}') + + # SDK accepts both meeting_uuid and session_id transparently + client.join(payload) + +# Main loop +if __name__ == '__main__': + print('Webhook server running...') + rtms.run() +``` + +## Environment Variables Reference + +```bash +# Required +ZM_RTMS_CLIENT=your_client_id +ZM_RTMS_SECRET=your_client_secret + +# Optional +ZM_RTMS_PORT=8080 # Webhook server port +ZM_RTMS_PATH=/webhook # Webhook endpoint path + +# Logging +ZM_RTMS_LOG_LEVEL=info # error, warn, info, debug, trace +ZM_RTMS_LOG_FORMAT=progressive # progressive or json +ZM_RTMS_LOG_ENABLED=true +``` + +## Common Issues + +| Issue | Solution | +|-------|----------| +| Segmentation fault | Upgrade to Node.js 20.3.0+ (24 LTS recommended) | +| Audio metadata missing userId | Use `onActiveSpeakerEvent` for speaker identification with mixed stream | +| Video params ignored | Call `setVideoParams` BEFORE `setAudioParams` | + +## Next Steps + +- **[Manual WebSocket](manual-websocket.md)** - Full protocol control without SDK +- **[AI Integration](ai-integration.md)** - Transcription and analysis patterns +- **[Media Types](../references/media-types.md)** - All configuration options diff --git a/plugins/zoom-developers/skills/rtms/references/connection.md b/plugins/zoom-developers/skills/rtms/references/connection.md new file mode 100644 index 00000000..f26126fd --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/references/connection.md @@ -0,0 +1,276 @@ +# RTMS - Connection + +WebSocket connection protocol details. + +## Connection Flow + +``` +1. Receive meeting/webinar/session.rtms_started webhook + ↓ +2. Extract server_urls, stream_id, and meeting_uuid or session_id + ↓ +3. Generate signature (HMAC-SHA256) using meeting_uuid or session_id + ↓ +4. Connect to signaling WebSocket + ↓ +5. Send handshake request (msg_type 1) + ↓ +6. Receive handshake response (msg_type 2) with media server URL + ↓ +7. Connect to media WebSocket(s) + ↓ +8. Send media handshake (msg_type 3) + ↓ +9. Receive media handshake response (msg_type 4) + ↓ +10. Send ready to receive (msg_type 7) + ↓ +11. Receive media data (msg_type 14-18) + ↓ +12. Respond to heartbeats (msg_type 12 → 13) + ↓ +13. Optionally react to `PARTICIPANT_VIDEO_ON/OFF`, send `VIDEO_SUBSCRIPTION_REQ`, or gracefully terminate with `STREAM_CLOSE_REQ` +``` + +## Signature Generation + +```javascript +const crypto = require('crypto'); + +// For meetings and webinars: use meeting_uuid +// For Video SDK: use session_id +// Webinars still use meeting_uuid (NOT webinar_uuid) +function generateSignature(clientId, idValue, streamId, clientSecret) { + const message = `${clientId},${idValue},${streamId}`; + return crypto.createHmac('sha256', clientSecret).update(message).digest('hex'); +} + +// Extract the correct ID from any product's webhook payload +const idValue = payload.meeting_uuid || payload.session_id; +``` + +## Signaling Message Types + +| msg_type | Name | Direction | Description | +|----------|------|-----------|-------------| +| 1 | Handshake Request | Client → Server | Initiate connection | +| 2 | Handshake Response | Server → Client | Returns media server URL | +| 3 | Media Handshake Request | Client → Server | Request specific media types | +| 4 | Media Handshake Response | Server → Client | Confirms media subscription | +| 7 | Ready to Receive | Client → Server | Signal ready for data | +| 12 | Keep Alive Request | Server → Client | Heartbeat ping | +| 13 | Keep Alive Response | Client → Server | Heartbeat pong | + +## Media Message Types + +| msg_type | Media Type | +|----------|------------| +| 14 | Audio | +| 15 | Video | +| 16 | Screen Share | +| 17 | Transcript | +| 18 | Chat | + +## Critical Gotchas + +### 1. Only ONE Connection Per Stream! + +```javascript +// WRONG - Connecting twice kicks out first connection +connectToRTMS(serverUrl, streamId); // Connection 1 +connectToRTMS(serverUrl, streamId); // Connection 2 - kicks out Connection 1! + +// CORRECT - Only connect once +if (!activeConnections.has(streamId)) { + connectToRTMS(serverUrl, streamId); + activeConnections.set(streamId, ws); +} +``` + +### 2. Heartbeat is MANDATORY + +When you receive msg_type 12, you MUST respond with msg_type 13: + +```javascript +ws.on('message', (data) => { + const msg = JSON.parse(data); + + if (msg.msg_type === 12) { // Keep Alive Request + ws.send(JSON.stringify({ + msg_type: 13, // Keep Alive Response + timestamp: msg.timestamp + })); + } +}); +``` + +### 3. Reconnection is YOUR Responsibility + +RTMS does NOT auto-reconnect. Implement your own retry logic: + +| Server Type | Timeout | +|-------------|---------| +| Media Server | 65 seconds keep-alive tolerance before timeout | +| Signaling Server | 60 seconds to reconnect | + +```javascript +ws.on('close', () => { + // Implement exponential backoff + setTimeout(() => reconnect(), retryDelay); + retryDelay = Math.min(retryDelay * 2, 30000); +}); +``` + +## Transcript LID Control + +The transcript media handshake now supports explicit Language Identification control. + +```javascript +mediaWs.send(JSON.stringify({ + msg_type: 3, + protocol_version: 1, + meeting_uuid: idValue, + rtms_stream_id: streamId, + signature, + media_type: 8, // TRANSCRIPT + media_params: { + transcript: { + content_type: 5, // TEXT + src_language: 9, // English + enable_lid: false // Lock to src_language instead of auto-switching + } + } +})); +``` + +Use `enable_lid: false` when: + +- the meeting should stay on a known language +- language-switching is undesirable +- you want more predictable downstream transcript processing + +## Single Individual Video Subscription Flow + +RTMS now supports subscribing to **one participant camera stream at a time**. + +1. Open a video media socket with `data_opt = VIDEO_SINGLE_INDIVIDUAL_STREAM` +2. Subscribe to `PARTICIPANT_VIDEO_ON` and `PARTICIPANT_VIDEO_OFF` +3. When an event arrives, choose the `user_id` you want +4. Send `VIDEO_SUBSCRIPTION_REQ` on the signaling socket +5. Wait for `VIDEO_SUBSCRIPTION_RESP` +6. Expect the newest successful subscription to replace the previous participant stream + +```javascript +// Signaling socket: subscribe to control-plane events +signalingWs.send(JSON.stringify({ + msg_type: 5, // EVENT_SUBSCRIPTION + events: [ + { event_type: 8, subscribe: true }, // PARTICIPANT_VIDEO_ON + { event_type: 9, subscribe: true } // PARTICIPANT_VIDEO_OFF + ] +})); + +// Signaling socket: select a participant stream +signalingWs.send(JSON.stringify({ + msg_type: 28, // VIDEO_SUBSCRIPTION_REQ + user_id: selectedUserId, + subscribe: true, + timestamp: Date.now() +})); +``` + +The March 2026 changelog did not publish the numeric values for the new message types. Use the protocol definitions before hard-coding them. + +## Graceful Stream Closure + +The backend can now request clean shutdown over the signaling socket: + +```javascript +signalingWs.send(JSON.stringify({ + msg_type: 21, // STREAM_CLOSE_REQ + rtms_stream_id: streamId +})); +``` + +Expect: + +- `STREAM_CLOSE_RESP` +- then normal connection shutdown / cleanup + +Use this when your app wants deterministic teardown instead of waiting for a stop webhook or socket failure. + +## Split vs Unified Mode + +| Mode | Description | Best For | +|------|-------------|----------| +| **Split** | One connection per media type | Most use cases. Media server supports multiple connections with different media types | +| **Unified** | One connection for all media | Real-time audio+video streaming/muxing where sync matters | + +## Low-Level Connection Example + +```javascript +const WebSocket = require('ws'); +const crypto = require('crypto'); + +async function connectRTMS(webhookPayload) { + const { server_urls, rtms_stream_id } = webhookPayload; + // meeting_uuid for meetings/webinars, session_id for Video SDK + const idValue = webhookPayload.meeting_uuid || webhookPayload.session_id; + + // Generate signature + const signature = crypto + .createHmac('sha256', process.env.ZOOM_CLIENT_SECRET) + .update(`${process.env.ZOOM_CLIENT_ID},${idValue},${rtms_stream_id}`) + .digest('hex'); + + // Connect to signaling server + const signalingWs = new WebSocket(server_urls, { + headers: { + 'X-Zoom-RTMS-Stream-Id': rtms_stream_id, + 'X-Zoom-RTMS-Signature': signature + } + }); + + signalingWs.on('open', () => { + // Send handshake request + signalingWs.send(JSON.stringify({ + msg_type: 1, + protocol_version: 1, + client_id: process.env.ZOOM_CLIENT_ID, + meeting_uuid: idValue, // Works for both meeting_uuid and session_id + stream_id: rtms_stream_id, + signature: signature, + media_type: 9 // AUDIO(1) | TRANSCRIPT(8) + })); + }); + + signalingWs.on('message', (data) => { + const msg = JSON.parse(data); + + switch (msg.msg_type) { + case 2: // Handshake response + // Connect to media server from msg.media_server_url + connectMediaServer(msg.media_server_url); + break; + case 12: // Keep alive request + signalingWs.send(JSON.stringify({ msg_type: 13, timestamp: msg.timestamp })); + break; + } + }); + + signalingWs.on('error', (error) => { + console.error('Signaling error:', error); + }); + + signalingWs.on('close', (code, reason) => { + console.log('Signaling closed:', code, reason); + // Implement reconnection logic + }); +} +``` + +## Resources + +- **RTMS_CONNECTION_FLOW.md**: https://github.com/zoom/rtms-samples/blob/main/RTMS_CONNECTION_FLOW.md +- **ARCHITECTURE.md**: https://github.com/zoom/rtms-samples/blob/main/ARCHITECTURE.md +- **TROUBLESHOOTING.md**: https://github.com/zoom/rtms-samples/blob/main/TROUBLESHOOTING.md diff --git a/plugins/zoom-developers/skills/rtms/references/data-types.md b/plugins/zoom-developers/skills/rtms/references/data-types.md new file mode 100644 index 00000000..47b50d2d --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/references/data-types.md @@ -0,0 +1,322 @@ +# RTMS Data Types + +Complete reference for all RTMS enums and constants. + +## Message Types (RTMS_MESSAGE_TYPE) + +| Value | Name | Direction | Description | +|-------|------|-----------|-------------| +| 0 | UNDEFINED | - | Default | +| 1 | SIGNALING_HAND_SHAKE_REQ | Client -> Server | Signaling handshake request | +| 2 | SIGNALING_HAND_SHAKE_RESP | Server -> Client | Signaling handshake response | +| 3 | DATA_HAND_SHAKE_REQ | Client -> Server | Media handshake request | +| 4 | DATA_HAND_SHAKE_RESP | Server -> Client | Media handshake response | +| 5 | EVENT_SUBSCRIPTION | Client -> Server | Subscribe/unsubscribe events | +| 6 | EVENT_UPDATE | Server -> Client | Event notification | +| 7 | CLIENT_READY_ACK | Client -> Server | Ready to receive media | +| 8 | STREAM_STATE_UPDATE | Server -> Client | Stream state changed | +| 9 | SESSION_STATE_UPDATE | Server -> Client | Session state changed | +| 10 | SESSION_STATE_REQ | Client -> Server | Request session state | +| 11 | SESSION_STATE_RESP | Server -> Client | Session state response | +| 12 | KEEP_ALIVE_REQ | Server -> Client | Heartbeat ping | +| 13 | KEEP_ALIVE_RESP | Client -> Server | Heartbeat pong | +| 14 | MEDIA_DATA_AUDIO | Server -> Client | Audio data | +| 15 | MEDIA_DATA_VIDEO | Server -> Client | Video data | +| 16 | MEDIA_DATA_SHARE | Server -> Client | Screen share data | +| 17 | MEDIA_DATA_TRANSCRIPT | Server -> Client | Transcript data | +| 18 | MEDIA_DATA_CHAT | Server -> Client | Chat data | +| 19 | STREAM_STATE_REQ | Client -> Server | Request stream state | +| 20 | STREAM_STATE_RESP | Server -> Client | Stream state response | +| 21 | STREAM_CLOSE_REQ | Client -> Server | Request graceful stream shutdown | +| 22 | STREAM_CLOSE_RESP | Server -> Client | Response to close stream request | +| 23 | META_DATA_AUDIO | Server -> Client | Audio metadata | +| 24 | META_DATA_VIDEO | Server -> Client | Reserved video metadata | +| 25 | META_DATA_SHARE | Server -> Client | Reserved share metadata | +| 26 | META_DATA_TRANSCRIPT | Server -> Client | Reserved transcript metadata | +| 27 | META_DATA_CHAT | Server -> Client | Reserved chat metadata | +| 28 | VIDEO_SUBSCRIPTION_REQ | Client -> Server | Subscribe or unsubscribe one participant video stream | +| 29 | VIDEO_SUBSCRIPTION_RESP | Server -> Client | Response to participant video subscription | + +## Event Types (RTMS_EVENT_TYPE) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | UNDEFINED | Default | +| 1 | FIRST_PACKET_TIMESTAMP | First packet received | +| 2 | ACTIVE_SPEAKER_CHANGE | Active speaker changed | +| 3 | PARTICIPANT_JOIN | Participant joined | +| 4 | PARTICIPANT_LEAVE | Participant left | +| 5 | SHARING_START | Screen sharing started | +| 6 | SHARING_STOP | Screen sharing stopped | +| 7 | MEDIA_CONNECTION_INTERRUPTED | Connection interrupted | +| 8 | PARTICIPANT_VIDEO_ON | Participant camera turned on | +| 9 | PARTICIPANT_VIDEO_OFF | Participant camera turned off | + +## Zoom Contact Center Voice Event Types (RTMS_ZCC_VOICE_EVENT_TYPE) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | UNDEFINED | Default | +| 8 | CONSUMER_ANSWERED | Consumer answered the call | +| 9 | CONSUMER_END | Consumer ended the call | +| 10 | USER_ANSWERED | User or agent answered the call | +| 11 | USER_END | User or agent ended the call | +| 12 | USER_HOLD | User placed the call on hold | +| 13 | USER_UNHOLD | User resumed the call | +| 14 | MONITOR_STARTED | Monitoring started | +| 15 | MONITOR_TRANSITIONED | Monitoring transitioned | +| 16 | MONITOR_ENDED | Monitoring ended | +| 17 | TAKEOVER_STARTED | Takeover started | +| 18 | TRANSFER_INITIATED | Transfer initiated | +| 19 | TRANSFER_CANCELED | Transfer canceled | +| 20 | TRANSFER_ACCEPTED | Transfer accepted | +| 21 | TRANSFER_COMPLETED | Transfer completed | +| 22 | TRANSFER_REJECTED | Transfer rejected | +| 23 | TRANSFER_TIMEOUT | Transfer timed out | +| 24 | CONFERENCE_CANCELED | Conference canceled | +| 25 | CONFERENCE_PARTICIPANT_CANCELED | Conference participant canceled | +| 26 | CONFERENCE_PARTICIPANT_INVITED | Conference participant invited | +| 27 | CONFERENCE_PARTICIPANT_REJECTED | Conference participant rejected | +| 28 | CONFERENCE_PARTICIPANT_TIMEOUT | Conference participant timed out | + +## Status Codes (RTMS_STATUS_CODE) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | STATUS_OK | Success | +| 1 | STATUS_INVALID_MESSAGE_TYPE | Invalid message type | +| 2 | STATUS_INVALID_RTMS_STREAM_ID | Invalid RTMS stream ID | +| 3 | STATUS_INVALID_SIGNATURE | Invalid signature | +| 4 | STATUS_INVALID_PAYLOAD | Invalid payload | +| 5 | STATUS_INVALID_EVENTS | Invalid event list | +| 6 | STATUS_INVALID_EVENT_TYPE | Invalid event type | +| 7 | STATUS_INVALID_MEDIA_TYPE | Invalid media type | +| 8 | STATUS_DUPLICATE_SIGNAL_REQUEST | Duplicate signaling connection | +| 9 | STATUS_MEDIA_TYPE_AUDIO_NOT_SUPPORT | Audio stream not supported | +| 10 | STATUS_MEDIA_TYPE_VIDEO_NOT_SUPPORT | Video stream not supported | +| 11 | STATUS_MEDIA_TYPE_DESKSHARE_NOT_SUPPORT | Screen share stream not supported | +| 12 | STATUS_MEDIA_TYPE_TRANSCRIPT_NOT_SUPPORT | Transcript stream not supported | +| 13 | STATUS_MEDIA_TYPE_CHAT_NOT_SUPPORT | Chat stream not supported | +| 14 | STATUS_MEDIA_TYPE_INVALID_VALUE | Invalid media type value | +| 15 | STATUS_MEDIA_DATA_ALL_CONNECTION_EXIST | All media data connections already exist | +| 16 | STATUS_DUPLICATE_MEDIA_DATA_CONNECTION | Duplicate media data connection | +| 17 | STATUS_INVALID_MEDIA_PARAMS | Invalid media params | +| 18 | STATUS_INVALID_MEDIA_AUDIO_PARAMS | Invalid audio params | +| 19 | STATUS_INVALID_MEDIA_AUDIO_CONTENT_TYPE | Invalid audio content type | +| 20 | STATUS_INVALID_MEDIA_AUDIO_SAMPLE_RATE | Invalid audio sample rate | +| 21 | STATUS_INVALID_MEDIA_AUDIO_CHANNEL | Invalid audio channel count | +| 22 | STATUS_INVALID_MEDIA_AUDIO_CODEC | Invalid audio codec | +| 23 | STATUS_INVALID_MEDIA_AUDIO_DATA_OPT | Invalid audio data option | +| 24 | STATUS_INVALID_MEDIA_AUDIO_SEND_RATE | Invalid audio send rate | +| 25 | STATUS_INVALID_MEDIA_VIDEO_PARAMS | Invalid video params | +| 26 | STATUS_INVALID_MEDIA_VIDEO_CONTENT_TYPE | Invalid video content type | +| 27 | STATUS_INVALID_MEDIA_VIDEO_CODEC | Invalid video codec | +| 28 | STATUS_INVALID_MEDIA_VIDEO_RESOLUTION | Invalid video resolution | +| 29 | STATUS_INVALID_MEDIA_VIDEO_DATA_OPT | Invalid video data option | +| 30 | STATUS_INVALID_MEDIA_VIDEO_FPS | Invalid video FPS | +| 31 | STATUS_INVALID_MEDIA_DESKSHARE_PARAMS | Invalid deskshare params | +| 32 | STATUS_INVALID_MEDIA_DESKSHARE_CONTENT_TYPE | Invalid deskshare content type | +| 33 | STATUS_INVALID_MEDIA_DESKSHARE_CODEC | Invalid deskshare codec | +| 34 | STATUS_INVALID_MEDIA_DESKSHARE_RESOLUTION | Invalid deskshare resolution | +| 35 | STATUS_INVALID_MEDIA_DESKSHARE_FPS | Invalid deskshare FPS | +| 36 | STATUS_INVALID_MEDIA_TRANSCRIPT_PARAMS | Invalid transcript params | +| 37 | STATUS_INVALID_MEDIA_TRANSCRIPT_CONTENT_TYPE | Invalid transcript content type | +| 38 | STATUS_INVALID_MEDIA_CHAT_PARAMS | Invalid chat params | +| 39 | STATUS_INVALID_MEDIA_CHAT_CONTENT_TYPE | Invalid chat content type | +| 40 | STATUS_INVALID_RTMS_SESSION_ID | Invalid RTMS session ID | +| 41 | STATUS_INVALID_CLIENT_READY_ACK | Invalid client ready ack | +| 42 | STATUS_INVALID_EVENT_SUBSCRIBE | Invalid event subscription payload | +| 43 | STATUS_INVALID_MEDIA_TRANSCRIPT_SROUCE_LANGUAGE | Invalid transcript source language | + +## Media Data Types (MEDIA_DATA_TYPE) + +Use bitwise OR to combine: + +| Value | Name | Constant | +|-------|------|----------| +| 1 | AUDIO | 0x01 | +| 2 | VIDEO | 0x01 << 1 | +| 4 | DESKSHARE | 0x01 << 2 | +| 8 | TRANSCRIPT | 0x01 << 3 | +| 16 | CHAT | 0x01 << 4 | +| 32 | ALL | 0x01 << 5 | + +**Common combinations**: +- Audio + Transcript: `1 | 8 = 9` +- Audio + Video: `1 | 2 = 3` +- All media: `32` + +## Content Types (MEDIA_CONTENT_TYPE) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | UNDEFINED | Default | +| 1 | RTP | Real-time audio/video | +| 2 | RAW_AUDIO | Raw audio data | +| 3 | RAW_VIDEO | Raw video data | +| 4 | FILE_STREAM | File stream | +| 5 | TEXT | Text (transcript, chat) | + +## Payload Types / Codecs (MEDIA_PAYLOAD_TYPE) + +| Value | Name | Use | +|-------|------|-----| +| 0 | UNDEFINED | - | +| 1 | L16 | Audio - PCM uncompressed | +| 2 | G711 | Audio - compressed | +| 3 | G722 | Audio - compressed | +| 4 | OPUS | Audio - compressed | +| 5 | JPG | Video/Share - when fps <= 5 | +| 6 | PNG | Video/Share - when fps <= 5 | +| 7 | H264 | Video/Share - when fps > 5 | + +## Media Data Options (MEDIA_DATA_OPTION) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | UNDEFINED | - | +| 1 | AUDIO_MIXED_STREAM | All audio combined | +| 2 | AUDIO_MULTI_STREAMS | Per-participant audio | +| 3 | VIDEO_SINGLE_ACTIVE_STREAM | Active speaker video | +| 4 | VIDEO_SINGLE_INDIVIDUAL_STREAM | One manually selected participant video stream | + +## Media Resolution (MEDIA_RESOLUTION) + +| Value | Name | Pixels | +|-------|------|--------| +| 1 | SD | 854x480 or 640x360 | +| 2 | HD | 1280x720 | +| 3 | FHD | 1920x1080 | +| 4 | QHD | 2560x1440 | + +## Audio Sample Rate (AUDIO_SAMPLE_RATE) + +| Value | Name | Rate | +|-------|------|------| +| 0 | SR_8K | 8000 Hz | +| 1 | SR_16K | 16000 Hz | +| 2 | SR_32K | 32000 Hz | +| 3 | SR_48K | 48000 Hz | + +## Audio Channel (AUDIO_CHANNEL) + +| Value | Name | Note | +|-------|------|------| +| 1 | MONO | Default | +| 2 | STEREO | Only with OPUS codec! | + +## Session State (RTMS_SESSION_STATE) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | INACTIVE | Default | +| 1 | INITIALIZE | Session initializing | +| 2 | STARTED | Session started | +| 3 | PAUSED | Session paused | +| 4 | RESUMED | Session resumed | +| 5 | STOPPED | Session stopped | + +## Stream State (RTMS_STREAM_STATE) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | INACTIVE | Default | +| 1 | ACTIVE | Streaming | +| 2 | INTERRUPTED | Connection problem | +| 3 | TERMINATING | Ending | +| 4 | TERMINATED | Ended | +| 5 | PAUSED | Paused | +| 6 | RESUMED | Resumed | + +## Stop Reasons (RTMS_STOP_REASON) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | UNDEFINED | Default | +| 1 | STOP_BC_HOST_TRIGGERED | Host stopped | +| 2 | STOP_BC_USER_TRIGGERED | User stopped | +| 3 | STOP_BC_USER_LEFT | User left meeting | +| 4 | STOP_BC_USER_EJECTED | User ejected by host | +| 5 | STOP_BC_HOST_DISABLED_APP | Host disabled app | +| 6 | STOP_BC_MEETING_ENDED | Meeting ended | +| 7 | STOP_BC_STREAM_CANCELED | Stream canceled | +| 8 | STOP_BC_STREAM_REVOKED | Stream revoked (delete assets!) | +| 9 | STOP_BC_ALL_APPS_DISABLED | All apps disabled | +| 10 | STOP_BC_INTERNAL_EXCEPTION | Internal error | +| 11 | STOP_BC_CONNECTION_TIMEOUT | Connection timeout | +| 12 | STOP_BC_INSTANCE_CONNECTION_INTERRUPTED | Instance/media connection interrupted | +| 13 | STOP_BC_SIGNAL_CONNECTION_INTERRUPTED | Signaling issue | +| 14 | STOP_BC_DATA_CONNECTION_INTERRUPTED | Data connection issue | +| 15 | STOP_BC_SIGNAL_CONNECTION_CLOSED_ABNORMALLY | Abnormal close | +| 16 | STOP_BC_DATA_CONNECTION_CLOSED_ABNORMALLY | Abnormal close | +| 17 | STOP_BC_EXIT_SIGNAL | Exit signal received | +| 18 | STOP_BC_AUTHENTICATION_FAILURE | Auth failed | +| 19 | STOP_BC_AWAIT_RECONNECTION_TIMEOUT | Awaited reconnection timed out | +| 20 | STOP_BC_RECEIVER_REQUEST_CLOSE | Receiver requested stream close | +| 21 | STOP_BC_CUSTOMER_DISCONNECTED | Contact Center customer disconnected | +| 22 | STOP_BC_AGENT_DISCONNECTED | Contact Center agent disconnected | +| 23 | STOP_BC_ADMIN_DISABLED_APP | Admin disabled app | +| 24 | STOP_BC_KEEP_ALIVE_TIMEOUT | Three keep-alive requests missed | +| 25 | STOP_BC_MANUAL_API_TRIGGERED | ZCC Voice API triggered stop | +| 26 | STOP_BC_STREAMING_NOT_SUPPORTED | Queue does not support streaming | + +## Transcript Languages (RTMS_TRANSCRIPT_LANGUAGE) + +| Value | Name | Language | +|-------|------|----------| +| -1 | LANGUAGE_ID_NONE | None/Auto-detect | +| 0 | LANGUAGE_ID_ARABIC | Arabic | +| 1 | LANGUAGE_ID_BENGALI | Bengali | +| 2 | LANGUAGE_ID_CANTONESE | Cantonese | +| 3 | LANGUAGE_ID_CATALAN | Catalan | +| 4 | LANGUAGE_ID_CHINESE_SIMPLIFIED | Chinese (Simplified) | +| 5 | LANGUAGE_ID_CHINESE_TRADITIONAL | Chinese (Traditional) | +| 6 | LANGUAGE_ID_CZECH | Czech | +| 7 | LANGUAGE_ID_DANISH | Danish | +| 8 | LANGUAGE_ID_DUTCH | Dutch | +| **9** | **LANGUAGE_ID_ENGLISH** | **English (Default)** | +| 10 | LANGUAGE_ID_ESTONIAN | Estonian | +| 11 | LANGUAGE_ID_FINNISH | Finnish | +| 12 | LANGUAGE_ID_FRENCH_CANADA | French (Canada) | +| 13 | LANGUAGE_ID_FRENCH_FRANCE | French (France) | +| 14 | LANGUAGE_ID_GERMAN | German | +| 15 | LANGUAGE_ID_HEBREW | Hebrew | +| 16 | LANGUAGE_ID_HINDI | Hindi | +| 17 | LANGUAGE_ID_HUNGARIAN | Hungarian | +| 18 | LANGUAGE_ID_INDONESIAN | Indonesian | +| 19 | LANGUAGE_ID_ITALIAN | Italian | +| 20 | LANGUAGE_ID_JAPANESE | Japanese | +| 21 | LANGUAGE_ID_KOREAN | Korean | +| 22 | LANGUAGE_ID_MALAY | Malay | +| 23 | LANGUAGE_ID_PERSIAN | Persian | +| 24 | LANGUAGE_ID_POLISH | Polish | +| 25 | LANGUAGE_ID_PORTUGUESE | Portuguese | +| 26 | LANGUAGE_ID_ROMANIAN | Romanian | +| 27 | LANGUAGE_ID_RUSSIAN | Russian | +| 28 | LANGUAGE_ID_SPANISH | Spanish | +| 29 | LANGUAGE_ID_SWEDISH | Swedish | +| 30 | LANGUAGE_ID_TAGALOG | Tagalog | +| 31 | LANGUAGE_ID_TAMIL | Tamil | +| 32 | LANGUAGE_ID_TELUGU | Telugu | +| 33 | LANGUAGE_ID_THAI | Thai | +| 34 | LANGUAGE_ID_TURKISH | Turkish | +| 35 | LANGUAGE_ID_UKRAINIAN | Ukrainian | +| 36 | LANGUAGE_ID_VIETNAMESE | Vietnamese | + +## Transcript Handshake Controls + +Transcript media handshakes now use: + +- `src_language`: fixed requested transcription language +- `enable_lid`: boolean switch for Language Identification + +Behavior: + +- `enable_lid: true` or omitted: RTMS can automatically switch languages during transcription +- `enable_lid: false`: RTMS stays on `src_language` and does not auto-switch + +## Next Steps + +- **[Media Types](media-types.md)** - Parameter configurations +- **[Connection](connection.md)** - Protocol details +- **[Manual WebSocket](../examples/manual-websocket.md)** - Implementation diff --git a/plugins/zoom-developers/skills/rtms/references/environment-variables.md b/plugins/zoom-developers/skills/rtms/references/environment-variables.md new file mode 100644 index 00000000..14e80767 --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/references/environment-variables.md @@ -0,0 +1,26 @@ +# Zoom RTMS Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_CLIENT_ID` | OAuth mode | RTMS subscription/auth (Meetings/Webinars mode) | Zoom Marketplace -> OAuth app credentials | +| `ZOOM_CLIENT_SECRET` | OAuth mode | RTMS subscription/auth (Meetings/Webinars mode) | Zoom Marketplace -> OAuth app credentials | +| `ZOOM_ACCOUNT_ID` | S2S OAuth mode | Account-level RTMS token grants | Zoom Marketplace -> Server-to-Server OAuth app credentials | +| `ZOOM_VIDEO_SDK_KEY` | Video SDK RTMS mode | RTMS with Video SDK sessions | Zoom Marketplace -> Video SDK app credentials | +| `ZOOM_VIDEO_SDK_SECRET` | Video SDK RTMS mode | Video SDK session auth/signing | Zoom Marketplace -> Video SDK app credentials | +| `ZOOM_SECRET_TOKEN` or `WEBHOOK_SECRET_TOKEN` | Yes when validating events | Event signature verification | Zoom Marketplace -> Event Subscriptions -> Secret Token | + +## Connection tuning (optional) + +- `RTMS_CONNECTION_TIMEOUT_MS` +- `RTMS_CONNECTION_MAX_ATTEMPTS` +- `RTMS_CONNECTION_RETRY_DELAY_MS` +- `RTMS_RECONNECT_MAX_ATTEMPTS` +- `RTMS_RECONNECT_BASE_DELAY_MS` +- `RTMS_KEEPALIVE_INTERVAL_MS` +- `RTMS_KEEPALIVE_TIMEOUT_MS` + +## Notes + +- Choose one credential mode per deployment: OAuth or Video SDK credentials. diff --git a/plugins/zoom-developers/skills/rtms/references/full-guide.md b/plugins/zoom-developers/skills/rtms/references/full-guide.md new file mode 100644 index 00000000..0b91c623 --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/references/full-guide.md @@ -0,0 +1,557 @@ +# Zoom Realtime Media Streams (RTMS) + +Background reference for live Zoom media pipelines. Prefer `build-zoom-bot` first, then use this skill for stream types, capabilities, and RTMS-specific implementation constraints. + +# Zoom Realtime Media Streams (RTMS) + +Expert guidance for accessing live audio, video, transcript, chat, and screen share data from Zoom meetings, webinars, Video SDK sessions, and Zoom Contact Center Voice in real-time. RTMS uses a WebSocket-based protocol with open standards and does not require a meeting bot to capture the media plane. + +## Read This First (Critical) + +RTMS is primarily a **backend media ingestion service**. + +- Your backend receives and processes live media: **audio, video, screen share, chat, transcript**. +- RTMS is not a frontend UI SDK by itself. +- Processing is **event-triggered**: backend waits for RTMS start webhook events before stream handling begins. + +Optional architecture (common): + +- Add a **Zoom App SDK** frontend for in-client UI/controls. +- Stream backend RTMS outputs to frontend via **WebSocket** (or SSE, gRPC, queue workers, etc.). + +Use RTMS for media/data plane, and use frontend frameworks/Zoom Apps for presentation + user interactions. + +**Official Documentation**: https://developers.zoom.us/docs/rtms/ +**SDK Reference (JS)**: https://zoom.github.io/rtms/js/ +**SDK Reference (Python)**: https://zoom.github.io/rtms/py/ +**Sample Repository**: https://github.com/zoom/rtms-samples + +## Quick Links + +**New to RTMS? Follow this path:** + +1. **[Connection Architecture](../concepts/connection-architecture.md)** - Two-phase WebSocket design +2. **[SDK Quickstart](../examples/sdk-quickstart.md)** - Fastest way to receive media (recommended) +3. **[Manual WebSocket](../examples/manual-websocket.md)** - Full protocol control without SDK +4. **[Media Types](../references/media-types.md)** - Audio, video, transcript, chat, screen share + +**Complete Implementation:** +- **[RTMS Bot](../examples/rtms-bot.md)** - End-to-end bot implementation guide + +**Reference:** +- **[Lifecycle Flow](../concepts/lifecycle-flow.md)** - Complete webhook-to-streaming flow +- **[Data Types](../references/data-types.md)** - All enums and constants +- **[Webhooks](../references/webhooks.md)** - Event subscription details +- **[Environment Variables](../references/environment-variables.md)** - credential modes and runtime knobs +- **[Quickstart Notes](../references/quickstart.md)** - Secondary quickstart guide +- **Integrated Index** - see the section below in this file + +**Having issues?** +- Connection fails -> [Common Issues](../troubleshooting/common-issues.md) +- Duplicate connections -> [Webhook Gotchas](../troubleshooting/common-issues.md) +- No audio/video -> [Media Configuration](../references/media-types.md) +- Start with preflight checks -> [5-Minute Runbook](../RUNBOOK.md) + +## Supported Products + +| Product | Webhook Event | Payload ID | App Type | +|---------|--------------|------------|----------| +| **Meetings** | `meeting.rtms_started` / `meeting.rtms_stopped` | `meeting_uuid` | General App | +| **Webinars** | `webinar.rtms_started` / `webinar.rtms_stopped` | `meeting_uuid` (same!) | General App | +| **Video SDK** | `session.rtms_started` / `session.rtms_stopped` | `session_id` | Video SDK App | +| **Zoom Contact Center Voice** | Product-specific RTMS/ZCC Voice events | Product-specific stream/session identifiers | Contact Center / approved RTMS integration | + +Once connected, the core signaling/media socket model is shared across products. Meetings, webinars, and Video SDK sessions use the familiar start/stop webhooks. Zoom Contact Center Voice adds its own RTMS/ZCC Voice event family and should be treated as the same transport model with product-specific event payloads. + +## RTMS Overview + +RTMS is a data pipeline that gives your app access to live media from Zoom meetings, webinars, and Video SDK sessions **without participant bots**. Instead of having automated clients join meetings, use RTMS to collect media data directly from Zoom's infrastructure. + +### What RTMS Provides + +| Media Type | Format | Use Cases | +|------------|--------|-----------| +| **Audio** | PCM (L16), G.711, G.722, Opus | Transcription, voice analysis, recording | +| **Video** | H.264, JPG, PNG | Recording, AI vision, thumbnails, active participant selection | +| **Screen Share** | H.264, JPG, PNG | Content capture, slide extraction | +| **Transcript** | JSON text | Meeting notes, search, compliance | +| **Chat** | JSON text | Archive, sentiment analysis | + +### March 2026 Protocol Changes + +- **Zoom Contact Center Voice support**: RTMS now covers Contact Center Voice audio and transcript scenarios. +- **Transcript Language Identification control**: transcript media handshakes now support `src_language` and `enable_lid`. Default behavior is LID enabled. Set `enable_lid: false` to force a fixed language. +- **Single individual video stream subscription**: RTMS can now stream one participant's camera feed at a time when `data_opt` is set to `VIDEO_SINGLE_INDIVIDUAL_STREAM`. +- **Graceful client-initiated shutdown**: backends can send `STREAM_CLOSE_REQ` over the signaling socket and wait for `STREAM_CLOSE_RESP`. +- **Media keep-alive tolerance increased**: media socket keep-alive timeout is now **65 seconds**, not 35. + +### Two Approaches + +| Approach | Best For | Complexity | +|----------|----------|------------| +| **SDK** (`@zoom/rtms`) | Most use cases | Low - handles WebSocket complexity | +| **Manual WebSocket** | Custom protocols, other languages | High - full protocol implementation | + +## Prerequisites + +- **Node.js 20.3.0+** (24 LTS recommended) for JavaScript SDK +- **Python 3.10+** for Python SDK +- Zoom General App (for meetings/webinars) or Video SDK App (for Video SDK) with RTMS feature enabled +- Webhook endpoint for RTMS events +- Server to receive WebSocket streams + +> **Need RTMS access?** Post in [Zoom Developer Forum](https://devforum.zoom.us/) requesting RTMS access with your use case. + +## Quick Start (SDK - Recommended) + +```javascript +import rtms from "@zoom/rtms"; + +// All RTMS start/stop events across products +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; + +// Handle webhook events +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + client.onAudioData((data, timestamp, metadata) => { + console.log(`Audio from ${metadata.userName}: ${data.length} bytes`); + }); + + client.onTranscriptData((data, timestamp, metadata) => { + const text = data.toString('utf8'); + console.log(`${metadata.userName}: ${text}`); + }); + + client.onJoinConfirm((reason) => { + console.log(`Joined session: ${reason}`); + }); + + // SDK handles all WebSocket connections automatically + // Accepts both meeting_uuid and session_id transparently + client.join(payload); +}); +``` + +## Quick Start (Manual WebSocket) + +For full control or non-SDK languages, implement the two-phase WebSocket protocol: + +```javascript +const WebSocket = require('ws'); +const crypto = require('crypto'); + +const RTMS_EVENTS = ['meeting.rtms_started', 'webinar.rtms_started', 'session.rtms_started']; + +// 1. Generate signature +// For meetings/webinars: uses meeting_uuid. For Video SDK: uses session_id. +function generateSignature(clientId, idValue, streamId, clientSecret) { + const message = `${clientId},${idValue},${streamId}`; + return crypto.createHmac('sha256', clientSecret).update(message).digest('hex'); +} + +// 2. Handle webhook +app.post('/webhook', (req, res) => { + res.status(200).send(); // CRITICAL: Respond immediately! + + const { event, payload } = req.body; + if (RTMS_EVENTS.includes(event)) { + connectToRTMS(payload); + } +}); + +// 3. Connect to signaling WebSocket +function connectToRTMS(payload) { + const { server_urls, rtms_stream_id } = payload; + // meeting_uuid for meetings/webinars, session_id for Video SDK + const idValue = payload.meeting_uuid || payload.session_id; + const signature = generateSignature(CLIENT_ID, idValue, rtms_stream_id, CLIENT_SECRET); + + const signalingWs = new WebSocket(server_urls); + + signalingWs.on('open', () => { + signalingWs.send(JSON.stringify({ + msg_type: 1, // Handshake request + protocol_version: 1, + meeting_uuid: idValue, + rtms_stream_id, + signature, + media_type: 9 // AUDIO(1) | TRANSCRIPT(8) + })); + }); + + // ... handle responses, connect to media WebSocket +} +``` + +**See**: [Manual WebSocket Guide](../examples/manual-websocket.md) for complete implementation. + +## Media Type Bitmask + +Combine types with bitwise OR: + +| Type | Value | Description | +|------|-------|-------------| +| Audio | 1 | PCM audio samples | +| Video | 2 | H.264/JPG video frames | +| Screen Share | 4 | **Separate from video!** | +| Transcript | 8 | Real-time speech-to-text | +| Chat | 16 | In-meeting chat messages | +| All | 32 | All media types | + +**Example**: Audio + Transcript = `1 | 8` = `9` + +## Critical Gotchas + +| Issue | Solution | +|-------|----------| +| **Only 1 connection allowed** | New connections kick out existing ones. Track active sessions! | +| **Respond 200 immediately** | If webhook delays, Zoom retries creating duplicate connections | +| **Heartbeat mandatory** | Respond to msg_type 12 with msg_type 13, or connection dies | +| **Reconnection is YOUR job** | RTMS doesn't auto-reconnect. Media keep-alive tolerance is now about **65s**; signaling remains around **60s** | +| **Transcript language drift** | Use `src_language` plus `enable_lid: false` when you want fixed-language transcription instead of automatic language switching | +| **Single participant video only** | `VIDEO_SINGLE_INDIVIDUAL_STREAM` supports one participant at a time. A new `VIDEO_SUBSCRIPTION_REQ` overrides the previous selection | +| **Graceful close is explicit now** | Use `STREAM_CLOSE_REQ` / `STREAM_CLOSE_RESP` when your backend wants to terminate the stream cleanly | + +## Environment Variables + +### SDK Environment Variables + +```bash +# Required - Authentication +ZM_RTMS_CLIENT=your_client_id # Zoom OAuth Client ID +ZM_RTMS_SECRET=your_client_secret # Zoom OAuth Client Secret + +# Optional - Webhook server +ZM_RTMS_PORT=8080 # Default: 8080 +ZM_RTMS_PATH=/webhook # Default: / + +# Optional - Logging +ZM_RTMS_LOG_LEVEL=info # error, warn, info, debug, trace +ZM_RTMS_LOG_FORMAT=progressive # progressive or json +ZM_RTMS_LOG_ENABLED=true +``` + +### Manual Implementation Variables + +```bash +ZOOM_CLIENT_ID=your_client_id +ZOOM_CLIENT_SECRET=your_client_secret +ZOOM_SECRET_TOKEN=your_webhook_token # For webhook validation +``` + +## Zoom App Setup + +### For Meetings and Webinars (General App) + +1. Go to [marketplace.zoom.us](https://marketplace.zoom.us) -> Develop -> Build App +2. Choose **General App** -> **User-Managed** +3. Features -> Access -> **Enable Event Subscription** +4. Add Events -> Search "rtms" -> Select: + - `meeting.rtms_started` + - `meeting.rtms_stopped` + - `webinar.rtms_started` (if using webinars) + - `webinar.rtms_stopped` (if using webinars) +5. Scopes -> Add Scopes -> Search "rtms" -> Add: + - `meeting:read:meeting_audio` + - `meeting:read:meeting_video` + - `meeting:read:meeting_transcript` + - `meeting:read:meeting_chat` + - `webinar:read:webinar_audio` (if using webinars) + - `webinar:read:webinar_video` (if using webinars) + - `webinar:read:webinar_transcript` (if using webinars) + - `webinar:read:webinar_chat` (if using webinars) + +### For Video SDK (Video SDK App) + +1. Go to [marketplace.zoom.us](https://marketplace.zoom.us) -> Develop -> Build App +2. Choose **Video SDK App** +3. Use your SDK Key and SDK Secret (not OAuth Client ID/Secret) +4. Add Events: + - `session.rtms_started` + - `session.rtms_stopped` + +## Sample Repositories + +### Official Samples + +| Repository | Description | +|------------|-------------| +| [rtms-samples](https://github.com/zoom/rtms-samples) | RTMSManager, boilerplates, AI samples | +| [rtms-quickstart-js](https://github.com/zoom/rtms-quickstart-js) | JavaScript SDK quickstart | +| [rtms-quickstart-py](https://github.com/zoom/rtms-quickstart-py) | Python SDK quickstart | +| [rtms-sdk-cpp](https://github.com/zoom/rtms-sdk-cpp) | C++ SDK | +| [zoom-rtms](https://github.com/zoom/rtms) | Main SDK repository | + +### AI Integration Samples + +| Sample | Description | +|--------|-------------| +| [rtms-meeting-assistant-starter-kit](https://github.com/zoom/rtms-meeting-assistant-starter-kit) | AI meeting assistant with summaries | +| [arlo-meeting-assistant](https://github.com/zoom/arlo-meeting-assistant) | Production meeting assistant with DB | +| [videosdk-rtms-transcribe-audio](https://github.com/zoom/videosdk-rtms-transcribe-audio) | Whisper transcription | + +## Complete Documentation + +### Concepts +- **[Connection Architecture](../concepts/connection-architecture.md)** - Two-phase WebSocket design +- **[Lifecycle Flow](../concepts/lifecycle-flow.md)** - Webhook to streaming flow + +### Examples +- **[SDK Quickstart](../examples/sdk-quickstart.md)** - Using @zoom/rtms SDK +- **[Manual WebSocket](../examples/manual-websocket.md)** - Raw protocol implementation +- **[RTMS Bot](../examples/rtms-bot.md)** - Complete bot implementation guide +- **[AI Integration](../examples/ai-integration.md)** - Transcription and analysis patterns + +### References +- **[Media Types](../references/media-types.md)** - Audio, video, transcript, chat, screen share +- **[Data Types](../references/data-types.md)** - All enums and constants +- **[Connection](../references/connection.md)** - WebSocket protocol details +- **[Webhooks](../references/webhooks.md)** - Event subscription + +### Troubleshooting +- **[Common Issues](../troubleshooting/common-issues.md)** - FAQ and solutions + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/rtms/ +- **Data types**: https://developers.zoom.us/docs/rtms/data-types/ +- **Media params**: https://developers.zoom.us/docs/rtms/media-parameter-definition/ +- **Developer forum**: https://devforum.zoom.us/ + +--- + +**Need help?** Start with Integrated Index section below for complete navigation. + +--- + +## Integrated Index + +_This section was migrated from `SKILL.md`._ + +RTMS provides real-time access to live audio, video, transcript, chat, and screen share from Zoom meetings, webinars, and Video SDK sessions. + +## Critical Positioning + +Treat RTMS as a **backend service** for receiving and processing media streams. + +- Backend role: ingest audio/video/share/chat/transcript, run AI/analytics, persist/forward data. +- Optional frontend role: Zoom App SDK or web dashboard that consumes processed stream data from backend transport (WebSocket/SSE/other). +- Kickoff model: backend waits for RTMS start webhook events, then starts stream processing. + +Do not model RTMS as a frontend-only SDK. + +## Quick Start Path + +**If you're new to RTMS, follow this order:** + +1. **Run preflight checks first** -> [RUNBOOK.md](../RUNBOOK.md) +2. **Understand the architecture** -> [concepts/connection-architecture.md](../concepts/connection-architecture.md) + - Two-phase WebSocket: Signaling + Media + - Why RTMS doesn't use bots + +3. **Choose your approach** -> SDK or Manual + - SDK (recommended): [examples/sdk-quickstart.md](../examples/sdk-quickstart.md) + - Manual WebSocket: [examples/manual-websocket.md](../examples/manual-websocket.md) + +4. **Understand the lifecycle** -> [concepts/lifecycle-flow.md](../concepts/lifecycle-flow.md) + - Webhook -> Signaling -> Media -> Streaming + +5. **Configure media types** -> [references/media-types.md](../references/media-types.md) + - Audio, video, transcript, chat, screen share + +6. **Troubleshoot issues** -> [troubleshooting/common-issues.md](../troubleshooting/common-issues.md) + - Connection problems, duplicate webhooks, missing data + +--- + +## Documentation Structure + +``` +rtms/ +├── SKILL.md # Main skill overview +├── SKILL.md # This file - navigation guide +│ +├── concepts/ # Core architectural patterns +│ ├── connection-architecture.md # Two-phase WebSocket design +│ └── lifecycle-flow.md # Webhook to streaming flow +│ +├── examples/ # Complete working code +│ ├── sdk-quickstart.md # Using @zoom/rtms SDK +│ ├── manual-websocket.md # Raw protocol implementation +│ ├── rtms-bot.md # Complete RTMS bot implementation +│ └── ai-integration.md # Transcription and analysis +│ +├── references/ # Reference documentation +│ ├── media-types.md # Audio, video, transcript, chat, share +│ ├── data-types.md # All enums and constants +│ ├── connection.md # WebSocket protocol details +│ └── webhooks.md # Event subscription +│ +└── troubleshooting/ # Problem solving guides + └── common-issues.md # FAQ and solutions +``` + +--- + +## By Use Case + +### I want to get meeting transcripts +1. [SDK Quickstart](../examples/sdk-quickstart.md) - Fastest approach +2. [Media Types](../references/media-types.md) - Transcript configuration +3. [AI Integration](../examples/ai-integration.md) - Whisper, Deepgram, AssemblyAI + +### I want to record meetings +1. [Media Types](../references/media-types.md) - Audio + Video configuration +2. [SDK Quickstart](../examples/sdk-quickstart.md) - Receiving media +3. [AI Integration](../examples/ai-integration.md) - Gap-filled recording + +### I want to build an AI meeting assistant +1. [AI Integration](../examples/ai-integration.md) - Complete patterns +2. [SDK Quickstart](../examples/sdk-quickstart.md) - Media ingestion +3. [Lifecycle Flow](../concepts/lifecycle-flow.md) - Event handling + +### I want to build a complete RTMS bot +1. [RTMS Bot](../examples/rtms-bot.md) - **Complete implementation guide** +2. [Lifecycle Flow](../concepts/lifecycle-flow.md) - Webhook to streaming flow +3. [Connection Architecture](../concepts/connection-architecture.md) - Two-phase design + +### I need full protocol control +1. [Manual WebSocket](../examples/manual-websocket.md) - **START HERE** +2. [Connection Architecture](../concepts/connection-architecture.md) - Two-phase design +3. [Data Types](../references/data-types.md) - All message types and enums +4. [Connection](../references/connection.md) - Protocol details + +### I'm getting connection errors +1. [Common Issues](../troubleshooting/common-issues.md) - Diagnostic checklist +2. [Connection Architecture](../concepts/connection-architecture.md) - Verify flow +3. [Webhooks](../references/webhooks.md) - Validation and timing + +### I want to understand the architecture +1. [Connection Architecture](../concepts/connection-architecture.md) - Two-phase WebSocket +2. [Lifecycle Flow](../concepts/lifecycle-flow.md) - Complete flow diagram +3. [Data Types](../references/data-types.md) - Protocol constants + +--- + +## By Product + +### I'm building for Zoom Meetings +- Standard RTMS setup. Webhook event: `meeting.rtms_started`. Uses General App with OAuth. +- Start with [SDK Quickstart](../examples/sdk-quickstart.md) or [Manual WebSocket](../examples/manual-websocket.md). + +### I'm building for Zoom Webinars +- Same as meetings, but webhook event is `webinar.rtms_started`. Payload still uses `meeting_uuid` (NOT `webinar_uuid`). +- Add webinar scopes and event subscriptions. See [Webhooks](../references/webhooks.md). +- Only **panelist** streams are confirmed available. Attendee streams may not be individual. + +### I'm building for Zoom Video SDK +- Webhook event: `session.rtms_started`. Payload uses `session_id` (NOT `meeting_uuid`). +- Requires a **Video SDK App** with SDK Key/Secret (not OAuth Client ID/Secret). +- Once connected, the protocol is **identical** to meetings. +- See [Webhooks](../references/webhooks.md) for payload details. + +--- + +## Key Documents + +### 1. Connection Architecture (CRITICAL) +**[concepts/connection-architecture.md](../concepts/connection-architecture.md)** + +RTMS uses **two separate WebSocket connections**: +- **Signaling WebSocket**: Authentication, control, heartbeats +- **Media WebSocket**: Actual audio/video/transcript data + +### 2. SDK vs Manual (DECISION POINT) +**[examples/sdk-quickstart.md](../examples/sdk-quickstart.md)** vs **[examples/manual-websocket.md](../examples/manual-websocket.md)** + +| SDK | Manual | +|-----|--------| +| Handles WebSocket complexity | Full protocol control | +| Automatic reconnection | DIY reconnection | +| Less code | More code | +| Best for most use cases | Best for custom requirements | + +### 3. Critical Gotchas (MOST COMMON ISSUES) +**[troubleshooting/common-issues.md](../troubleshooting/common-issues.md)** + +1. **Respond 200 immediately** - Delayed webhook responses cause duplicates +2. **Only 1 connection per stream** - New connections kick out existing +3. **Heartbeat required** - Must respond to keep-alive or connection dies +4. **Track active sessions** - Prevent duplicate join attempts + +--- + +## Key Learnings + +### Critical Discoveries: + +1. **Two-Phase WebSocket Design** + - Signaling: Control plane (handshake, heartbeat, start/stop) + - Media: Data plane (audio, video, transcript, chat, share) + - See: [Connection Architecture](../concepts/connection-architecture.md) + +2. **Webhook Response Timing** + - MUST respond 200 BEFORE any processing + - Delayed response -> Zoom retries -> duplicate connections + - See: [Common Issues](../troubleshooting/common-issues.md) + +3. **Heartbeat is Mandatory** + - Signaling: Receive msg_type 12, respond with msg_type 13 + - Media: Same pattern + - Failure to respond = connection closed + - See: [Connection](../references/connection.md) + +4. **Signature Generation** + - Format: `HMAC-SHA256(clientSecret, "clientId,meetingUuid,streamId")` + - For Video SDK, use `session_id` in place of `meetingUuid` + - Webinars still use `meeting_uuid` (not `webinar_uuid`) + - Required for both signaling and media handshakes + - See: [Manual WebSocket](../examples/manual-websocket.md) + +5. **Media Types are Bitmasks** + - Audio=1, Video=2, Share=4, Transcript=8, Chat=16, All=32 + - Combine with OR: Audio+Transcript = 1|8 = 9 + - See: [Media Types](../references/media-types.md) + +6. **Screen Share is SEPARATE from Video** + - Different msg_type (16 vs 15) + - Different media flag (4 vs 2) + - Must subscribe separately + - See: [Media Types](../references/media-types.md) + +--- + +## Quick Reference + +### "Connection fails" +-> [Common Issues](../troubleshooting/common-issues.md) + +### "Duplicate connections" +-> [Webhook timing](../troubleshooting/common-issues.md) + +### "No audio/video data" +-> [Media Types](../references/media-types.md) - Check configuration + +### "How do I implement manually?" +-> [Manual WebSocket](../examples/manual-websocket.md) + +### "What message types exist?" +-> [Data Types](../references/data-types.md) + +### "How do I integrate AI?" +-> [AI Integration](../examples/ai-integration.md) + +--- + +## Document Version + +Based on **Zoom RTMS SDK v1.x** and official documentation as of 2026. + +--- + +**Happy coding!** + +Remember: Start with [SDK Quickstart](../examples/sdk-quickstart.md) for the fastest path, or [Manual WebSocket](../examples/manual-websocket.md) if you need full control. diff --git a/plugins/zoom-developers/skills/rtms/references/media-types.md b/plugins/zoom-developers/skills/rtms/references/media-types.md new file mode 100644 index 00000000..d34dd7f0 --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/references/media-types.md @@ -0,0 +1,240 @@ +# RTMS - Media Types + +Audio, video, transcript, chat, and screen share data formats. + +## Media Type Bitmask + +Use bitwise OR to combine types: + +| Type | Value | Event Name | Description | +|------|-------|------------|-------------| +| Audio | 1 | `audio` | PCM audio samples | +| Video | 2 | `video` | H.264 encoded frames | +| Screen Share | 4 | `sharescreen` | **Separate from video!** | +| Transcript | 8 | `transcript` | Real-time speech-to-text | +| Chat | 16 | `chat` | In-meeting chat messages | +| All | 32 | all events | All media types | + +**Example**: Audio + Transcript = `1 | 8` = `9` + +```javascript +const mediaTypes = RTMSManager.MEDIA.AUDIO | RTMSManager.MEDIA.TRANSCRIPT; // 9 +``` + +## Audio + +| Property | Options | +|----------|---------| +| Sample Rate | 8kHz (0), **16kHz (1)**, 32kHz (2), 48kHz (3) | +| Codec | **L16/PCM (1)**, G.711 (2), G.722 (3), Opus (4) | +| Channels | **Mono (1)**, Stereo (2) | +| Data Option | **Mixed (1)**, Multi-stream (2) | +| Send Rate | **20ms** (recommended) | + +**Important**: Stereo is ONLY supported with Opus codec! + +### Audio Configuration Example + +```javascript +const audioParams = { + content_type: 1, // MEDIA_CONTENT_TYPE_RTP + sample_rate: 1, // 16kHz + channel: 1, // Mono + codec: 1, // L16 (PCM) + data_opt: 1, // Mixed stream (all participants) + send_rate: 20 // 20ms intervals +}; +``` + +### Processing Audio + +```javascript +RTMSManager.on('audio', ({ buffer, userName, timestamp }) => { + // buffer = PCM 16-bit samples + // Send to transcription service, save to file, etc. + transcriptionService.process(buffer); +}); +``` + +## Video + +| Property | Options | +|----------|---------| +| Codec | **H.264 (7)**, JPG (5), PNG (6) | +| Resolution | SD (1), **HD 720p (2)**, FHD 1080p (3), QHD 2K (4) | +| FPS | 1-30 (typically 25) | +| Data Option | **Single active (3)**, Speaker view (4), Gallery view (5), **Single individual stream** (March 2026) | + +**Rule**: Use JPG/PNG when fps <= 5, H.264 when fps > 5 + +### Video Configuration Example + +```javascript +const videoParams = { + codec: 7, // H.264 + resolution: 2, // HD 720p + fps: 25, + data_opt: 3 // Single active speaker +}; +``` + +### Single Individual Participant Video + +March 2026 added a new pattern for selecting **one participant camera stream at a time**. + +Use it when you need: + +- per-user vision processing +- a moderator-selected camera feed +- deterministic participant focus instead of active speaker switching + +Configuration rules: + +- set the video `data_opt` to `VIDEO_SINGLE_INDIVIDUAL_STREAM` +- subscribe to `PARTICIPANT_VIDEO_ON` / `PARTICIPANT_VIDEO_OFF` +- send `VIDEO_SUBSCRIPTION_REQ` with the chosen `user_id` +- a new subscription overrides the previous participant stream + +This is not a multi-participant subscription feature. RTMS currently supports only **one** individual participant video stream at a time. + +### Processing Video + +```javascript +RTMSManager.on('video', ({ buffer, userName, timestamp }) => { + // buffer = H.264 NAL units + // Decode with FFmpeg, save, or stream + videoDecoder.decode(buffer); +}); +``` + +## Screen Share (SEPARATE from Video!) + +Screen share has a **different event** from regular video (msg_type 16 vs 15). + +| Property | Options | +|----------|---------| +| Codec | **JPG (5)**, PNG (6), H.264 (7) | +| Resolution | SD (1), **HD 720p (2)**, FHD 1080p (3), QHD 2K (4) | +| FPS | **1-5** for static content, 15-30 for animations | + +### Screen Share Configuration + +```javascript +const deskshareParams = { + codec: 5, // JPG (good for static slides) + resolution: 2, // HD + fps: 1 // Low FPS for slides +}; +``` + +### Processing Screen Share + +```javascript +RTMSManager.on('sharescreen', ({ buffer, userName, timestamp }) => { + // buffer = JPG/PNG image or H.264 frame + saveScreenCapture(buffer); +}); +``` + +## Transcript + +| Property | Value | +|----------|-------| +| Format | JSON text | +| Content Type | 5 (MEDIA_CONTENT_TYPE_TEXT) | +| Languages | 36 supported (see below) | +| `src_language` | Fixed requested language | +| `enable_lid` | Toggle Language Identification (default enabled) | + +### Language IDs (Common) + +| Language | ID | +|----------|-----| +| English | 9 | +| Chinese (Simplified) | 4 | +| Chinese (Traditional) | 5 | +| Japanese | 20 | +| Korean | 21 | +| Spanish | 28 | +| French (France) | 13 | +| German | 14 | + +**Tip**: Use `src_language` plus `enable_lid: false` to force a fixed language. Leave `enable_lid` enabled when you want automatic language switching. + +### Transcript Structure + +```json +{ + "user_id": "user_id", + "user_name": "Speaker Name", + "text": "Transcribed text content", + "timestamp": 1234567890, + "is_final": true +} +``` + +### Processing Transcript + +```javascript +RTMSManager.on('transcript', ({ text, userName, timestamp }) => { + // text = transcribed speech + // is_final = true for finalized segments + saveTranscript(userName, text); +}); +``` + +## Chat + +| Property | Value | +|----------|-------| +| Format | JSON text | +| Content Type | 5 (MEDIA_CONTENT_TYPE_TEXT) | + +### Processing Chat + +```javascript +RTMSManager.on('chat', ({ text, userName, timestamp }) => { + console.log(`[Chat] ${userName}: ${text}`); + saveChatMessage(userName, text); +}); +``` + +## Complete Media Configuration + +```javascript +const mediaParams = { + audio: { + content_type: 1, // RTP + sample_rate: 1, // 16kHz + channel: 1, // Mono + codec: 1, // L16 (PCM) + data_opt: 1, // Mixed stream + send_rate: 20 + }, + video: { + codec: 7, // H.264 + resolution: 2, // HD 720p + fps: 25, + data_opt: 3 // Single active speaker + }, + deskshare: { + codec: 5, // JPG + resolution: 2, // HD + fps: 1 + }, + transcript: { + content_type: 5, // TEXT + src_language: 9, // English + enable_lid: false + }, + chat: { + content_type: 5 // TEXT + } +}; +``` + +## Resources + +- **Data types**: https://developers.zoom.us/docs/rtms/data-types/ +- **Media params**: https://developers.zoom.us/docs/rtms/media-parameter-definition/ +- **MEDIA_PARAMETERS.md**: https://github.com/zoom/rtms-samples/blob/main/MEDIA_PARAMETERS.md diff --git a/plugins/zoom-developers/skills/rtms/references/quickstart.md b/plugins/zoom-developers/skills/rtms/references/quickstart.md new file mode 100644 index 00000000..830d6b3c --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/references/quickstart.md @@ -0,0 +1,215 @@ +# RTMS - Quickstart + +Get started with Zoom Realtime Media Streams. + +## Prerequisites + +1. **Node.js 20.3.0+** (24 LTS recommended) +2. Zoom General App (for meetings/webinars), Video SDK App (for Video SDK), or approved Contact Center / RTMS integration for Zoom Contact Center Voice +3. Webhook endpoint configured +4. Server to handle WebSocket connections + +## Environment Setup + +```bash +# .env file +ZOOM_CLIENT_ID=your_client_id # From App Credentials +ZOOM_CLIENT_SECRET=your_client_secret # From App Credentials +ZOOM_SECRET_TOKEN=your_webhook_token # From Feature → Webhook +``` + +## Option 1: RTMSManager (Recommended) + +Clone the official samples and use RTMSManager: + +```bash +git clone https://github.com/zoom/rtms-samples.git +cd rtms-samples/boilerplate/working_js +npm install +``` + +```javascript +import { RTMSManager } from './library/javascript/rtmsManager/RTMSManager.js'; +import express from 'express'; +import crypto from 'crypto'; + +const app = express(); +app.use(express.json()); + +// Initialize with credentials +await RTMSManager.init({ + credentials: { + meeting: { + clientId: process.env.ZOOM_CLIENT_ID, + clientSecret: process.env.ZOOM_CLIENT_SECRET, + secretToken: process.env.ZOOM_SECRET_TOKEN, + } + }, + // Use bitmask: AUDIO(1) | TRANSCRIPT(8) = 9 + mediaTypes: RTMSManager.MEDIA.AUDIO | RTMSManager.MEDIA.TRANSCRIPT, + logging: 'info' +}); + +// Handle media events +RTMSManager.on('audio', ({ buffer, userName }) => { + console.log(`Audio from ${userName}: ${buffer.length} bytes`); +}); + +RTMSManager.on('transcript', ({ text, userName }) => { + console.log(`${userName}: ${text}`); +}); + +RTMSManager.on('chat', ({ text, userName }) => { + console.log(`[Chat] ${userName}: ${text}`); +}); + +RTMSManager.on('sharescreen', ({ buffer, userName }) => { + console.log(`Screen share from ${userName}`); +}); + +// CRITICAL: Respond 200 IMMEDIATELY before any processing! +app.post('/webhook', (req, res) => { + res.status(200).send(); // FIRST! + + const { event, payload } = req.body; + + // Handle URL validation challenge + if (event === 'endpoint.url_validation') { + const hash = crypto + .createHmac('sha256', process.env.ZOOM_SECRET_TOKEN) + .update(payload.plainToken) + .digest('hex'); + return res.json({ plainToken: payload.plainToken, encryptedToken: hash }); + } + + // Feed RTMS events to manager + RTMSManager.handleEvent(event, payload); +}); + +await RTMSManager.start(); +app.listen(3000, () => console.log('RTMS server on port 3000')); +``` + +## Option 2: @zoom/rtms SDK + +```bash +npm install @zoom/rtms express +``` + +```javascript +import rtms from "@zoom/rtms"; + +const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"]; + +rtms.onWebhookEvent(({ event, payload }) => { + if (!RTMS_EVENTS.includes(event)) return; + + const client = new rtms.Client(); + + client.onAudioData((data, timestamp, metadata) => { + console.log(`Audio from ${metadata.userName}`); + }); + + client.onTranscriptData((data, timestamp, metadata) => { + console.log(`${metadata.userName}: ${data}`); + }); + + client.onChatData((data, timestamp, metadata) => { + console.log(`[Chat] ${metadata.userName}: ${data}`); + }); + + client.onScreenShareData((data, timestamp, metadata) => { + console.log(`Screen share from ${metadata.userName}`); + }); + + // SDK accepts both meeting_uuid and session_id transparently + client.join(payload); +}); +``` + +## Zoom App Setup Steps + +### For Meetings and Webinars (General App) + +1. Go to [marketplace.zoom.us](https://marketplace.zoom.us) +2. Click **Develop** → **Build App** +3. Choose **General App** → **User-Managed** +4. Features → Access → **Enable Event Subscription** +5. Add Events → Search "rtms" → Select RTMS endpoints: + - `meeting.rtms_started` + - `meeting.rtms_stopped` + - `webinar.rtms_started` (if using webinars) + - `webinar.rtms_stopped` (if using webinars) +6. Scopes → Add Scopes → Search "rtms" → Add: + - `meeting:read:meeting_audio` + - `meeting:read:meeting_video` + - `meeting:read:meeting_transcript` + - `meeting:read:meeting_chat` + - `webinar:read:webinar_audio` (if using webinars) + - `webinar:read:webinar_video` (if using webinars) + - `webinar:read:webinar_transcript` (if using webinars) + - `webinar:read:webinar_chat` (if using webinars) + +### For Video SDK (Video SDK App) + +1. Go to [marketplace.zoom.us](https://marketplace.zoom.us) +2. Click **Develop** → **Build App** +3. Choose **Video SDK App** +4. Add Events: + - `session.rtms_started` + - `session.rtms_stopped` +5. Use SDK Key and SDK Secret for authentication (not OAuth credentials) + +## How to Start RTMS + +RTMS must be started for each meeting/webinar/session. Options: + +| Product | How to Start | Webhook Event | +|---------|--------------|---------------| +| Meeting | Zoom client, REST API, Zoom App SDK, or **autostart** (zoom.us settings) | `meeting.rtms_started` | +| Webinar | Zoom client, REST API, Zoom App SDK, or **autostart** (zoom.us settings) | `webinar.rtms_started` | +| Video SDK | Video SDK client or REST API | `session.rtms_started` | +| Zoom Contact Center Voice | Product-specific RTMS/ZCC Voice flow | Product-specific RTMS/ZCC Voice events | + +> **Webinar note**: Panelists have full audio/video streams. Attendee streams may not be available individually. + +> **March 2026 note**: transcript handshakes now support `src_language` plus `enable_lid`, media socket keep-alive tolerance is now about **65s**, and RTMS supports one selected participant camera stream at a time via `VIDEO_SINGLE_INDIVIDUAL_STREAM` + `VIDEO_SUBSCRIPTION_REQ`. + +## Deployment with Reverse Proxy + +When deploying behind nginx with a path prefix (e.g., `/my-app/`): + +1. **Socket.IO path must match nginx config**: +```javascript +const socketPath = window.location.pathname.includes('/my-app') + ? '/my-app/rtms-socket' + : '/rtms-socket'; +const socket = io({ path: socketPath }); +``` + +2. **API calls must use relative paths**: +```javascript +const basePath = window.location.pathname.replace(/\/$/, ''); +fetch(`${basePath}/api/sessions`); // NOT fetch('/api/sessions') +``` + +3. **Nginx WebSocket proxy**: +```nginx +location /my-app/rtms-socket { + proxy_pass http://YOUR_RTMS_BACKEND_HOST:3000/rtms-socket; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; +} +``` + +## Next Steps + +- [Media Types](media-types.md) - All 5 data types (audio, video, transcript, chat, screen share) +- [Connection](connection.md) - WebSocket protocol & message types +- [Webhooks](webhooks.md) - Event subscription details + +## Resources + +- **RTMS docs**: https://developers.zoom.us/docs/rtms/ +- **rtms-samples**: https://github.com/zoom/rtms-samples diff --git a/plugins/zoom-developers/skills/rtms/references/webhooks.md b/plugins/zoom-developers/skills/rtms/references/webhooks.md new file mode 100644 index 00000000..b3babf84 --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/references/webhooks.md @@ -0,0 +1,275 @@ +# RTMS - Webhooks + +RTMS-related webhook events and configuration. + +## CRITICAL: Respond 200 IMMEDIATELY! + +**The #1 cause of random disconnects:** + +If your webhook handler takes too long to respond, Zoom assumes failure and retries. The retry creates a second connection, which kicks out your first connection (only 1 connection allowed per stream). + +```javascript +// CORRECT: Respond first, process async +app.post('/webhook', (req, res) => { + res.status(200).send(); // IMMEDIATELY! + + // Then process asynchronously + handleRTMSEvent(req.body); +}); + +// WRONG: Processing before responding +app.post('/webhook', async (req, res) => { + await heavyProcessing(req.body); // Zoom may retry while waiting! + res.status(200).send(); +}); +``` + +## URL Validation Challenge + +When configuring your webhook URL, Zoom sends a validation challenge: + +```javascript +app.post('/webhook', (req, res) => { + const { event, payload } = req.body; + + // Handle URL validation + if (event === 'endpoint.url_validation') { + const hash = crypto + .createHmac('sha256', process.env.ZOOM_SECRET_TOKEN) + .update(payload.plainToken) + .digest('hex'); + + return res.json({ + plainToken: payload.plainToken, + encryptedToken: hash + }); + } + + res.status(200).send(); + // ... handle other events +}); +``` + +## Events + +### meeting.rtms_started + +Sent when RTMS stream is ready for a meeting. + +```json +{ + "event": "meeting.rtms_started", + "payload": { + "account_id": "account_id", + "object": { + "meeting_id": "meeting_id", + "meeting_uuid": "meeting_uuid", + "host_id": "host_user_id", + "rtms_stream_id": "stream_id", + "server_urls": "wss://rtms-sjc1.zoom.us/...", + "signature": "auth_signature" + } + } +} +``` + +### meeting.rtms_stopped + +Sent when RTMS stream ends. + +```json +{ + "event": "meeting.rtms_stopped", + "payload": { + "account_id": "account_id", + "object": { + "meeting_id": "meeting_id", + "rtms_stream_id": "stream_id" + } + } +} +``` + +### webinar.rtms_started + +Sent when RTMS stream is ready for a webinar. + +```json +{ + "event": "webinar.rtms_started", + "payload": { + "account_id": "account_id", + "object": { + "meeting_id": "meeting_id", + "meeting_uuid": "meeting_uuid", + "host_id": "host_user_id", + "rtms_stream_id": "stream_id", + "server_urls": "wss://rtms-sjc1.zoom.us/...", + "signature": "auth_signature" + } + } +} +``` + +> **Important**: Webinar payloads use `meeting_uuid`, NOT `webinar_uuid`. The signature and connection flow are identical to meetings. + +**Webinar-specific considerations:** +- **Panelists**: Full audio/video streams are available for panelists. +- **Attendees**: View-only participants; individual streams may not be available. +- **Practice sessions**: Not documented for RTMS. +- **Q&A/Polls**: Not exposed via RTMS. + +### webinar.rtms_stopped + +Sent when RTMS stream ends for a webinar. + +```json +{ + "event": "webinar.rtms_stopped", + "payload": { + "account_id": "account_id", + "object": { + "meeting_id": "meeting_id", + "rtms_stream_id": "stream_id" + } + } +} +``` + +### session.rtms_started + +Sent when RTMS stream is ready for a Video SDK session. + +```json +{ + "event": "session.rtms_started", + "payload": { + "account_id": "account_id", + "object": { + "session_id": "session_id", + "rtms_stream_id": "stream_id", + "server_urls": "wss://rtms-sjc1.zoom.us/...", + "signature": "auth_signature" + } + } +} +``` + +> **Important**: Video SDK payloads use `session_id` instead of `meeting_uuid`. The HMAC signature must use `session_id` in place of `meeting_uuid`. + +**Video SDK-specific considerations:** +- Uses **SDK Key/Secret** (not OAuth Client ID/Secret) for authentication. +- Requires a **Video SDK App** (not a General App) in Zoom Marketplace. +- Once connected, the WebSocket protocol is identical to meetings. + +### session.rtms_stopped + +Sent when RTMS stream ends for a Video SDK session. + +```json +{ + "event": "session.rtms_stopped", + "payload": { + "account_id": "account_id", + "object": { + "session_id": "session_id", + "rtms_stream_id": "stream_id" + } + } +} +``` + +### Screen Share Events (via msg_type 5) + +Subscribe to receive `SHARING_START` and `SHARING_STOP` events when participants start/stop screen sharing. + +## Payload Fields + +| Field | Description | +|-------|-------------| +| `rtms_stream_id` | Unique stream identifier | +| `server_urls` | WebSocket signaling server URL | +| `meeting_uuid` | Meeting unique identifier (needed for signature) | +| `signature` | Pre-computed auth signature (alternative to self-generating) | + +## Server URL Geo-Routing + +Server URLs contain airport/region codes: + +| Code | Location | +|------|----------| +| `sjc` | San Jose, California | +| `iad` | Washington DC | +| `sin` | Singapore | +| `fra` | Frankfurt, Germany | +| `syd` | Sydney, Australia | + +```javascript +// Extract region from server URL +const hostname = new URL(serverUrl).hostname; // rtms-sjc1.zoom.us +const region = hostname.split('-')[1].replace(/[0-9]/g, ''); // sjc +``` + +**Tip**: For production, route webhooks to workers in the same region as the Zoom server. + +## Subscribing to RTMS Events + +### In Zoom Marketplace (General App - Meetings and Webinars) + +1. Go to your app settings +2. Navigate to **Features** → **Access** +3. **Enable Event Subscription** +4. Click **Add Event Subscription** +5. Enter your webhook endpoint URL +6. Search "rtms" and select: + - `meeting.rtms_started` + - `meeting.rtms_stopped` + - `webinar.rtms_started` (if using webinars) + - `webinar.rtms_stopped` (if using webinars) +7. Click **Done** then **Save** + +### In Zoom Marketplace (Video SDK App) + +1. Go to your Video SDK app settings +2. Add Event Subscription: + - `session.rtms_started` + - `session.rtms_stopped` + +### Required Scopes + +**For Meetings** (Features → Scopes → Add Scopes → search "rtms"): + +| Scope | Purpose | +|-------|---------| +| `meeting:read:meeting_audio` | Access meeting audio | +| `meeting:read:meeting_video` | Access meeting video | +| `meeting:read:meeting_transcript` | Access transcripts | +| `meeting:read:meeting_chat` | Access chat messages | + +**For Webinars** (add these in addition to meeting scopes): + +| Scope | Purpose | +|-------|---------| +| `webinar:read:webinar_audio` | Access webinar audio | +| `webinar:read:webinar_video` | Access webinar video | +| `webinar:read:webinar_transcript` | Access webinar transcripts | +| `webinar:read:webinar_chat` | Access webinar chat messages | + +**For Video SDK**: Uses SDK Key/Secret credentials instead of OAuth scopes. + +## Products Supporting RTMS + +| Product | Start Event | Stop Event | Payload ID | App Type | +|---------|-------------|------------|------------|----------| +| **Zoom Meetings** | `meeting.rtms_started` | `meeting.rtms_stopped` | `meeting_uuid` | General App | +| **Zoom Webinars** | `webinar.rtms_started` | `webinar.rtms_stopped` | `meeting_uuid` (not webinar_uuid!) | General App | +| **Zoom Video SDK** | `session.rtms_started` | `session.rtms_stopped` | `session_id` | Video SDK App | +| Zoom Contact Center | `contactcenter.rtms_*` | `contactcenter.rtms_*` | See Zoom docs | Contact Center App | +| Zoom Phone | `phone.rtms_*` | `phone.rtms_*` | See Zoom docs | General App | + +> **Key differences**: Meetings and webinars use a General App with OAuth credentials. Video SDK uses a Video SDK App with SDK Key/Secret. Once connected, the WebSocket protocol is identical across all products. + +## Resources + +- **Event reference**: https://developers.zoom.us/docs/rtms/event-reference/ +- **RTMS docs**: https://developers.zoom.us/docs/rtms/ diff --git a/plugins/zoom-developers/skills/rtms/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/rtms/troubleshooting/common-issues.md new file mode 100644 index 00000000..d5246857 --- /dev/null +++ b/plugins/zoom-developers/skills/rtms/troubleshooting/common-issues.md @@ -0,0 +1,404 @@ +# Common Issues + +Troubleshooting guide for Zoom RTMS. + +## Quick Diagnostics + +| Symptom | Likely Cause | Solution | +|---------|--------------|----------| +| Connection fails | Invalid signature | Check signature generation | +| Duplicate connections | Slow webhook response | Respond 200 immediately | +| No data received | Wrong media type | Check media_type bitmask | +| Connection closes | Missing heartbeat | Respond to msg_type 12 | +| Segmentation fault | Old Node.js | Upgrade to 20.3.0+ | + +## Connection Issues + +### Webhook Response Timing + +**Problem**: Random disconnections, duplicate connections + +**Cause**: If your webhook handler takes too long to respond, Zoom retries. The retry creates a second connection, which kicks out the first (only 1 connection allowed per stream). + +**Solution**: Respond 200 IMMEDIATELY before any processing: + +```javascript +// CORRECT +app.post('/webhook', (req, res) => { + res.status(200).send(); // FIRST! + + // Then process asynchronously + setImmediate(() => { + handleRTMSEvent(req.body); + }); +}); + +// WRONG +app.post('/webhook', async (req, res) => { + await heavyProcessing(req.body); // Zoom retries while waiting! + res.status(200).send(); +}); +``` + +### Duplicate Connection Prevention + +**Problem**: Multiple connections to same stream + +**Solution**: Track active sessions: + +```javascript +const activeSessions = new Map(); + +function handleRTMSStarted(payload) { + const streamId = payload.rtms_stream_id; + + if (activeSessions.has(streamId)) { + console.log('Already connected, ignoring duplicate'); + return; + } + + activeSessions.set(streamId, Date.now()); + connectToRTMS(payload); +} + +function handleRTMSStopped(payload) { + activeSessions.delete(payload.rtms_stream_id); +} +``` + +### Invalid Signature + +**Problem**: Handshake fails with status_code 3 + +**Cause**: Signature generation incorrect + +**Solution**: Verify format: + +```javascript +// Message format: "clientId,meetingUuid,streamId" +const message = `${clientId},${meetingUuid},${streamId}`; +const signature = crypto.createHmac('sha256', clientSecret) + .update(message) + .digest('hex'); +``` + +**Checklist**: +- [ ] Using correct clientId (not app name) +- [ ] Using correct clientSecret +- [ ] No extra spaces in message +- [ ] Using hex output (not base64) + +### Connection Timeout + +**Problem**: WebSocket connection times out + +**Causes**: +- Network issues +- Firewall blocking WebSocket +- Server URL expired + +**Solution**: +1. Check network connectivity +2. Ensure firewall allows WSS +3. Use fresh webhook payload (don't cache URLs) + +## Heartbeat Issues + +### Connection Closes Unexpectedly + +**Problem**: Connection closes after ~60 seconds + +**Cause**: Not responding to heartbeat + +**Solution**: Respond to msg_type 12 with msg_type 13: + +```javascript +ws.on('message', (data) => { + const msg = JSON.parse(data); + + if (msg.msg_type === 12) { + ws.send(JSON.stringify({ + msg_type: 13, + timestamp: msg.timestamp + })); + } +}); +``` + +**Timeouts**: +- Signaling: ~60 seconds +- Media: ~65 seconds + +## Media Data Issues + +### No Audio Data + +**Causes**: +1. Wrong media_type in handshake +2. No participants speaking +3. Audio not enabled in meeting + +**Solution**: +1. Verify media_type includes AUDIO (1): + ```javascript + media_type: 1 // Just audio + media_type: 9 // Audio + Transcript + media_type: 32 // All media + ``` +2. Wait for participant to speak +3. Check meeting audio settings + +### No Video Data + +**Causes**: +1. Wrong media_type +2. No video enabled +3. Wrong codec for FPS +4. Individual video mode enabled but no participant subscription sent + +**Solution**: +1. Include VIDEO (2) in media_type +2. Use H.264 for fps > 5: + ```javascript + video: { + codec: 7, // H.264 + resolution: 2, // HD + fps: 25 // > 5 requires H.264 + } + ``` +3. If using `VIDEO_SINGLE_INDIVIDUAL_STREAM`, also: + - subscribe to `PARTICIPANT_VIDEO_ON` / `PARTICIPANT_VIDEO_OFF` + - send `VIDEO_SUBSCRIPTION_REQ` with a live `user_id` + - remember a new request replaces the previous participant stream + +### No Screen Share Data + +**Problem**: Not receiving screen share even when active + +**Cause**: Screen share is SEPARATE from video + +**Solution**: Include DESKSHARE (4) in media_type: +```javascript +media_type: 4 // Just screen share +media_type: 5 // Audio + screen share +media_type: 32 // All media +``` + +### Transcript Language Delay + +**Problem**: noticeable startup delay before transcription stabilizes + +**Cause**: Language Identification (LID) is enabled and RTMS is auto-detecting / auto-switching languages + +**Solution**: Set a source language and disable LID when you want a fixed language: +```javascript +transcript: { + content_type: 5, + src_language: 9, // English + enable_lid: false // Fixed language, no auto-switch +} +``` + +**Language IDs**: +| ID | Language | +|----|----------| +| 9 | English | +| 4 | Chinese (Simplified) | +| 20 | Japanese | +| 21 | Korean | +| 28 | Spanish | + +See [Data Types](../references/data-types.md#transcript-languages) for full list. + +### Participant Video Events Arrive But No Video Stream Follows + +**Problem**: You receive `PARTICIPANT_VIDEO_ON`, but no actual participant video frames arrive. + +**Cause**: Those events only tell you whose camera is currently available. They do not automatically switch the data socket to that participant. + +**Solution**: + +1. open the video media socket with `VIDEO_SINGLE_INDIVIDUAL_STREAM` +2. handle `PARTICIPANT_VIDEO_ON` / `PARTICIPANT_VIDEO_OFF` +3. choose one `user_id` +4. send `VIDEO_SUBSCRIPTION_REQ` +5. wait for `VIDEO_SUBSCRIPTION_RESP` + +Also remember: + +- only one participant stream is supported at a time +- a newer subscription overrides the previous participant stream + +### Stream Never Closes Cleanly From Backend + +**Problem**: Your app finishes processing, but the RTMS stream remains open until external stop events arrive. + +**Solution**: Use the new graceful-close control message on the signaling socket: + +```javascript +signalingWs.send(JSON.stringify({ + msg_type: 21, // STREAM_CLOSE_REQ + rtms_stream_id: streamId +})); +``` + +Treat `STREAM_CLOSE_RESP` as acknowledgement, then continue with local cleanup. + +## SDK-Specific Issues + +### Segmentation Fault + +**Problem**: App crashes with segmentation fault + +**Cause**: Node.js version < 20.3.0 + +**Solution**: +```bash +# Check version +node --version + +# Upgrade with nvm +nvm install 24 +nvm use 24 + +# Clear cache and reinstall +npm cache clean --force +rm -rf node_modules package-lock.json +npm install +``` + +### Audio Metadata Missing userId + +**Problem**: `onAudioData` metadata doesn't include speaker userId + +**Cause**: Using AUDIO_MIXED_STREAM (all audio combined) + +**Solution**: Use `onActiveSpeakerEvent` for speaker identification: +```javascript +client.onActiveSpeakerEvent((timestamp, userId, userName) => { + console.log(`Current speaker: ${userName}`); +}); +``` + +Or use AUDIO_MULTI_STREAMS: +```javascript +client.setAudioParams({ + dataOpt: 2 // Per-participant streams +}); +``` + +### Video Parameters Ignored + +**Problem**: `setVideoParams` not taking effect + +**Cause**: SDK bug - video params ignored after audio params + +**Workaround**: Call `setVideoParams` BEFORE `setAudioParams`: +```javascript +// CORRECT ORDER +client.setVideoParams({ codec: 7, fps: 25 }); +client.setAudioParams({ codec: 4, sampleRate: 3 }); +client.join(payload); +``` + +### SDK Invalid State + +**Problem**: "Invalid status" error on join + +**Cause**: SDK still cleaning up from previous session + +**Solution**: Retry with delay: +```javascript +try { + client.join(payload); +} catch (error) { + if (error.message?.includes('Invalid status')) { + console.warn('SDK cleaning up, retrying in 2s'); + + setTimeout(() => { + client.join(payload); + }, 2000); + } +} +``` + +## Platform Issues + +### Platform Not Supported + +**Problem**: SDK installation fails + +**Currently Supported**: +- darwin-arm64 (Apple Silicon) +- linux-x64 + +**Not Yet Supported**: +- Windows +- darwin-x64 (Intel Mac) +- linux-arm64 + +**Workaround**: Use [Manual WebSocket](../examples/manual-websocket.md) implementation. + +## Status Code Reference + +| Code | Name | Description | +|------|------|-------------| +| 0 | STATUS_OK | Success | +| 3 | STATUS_INVALID_SIGNATURE | Invalid signature | +| 8 | STATUS_DUPLICATE_SIGNAL_REQUEST | Already connected (signaling) | +| 16 | STATUS_DUPLICATE_MEDIA_DATA_CONNECTION | Already connected (media) | +| 40 | STATUS_INVALID_RTMS_SESSION_ID | Invalid RTMS session ID | +| 43 | STATUS_INVALID_MEDIA_TRANSCRIPT_SROUCE_LANGUAGE | Invalid transcript source language | + +See [Data Types](../references/data-types.md) for complete list. + +## Product-Specific Issues + +### Video SDK Issues + +**Problem**: Signature validation fails for Video SDK sessions + +**Cause**: Using OAuth Client ID/Secret instead of SDK Key/Secret, or using `meeting_uuid` instead of `session_id`. + +**Solution**: +- Video SDK apps use **SDK Key** (as `clientId`) and **SDK Secret** (as `clientSecret`). +- Video SDK webhook payloads contain `session_id`, NOT `meeting_uuid`. +- The HMAC signature must use `session_id`: `HMAC-SHA256(sdkSecret, "sdkKey,sessionId,streamId")` + +```javascript +// Extract the correct ID based on product +const idValue = payload.meeting_uuid || payload.session_id; +const signature = generateSignature(clientId, idValue, streamId, clientSecret); +``` + +### Webinar Issues + +**Problem**: Looking for `webinar_uuid` in the payload + +**Cause**: Expecting a webinar-specific UUID field. + +**Solution**: Webinar RTMS payloads still use `meeting_uuid` (NOT `webinar_uuid`). This is a common gotcha. The signature, connection flow, and protocol are identical to meetings. + +**Problem**: Missing attendee streams in webinars + +**Cause**: Webinar attendees are view-only participants. + +**Solution**: Only **panelist** audio/video streams are confirmed to be available via RTMS. Attendee streams may not be available individually. Design your application to work with panelist streams only. + +**Problem**: Practice session not triggering RTMS + +**Cause**: Practice sessions are not documented for RTMS support. + +**Solution**: RTMS events are expected when the webinar goes live to attendees, not during practice sessions. Q&A and Polls data are also not exposed via RTMS. + +## Getting Help + +1. **Developer Forum**: https://devforum.zoom.us/ +2. **GitHub Issues**: https://github.com/zoom/rtms/issues +3. **Official Docs**: https://developers.zoom.us/docs/rtms/ + +## Next Steps + +- **[Connection Architecture](../concepts/connection-architecture.md)** - Understand the protocol +- **[Lifecycle Flow](../concepts/lifecycle-flow.md)** - Correct connection sequence +- **[Data Types](../references/data-types.md)** - All status codes and enums diff --git a/plugins/zoom-developers/skills/scribe/RUNBOOK.md b/plugins/zoom-developers/skills/scribe/RUNBOOK.md new file mode 100644 index 00000000..bb9dc2c6 --- /dev/null +++ b/plugins/zoom-developers/skills/scribe/RUNBOOK.md @@ -0,0 +1,88 @@ +# Scribe 5-Minute Preflight Runbook + +Use this before deep debugging. + +## 1) Confirm the Right Product + +- File-based or storage-based transcription -> stay on `scribe`. +- Live meeting media stream or botless live transcription -> use `rtms` instead. +- Meeting bot that joins and records before transcription -> chain Meeting SDK Linux first. + +## 2) Confirm Credentials + +- Build-platform issuer credential pair available. +- JWT generation uses `HS256` with one-hour-or-less expiry. +- Secret stays server-side. +- Reject placeholder values such as `${ZOOM_API_KEY}` and `${ZOOM_API_SECRET}`. They can make a naive health check look configured while every real call still fails. + +## 3) Confirm Mode Selection + +- **Fast mode** for one short file and immediate JSON response. +- **Batch mode** for many files, long recordings, or archive-style processing. +- **Browser microphone pseudo-streaming** for short repeated chunks uploaded through the async fast-mode wrapper. +- Fast mode current limits from the API spec: + - maximum file size: `100 MB` + - maximum duration: `2 hours` +- If fast mode is exposed through a hosted browser UI, prefer an async wrapper: + - browser uploads once + - backend returns `202` with a request ID + - frontend polls for completion + This avoids losing successful transcriptions to edge/client timeout races. +- Observed hosted timing from the deployed sample: + - ~17.2 MB MP4 completed in ~26s + - ~38.6 MB MP4 completed in ~26-37s + - ~59.2 MB MP4 completed in ~32-34s on the backend + - some ~59.2 MB requests still surfaced as frontend `504` even though the backend later completed with `200` + Treat these as deployment observations, not hard API guarantees. +- Recommended starting browser mic cadence: + - chunk size: `5 seconds` + - acceptable range: `5-10 seconds` + - keep only `2-3` chunks in flight at once + This gives incremental transcript updates without trying to hold a single long browser request open. +- For browser mic capture, rotate the recorder per chunk so each uploaded blob is a standalone file. + Do not assume `MediaRecorder.start(timeslice)` later chunks will always be independently transcribable. +- Do not treat this as the default production solution for live transcription. + Prefer `rtms` when the user actually needs a live-audio product instead of a browser demo. + +## 4) Confirm Storage / Webhook Inputs + +- Fast mode file URL or upload path resolves. +- Batch input/output URIs are valid. +- AWS or pre-signed access is set correctly for S3 mode. +- Webhook URL is public HTTPS if you expect notifications. + +## 5) Confirm Post-Processing Contract + +- Decide whether downstream code expects `text_display`, segments, or word-level timings. +- Decide whether channel separation or diarization is required before shipping. + +## 6) Quick Probes + +- JWT generation works locally. +- `POST /aiservices/scribe/transcribe` succeeds with a known small file. +- For browser-uploaded files, backend forwarding should use `multipart/form-data` to Zoom, not a JSON `data:` URI wrapper. +- Batch submit returns `201` with `job_id`. +- Webhook signature verification works with the configured secret. + +## 7) Fast Decision Tree + +- `401`/auth failure -> wrong credential pair or expired JWT. +- Fast mode returns schema error -> wrong request body or config fields. +- Fast mode returns `413 Request Entity Too Large` before the app logs anything -> reverse proxy limit, not Scribe. +- Frontend returns `504` but backend logs later show `200` -> browser/edge timeout race; poll by request ID instead of assuming failure. +- Browser mic feature needs true continuous low-latency media instead of chunked uploads -> switch to `rtms`, not `scribe`. +- Browser mic chunk 1 works but chunk 2 onward is empty -> recorder/container boundary issue; restart the recorder for each chunk. +- Batch jobs queue but never complete -> storage auth / URI / webhook issues. +- Missing transcripts for some files -> inspect `/jobs/{jobId}/files` before re-submitting whole batch. + +## 8) Source Checkpoints + +### Official docs + +- https://developers.zoom.us/docs/ai-services/ +- https://developers.zoom.us/docs/ai-services/scribe/ +- https://developers.zoom.us/api-hub/ai-services/methods/endpoints.json + +### Raw docs in repo tooling output + +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/ai-services/` diff --git a/plugins/zoom-developers/skills/scribe/SKILL.md b/plugins/zoom-developers/skills/scribe/SKILL.md new file mode 100644 index 00000000..84ac7216 --- /dev/null +++ b/plugins/zoom-developers/skills/scribe/SKILL.md @@ -0,0 +1,26 @@ +--- +name: scribe +description: Use when using Scribe. +--- + +# Zoom AI Services Scribe + +Use this skill for Zoom Scribe transcription pipelines over uploaded or stored media. If the user needs live meeting media, compare against RTMS or Meeting SDK bot workflows first. + +## Workflow + +1. Confirm the media source, size, language, expected latency, and whether fast mode or batch mode fits. +2. Set up Build-platform credentials and JWT handling separately from application transcription logic. +3. Design ingestion, chunking, retries, webhook callbacks, and persistence before connecting downstream AI workflows. +4. Implement a minimal transcription request and response parser, then add batching and operational monitoring. +5. Debug by checking credential audience, processing mode, media format, file size, backend timeout, and webhook delivery. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- Auth and processing modes: [concepts/auth-and-processing-modes.md](concepts/auth-and-processing-modes.md) +- High-level scenarios: [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +- Fast mode Node example: [examples/fast-mode-node.md](examples/fast-mode-node.md) +- Batch webhook pipeline: [examples/batch-webhook-pipeline.md](examples/batch-webhook-pipeline.md) +- API reference: [references/api-reference.md](references/api-reference.md) +- Common drift and breaks: [troubleshooting/common-drift-and-breaks.md](troubleshooting/common-drift-and-breaks.md) diff --git a/plugins/zoom-developers/skills/scribe/concepts/auth-and-processing-modes.md b/plugins/zoom-developers/skills/scribe/concepts/auth-and-processing-modes.md new file mode 100644 index 00000000..55ac0a1b --- /dev/null +++ b/plugins/zoom-developers/skills/scribe/concepts/auth-and-processing-modes.md @@ -0,0 +1,95 @@ +# Auth and Processing Modes + +## Authentication Model + +Scribe uses a Build-platform JWT bearer token. + +JWT shape: +- algorithm: `HS256` +- issuer claim: Build-platform credential identifier used by the Scribe API +- expiration: keep to one hour or less + +Node example: + +```js +import { KJUR } from 'jsrsasign'; + +export function generateJWT(apiKey, apiSecret) { + const iat = Math.round(Date.now() / 1000) - 30; + const exp = iat + 60 * 60; + return KJUR.jws.JWS.sign( + 'HS256', + JSON.stringify({ alg: 'HS256', typ: 'JWT' }), + JSON.stringify({ iss: apiKey, iat, exp }), + apiSecret, + ); +} +``` + +## Credential Naming Drift + +Zoom docs currently use inconsistent labels across AI Services pages: +- `API key` / `API secret` +- `SDK key` / `SDK secret` +- `Build platform credentials` + +For implementation, treat them as the Build-platform JWT issuer/secret pair used to sign Scribe requests. Verify the exact labels in the current portal UI before shipping. + +## Fast Mode vs Batch Mode + +| Mode | Best for | Transport | Result timing | +|------|----------|-----------|---------------| +| Fast mode | One short file, interactive UX | `POST /transcribe` | Immediate synchronous JSON | +| Batch mode | Archives, long media, many files | `POST /jobs` then status/webhook | Asynchronous | + +## Fast Mode Request Shape + +- required: `file`, `config` +- common config: `language`, `word_time_offsets`, `channel_separation`, `timestamps`, `output_format`, `profanity_filter`, `diarization` + +## Batch Mode Request Shape + +- required: `input`, `output`, `config` +- input modes: `SINGLE`, `PREFIX`, `MANIFEST` +- storage provider currently surfaced in the OpenAPI as `S3` +- optional webhook callback: `notifications.webhook_url` + `notifications.secret` + +## Operational Choice + +Choose fast mode when: +- user uploads one file +- latency matters more than throughput +- file size and duration are manageable +- you are building pseudo-streaming over short microphone chunks from a browser UI + +Choose batch mode when: +- many files must be processed +- transcripts can arrive later +- storage-centric workflows fit better than direct upload + +## Browser Microphone Pseudo-Streaming + +Scribe is file-oriented, so a browser microphone UX should be modeled as repeated short uploads, not a long-lived stream. + +Recommended pattern: +1. capture browser microphone audio with `MediaRecorder` +2. flush short chunks to your backend +3. submit each chunk through the async fast-mode wrapper +4. poll by request ID +5. append transcript chunks in order + +Recommended starting values: +- chunk size: `5 seconds` +- acceptable range: `5-10 seconds` +- concurrent in-flight chunks: `2-3` + +Why this works: +- lowers the chance of frontend `504` on longer synchronous requests +- gives incremental transcript updates without waiting for one long request + +Guardrail: +- this is pseudo-streaming over file uploads +- this is not the preferred production design for live audio capture +- use it only when a lightweight browser demo or rough incremental transcript is acceptable +- avoid it when you need stable low-latency live transcription, lower overhead, or stronger continuity across utterances +- for true live media streams, low-latency server ingest, or continuous in-meeting audio, use `rtms` diff --git a/plugins/zoom-developers/skills/scribe/examples/batch-webhook-pipeline.md b/plugins/zoom-developers/skills/scribe/examples/batch-webhook-pipeline.md new file mode 100644 index 00000000..5716f777 --- /dev/null +++ b/plugins/zoom-developers/skills/scribe/examples/batch-webhook-pipeline.md @@ -0,0 +1,65 @@ +# Batch Job + Webhook Pipeline + +Use batch mode when you need to process stored archives asynchronously. + +## Flow + +```text +submit batch job + -> receive job_id + -> poll /jobs or wait for webhook + -> inspect /jobs/{jobId}/files + -> ingest transcript outputs +``` + +## Submit Example + +```bash +curl -X POST https://api.zoom.us/v2/aiservices/scribe/jobs -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{ + "input": { + "mode": "PREFIX", + "source": "S3", + "uri": "s3://example-bucket/audio/", + "auth": { + "aws": { + "access_key_id": "...", + "secret_access_key": "...", + "session_token": "..." + } + } + }, + "output": { + "destination": "S3", + "uri": "s3://example-bucket/transcripts/", + "layout": "PREFIX", + "auth": { + "aws": { + "access_key_id": "...", + "secret_access_key": "...", + "session_token": "..." + } + } + }, + "config": { + "language": "en-US", + "word_time_offsets": true, + "channel_separation": true + }, + "notifications": { + "webhook_url": "https://example.com/webhooks/scribe", + "secret": "replace-me" + } + }' +``` + +## Webhook Verification Pattern + +```js +import crypto from 'crypto'; + +function verifyZoomWebhook(rawBody, timestamp, signature, secret) { + const message = `v0:${timestamp}:${rawBody}`; + const expected = `sha256=${crypto.createHmac('sha256', secret).update(message).digest('hex')}`; + return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); +} +``` diff --git a/plugins/zoom-developers/skills/scribe/examples/fast-mode-node.md b/plugins/zoom-developers/skills/scribe/examples/fast-mode-node.md new file mode 100644 index 00000000..5ceb10b6 --- /dev/null +++ b/plugins/zoom-developers/skills/scribe/examples/fast-mode-node.md @@ -0,0 +1,64 @@ +# Fast Mode Node Example + +Minimal backend proxy for synchronous transcription. + +```js +import express from 'express'; +import multer from 'multer'; +import { KJUR } from 'jsrsasign'; + +const app = express(); +const upload = multer({ storage: multer.memoryStorage() }); +app.use(express.json()); + +function generateJWT() { + const iat = Math.round(Date.now() / 1000) - 30; + const exp = iat + 60 * 60; + return KJUR.jws.JWS.sign( + 'HS256', + JSON.stringify({ alg: 'HS256', typ: 'JWT' }), + JSON.stringify({ iss: process.env.ZOOM_API_KEY, iat, exp }), + process.env.ZOOM_API_SECRET, + ); +} + +app.post('/transcribe', upload.single('file'), async (req, res) => { + const token = generateJWT(); + const config = { + language: req.body.language || 'en-US', + word_time_offsets: true, + channel_separation: false, + }; + + let response; + if (req.file) { + const form = new FormData(); + form.append('file', new Blob([new Uint8Array(req.file.buffer)]), req.file.originalname); + form.append('config', JSON.stringify(config)); + response = await fetch('https://api.zoom.us/v2/aiservices/scribe/transcribe', { + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + body: form, + }); + } else { + response = await fetch('https://api.zoom.us/v2/aiservices/scribe/transcribe', { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + file: req.body.file, + config, + }), + }); + } + + const text = await response.text(); + res.status(response.status).type('application/json').send(text); +}); +``` + +Use this pattern when: +- the caller uploads a file to your backend and you forward it as multipart +- or the caller already has a URL-accessible media file and you submit the JSON URL form diff --git a/plugins/zoom-developers/skills/scribe/references/api-reference.md b/plugins/zoom-developers/skills/scribe/references/api-reference.md new file mode 100644 index 00000000..5b890e3c --- /dev/null +++ b/plugins/zoom-developers/skills/scribe/references/api-reference.md @@ -0,0 +1,126 @@ +# Zoom AI Services Scribe API Reference + +Canonical sources: +- OpenAPI JSON: https://developers.zoom.us/api-hub/ai-services/methods/endpoints.json +- Docs overview: https://developers.zoom.us/docs/ai-services/scribe/ +- Base URL: `https://api.zoom.us/v2` + +## Endpoint Inventory + +| Method | Endpoint | Summary | Operation ID | +|--------|----------|---------|-------------| +| POST | `/aiservices/scribe/transcribe` | Scribe (Synchronous) | `createFastAsr` | +| POST | `/aiservices/scribe/jobs` | Submit Batch Scribe Job | `submitBatchAsr` | +| GET | `/aiservices/scribe/jobs` | List Batch Jobs | `listBatchJobs` | +| GET | `/aiservices/scribe/jobs/{jobId}` | Get Batch Job Status | `getBatchJobStatus` | +| DELETE | `/aiservices/scribe/jobs/{jobId}` | Cancel Batch Job | `cancelBatchJob` | +| GET | `/aiservices/scribe/jobs/{jobId}/files` | List Batch Job Files | `listBatchJobFiles` | + +## Request Shapes + +### `POST /aiservices/scribe/transcribe` + +Required top-level fields: +- `file` +- `config` + +Common config fields: +- `language` +- `word_time_offsets` +- `channel_separation` +- `timestamps` +- `output_format` +- `profanity_filter` +- `diarization` + +Response keys: +- `request_id` +- `duration_sec` +- `model` +- `result` + +### `POST /aiservices/scribe/jobs` + +Required top-level fields: +- `input` +- `output` +- `config` + +Input subfields: +- `mode` (`SINGLE`, `PREFIX`, `MANIFEST`) +- `source` (`S3` in current spec) +- `uri` +- `manifest` +- `filters.include_globs` +- `filters.exclude_globs` +- `auth.aws.access_key_id` +- `auth.aws.secret_access_key` +- `auth.aws.session_token` + +Output subfields: +- `destination` +- `uri` +- `layout` (`SINGLE`, `PREFIX`, `ADJACENT`) +- `auth.aws.*` + +Config subfields: +- `language` +- `word_time_offsets` +- `channel_separation` +- `diarization` +- `profanity_filter` +- `output_format` +- `segmentation_mode` + +Optional: +- `reference_id` +- `notifications.webhook_url` +- `notifications.secret` + +Response keys: +- `job_id` +- `state` +- `submitted_at` + +### `GET /aiservices/scribe/jobs` + +Query params: +- `state` +- `page_size` +- `next_page_token` + +Response keys: +- `jobs` +- `next_page_token` + +### `GET /aiservices/scribe/jobs/{jobId}` + +Path params: +- `jobId` + +Response keys: +- `job_id` +- `state` +- `submitted_at` +- `summary` + +### `GET /aiservices/scribe/jobs/{jobId}/files` + +Path params: +- `jobId` + +Query params: +- `page_size` +- `next_page_token` + +Response keys: +- `files` +- `next_page_token` + +## Current Limits and Constraints Observed in Sources + +- Batch manifest max: `1000` file URIs. +- `include_globs` max items: `10`. +- `exclude_globs` max items: `10`. +- Audio/media formats called out in docs: `WAV`, `MP3`, `M4A`, `MP4`. +- Batch job rate limit label in the OpenAPI description: `LIGHT`. diff --git a/plugins/zoom-developers/skills/scribe/references/environment-variables.md b/plugins/zoom-developers/skills/scribe/references/environment-variables.md new file mode 100644 index 00000000..fc4389d6 --- /dev/null +++ b/plugins/zoom-developers/skills/scribe/references/environment-variables.md @@ -0,0 +1,41 @@ +# Environment Variables + +## Required for JWT Auth + +| Variable | Required | Description | +|----------|----------|-------------| +| `ZOOM_API_KEY` | Yes | Build-platform issuer key used in the JWT `iss` claim | +| `ZOOM_API_SECRET` | Yes | Build-platform signing secret for `HS256` JWT generation | + +Do not treat shell placeholders such as `${ZOOM_API_KEY}` as valid configured values. + +## Common App Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `PORT` | No | Local server port | +| `LANGUAGE` | No | Default language code such as `en-US` | + +## Batch / S3 Variables + +| Variable | Required for batch | Description | +|----------|--------------------|-------------| +| `S3_INPUT_URI` | Usually | Input prefix or file URI | +| `S3_OUTPUT_URI` | Usually | Output transcript destination | +| `AWS_ACCESS_KEY_ID` | If not using pre-signed access | AWS credential | +| `AWS_SECRET_ACCESS_KEY` | If not using pre-signed access | AWS credential | +| `AWS_SESSION_TOKEN` | Often | Temporary credential token | + +## Webhook Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `WEBHOOK_URL` | Optional | Public HTTPS callback for batch notifications | +| `WEBHOOK_SECRET` | Optional but recommended | HMAC secret used to verify Zoom callback signatures | + +## Where to Find These Values + +- Build-platform credentials: Zoom developer portal / Build app credential page. +- S3 URIs: your cloud storage path design. +- AWS credentials: IAM or STS-issued temporary credentials. +- Webhook URL: public HTTPS endpoint you control. diff --git a/plugins/zoom-developers/skills/scribe/references/full-guide.md b/plugins/zoom-developers/skills/scribe/references/full-guide.md new file mode 100644 index 00000000..5aad3934 --- /dev/null +++ b/plugins/zoom-developers/skills/scribe/references/full-guide.md @@ -0,0 +1,102 @@ +# Zoom AI Services Scribe + +Background reference for Zoom AI Services Scribe across: +- synchronous single-file transcription (`POST /aiservices/scribe/transcribe`) +- asynchronous batch jobs (`/aiservices/scribe/jobs*`) +- browser microphone pseudo-streaming via repeated short file uploads +- webhook-driven batch status updates +- Build-platform JWT generation and credential handling + +Official docs: +- https://developers.zoom.us/docs/ai-services/ +- https://developers.zoom.us/docs/ai-services/scribe/ +- https://developers.zoom.us/docs/api/ai-services/ +- https://developers.zoom.us/api-hub/ai-services/methods/endpoints.json +- Quickstart sample: https://github.com/zoom/scribe-quickstart/ + +## Routing Guardrail + +- If the user needs **uploaded or stored media transcribed into text**, route here first. +- If the user needs **live meeting media** without file-based upload/batch jobs, route to [../rtms/SKILL.md](../../rtms/SKILL.md). +- If the user needs **Zoom REST API inventory** for AI Services paths, chain [../rest-api/SKILL.md](../../rest-api/SKILL.md). +- If the user needs webhook signature patterns or generic HMAC receiver hardening, optionally chain [../webhooks/SKILL.md](../../webhooks/SKILL.md). + +## Quick Links + +1. [concepts/auth-and-processing-modes.md](../concepts/auth-and-processing-modes.md) +2. [scenarios/high-level-scenarios.md](../scenarios/high-level-scenarios.md) +3. [examples/fast-mode-node.md](../examples/fast-mode-node.md) +4. [examples/batch-webhook-pipeline.md](../examples/batch-webhook-pipeline.md) +5. [references/api-reference.md](../references/api-reference.md) +6. [references/environment-variables.md](../references/environment-variables.md) +7. [references/samples-validation.md](../references/samples-validation.md) +8. [references/versioning-and-drift.md](../references/versioning-and-drift.md) +9. [troubleshooting/common-drift-and-breaks.md](../troubleshooting/common-drift-and-breaks.md) +10. [RUNBOOK.md](../RUNBOOK.md) + +## Core Workflow + +1. Get Build-platform credentials and generate an HS256 JWT. +2. Choose **fast mode** for one short file or **batch mode** for stored archives / large sets. +3. Submit the transcription request. +4. For batch jobs, poll job/file status or receive webhook notifications. +5. Persist and post-process transcript JSON. + +## Hosted Fast-Mode Guardrail + +- The formal fast-mode API limits are `100 MB` and `2 hours`, but hosted browser flows can still time out before the upstream response returns. +- Current deployed-sample observations: + - ~17.2 MB MP4 completed in about `26s` + - ~38.6 MB MP4 completed in about `26-37s` + - ~59.2 MB MP4 completed in about `32-34s` on the backend + - some ~59.2 MB browser requests still surfaced as frontend `504` while backend logs later showed `200` +- Treat frontend `504` plus backend `200` as a browser/edge timeout race, not an automatic transcription failure. +- For hosted UIs, prefer an async request/polling wrapper for fast mode instead of holding the browser open for the full upstream response. +- For larger or less predictable media, prefer batch mode even when the file is still within the formal fast-mode size limit. + +## Browser Microphone Pattern + +- `scribe` does not expose a documented real-time streaming API surface. +- If you want a browser microphone experience, use pseudo-streaming: + 1. capture microphone audio in short chunks + 2. upload each chunk through the async fast-mode wrapper + 3. poll for completion + 4. append chunk transcripts in sequence +- Recommended starting cadence: + - chunk size: `5 seconds` + - acceptable range: `5-10 seconds` + - in-flight chunk requests: `2-3` +- This is a practical UI pattern for incremental transcript updates, not a substitute for `rtms`. +- Treat this as a fallback demo pattern, not the preferred production architecture. +- It adds repeated upload overhead, chunk-boundary drift, browser codec/container variability, and transcript stitching complexity. +- If the user asks for actual live stream ingestion, low-latency continuous media, or server-push media transport, route to [../rtms/SKILL.md](../../rtms/SKILL.md) instead. + +## Endpoint Surface + +| Mode | Method | Path | Use | +|------|--------|------|-----| +| Fast | `POST` | `/aiservices/scribe/transcribe` | Synchronous transcription for one file | +| Batch | `POST` | `/aiservices/scribe/jobs` | Submit asynchronous batch job | +| Batch | `GET` | `/aiservices/scribe/jobs` | List jobs | +| Batch | `GET` | `/aiservices/scribe/jobs/{jobId}` | Inspect job summary/state | +| Batch | `DELETE` | `/aiservices/scribe/jobs/{jobId}` | Cancel queued/processing job | +| Batch | `GET` | `/aiservices/scribe/jobs/{jobId}/files` | Inspect per-file results | + +## High-Level Scenarios + +- On-demand clip transcription after a user uploads one recording. +- Batch transcription of stored S3 call archives. +- Webhook-driven ETL pipeline that writes transcripts to your database/search index. +- Re-transcription of Zoom-managed recordings after exporting them to your own storage. +- Offline compliance or QA workflows that need timestamps, channel separation, and speaker hints. + +## Chaining + +- Stored Zoom recordings -> [../rest-api/SKILL.md](../../rest-api/SKILL.md) + `scribe` +- Webhook verification hardening -> [../webhooks/SKILL.md](../../webhooks/SKILL.md) +- Real-time live transcript/media -> [../rtms/SKILL.md](../../rtms/SKILL.md) +- Cross-product routing -> [../general/SKILL.md](../../general/SKILL.md) + +## Operations + +- [RUNBOOK.md](../RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/scribe/references/samples-validation.md b/plugins/zoom-developers/skills/scribe/references/samples-validation.md new file mode 100644 index 00000000..27c963e0 --- /dev/null +++ b/plugins/zoom-developers/skills/scribe/references/samples-validation.md @@ -0,0 +1,45 @@ +# Samples Validation + +Validated against: +- https://github.com/zoom/scribe-quickstart/ +- official docs pages under `docs/ai-services/` +- AI Services OpenAPI inventory at `api-hub/ai-services/methods/endpoints.json` +- Zoom blog context: + - `introducing-zoom-ai-services` + - `voice-insights-modernize-customer-support-with-scribe` + +## What the official quickstart confirms + +- Node/Express proxy architecture is a valid implementation model. +- Fast mode can be proxied as multipart upload handling on your server even though the docs show JSON examples. +- Batch mode commonly injects AWS credentials into request payloads. +- Webhook verification uses `x-zm-signature` + `x-zm-request-timestamp` with HMAC-SHA256 and `sha256=` prefix. +- The quickstart uses `ZOOM_API_KEY` / `ZOOM_API_SECRET` naming. + +## Useful implementation details from the sample + +- `multer` memory storage is enough for a small fast-mode demo. +- Batch helper routes are naturally expressed as: + - `POST /batch/jobs` + - `GET /batch/jobs` + - `GET /batch/jobs/:jobId` + - `GET /batch/jobs/:jobId/files` + - `DELETE /batch/jobs/:jobId` +- It is practical to keep one `generateJWT()` helper and inject the bearer token per request. + +## Caveats from the sample + +- It assumes Node `>=24`, which is stricter than many deployment environments actually need. Verify your runtime before copying that constraint unchanged. +- It uses environment-injected AWS credentials. Production pipelines may prefer pre-signed URLs or short-lived STS credentials only. +- The sample is an app demo, not a complete production reference for job retry policy, durable queues, or transcript storage. + +## What the blog posts add + +- They reinforce the highest-value downstream use cases: + - post-call summaries + - ticket enrichment + - compliance/audit logging + - searchable archives + - customer-support QA workflows +- They are useful for scenario framing, but not as authoritative API surface documentation. +- Keep endpoint and request-shape decisions anchored to the AI Services docs and API Hub inventory, not the blog wording. diff --git a/plugins/zoom-developers/skills/scribe/references/versioning-and-drift.md b/plugins/zoom-developers/skills/scribe/references/versioning-and-drift.md new file mode 100644 index 00000000..961b1b43 --- /dev/null +++ b/plugins/zoom-developers/skills/scribe/references/versioning-and-drift.md @@ -0,0 +1,56 @@ +# Versioning and Drift + +## Naming Drift in Docs + +The current Zoom docs are inconsistent about credential naming: +- AI Services auth page uses `API key` / `API secret`. +- Build-platform credentials page uses `SDK key` / `SDK secret`. +- Quickstart code uses `ZOOM_API_KEY` / `ZOOM_API_SECRET`. + +Treat these as a portal/documentation naming drift issue and verify the current credential labels in the Zoom developer UI before changing production code. + +## Product Positioning Drift + +Scribe sits under `AI Services`, but related Zoom products may point users toward: +- RTMS for live meeting streams +- Meeting SDK Linux bots for visible in-meeting capture +- AI Companion / REST APIs for Zoom-generated summaries and transcripts +- blog or marketing material that frames Scribe inside broader speech/insights workflows + +Keep the guardrail clear: +- `scribe` = file/storage transcription service +- `rtms` = live media stream ingestion +- Meeting SDK Linux = participant bot capture / raw recording + +## Workflow-Claim Drift + +Some AI Services and Scribe blog material frames Scribe inside broader voice-insights workflows such as: +- post-call summaries +- ticket enrichment +- compliance logging +- searchable archives +- customer-support QA pipelines +- sentiment or keyword-driven downstream analytics + +These are valid architectural use cases, but they do not expand the current documented Scribe endpoint surface. + +Implementation rule: +- use `scribe` for transcript generation +- use your own downstream pipeline for sentiment, classification, QA scoring, or summarization +- do not infer undocumented real-time or analytics endpoints from blog phrasing alone + +## API Surface Drift Watchpoints + +Watch for changes in: +- storage providers beyond `S3` +- request field names in `config` +- webhook signature header conventions +- response summary/file schemas +- language / output-format support + +## Review Trigger + +Re-review this skill when: +- `api-hub/ai-services/methods/endpoints.json` changes +- AI Services docs rename Build/API credentials again +- quickstart sample changes webhook or upload patterns diff --git a/plugins/zoom-developers/skills/scribe/scenarios/high-level-scenarios.md b/plugins/zoom-developers/skills/scribe/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..4f4c4b6f --- /dev/null +++ b/plugins/zoom-developers/skills/scribe/scenarios/high-level-scenarios.md @@ -0,0 +1,86 @@ +# High-Level Scenarios + +## Scenario 1: On-Demand Upload Transcription + +Use fast mode when a user uploads one file and expects a transcript immediately. + +Flow: +1. Browser uploads file to your backend. +2. Backend generates Build JWT. +3. Backend calls `POST /aiservices/scribe/transcribe`. +4. Backend returns transcript JSON to the caller. + +Common downstream uses: +- post-call summaries +- ticket enrichment +- searchable clip libraries +- internal review or handoff notes + +## Scenario 2: Batch S3 Archive Transcription + +Use batch mode when call archives or media libraries already live in S3. + +Flow: +1. Build a batch request with input prefix and output prefix. +2. Submit `POST /aiservices/scribe/jobs`. +3. Track state by webhook or polling. +4. Read `/jobs/{jobId}/files` for per-file success/failure. +5. Ingest outputs into search, analytics, or storage. + +Common downstream uses: +- compliance and audit logging +- searchable webinar or podcast archives +- bulk transcript backfills +- QA scoring inputs + +## Scenario 3: Zoom Recording Export + Re-Transcription + +Use when you must re-process Zoom-managed recordings with your own transcript settings. + +Skill chain: +- `zoom-rest-api` to fetch/download recordings +- `scribe` to transcribe exported media + +Typical reasons: +- you need your own retention/search pipeline +- you need different transcript settings than Zoom-managed defaults +- you want to enrich recordings with your own summarization or tagging flow + +## Scenario 4: Compliance / QA Processing + +Use batch mode when transcripts must be generated offline for audits, QA scoring, or archival search. + +Prefer: +- `word_time_offsets=true` when reviewers need precise excerpts +- `channel_separation=true` for stereo call recordings +- webhook + queue ingestion instead of synchronous polling for large volumes + +## Scenario 5: Customer Support Voice-to-Insights Pipeline + +Use when support call recordings should feed operational analytics instead of stopping at raw transcript text. + +Flow: +1. Ingest call recordings from storage or exported meeting assets. +2. Transcribe with `scribe`. +3. Store transcript plus speaker/timing metadata. +4. Run downstream sentiment, keyword, escalation, or QA logic in your own pipeline. + +Guardrail: +- keep `scribe` focused on transcription +- do sentiment analysis, keyword detection, or scoring in downstream services after transcript generation + +## Scenario 6: Browser Microphone Incremental Transcript + +Use when a web page should capture microphone audio and show transcript updates every few seconds without switching to RTMS. + +Flow: +1. Browser captures microphone audio with `MediaRecorder`. +2. Browser flushes one chunk every `5 seconds`. +3. Backend accepts each chunk as a normal fast-mode upload through the async wrapper. +4. Frontend polls by request ID and appends transcript chunks in order. + +Guardrail: +- this is pseudo-streaming over repeated file uploads +- this is best kept as a lightweight demo or constrained fallback +- do not choose it first for a true live-transcription product +- if the requirement is truly live media stream ingestion or lower-latency continuous audio, route to `rtms` diff --git a/plugins/zoom-developers/skills/scribe/troubleshooting/common-drift-and-breaks.md b/plugins/zoom-developers/skills/scribe/troubleshooting/common-drift-and-breaks.md new file mode 100644 index 00000000..14d75bbd --- /dev/null +++ b/plugins/zoom-developers/skills/scribe/troubleshooting/common-drift-and-breaks.md @@ -0,0 +1,134 @@ +# Common Drift and Breaks + +## 1. Auth fails even though credentials look correct + +Likely causes: +- wrong credential pair from the portal +- expired JWT +- mixing Build-platform credentials with non-Build Zoom app credentials +- valid-looking key/secret pair that is not authorized for AI Services Scribe + +Check: +- `iss` value +- `exp` window +- current credential labels in portal +- if the API responds with `{"code":124,"message":"Invalid Access token"}`, treat that as a real upstream auth failure, not a transport problem + +## 2. Fast mode request shape mismatch + +Docs show JSON with `file` URL, but the official quickstart also proxies multipart upload and forwards a `FormData` request. + +Use one clear model per service boundary: +- client upload -> your backend multipart +- backend upload proxy -> `multipart/form-data` to `/aiservices/scribe/transcribe` +- backend URL-based submit -> JSON body with `file` URL + +Symptoms: +- browser request stays pending for a long time +- backend eventually returns timeout or empty upstream reply + +Preferred fix: +- treat uploaded files and URL-based files as two separate request paths instead of forcing both through one JSON shape + +## 3. Fast mode returns `413 Request Entity Too Large` + +Likely cause: +- reverse proxy rejected the upload before the request reached your app + +Known deployment check: +- if nginx fronts the app, raise `client_max_body_size` to match or exceed your server-side upload limit + +Current Scribe fast-mode API limits: +- maximum file size: `100 MB` +- maximum duration: `2 hours` + +## 4. Fast mode returns `504 Gateway time-out` + +Likely cause: +- the request reached your backend, but synchronous processing took too long for the edge/proxy path + +Observed deployment behavior: +- public HTTPS can time out even when the same request path is valid and the backend is healthy +- observed hosted sample timings: + - ~17.2 MB MP4: ~26s + - ~38.6 MB MP4: ~26-37s + - ~59.2 MB MP4: ~32-34s backend completion, but some browser requests still timed out first + +Guardrail: +- use fast mode for smaller, interactive files +- use batch mode for large uploads or longer media where waiting synchronously through the web UI is brittle +- add request-level logging for: + - file name + - file size + - mime type + - upstream elapsed time + - response payload size and top-level keys + so you can tell whether the origin completed successfully while the browser/edge timed out +- for hosted UIs, wrap fast mode in an async request/polling flow instead of holding the browser open for the entire upstream response +- if nginx access logs show `499` while app logs later show `zoom_request_finished status: 200`, the transcription succeeded and only the browser-side request path was lost + +## 5. Batch job accepted but outputs never appear + +Likely causes: +- S3 URI/auth mismatch +- expired STS credentials +- output layout/URI mismatch +- webhook endpoint unreachable if you rely on callbacks + +Check: +- `/jobs/{jobId}` summary +- `/jobs/{jobId}/files` +- cloud storage permissions + +## 6. Webhook verification fails + +Current sample pattern uses: +- `x-zm-signature` +- `x-zm-request-timestamp` +- HMAC-SHA256 with `sha256=` prefix + +If verification fails: +- confirm raw body capture before JSON parsing +- confirm timestamp header was included in the signed string +- confirm the shared secret matches the job notification config + +## 7. Health check says credentials exist, but API calls still fail + +Likely cause: +- environment file contains literal placeholders such as `${ZOOM_API_KEY}` or `${ZOOM_API_SECRET}` + +Guardrail: +- only treat credentials as present if they are real values, not unresolved shell placeholders +- fail fast with a clear credential error before attempting Zoom calls + +## 8. Wrong product chosen + +Symptoms: +- trying to use Scribe for live in-meeting media +- trying to use RTMS for offline archive transcription + +Guardrail: +- file/storage transcription -> `scribe` +- live meeting media -> `rtms` + +## 9. Browser microphone chunk 1 works but later chunks are empty + +Likely cause: +- the browser emitted a valid first container chunk, but later `MediaRecorder` timeslice blobs were partial WebM/Opus clusters without fresh container headers + +Symptoms: +- chunk 1 transcribes normally +- chunk 2 onward returns empty transcript text or much weaker results +- auth and request flow still look healthy + +Preferred fix: +- do not rely on one long `MediaRecorder.start(timeslice)` session for standalone chunk uploads +- rotate the recorder per chunk instead: + - start recorder + - record one chunk window + - stop recorder + - upload that blob + - start a new recorder for the next chunk + +Guardrail: +- treat browser microphone pseudo-streaming as a file-container problem first, not a Scribe-language-model problem diff --git a/plugins/zoom-developers/skills/setup-zoom-oauth/SKILL.md b/plugins/zoom-developers/skills/setup-zoom-oauth/SKILL.md new file mode 100644 index 00000000..1c090b66 --- /dev/null +++ b/plugins/zoom-developers/skills/setup-zoom-oauth/SKILL.md @@ -0,0 +1,38 @@ +--- +name: setup-zoom-oauth +description: Use when setting up OAuth. +--- + +# /setup-zoom-oauth + +Use this skill when auth is the blocker or when auth choices will shape the entire integration. + +## Scope + +- App type selection +- OAuth grant selection +- Scope planning +- Token exchange and refresh +- Auth debugging and environment assumptions + +## Workflow + +1. Determine the app model and who is authorizing whom. +2. Choose the correct grant flow. +3. Identify minimum scopes for the user flow. +4. Define token storage and refresh behavior. +5. Route into the deepest relevant reference docs only after the above is clear. + +## Primary References + +- [oauth](../oauth/SKILL.md) +- [general](../general/SKILL.md) +- [rest-api](../rest-api/SKILL.md) + +## Common Mistakes + +- Picking a grant before clarifying the actor and tenant model +- Asking for broad scopes before confirming the exact workflow +- Forgetting refresh-token behavior and token lifecycle handling +- Reusing an old refresh token after a successful refresh instead of storing the newly returned one +- Treating auth failures as API failures without checking app configuration first diff --git a/plugins/zoom-developers/skills/start/SKILL.md b/plugins/zoom-developers/skills/start/SKILL.md new file mode 100644 index 00000000..1668f317 --- /dev/null +++ b/plugins/zoom-developers/skills/start/SKILL.md @@ -0,0 +1,44 @@ +--- +name: start +description: Use when starting Zoom work. +--- + +# Start + +Use this as the default entry skill for the plugin. + +## What This Skill Does + +- Classifies the request by job-to-be-done, not by product name alone +- Routes into the right implementation skill +- Pulls in product-specific Zoom references only after the route is clear +- Prevents common early mistakes, especially Meeting SDK vs Video SDK and REST API vs SDK confusion + +## Routing Table + +| If the user wants to... | Route to | +|---|---| +| Choose the right Zoom surface for a new project | [plan-zoom-product](../plan-zoom-product/SKILL.md) | +| Set up OAuth, tokens, scopes, or app credentials | [setup-zoom-oauth](../setup-zoom-oauth/SKILL.md) | +| Embed or customize a Zoom meeting flow | [build-zoom-meeting-app](../build-zoom-meeting-app/SKILL.md) | +| Build a bot, recorder, or real-time meeting processor | [build-zoom-bot](../build-zoom-bot/SKILL.md) | +| Debug a broken integration | [debug-zoom](../debug-zoom/SKILL.md) | + +## Supporting Zoom References + +Use these only after selecting the workflow: + +- [general](../general/SKILL.md) +- [rest-api](../rest-api/SKILL.md) +- [meeting-sdk](../meeting-sdk/SKILL.md) +- [video-sdk](../video-sdk/SKILL.md) +- [webhooks](../webhooks/SKILL.md) +- [websockets](../websockets/SKILL.md) +- [oauth](../oauth/SKILL.md) + +## Operating Rules + +1. Prefer one clear recommendation over a product catalog dump. +2. Ask a short clarifier only when the route is genuinely ambiguous. +3. Keep the first response architectural and actionable, then go deep. +4. Pull in deeper references only when they directly help the current decision or implementation. diff --git a/plugins/zoom-developers/skills/team-chat/RUNBOOK.md b/plugins/zoom-developers/skills/team-chat/RUNBOOK.md new file mode 100644 index 00000000..fa155e47 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/RUNBOOK.md @@ -0,0 +1,85 @@ +# Team Chat 5-Minute Preflight Runbook + +Use this before deep debugging. It catches the most common Team Chat failures fast. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Integration Type + +- User type (Team Chat API): user OAuth + `/v2/chat/users/...` +- Bot type (Chatbot API): client credentials + `/v2/im/chat/messages` + +If this is wrong, everything else will fail. + +## 2) Confirm OAuth Endpoints + +- Authorize URL: `https://zoom.us/oauth/authorize` +- Token URL: `https://zoom.us/oauth/token` + +If token requests hit `/oauth/token`, expect 404/HTML. + +## 3) Confirm Runtime Env Loading + +If credentials are split by mode, verify your server loads the actual files at runtime: + +- `project/team-chat-api/.env` +- `project/chatbot-api/.env` + +Do not assume root `.env` is enough. + +## 4) Confirm App Routes + Reverse Proxy + +- Current demo pages: + - `/team-chat/user-demo` + - `/team-chat/bot-demo` +- API path should resolve: `/team-chat/api/*` + +If browser calls old routes (`/api/channel/*`) and gets 404, either update frontend or keep compatibility routes. + +## 5) Run Curl Probes + +Use backend probes before browser debugging. + +```bash +TEAM_CHAT_BASE_URL="http://YOUR_HOST:YOUR_PORT" + +curl -sS "$TEAM_CHAT_BASE_URL/team-chat/api/config" +curl -sS -i "$TEAM_CHAT_BASE_URL/team-chat/api/bot/token" +curl -sS -i "$TEAM_CHAT_BASE_URL/team-chat/api/channel/list" +``` + +Expected: +- `api/config` shows required flags as configured. +- `api/bot/token` should return JSON (200 or actionable 4xx), never HTML 404 page. +- `api/channel/list` returns validation errors or data, not generic 404. + +## 6) Browser-Specific Reality Check + +`ERR_BLOCKED_BY_CLIENT` usually means extension/adblock/privacy filter interference. + +- Re-test in Incognito. +- Temporarily disable blockers for host. +- Validate with curl first. + +## 7) User OAuth Callback Flow (In-App) + +For user-demo, avoid manual copy/paste flow: + +1. UI button triggers backend authorize URL generation with `state`. +2. Browser redirects to Zoom consent page. +3. Callback validates `state` and exchanges `code` server-side. +4. Token is stored where UI expects (session/db/local storage for demo). +5. Redirect back to user-demo. + +If callback returns but token is missing, focus on `state` validation and persistence path. + +## 8) Fast Decision Tree + +- **404 on bot token** -> check token URL (`/oauth/token`), then proxy path. +- **All channel APIs 404** -> route mismatch (old UI vs new backend routes). +- **OAuth works but sends fail** -> wrong scopes or app type mismatch. +- **Works by curl but fails in browser** -> blocked client/cached old JS. diff --git a/plugins/zoom-developers/skills/team-chat/SKILL.md b/plugins/zoom-developers/skills/team-chat/SKILL.md new file mode 100644 index 00000000..8e352c7f --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/SKILL.md @@ -0,0 +1,27 @@ +--- +name: build-zoom-team-chat-app +description: Use when building Team Chat. +--- + +# Build Zoom Team Chat App + +Use this skill when the target surface is Zoom Team Chat. First decide whether the integration is user-scoped messaging, a chatbot, an interactive message-card workflow, or a webhook-driven automation. + +## Workflow + +1. Choose the API surface: Team Chat API for user-scoped actions, Chatbot API for bot identity and chatbot workflows. +2. Confirm app type, scopes, role enablement, and whether the account has Zoom for Developers enabled. +3. Model message structure before coding: plain messages, rich cards, buttons, dropdowns, forms, slash commands, and threaded replies. +4. Implement OAuth and token refresh separately from message sending, with clear storage boundaries. +5. Add webhook handlers for interactivity and lifecycle events with signature verification and retry-safe processing. +6. Debug by checking JID formats, channel membership, bot installation, scopes, role settings, and message-card payload shape. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- API selection: [concepts/api-selection.md](concepts/api-selection.md) +- Message structure: [concepts/message-structure.md](concepts/message-structure.md) +- Message cards: [references/message-cards.md](references/message-cards.md) +- Scopes: [references/scopes.md](references/scopes.md) +- Chatbot setup: [examples/chatbot-setup.md](examples/chatbot-setup.md) +- Common issues: [troubleshooting/common-issues.md](troubleshooting/common-issues.md) diff --git a/plugins/zoom-developers/skills/team-chat/concepts/api-selection.md b/plugins/zoom-developers/skills/team-chat/concepts/api-selection.md new file mode 100644 index 00000000..b46683ec --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/concepts/api-selection.md @@ -0,0 +1,231 @@ +# API Selection Guide + +Zoom Team Chat offers **two distinct APIs** for different use cases. Choose the right one before you start building. + +## Critical First Decision + +Pick one of these integration types before writing code: + +- **User type** -> Team Chat API -> User OAuth -> `/v2/chat/users/...` +- **Bot type** -> Chatbot API -> Client Credentials -> `/v2/im/chat/messages` + +Most implementation issues come from mixing user-type auth with bot-type endpoints (or the opposite). + +## Quick Decision Matrix + +| Use Case | API to Use | Messages Appear As | +|----------|------------|-------------------| +| Send notifications from scripts/CI/CD | **Team Chat API** | Authenticated user | +| Automate messages as a user | **Team Chat API** | Authenticated user | +| Build an interactive chatbot | **Chatbot API** | Your bot | +| Respond to slash commands | **Chatbot API** | Your bot | +| Create messages with buttons/forms | **Chatbot API** | Your bot | +| Handle user interactions | **Chatbot API** | Your bot | + +## Team Chat API (User-Level Messaging) + +### What It Is + +The Team Chat API allows your application to send messages **as an authenticated user**. Messages appear in Team Chat as if the user sent them manually. + +### When to Use + +✅ **Use Team Chat API when:** +- You want to send simple text messages programmatically +- Messages should appear as sent by a specific user +- You're building CI/CD notifications +- You're automating user-level messaging +- You don't need interactive components (buttons, forms) + +### Key Characteristics + +| Aspect | Details | +|--------|---------| +| **Authentication** | User OAuth (authorization_code flow) | +| **Endpoint** | `POST https://api.zoom.us/v2/chat/users/me/messages` | +| **Message Format** | Plain text or markdown | +| **Scopes** | `chat_message:write`, `chat_channel:read` | +| **User Experience** | Messages appear from the authenticated user | + +### Example Use Cases + +1. **CI/CD Notifications** + ``` + User: "Build #123 completed successfully" + ``` + +2. **Automated Reporting** + ``` + User: "Daily sales report: $10,000" + ``` + +3. **Task Reminders** + ``` + User: "Reminder: Team meeting in 15 minutes" + ``` + +## Chatbot API (Bot-Level Interactions) + +### What It Is + +The Chatbot API allows your application to send messages **as a bot**. Bots can send rich, interactive messages with buttons, forms, images, and handle user interactions via webhooks. + +### When to Use + +✅ **Use Chatbot API when:** +- You want to build an interactive chatbot +- You need rich message formatting (cards, buttons, forms) +- You want to handle slash commands (e.g., `/weather`) +- You need to respond to button clicks or form submissions +- You're integrating LLMs or AI provider APIs +- You want scheduled notifications + +### Key Characteristics + +| Aspect | Details | +|--------|---------| +| **Authentication** | Client Credentials grant | +| **Endpoint** | `POST https://api.zoom.us/v2/im/chat/messages` | +| **Message Format** | Rich cards with components | +| **Scopes** | `imchat:bot` (auto-added) | +| **User Experience** | Messages appear from your bot | +| **Interactivity** | Buttons, forms, dropdowns, webhooks | + +### Example Use Cases + +1. **Support Bot** + ``` + Bot: "How can I help you?" + [Help Center] [Contact Support] [Report Bug] + ``` + +2. **Approval Workflow** + ``` + Bot: "Expense Report: $500" + Branch: main + Requester: John + [Approve] [Reject] + ``` + +3. **AI Assistant** + ``` + User: "/ask What's the weather?" + Bot: "The weather in San Francisco is 72°F and sunny." + ``` + +## Feature Comparison + +| Feature | Team Chat API | Chatbot API | +|---------|---------------|-------------| +| **Plain Text Messages** | ✅ | ✅ | +| **Markdown** | ✅ | ✅ | +| **Rich Cards** | ❌ | ✅ | +| **Buttons** | ❌ | ✅ | +| **Forms** | ❌ | ✅ | +| **Dropdowns** | ❌ | ✅ | +| **Images** | ✅ (basic) | ✅ (rich) | +| **Slash Commands** | ❌ | ✅ | +| **Webhooks** | ❌ | ✅ | +| **Button Click Handling** | ❌ | ✅ | +| **Form Submissions** | ❌ | ✅ | + +## Authentication Comparison + +### Team Chat API (User OAuth) + +**Flow**: authorization_code +**Requires**: User login and consent +**Token Scope**: User's data only + +```javascript +// Step 1: Redirect user to OAuth consent page +const authUrl = `https://zoom.us/oauth/authorize?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}`; + +// Step 2: Exchange auth code for access token +const tokens = await exchangeCodeForToken(code); + +// Step 3: Use access token to send messages +fetch('https://api.zoom.us/v2/chat/users/me/messages', { + headers: { 'Authorization': `Bearer ${tokens.access_token}` } +}); +``` + +### Chatbot API (Client Credentials) + +**Flow**: client_credentials +**Requires**: No user login +**Token Scope**: Bot actions only + +```javascript +// Step 1: Get bot token (no user interaction) +const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64'); +const response = await fetch('https://zoom.us/oauth/token', { + method: 'POST', + headers: { 'Authorization': `Basic ${credentials}` }, + body: 'grant_type=client_credentials' +}); + +const { access_token } = await response.json(); + +// Step 2: Use access token to send bot messages +fetch('https://api.zoom.us/v2/im/chat/messages', { + headers: { 'Authorization': `Bearer ${access_token}` } +}); +``` + +## Can I Use Both? + +**Yes!** You can use both APIs in the same application. + +**Example**: Task management app +- **Team Chat API**: User creates a task → message appears as "User created task #123" +- **Chatbot API**: Bot sends reminders → "Task #123 is due today [View] [Snooze]" + +## Decision Tree + +``` +Need rich interactive messages? +├─ Yes → Chatbot API +└─ No + └─ Need webhooks (slash commands, button clicks)? + ├─ Yes → Chatbot API + └─ No + └─ Messages should appear as user? + ├─ Yes → Team Chat API + └─ No → Chatbot API +``` + +## Common Misconceptions + +### ❌ "I need to use Server-to-Server OAuth for bots" +**Reality**: Chatbots require **General App (OAuth)**, not Server-to-Server OAuth. S2S apps don't support the Chatbot feature. + +### ❌ "Team Chat API can send buttons" +**Reality**: Only Chatbot API supports interactive components (buttons, forms, dropdowns). + +### ❌ "Chatbot API requires user login" +**Reality**: Chatbot API uses client_credentials flow (no user login needed). + +### ❌ "OAuth token endpoint is `/oauth/token`" +**Reality**: Use `https://zoom.us/oauth/token` for token exchange. Keep `https://zoom.us/oauth/authorize` for the user consent step. + +### ❌ "I can only use one API per app" +**Reality**: You can use both APIs in the same application. + +## Next Steps + +### If you chose **Team Chat API**: +1. [Environment Setup](environment-setup.md) - Get credentials +2. [OAuth Setup](../examples/oauth-setup.md) - Implement OAuth flow +3. [Send Message](../examples/send-message.md) - Send your first message + +### If you chose **Chatbot API**: +1. [Environment Setup](environment-setup.md) - Get credentials (including Bot JID) +2. [Chatbot Setup](../examples/chatbot-setup.md) - Build your first bot +3. [Webhook Architecture](webhooks.md) - Understand webhook events + +## Resources + +- [Official Team Chat API Docs](https://developers.zoom.us/docs/api/rest/reference/chat/) +- [Official Chatbot API Docs](https://developers.zoom.us/docs/api/rest/reference/chatbot/) +- [Chatbot Quickstart Sample](https://github.com/zoom/chatbot-nodejs-quickstart) diff --git a/plugins/zoom-developers/skills/team-chat/concepts/authentication.md b/plugins/zoom-developers/skills/team-chat/concepts/authentication.md new file mode 100644 index 00000000..5e9d27b5 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/concepts/authentication.md @@ -0,0 +1,36 @@ +# Authentication Flows (Team Chat vs Chatbot) + +Zoom Team Chat integrations commonly use one of two auth models: + +## Team Chat API (user-level) + +Use **User OAuth (authorization code)** when you want messages/actions to appear as a user. + +- Typical endpoints: + - Send message (as the user): `POST /v2/chat/users/me/messages` +- Typical scopes: + - `chat_message:write` + - `chat_channel:read` (for listing channels) + +## Chatbot API (bot-level) + +Use **client credentials** when you want messages/actions to appear as a bot. + +- Typical endpoint: + - Send bot message: `POST /v2/im/chat/messages` +- Typical “scope”: + - `imchat:bot` (added by enabling Chatbot feature on the app) + +## Decision Checklist + +- If you need to post to a channel “as a bot” and handle slash command interactions: use **Chatbot API**. +- If you need to post “as the user” (and respect the user’s channel membership): use **Team Chat API**. + +## Common Pitfalls + +- **Server-to-Server OAuth** is not a fit for Zoom Team Chat chatbot features. +- Team Chat API calls require a user token with the right scopes; “invalid access token” errors are almost always missing scopes or wrong app type. +- OAuth URL split is easy to mix up: + - authorize step: `https://zoom.us/oauth/authorize` + - token step (all grant types): `https://zoom.us/oauth/token` +- In browser demos, complete OAuth end-to-end in app (state verify -> callback -> code exchange -> token store) to avoid copy/paste mistakes. diff --git a/plugins/zoom-developers/skills/team-chat/concepts/deployment.md b/plugins/zoom-developers/skills/team-chat/concepts/deployment.md new file mode 100644 index 00000000..755abeae --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/concepts/deployment.md @@ -0,0 +1,23 @@ +# Deployment Guide (Team Chat / Chatbot) + +## Basic Requirements + +- Your webhook endpoint must be reachable by Zoom (public HTTPS). +- Keep secrets out of the repo: + - `ZOOM_CLIENT_ID` + - `ZOOM_CLIENT_SECRET` + - `ZOOM_BOT_JID` (chatbot) + - `ZOOM_SECRET_TOKEN` (chatbot verification) + +## Recommended Production Setup + +- Run behind a reverse proxy (TLS termination). +- Use a persistent store for: + - OAuth tokens (Team Chat API) + - installation state (Chatbot API) + - idempotency keys for webhooks (avoid double-processing) + +## Local Testing + +- Use a tunneling tool to expose your local development host over HTTPS for webhook testing. +- Keep a "dev" app and "prod" app to avoid breaking production while iterating. diff --git a/plugins/zoom-developers/skills/team-chat/concepts/environment-setup.md b/plugins/zoom-developers/skills/team-chat/concepts/environment-setup.md new file mode 100644 index 00000000..2dfcac63 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/concepts/environment-setup.md @@ -0,0 +1,301 @@ +# Environment Setup + +Complete guide to configuring your Zoom Team Chat development environment, obtaining credentials, and setting up your app. + +## Prerequisites + +- Zoom account +- Account owner, admin, or **Zoom for developers** role enabled + +### Enable "Zoom for developers" Role + +If you don't have owner/admin privileges: + +1. Ask your admin to enable the **Zoom for developers** role +2. Navigate to: **User Management** → **Roles** → **Role Settings** → **Advanced features** +3. Enable **View** and **Edit** checkboxes for **Zoom for developers** + +![Zoom for developers role](https://developers.zoom.us/img/nextImageExportOptimizer/UBF-role-prerequisite-opt-1080.WEBP) + +## Step 1: Create Zoom App + +### 1.1 Access App Marketplace + +1. Go to [Zoom App Marketplace](https://marketplace.zoom.us/) +2. Click **Develop** → **Build App** + +### 1.2 Select App Type + +**Select**: **General App** (OAuth) + +> ⚠️ **CRITICAL**: Do NOT select "Server-to-Server OAuth" +> +> **Why**: Server-to-Server OAuth apps do NOT support the Team Chat/Chatbot features. Only General App (OAuth) supports chatbots and team chat integrations. + +## Step 2: Basic Information + +On the **Basic Info** page, configure your app: + +### 2.1 App Name + +Update the auto-generated app name: +- Click the edit icon (pencil) +- Enter your app name (e.g., "My Team Chat Bot") +- Click outside the field to save + +### 2.2 App Management Type + +Choose how your app is managed: + +| Type | Use Case | Token Flow | +|------|----------|------------| +| **Admin-managed** | Company-wide bots, notifications, helpdesk | Recommended for chatbots | +| **User-managed** | Personal bots, individual user tools | For user-specific apps | + +**For most chatbots**: Choose **Admin-managed** + +**Important**: App management type affects available features and scopes. If you change it later, reconfirm your selected features and scopes. + +### 2.3 App Credentials (Auto-generated) + +The build flow automatically generates: + +| Credential | Environment | +|------------|-------------| +| **Client ID** | Development & Production | +| **Client Secret** | Development & Production | + +**Note**: Development and production credentials are different. + +### 2.4 OAuth Information + +#### OAuth Redirect URL (Required) + +Enter your OAuth callback endpoint: + +**Local development**: +``` +http://YOUR_DEV_HOST:4000/auth/callback +``` + +**Production**: +``` +https://yourdomain.com/auth/callback +``` + +#### OAuth Allow Lists (Required) + +Add all URLs that Zoom should allow as valid OAuth redirects: + +**Examples**: +- Complete URL: `https://subdomain.domain.tld/path/oauth/callback` +- Base URL: `https://subdomain.domain.tld` + +## Step 3: Enable Team Chat (Chatbot API Only) + +> **Skip this step** if you're only using Team Chat API (user-level messaging) + +### 3.1 Navigate to Features Page + +Go to **Features** page → **Surface** tab + +### 3.2 Select Team Chat Product + +In **Select where to use your app**, check **Team Chat** + +### 3.3 Configure App URLs + +| Field | Value | Example | +|-------|-------|---------| +| **Home URL** | Your app's home page | `https://yourdomain.com` | +| **Domain Allow List** | URLs Zoom client should accept | `https://yourdomain.com` | + +### 3.4 Enable Team Chat Subscription + +Configure webhook settings: + +| Field | Value | Example | +|-------|-------|---------| +| **Slash Command** | Command to invoke bot | `/mybot` | +| **Bot Endpoint URL** | Webhook endpoint | `https://yourdomain.com/webhook` | + +> **Critical**: Your bot will NOT appear in Team Chat unless you enable Team Chat Subscription! + +## Step 4: Get Credentials + +### 4.1 App Credentials (Both APIs) + +Navigate to **App Credentials** → **Development**: + +| Credential | Where to Find | +|------------|---------------| +| **Client ID** | App Credentials → Development | +| **Client Secret** | App Credentials → Development (Click "View") | +| **Account ID** | App Credentials → Development | + +### 4.2 Bot JID (Chatbot API Only) + +> **Note**: Bot JID only appears AFTER enabling Chatbot in Features tab + +**To find Bot JID**: + +1. Go to **Features** tab in left sidebar +2. Ensure **Chatbot** toggle is **ON** +3. Click **Chatbot** section to expand +4. Scroll to **Bot Credentials** section +5. You'll see two JIDs: + - **Bot JID (Development)**: Use for testing + - **Bot JID (Production)**: Use for live apps + +**Format**: `v1abc123xyz@xmpp.zoom.us` + +### 4.3 Webhook Secret Token (Chatbot API Only) + +Navigate to **Features** → **Team Chat Subscriptions** → **Secret Token** + +This token is used to verify webhook signatures. + +### 4.4 Credentials Summary + +| Credential | Team Chat API | Chatbot API | Location | +|------------|---------------|-------------|----------| +| Client ID | ✅ Required | ✅ Required | App Credentials → Development | +| Client Secret | ✅ Required | ✅ Required | App Credentials → Development | +| Account ID | ❌ | ✅ Required | App Credentials → Development | +| Bot JID | ❌ | ✅ Required | Features → Chatbot → Bot Credentials | +| Secret Token | ❌ | ✅ Required | Features → Team Chat Subscriptions | + +## Step 5: Configure Scopes + +Navigate to **Scopes** page in your app. + +### Team Chat API Scopes + +Manually add these scopes: + +- `chat_message:write` - Send messages +- `chat_message:read` - Read messages +- `chat_channel:read` - List channels +- `chat_channel:write` - Create/manage channels + +### Chatbot API Scopes + +When you enable Team Chat Subscription, these scopes are **automatically added**: + +- `imchat:bot` - Basic chatbot functionality +- `team_chat:read:list_user_channels:admin` - List channels +- `team_chat:read:list_members:admin` - List members + +## Step 6: Create .env File + +### For Team Chat API (User-Level) + +```bash +# .env file +ZOOM_CLIENT_ID=your_client_id_here +ZOOM_CLIENT_SECRET=your_client_secret_here +ZOOM_REDIRECT_URI=http://YOUR_DEV_HOST:4000/auth/callback + +PORT=4000 +``` + +### For Chatbot API (Bot-Level) + +```bash +# .env file +ZOOM_CLIENT_ID=your_client_id_here +ZOOM_CLIENT_SECRET=your_client_secret_here +ZOOM_BOT_JID=v1abc123xyz@xmpp.zoom.us +ZOOM_VERIFICATION_TOKEN=your_webhook_secret_token +ZOOM_ACCOUNT_ID=your_account_id + +PORT=4000 +``` + +### .env.example Template + +Create this file in your project root: + +```bash +# Zoom App Credentials (Required for both APIs) +ZOOM_CLIENT_ID= +ZOOM_CLIENT_SECRET= +ZOOM_REDIRECT_URI=http://YOUR_DEV_HOST:4000/auth/callback + +# Chatbot Credentials (Required for Chatbot API only) +ZOOM_BOT_JID= +ZOOM_VERIFICATION_TOKEN= +ZOOM_ACCOUNT_ID= + +# Server Configuration +PORT=4000 +``` + +## Step 7: Test Your App + +On the **Local Test** page: + +### 7.1 Add App to Your Account + +1. Click **Add App Now** +2. Click **Allow** to authorize the app +3. You'll be redirected to your OAuth redirect URL + +### 7.2 Preview App Listing + +Click **Preview Your App Listing Page** to see how your app appears in the marketplace. + +### 7.3 Share with Team Members + +To share your app with other users on your account: + +1. Go to **Authorization URL** section +2. Click **Generate** +3. Click **Copy** +4. Share the URL with your team members + +> **Note**: Beta apps can only be installed by members of the developer's Zoom account (security restriction). + +## Common Setup Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| Bot JID not visible | Chatbot feature not enabled | Go to Features tab, toggle Chatbot ON | +| Can't find Secret Token | Team Chat Subscription not enabled | Enable Team Chat Subscription in Features → Surface | +| OAuth redirect error | Redirect URL not in allow list | Add full redirect URL to OAuth allow lists | +| Scopes not appearing | Wrong app type | Verify you created General App (OAuth), not S2S | +| App can't be added | Missing required configuration | Complete all steps in Basic Info and Features | + +## Verification Checklist + +Before proceeding to development, verify: + +- [ ] Created **General App (OAuth)** (not Server-to-Server) +- [ ] Selected appropriate App Management Type +- [ ] Configured OAuth redirect URL +- [ ] Added URLs to OAuth allow lists +- [ ] Enabled Team Chat in Surface tab (for chatbots) +- [ ] Configured Team Chat Subscription (for chatbots) +- [ ] Added all required scopes +- [ ] Obtained all required credentials +- [ ] Created .env file with credentials +- [ ] Successfully added app to your account + +## Next Steps + +### For Team Chat API: +1. [Authentication Flows](authentication.md) - Understand OAuth +2. [OAuth Setup Example](../examples/oauth-setup.md) - Implement OAuth +3. [Send Message Example](../examples/send-message.md) - Send first message + +### For Chatbot API: +1. [Webhook Architecture](webhooks.md) - Understand webhooks +2. [Chatbot Setup Example](../examples/chatbot-setup.md) - Build your bot +3. [Message Cards Reference](../references/message-cards.md) - Create rich messages + +## Resources + +- [Zoom App Marketplace](https://marketplace.zoom.us/) +- [OAuth Documentation](https://developers.zoom.us/docs/integrations/oauth/) +- [Chatbot Documentation](https://developers.zoom.us/docs/team-chat/chatbot/extend/) +- [Using Role Management](https://support.zoom.us/hc/en-us/articles/115001078646) diff --git a/plugins/zoom-developers/skills/team-chat/concepts/message-structure.md b/plugins/zoom-developers/skills/team-chat/concepts/message-structure.md new file mode 100644 index 00000000..2a6c9892 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/concepts/message-structure.md @@ -0,0 +1,22 @@ +# Message Card Structure (Chatbot API) + +Chatbot messages use a card-like JSON structure (often called "message cards"). + +## High-Level Shape + +- `content.head`: title + optional subhead +- `content.body`: array of blocks + - `message` blocks for text + - `fields` blocks for key/value rows + - `actions` blocks for buttons + - `attachments` blocks for images/links + +## Where To Look + +- Component reference: `../references/message-cards.md` + +## Common Pitfalls + +- Buttons must include a `value` you can route on when you receive an interaction webhook. +- Many issues that look like "Zoom didn't render my card" are just invalid JSON shape; validate your payload before sending. + diff --git a/plugins/zoom-developers/skills/team-chat/concepts/security.md b/plugins/zoom-developers/skills/team-chat/concepts/security.md new file mode 100644 index 00000000..092d9855 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/concepts/security.md @@ -0,0 +1,18 @@ +# Security Best Practices + +## Webhooks + +- Verify webhook requests using Zoom’s verification mechanism for Team Chat subscriptions. +- Treat webhook payloads as untrusted input; validate fields before using them. + +## OAuth + +- Store refresh tokens securely (encrypt at rest). +- Rotate client secrets if they leak. +- Use least-privilege scopes. + +## Operational + +- Add rate limiting on your webhook endpoint. +- Log request IDs and correlation IDs (but avoid logging tokens / PII). + diff --git a/plugins/zoom-developers/skills/team-chat/concepts/webhooks.md b/plugins/zoom-developers/skills/team-chat/concepts/webhooks.md new file mode 100644 index 00000000..6ad5ce13 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/concepts/webhooks.md @@ -0,0 +1,493 @@ +# Webhook Architecture + +Complete guide to understanding and implementing Zoom Team Chat webhooks for interactive chatbots. + +## Overview + +Webhooks are HTTP POST requests that Zoom sends to your **Bot Endpoint URL** when specific events occur (slash commands, button clicks, form submissions, etc.). + +### How It Works + +``` +User action in Zoom → Zoom sends webhook → Your server processes → Send response +``` + +**Example flow**: +``` +1. User types "/weather San Francisco" in Zoom Team Chat +2. Zoom sends POST request to your Bot Endpoint URL +3. Your server receives webhook with payload.cmd = "San Francisco" +4. Your server calls weather API +5. Your server sends chatbot message back with weather data +``` + +## Webhook Lifecycle + +### Setup (One-time) + +1. **Configure Bot Endpoint URL** in Zoom Marketplace: + - Development: `https://abc123.ngrok.io/webhook` + - Production: `https://yourdomain.com/webhook` + +2. **Verify endpoint** - Zoom sends validation request when you save the URL + +### Runtime (Per Event) + +``` +User action → Zoom webhook → Your handler → Response +``` + +## Webhook Events + +| Event | Trigger | When It Fires | +|-------|---------|---------------| +| `endpoint.url_validation` | URL configured/changed | Setup only | +| `bot_installed` | Bot added to account | Installation | +| `bot_notification` | User messages bot or uses slash command | User interaction | +| `interactive_message_actions` | Button clicked | User clicks button | +| `chat_message.submit` | Form submitted | User submits form | +| `app_deauthorized` | Bot removed from account | Uninstallation | + +**See**: [Webhook Events Reference](../references/webhook-events.md) for complete event catalog + +## Webhook Structure + +### Request Headers + +Every webhook includes these headers: + +```javascript +{ + 'x-zm-signature': 'v0=abc123...', // Signature for verification + 'x-zm-request-timestamp': '1234567890', // Unix timestamp + 'content-type': 'application/json' +} +``` + +### Request Body + +```javascript +{ + "event": "bot_notification", // Event type + "payload": { // Event-specific data + "accountId": "...", + "toJid": "...", + "cmd": "...", + // ... more fields + } +} +``` + +## Webhook Verification + +**CRITICAL**: Always verify webhook signatures to prevent unauthorized requests. + +### Why Verify? + +Without verification, anyone can send fake webhooks to your endpoint, potentially: +- Triggering unauthorized actions +- Causing denial-of-service attacks +- Accessing sensitive data + +### Verification Algorithm + +```javascript +const crypto = require('crypto'); + +function verifyZoomWebhookSignature(req) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + const secretToken = process.env.ZOOM_VERIFICATION_TOKEN; + + if (!signature || !timestamp) { + throw new Error('Missing signature headers'); + } + + // Construct message + const message = `v0:${timestamp}:${JSON.stringify(req.body)}`; + + // Calculate expected signature + const expectedSignature = crypto + .createHmac('sha256', secretToken) + .update(message) + .digest('hex'); + + // Compare signatures + if (signature !== `v0=${expectedSignature}`) { + throw new Error('Invalid webhook signature'); + } + + return true; +} +``` + +### Verification Flow + +``` +1. Extract signature and timestamp from headers +2. Construct message: "v0:{timestamp}:{JSON body}" +3. Calculate HMAC-SHA256 with secret token +4. Compare calculated signature with header signature +5. Accept if match, reject if mismatch +``` + +## Webhook Handler Pattern + +### Basic Handler + +```javascript +app.post('/webhook', (req, res) => { + try { + // Step 1: Verify signature + verifyZoomWebhookSignature(req); + + // Step 2: Extract event and payload + const { event, payload } = req.body; + + // Step 3: Handle event + switch (event) { + case 'endpoint.url_validation': + return handleUrlValidation(req, res); + + case 'bot_installed': + return handleBotInstalled(payload, res); + + case 'bot_notification': + return handleBotNotification(payload, res); + + case 'interactive_message_actions': + return handleButtonClick(payload, res); + + case 'app_deauthorized': + return handleBotUninstalled(payload, res); + + default: + console.log('Unsupported event:', event); + return res.status(200).json({ success: true }); + } + } catch (error) { + if (error.message.includes('signature')) { + return res.status(401).json({ error: 'Invalid webhook signature' }); + } + return res.status(500).json({ error: error.message }); + } +}); +``` + +## Event Handlers + +### 1. URL Validation (`endpoint.url_validation`) + +Zoom sends this when you configure or change your Bot Endpoint URL. + +**Purpose**: Verify you control the endpoint + +**Payload**: +```javascript +{ + "event": "endpoint.url_validation", + "payload": { + "plainToken": "xyz123abc" + } +} +``` + +**Required Response**: +```javascript +{ + "plainToken": "xyz123abc", + "encryptedToken": "hmac_sha256(plainToken, secret_token)" +} +``` + +**Implementation**: +```javascript +function handleUrlValidation(req, res) { + const { plainToken } = req.body.payload; + + const encryptedToken = crypto + .createHmac('sha256', process.env.ZOOM_VERIFICATION_TOKEN) + .update(plainToken) + .digest('hex'); + + return res.status(200).json({ + plainToken, + encryptedToken + }); +} +``` + +### 2. Bot Installed (`bot_installed`) + +Fired when someone adds your bot to their account. + +**Payload**: +```javascript +{ + "event": "bot_installed", + "payload": { + "accountId": "...", + "userId": "...", + "timestamp": 1234567890 + } +} +``` + +**Use Case**: Initialize bot state, send welcome message + +**Implementation**: +```javascript +async function handleBotInstalled(payload, res) { + console.log('Bot installed for account:', payload.accountId); + + // Optional: Initialize database, send welcome message + // await initializeBotForAccount(payload.accountId); + + return res.status(200).json({ success: true }); +} +``` + +### 3. Bot Notification (`bot_notification`) + +Fired when: +- User sends message to bot via slash command +- User sends direct message to bot + +**Payload**: +```javascript +{ + "event": "bot_notification", + "payload": { + "accountId": "...", + "toJid": "channel@conference.xmpp.zoom.us", + "robotJid": "bot@xmpp.zoom.us", + "userJid": "user@xmpp.zoom.us", + "cmd": "user's input text", + "userName": "John Doe", + "channelName": "Marketing", + "timestamp": 1234567890 + } +} +``` + +**Key Fields**: +- `cmd` - User's input after the slash command +- `toJid` - Where to send response (channel or DM) +- `accountId` - Account identifier + +**Use Case**: Process commands, integrate LLM, send responses + +**Implementation**: +```javascript +async function handleBotNotification(payload, res) { + const { toJid, cmd, accountId, userName } = payload; + + console.log(`${userName} sent: ${cmd}`); + + // Process command (e.g., call LLM) + const response = await processCommand(cmd); + + // Send response + await sendChatbotMessage(toJid, accountId, { + body: [{ type: 'message', text: response }] + }); + + return res.status(200).json({ success: true }); +} +``` + +### 4. Interactive Message Actions (`interactive_message_actions`) + +Fired when user clicks a button in a chatbot message. + +**Payload**: +```javascript +{ + "event": "interactive_message_actions", + "payload": { + "accountId": "...", + "toJid": "...", + "actionItem": { + "text": "Approve", + "value": "approve" // This is what you check + }, + "messageId": "...", + "userName": "John Doe" + } +} +``` + +**Key Field**: `actionItem.value` - The button's value you defined + +**Implementation**: +```javascript +async function handleButtonClick(payload, res) { + const { actionItem, toJid, accountId, userName } = payload; + + console.log(`${userName} clicked: ${actionItem.value}`); + + switch (actionItem.value) { + case 'approve': + await sendChatbotMessage(toJid, accountId, { + body: [{ type: 'message', text: '✅ Approved!' }] + }); + break; + + case 'reject': + await sendChatbotMessage(toJid, accountId, { + body: [{ type: 'message', text: '❌ Rejected' }] + }); + break; + + default: + console.log('Unknown action:', actionItem.value); + } + + return res.status(200).json({ success: true }); +} +``` + +## Webhook Best Practices + +### 1. Always Verify Signatures + +```javascript +// ✅ GOOD +app.post('/webhook', (req, res) => { + verifyZoomWebhookSignature(req); + // ... handle event +}); + +// ❌ BAD +app.post('/webhook', (req, res) => { + // No verification - vulnerable to fake webhooks! +}); +``` + +### 2. Respond Quickly + +Zoom expects a 200 response within 3 seconds. + +```javascript +// ✅ GOOD - Respond immediately, process async +app.post('/webhook', (req, res) => { + verifyZoomWebhookSignature(req); + + // Respond immediately + res.status(200).json({ success: true }); + + // Process asynchronously + processWebhookAsync(req.body); +}); + +// ❌ BAD - Slow processing blocks response +app.post('/webhook', async (req, res) => { + await slowLLMCall(); // May timeout! + res.status(200).json({ success: true }); +}); +``` + +### 3. Handle All Events Gracefully + +```javascript +// ✅ GOOD - Handle unknown events +switch (event) { + case 'bot_notification': + return handleBotNotification(payload, res); + default: + console.log('Unsupported event:', event); + return res.status(200).json({ success: true }); +} + +// ❌ BAD - Crash on unknown events +switch (event) { + case 'bot_notification': + return handleBotNotification(payload, res); + // Missing default case - crashes on new events! +} +``` + +### 4. Log Webhook Activity + +```javascript +app.post('/webhook', (req, res) => { + const { event, payload } = req.body; + + console.log(`[Webhook] ${event}`, { + timestamp: new Date().toISOString(), + accountId: payload.accountId, + userId: payload.userId + }); + + // ... handle event +}); +``` + +### 5. Use Environment Variables + +```javascript +// ✅ GOOD +const SECRET_TOKEN = process.env.ZOOM_VERIFICATION_TOKEN; + +// ❌ BAD - Hardcoded secret +const SECRET_TOKEN = 'abc123xyz'; +``` + +## Testing Webhooks + +### Local Development with ngrok + +```bash +# Install ngrok +npm install -g ngrok + +# Expose local server +ngrok http 4000 + +# Copy HTTPS URL to Zoom Marketplace +# Example: https://abc123.ngrok.io/webhook +``` + +### Manual Testing + +```bash +WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:4000" + +# Test with curl (will fail signature verification - expected) +curl -X POST "$WEBHOOK_BASE_URL/webhook" \ + -H "Content-Type: application/json" \ + -d '{"event":"test"}' + +# Expected response: "Invalid webhook signature" (this is correct!) +``` + +### Verify Webhook is Working + +**Success indicators**: +1. Zoom successfully validates your endpoint URL +2. `bot_installed` event fires when you add the bot +3. `bot_notification` fires when you use slash command +4. Button clicks trigger `interactive_message_actions` + +## Common Webhook Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| "Cannot GET /webhook" | Browser sends GET, webhook is POST | Normal - test with POST or Zoom | +| "Invalid signature" | Wrong secret token | Verify ZOOM_VERIFICATION_TOKEN matches Zoom Marketplace | +| URL validation fails | Response format incorrect | Return plainToken + encryptedToken | +| No webhooks received | Wrong endpoint URL | Verify URL in Zoom Marketplace matches your server | +| Webhooks timeout | Slow response | Return 200 immediately, process async | + +## Next Steps + +- [Webhook Events Reference](../references/webhook-events.md) - Complete event catalog +- [Button Actions Example](../examples/button-actions.md) - Handle button clicks +- [Slash Commands Example](../examples/slash-commands.md) - Process slash commands +- [LLM Integration Example](../examples/llm-integration.md) - Integrate an LLM provider + +## Resources + +- [Chatbot Webhook Events](https://developers.zoom.us/docs/api/chatbot/events/) +- [Webhook Verification](https://developers.zoom.us/docs/api/webhooks/#verify-webhook-events) +- [Chatbot Quickstart](https://github.com/zoom/chatbot-nodejs-quickstart) diff --git a/plugins/zoom-developers/skills/team-chat/examples/button-actions.md b/plugins/zoom-developers/skills/team-chat/examples/button-actions.md new file mode 100644 index 00000000..d0d9cdf3 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/examples/button-actions.md @@ -0,0 +1,18 @@ +# Button Actions (Chatbot API) + +Buttons in message cards send a webhook when clicked. + +## Pattern + +1. You send a card with `actions.items[]` where each button has a unique `value`. +2. Zoom sends `interactive_message_actions` to your webhook. +3. Your handler routes based on that `value`. + +## Routing Tip + +Use stable action IDs like: + +- `approve_request` +- `reject_request` +- `open_ticket:123` + diff --git a/plugins/zoom-developers/skills/team-chat/examples/channel-management.md b/plugins/zoom-developers/skills/team-chat/examples/channel-management.md new file mode 100644 index 00000000..1c4055c9 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/examples/channel-management.md @@ -0,0 +1,12 @@ +# Channel Management (Team Chat API) + +Typical use cases: + +- List channels a user can see (to let them pick a destination). +- Create channels (where supported by the API and account policy). + +## Pitfalls + +- Many "can't list channels" issues are missing `chat_channel:read`. +- Admin policies may prevent channel creation. + diff --git a/plugins/zoom-developers/skills/team-chat/examples/chatbot-setup.md b/plugins/zoom-developers/skills/team-chat/examples/chatbot-setup.md new file mode 100644 index 00000000..fd77767e --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/examples/chatbot-setup.md @@ -0,0 +1,519 @@ +# Chatbot Setup - Complete Working Example + +Build your first interactive Zoom chatbot from scratch. This guide provides complete, production-ready code. + +## Prerequisites + +- Completed [Environment Setup](../concepts/environment-setup.md) +- Obtained Bot JID, Client ID, Client Secret, Account ID, Secret Token +- Created .env file with credentials + +## Project Structure + +``` +my-zoom-chatbot/ +├── .env +├── .env.example +├── package.json +├── server.js +├── routes/ +│ └── webhook.js +└── utils/ + ├── auth.js + ├── chatbot.js + └── validation.js +``` + +## Step 1: Initialize Project + +```bash +mkdir my-zoom-chatbot +cd my-zoom-chatbot +npm init -y +``` + +## Step 2: Install Dependencies + +```bash +npm install express dotenv node-fetch +``` + +## Step 3: Create .env File + +```bash +# .env +ZOOM_CLIENT_ID=your_client_id_here +ZOOM_CLIENT_SECRET=your_client_secret_here +ZOOM_BOT_JID=v1abc123xyz@xmpp.zoom.us +ZOOM_VERIFICATION_TOKEN=your_webhook_secret_token +ZOOM_ACCOUNT_ID=your_account_id + +PORT=4000 +``` + +## Step 4: Create Utility Files + +### utils/auth.js + +```javascript +// utils/auth.js +const fetch = require('node-fetch'); + +/** + * Get chatbot access token using client_credentials flow + */ +async function getChatbotToken() { + const credentials = Buffer.from( + `${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}` + ).toString('base64'); + + const response = await fetch('https://zoom.us/oauth/token', { + method: 'POST', + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: 'grant_type=client_credentials' + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(`Token error: ${error.error_description || error.error}`); + } + + const data = await response.json(); + return data.access_token; +} + +module.exports = { getChatbotToken }; +``` + +### utils/validation.js + +```javascript +// utils/validation.js +const crypto = require('crypto'); + +/** + * Verify Zoom webhook signature + */ +function verifyZoomWebhookSignature(req) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + + if (!signature || !timestamp) { + throw new Error('Missing signature headers'); + } + + const message = `v0:${timestamp}:${JSON.stringify(req.body)}`; + const hash = crypto + .createHmac('sha256', process.env.ZOOM_VERIFICATION_TOKEN) + .update(message) + .digest('hex'); + + if (signature !== `v0=${hash}`) { + throw new Error('Invalid webhook signature'); + } + + return true; +} + +/** + * Sanitize message (4096 char limit) + */ +function sanitizeMessage(message) { + if (typeof message !== 'string') return ''; + return message + .trim() + .replace(/[\x00-\x1F\x7F]/g, '') + .substring(0, 4096); +} + +/** + * Validate JID format + */ +function isValidJID(jid) { + if (typeof jid !== 'string' || !jid.trim()) return false; + return /^[^@\s]+@[^@\s]+$/.test(jid); +} + +module.exports = { + verifyZoomWebhookSignature, + sanitizeMessage, + isValidJID +}; +``` + +### utils/chatbot.js + +```javascript +// utils/chatbot.js +const fetch = require('node-fetch'); +const { getChatbotToken } = require('./auth'); +const { sanitizeMessage } = require('./validation'); + +/** + * Send chatbot message + */ +async function sendChatbotMessage(toJid, accountId, content) { + const accessToken = await getChatbotToken(); + + const body = { + robot_jid: process.env.ZOOM_BOT_JID, + to_jid: toJid, + account_id: accountId, + content: content + }; + + const response = await fetch('https://api.zoom.us/v2/im/chat/messages', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(`Send message error: ${JSON.stringify(error)}`); + } + + return response.json(); +} + +/** + * Send simple text message + */ +async function sendTextMessage(toJid, accountId, text) { + return sendChatbotMessage(toJid, accountId, { + body: [ + { type: 'message', text: sanitizeMessage(text) } + ] + }); +} + +/** + * Send message with buttons + */ +async function sendMessageWithButtons(toJid, accountId, options) { + const { title, message, buttons } = options; + + return sendChatbotMessage(toJid, accountId, { + head: { + text: title + }, + body: [ + { type: 'message', text: sanitizeMessage(message) }, + { + type: 'actions', + items: buttons.map(btn => ({ + text: btn.text, + value: btn.value, + style: btn.style || 'Default' + })) + } + ] + }); +} + +/** + * Send message with fields + */ +async function sendMessageWithFields(toJid, accountId, options) { + const { title, fields } = options; + + return sendChatbotMessage(toJid, accountId, { + head: { + text: title + }, + body: [ + { + type: 'fields', + items: fields.map(field => ({ + key: field.key, + value: field.value + })) + } + ] + }); +} + +module.exports = { + sendChatbotMessage, + sendTextMessage, + sendMessageWithButtons, + sendMessageWithFields +}; +``` + +## Step 5: Create Webhook Handler + +### routes/webhook.js + +```javascript +// routes/webhook.js +const crypto = require('crypto'); +const { verifyZoomWebhookSignature } = require('../utils/validation'); +const { sendTextMessage, sendMessageWithButtons } = require('../utils/chatbot'); + +async function handleWebhook(req, res) { + try { + // Verify signature + verifyZoomWebhookSignature(req); + + const { event, payload } = req.body; + + switch (event) { + case 'endpoint.url_validation': + return handleUrlValidation(req, res); + + case 'bot_installed': + console.log('Bot installed for account:', payload.accountId); + return res.status(200).json({ success: true }); + + case 'bot_notification': + return handleBotNotification(payload, res); + + case 'interactive_message_actions': + return handleButtonClick(payload, res); + + case 'app_deauthorized': + console.log('Bot uninstalled for account:', payload.accountId); + return res.status(200).json({ success: true }); + + default: + console.log('Unsupported event:', event); + return res.status(200).json({ success: true }); + } + } catch (error) { + console.error('Webhook error:', error); + + if (error.message.includes('signature')) { + return res.status(401).json({ error: 'Invalid webhook signature' }); + } + + return res.status(500).json({ error: error.message }); + } +} + +/** + * Handle URL validation + */ +function handleUrlValidation(req, res) { + const { plainToken } = req.body.payload; + + const encryptedToken = crypto + .createHmac('sha256', process.env.ZOOM_VERIFICATION_TOKEN) + .update(plainToken) + .digest('hex'); + + return res.status(200).json({ + plainToken, + encryptedToken + }); +} + +/** + * Handle bot notification (slash command or direct message) + */ +async function handleBotNotification(payload, res) { + const { toJid, cmd, accountId, userName } = payload; + + console.log(`${userName} sent: ${cmd}`); + + // Respond immediately + res.status(200).json({ success: true }); + + // Process command asynchronously + try { + // Simple command router + if (cmd.toLowerCase().includes('help')) { + await sendTextMessage(toJid, accountId, + 'Available commands:\n- help: Show this message\n- ping: Test bot\n- demo: Show demo buttons' + ); + } + else if (cmd.toLowerCase().includes('ping')) { + await sendTextMessage(toJid, accountId, 'Pong! 🏓'); + } + else if (cmd.toLowerCase().includes('demo')) { + await sendMessageWithButtons(toJid, accountId, { + title: 'Demo Buttons', + message: 'Click a button below:', + buttons: [ + { text: 'Option A', value: 'option_a', style: 'Primary' }, + { text: 'Option B', value: 'option_b', style: 'Default' }, + { text: 'Cancel', value: 'cancel', style: 'Danger' } + ] + }); + } + else { + await sendTextMessage(toJid, accountId, + `You said: "${cmd}"\n\nType "help" to see available commands.` + ); + } + } catch (error) { + console.error('Error processing command:', error); + } +} + +/** + * Handle button click + */ +async function handleButtonClick(payload, res) { + const { actionItem, toJid, accountId, userName } = payload; + + console.log(`${userName} clicked: ${actionItem.value}`); + + // Respond immediately + res.status(200).json({ success: true }); + + // Process button click asynchronously + try { + switch (actionItem.value) { + case 'option_a': + await sendTextMessage(toJid, accountId, '✅ You selected Option A'); + break; + + case 'option_b': + await sendTextMessage(toJid, accountId, '✅ You selected Option B'); + break; + + case 'cancel': + await sendTextMessage(toJid, accountId, '❌ Cancelled'); + break; + + default: + await sendTextMessage(toJid, accountId, `Unknown action: ${actionItem.value}`); + } + } catch (error) { + console.error('Error processing button click:', error); + } +} + +module.exports = { handleWebhook }; +``` + +## Step 6: Create Main Server + +### server.js + +```javascript +// server.js +require('dotenv').config(); +const express = require('express'); +const { handleWebhook } = require('./routes/webhook'); + +const app = express(); +const PORT = process.env.PORT || 4000; + +// Middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Routes +app.get('/', (req, res) => { + res.json({ message: 'Zoom Team Chat Bot is running!' }); +}); + +app.post('/webhook', handleWebhook); + +// Start server +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); + console.log(`Webhook endpoint: ${process.env.PUBLIC_BASE_URL || 'https://YOUR_PUBLIC_BASE_URL'}/webhook`); +}); +``` + +## Step 7: Test Locally with ngrok + +```bash +# Install ngrok +npm install -g ngrok + +# Start your server +node server.js + +# In a new terminal, expose with ngrok +ngrok http 4000 + +# Copy the HTTPS URL (e.g., https://abc123.ngrok.io) +``` + +## Step 8: Configure Zoom Marketplace + +1. Go to your app in [Zoom Marketplace](https://marketplace.zoom.us/) +2. Navigate to **Features** → **Team Chat Subscription** +3. Set **Bot Endpoint URL**: `https://abc123.ngrok.io/webhook` +4. Set **Slash Command**: `/mybot` +5. Click **Save** + +Zoom will send a `endpoint.url_validation` request. If successful, you'll see a green checkmark. + +## Step 9: Install and Test + +1. Go to **Local Test** page in Zoom Marketplace +2. Click **Add App Now** +3. Click **Allow** +4. Open Zoom Team Chat +5. In any channel, type: `/mybot help` + +You should see the bot respond with the help message! + +## Testing Checklist + +- [ ] `/mybot help` - Shows help message +- [ ] `/mybot ping` - Responds with "Pong! 🏓" +- [ ] `/mybot demo` - Shows buttons +- [ ] Click button - Sends confirmation message + +## Production Deployment + +### Environment Variables + +```bash +# Production .env +ZOOM_CLIENT_ID=your_production_client_id +ZOOM_CLIENT_SECRET=your_production_client_secret +ZOOM_BOT_JID=v1abc123xyz@xmpp.zoom.us # Production Bot JID +ZOOM_VERIFICATION_TOKEN=your_production_token +ZOOM_ACCOUNT_ID=your_account_id + +PORT=4000 +NODE_ENV=production +``` + +### Deploy to Cloud + +**Options**: +- Heroku +- AWS Lambda +- Google Cloud Run +- Digital Ocean App Platform +- Vercel (with serverless functions) + +**Requirements**: +- HTTPS endpoint (required for production) +- Publicly accessible URL +- Update Bot Endpoint URL in Zoom Marketplace to production URL + +## Next Steps + +- [Button Actions](button-actions.md) - Advanced button handling +- [LLM Integration](llm-integration.md) - Add an LLM provider +- [Message Cards Reference](../references/message-cards.md) - Rich message components +- [Webhook Events Reference](../references/webhook-events.md) - All webhook events + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| "Invalid signature" | Verify ZOOM_VERIFICATION_TOKEN matches Zoom Marketplace | +| Bot doesn't respond | Check ngrok is running and URL is correct | +| URL validation fails | Ensure endpoint returns plainToken + encryptedToken | +| Messages not sending | Verify Bot JID and Account ID are correct | + +## Resources + +- [Chatbot Quickstart (Official)](https://github.com/zoom/chatbot-nodejs-quickstart) +- [Unsplash Chatbot Sample](https://github.com/zoom/unsplash-chatbot) diff --git a/plugins/zoom-developers/skills/team-chat/examples/database-integration.md b/plugins/zoom-developers/skills/team-chat/examples/database-integration.md new file mode 100644 index 00000000..ae04f16e --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/examples/database-integration.md @@ -0,0 +1,14 @@ +# Database Integration (Stateful Bots) + +Store state when you need: + +- multi-step workflows +- approvals +- linking Zoom users to internal system users + +## Suggested Tables + +- `installations` (account_id, bot_jid, created_at) +- `users` (zoom_jid, internal_user_id) +- `workflows` (workflow_id, status, payload_json) + diff --git a/plugins/zoom-developers/skills/team-chat/examples/dropdown-selects.md b/plugins/zoom-developers/skills/team-chat/examples/dropdown-selects.md new file mode 100644 index 00000000..c9690fd3 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/examples/dropdown-selects.md @@ -0,0 +1,14 @@ +# Dropdown Selects (Chatbot API) + +Dropdowns can be used for: + +- picking a channel +- selecting a user +- selecting from a fixed list + +## Pattern + +1. Send a card with a select/dropdown element. +2. User selects an option and submits (or triggers an action). +3. Handle selection via webhook and update state. + diff --git a/plugins/zoom-developers/skills/team-chat/examples/form-submissions.md b/plugins/zoom-developers/skills/team-chat/examples/form-submissions.md new file mode 100644 index 00000000..e718031a --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/examples/form-submissions.md @@ -0,0 +1,15 @@ +# Form Submissions (Chatbot API) + +Forms inside cards can collect user input; submissions arrive via webhook. + +## Pattern + +1. Send a card with form fields. +2. Receive `chat_message.submit` webhook. +3. Validate inputs and respond with an updated card or confirmation message. + +## Pitfalls + +- Always validate types (dates, numbers) server-side. +- Treat submitted text as untrusted input. + diff --git a/plugins/zoom-developers/skills/team-chat/examples/llm-integration.md b/plugins/zoom-developers/skills/team-chat/examples/llm-integration.md new file mode 100644 index 00000000..dcd7cdf7 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/examples/llm-integration.md @@ -0,0 +1,16 @@ +# LLM Integration + +Use an LLM to interpret user intent from Team Chat messages, then call Zoom APIs or respond with rich message cards. + +Recommended flow: + +1. Receive `bot_notification` event. +2. Extract user text and channel context. +3. Classify intent with your LLM (meeting actions, help, status). +4. Execute safe backend actions (for example, create/list meetings). +5. Send structured response back to Team Chat. + +Implementation references: + +- [Chatbot Setup](chatbot-setup.md) +- [Sample Repositories](../references/samples.md) diff --git a/plugins/zoom-developers/skills/team-chat/examples/multi-step-workflows.md b/plugins/zoom-developers/skills/team-chat/examples/multi-step-workflows.md new file mode 100644 index 00000000..05ea8e08 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/examples/multi-step-workflows.md @@ -0,0 +1,13 @@ +# Multi-Step Workflows (Chatbot API) + +## Pattern + +1. Send a card with buttons (step 1). +2. On click, update stored state and respond with step 2 card. +3. Repeat until completion. + +## Pitfalls + +- Webhooks can be delivered more than once; de-dupe by event ID if available. +- Avoid storing PII in logs. + diff --git a/plugins/zoom-developers/skills/team-chat/examples/oauth-setup.md b/plugins/zoom-developers/skills/team-chat/examples/oauth-setup.md new file mode 100644 index 00000000..618a9bac --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/examples/oauth-setup.md @@ -0,0 +1,49 @@ +# OAuth Setup (Team Chat API) + +This is for the **Team Chat API** (user-level actions). + +## What You Need + +- App type: **General App (OAuth)** +- Redirect URL: your app's callback URL +- Scopes (typical): + - `chat_message:write` + - `chat_channel:read` + +## Flow Summary + +1. Redirect user to Zoom authorize URL. +2. Receive `code` at your redirect URL. +3. Exchange `code` for `access_token` + `refresh_token`. +4. Store tokens per-user. +5. Refresh the access token when it expires. + +## In-App Web Flow Pattern (Recommended) + +For browser demos, keep the whole flow in your app to avoid manual copy/paste mistakes: + +1. User clicks **Connect Zoom User** in your UI. +2. Backend returns authorize URL (`https://zoom.us/oauth/authorize`) with a generated `state`. +3. Redirect browser to Zoom consent screen. +4. Callback route validates `state` and exchanges `code` at `https://zoom.us/oauth/token`. +5. Callback page stores token in app storage (for demo: localStorage, for production: server session/DB) and redirects back to app. + +## Token Exchange (Server Side) + +Pseudo-code (Node style): + +```js +// POST https://zoom.us/oauth/token +// grant_type=authorization_code&code=...&redirect_uri=... +// Authorization: Basic base64(client_id:client_secret) +``` + +## Common Errors + +- `Invalid redirect`: redirect URL mismatch between code exchange and Marketplace config. +- `Invalid access token, does not contain scopes`: missing scopes on the app or user didn't re-consent after scope change. + +## Next + +- `send-message.md` to post a message once you have a user token. +- `token-management.md` for refresh strategy. diff --git a/plugins/zoom-developers/skills/team-chat/examples/scheduled-alerts.md b/plugins/zoom-developers/skills/team-chat/examples/scheduled-alerts.md new file mode 100644 index 00000000..0b30e76c --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/examples/scheduled-alerts.md @@ -0,0 +1,14 @@ +# Scheduled Alerts (Team Chat) + +## Two Common Approaches + +1. Team Chat API (as user): + - Cron triggers, refresh user token, send message to a channel. +2. Chatbot API (as bot): + - Cron triggers, request bot token, send message card. + +## Pitfalls + +- Don’t store tokens in plaintext. +- Ensure your cron job is idempotent (avoid duplicate messages). + diff --git a/plugins/zoom-developers/skills/team-chat/examples/send-message.md b/plugins/zoom-developers/skills/team-chat/examples/send-message.md new file mode 100644 index 00000000..d408cdd9 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/examples/send-message.md @@ -0,0 +1,23 @@ +# Send Your First Message (Team Chat API) + +This is for **Team Chat API** (messages sent as the authenticated user). + +## Endpoint + +`POST https://api.zoom.us/v2/chat/users/me/messages` + +## Minimal Payload + +```json +{ + "message": "Hello from my integration", + "to_channel": "CHANNEL_ID" +} +``` + +## Common Pitfalls + +- `to_channel` must be a channel the user can access. +- Use the correct scopes: + - `chat_message:write` + diff --git a/plugins/zoom-developers/skills/team-chat/examples/slash-commands.md b/plugins/zoom-developers/skills/team-chat/examples/slash-commands.md new file mode 100644 index 00000000..c56d41ad --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/examples/slash-commands.md @@ -0,0 +1,16 @@ +# Slash Commands (Chatbot API) + +Slash commands are configured on the Marketplace app and trigger webhook events. + +## Pattern + +1. Configure `/yourcommand` in the Chatbot feature settings. +2. User runs the command in Team Chat. +3. Your webhook receives `bot_notification` (or equivalent) with the command text. +4. Parse args and respond with a message card. + +## Pitfalls + +- Commands are account-scoped; make sure you're testing in the right account. +- Don’t rely on client-side parsing; parse on your server. + diff --git a/plugins/zoom-developers/skills/team-chat/examples/token-management.md b/plugins/zoom-developers/skills/team-chat/examples/token-management.md new file mode 100644 index 00000000..a5e3506a --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/examples/token-management.md @@ -0,0 +1,20 @@ +# Token Management (Team Chat API) + +## Storage + +Store per-user: + +- `access_token` +- `refresh_token` +- `expires_at` (absolute timestamp) + +## Refresh Strategy + +- Refresh "just-in-time" when an API call fails with token expiry, or +- Refresh proactively when `now >= expires_at - 60s`. + +## Pitfalls + +- Refresh tokens can expire or be revoked (user removes app, admin blocks app). +- When you change scopes, existing users may need to reauthorize. + diff --git a/plugins/zoom-developers/skills/team-chat/get-started.md b/plugins/zoom-developers/skills/team-chat/get-started.md new file mode 100644 index 00000000..c6bb74e4 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/get-started.md @@ -0,0 +1,60 @@ +# Team Chat Get Started + +This is the fast path for Zoom Team Chat integrations. + +## Step 1: Pick Integration Type First + +- **User type** (Team Chat API) + - Auth: `authorization_code` (User OAuth) + - Endpoint family: `/v2/chat/users/...` + - Messages appear as user + +- **Bot type** (Chatbot API) + - Auth: `client_credentials` + - Endpoint family: `/v2/im/chat/messages` + - Messages appear as bot + +If this decision is wrong, auth/scopes/endpoints will all mismatch. + +## Step 2: Set Up App + Credentials + +1. Create **General App (OAuth)** in Zoom Marketplace. +2. Configure scopes and feature settings. +3. Gather credentials from app config: + - `ZOOM_CLIENT_ID` + - `ZOOM_CLIENT_SECRET` + - `ZOOM_BOT_JID` (bot type) + - `ZOOM_ACCOUNT_ID` (bot type use cases) + +See: `concepts/environment-setup.md` + +## Step 3A: User Type (Team Chat API) + +1. Implement OAuth code flow. +2. Call `POST /v2/chat/users/me/messages` with bearer token. +3. Use OAuth endpoints correctly: + - authorize: `https://zoom.us/oauth/authorize` + - token exchange: `https://zoom.us/oauth/token` + +See: +- `examples/oauth-setup.md` +- `examples/send-message.md` + +## Step 3B: Bot Type (Chatbot API) + +1. Get token via `grant_type=client_credentials`. +2. Call `POST /v2/im/chat/messages`. +3. Add webhook endpoint for interactive events. +4. Use `https://zoom.us/oauth/token` for `client_credentials` token requests. + +See: +- `examples/chatbot-setup.md` +- `concepts/webhooks.md` +- `references/message-cards.md` + +## Step 4: Validate with a Minimal Smoke Test + +- User type: send one plain text channel message. +- Bot type: send one plain text bot message. + +Then add advanced features (buttons/forms/slash commands). diff --git a/plugins/zoom-developers/skills/team-chat/references/api-reference.md b/plugins/zoom-developers/skills/team-chat/references/api-reference.md new file mode 100644 index 00000000..4ed8dc81 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/references/api-reference.md @@ -0,0 +1,23 @@ +# API Reference Pointers + +This doc is intentionally lightweight; prefer the official REST reference for the authoritative schema. + +## Team Chat API (user-level) + +- Send message: `POST /v2/chat/users/me/messages` +- Typical needs: + - list channels + - post to channel / DM + - thread replies + +## Chatbot API (bot-level) + +- Send bot message: `POST /v2/im/chat/messages` + +## Notes + +- If you see "invalid access token" errors, check: + - app type (General App OAuth vs others) + - scopes + - whether the user re-consented after scope changes + diff --git a/plugins/zoom-developers/skills/team-chat/references/environment-variables.md b/plugins/zoom-developers/skills/team-chat/references/environment-variables.md new file mode 100644 index 00000000..034ee706 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/references/environment-variables.md @@ -0,0 +1,21 @@ +# Zoom Team Chat Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_CLIENT_ID` | Yes | Team Chat app OAuth identity | Zoom Marketplace -> Team Chat app -> App Credentials | +| `ZOOM_CLIENT_SECRET` | Yes | OAuth token exchange | Zoom Marketplace -> Team Chat app -> App Credentials | +| `ZOOM_REDIRECT_URI` | OAuth code flow | Callback URL for installs/auth | Zoom Marketplace -> OAuth redirect/allow list | +| `ZOOM_BOT_JID` | Chatbot flows | Target bot identifier | Team Chat app/chatbot configuration after setup | +| `ZOOM_SECRET_TOKEN` | Recommended | Event/webhook signature verification | Zoom Marketplace -> Event Subscriptions -> Secret Token | +| `ZOOM_VERIFICATION_TOKEN` | Legacy only | Legacy verification path | Zoom Marketplace legacy fields (older apps) | + +## Runtime-only values + +- `ZOOM_ACCESS_TOKEN` +- `ZOOM_REFRESH_TOKEN` + +## Notes + +- Prefer secret-token signature verification over legacy verification token. diff --git a/plugins/zoom-developers/skills/team-chat/references/error-codes.md b/plugins/zoom-developers/skills/team-chat/references/error-codes.md new file mode 100644 index 00000000..b8731b0d --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/references/error-codes.md @@ -0,0 +1,22 @@ +# Error Codes (Common Patterns) + +## Auth Errors + +- `Invalid access token` + - wrong token type (bot token used for user API, or vice versa) + - missing scopes + - token expired / revoked + +## Webhook Errors + +- No events received: + - endpoint not reachable publicly + - verification failing + - wrong event subscription / wrong app/account + +## Message Rendering Issues + +- Card not rendering: + - invalid JSON payload + - unsupported component types + diff --git a/plugins/zoom-developers/skills/team-chat/references/full-guide.md b/plugins/zoom-developers/skills/team-chat/references/full-guide.md new file mode 100644 index 00000000..f81309c9 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/references/full-guide.md @@ -0,0 +1,672 @@ +# /build-zoom-team-chat-app + +Background reference for Zoom Team Chat integrations. Use this after the workflow is clear, especially when the Team Chat API versus Chatbot API distinction matters. + +## Read This First (Critical) + +There are two different integration types and they are not interchangeable: + +1. **Team Chat API (user type)** + - Sends messages as a real authenticated user + - Uses **User OAuth** (`authorization_code`) + - Endpoint family: `/v2/chat/users/...` + +2. **Chatbot API (bot type)** + - Sends messages as your bot identity + - Uses **Client Credentials** (`client_credentials`) + - Endpoint family: `/v2/im/chat/messages` + +If you choose the wrong type early, auth/scopes/endpoints all mismatch and implementation fails. + +**Official Documentation**: https://developers.zoom.us/docs/team-chat/ +**Chatbot Documentation**: https://developers.zoom.us/docs/team-chat/chatbot/extend/ +**API Reference**: https://developers.zoom.us/docs/api/rest/reference/chatbot/ + +## Quick Links + +**New to Team Chat? Follow this path:** + +1. **[Get Started](../get-started.md)** - End-to-end fast path (user type vs bot type) +2. **[Choose Your API](../concepts/api-selection.md)** - Team Chat API vs Chatbot API +3. **[Environment Setup](../concepts/environment-setup.md)** - Credentials, scopes, app configuration +4. **[OAuth Setup](../examples/oauth-setup.md)** - Complete authentication flow +5. **[Send First Message](../examples/send-message.md)** - Working code to send messages + +**Reference:** +- **[Chatbot Message Cards](../references/message-cards.md)** - Complete card component reference +- **[Webhook Events](../references/webhook-events.md)** - All webhook event types +- **[API Reference](../references/api-reference.md)** - Endpoints, methods, parameters +- **[Sample Applications](../references/samples.md)** - 10+ official sample apps +- **Integrated Index** - see the section below in this file + +**Having issues?** +- Authentication errors → [OAuth Troubleshooting](../troubleshooting/oauth-issues.md) +- Webhook not receiving events → [Webhook Setup Guide](../troubleshooting/webhook-issues.md) +- Messages not sending → [Common Issues](../troubleshooting/common-issues.md) +- Start with quick checks → [5-Minute Runbook](../RUNBOOK.md) + +**OAuth endpoint sanity check:** +- Authorize URL: `https://zoom.us/oauth/authorize` +- Token URL: `https://zoom.us/oauth/token` +- If `/oauth/token` returns 404/HTML, use `https://zoom.us/oauth/token`. + +**Building Interactive Bots?** +- [Button Actions](../examples/button-actions.md) - Handle button clicks +- [Form Submissions](../examples/form-submissions.md) - Process form data +- [Slash Commands](../examples/slash-commands.md) - Create custom commands + +## Quick Decision: Which API? + +| Use Case | API to Use | +|----------|------------| +| Send notifications from scripts/CI/CD | **Team Chat API** | +| Automate messages as a user | **Team Chat API** | +| Build an interactive chatbot | **Chatbot API** | +| Respond to slash commands | **Chatbot API** | +| Create messages with buttons/forms | **Chatbot API** | +| Handle user interactions | **Chatbot API** | + +### Team Chat API (User-Level) +- Messages appear as sent by **authenticated user** +- Requires **User OAuth** (authorization_code flow) +- Endpoint: `POST https://api.zoom.us/v2/chat/users/me/messages` +- Scopes: `chat_message:write`, `chat_channel:read` + +### Chatbot API (Bot-Level) +- Messages appear as sent by your **bot** +- Requires **Client Credentials** grant +- Endpoint: `POST https://api.zoom.us/v2/im/chat/messages` +- Scopes: `imchat:bot` (auto-added) +- **Rich cards**: buttons, forms, dropdowns, images + +## Prerequisites + +### System Requirements + +- Zoom account +- Account owner, admin, or **Zoom for developers** role enabled + - To enable: **User Management** → **Roles** → **Role Settings** → **Advanced features** → Enable **Zoom for developers** + +### Create Zoom App + +1. Go to [Zoom App Marketplace](https://marketplace.zoom.us/) +2. Click **Develop** → **Build App** +3. Select **General App** (OAuth) + +> ⚠️ **Do NOT use Server-to-Server OAuth** - S2S apps don't have the Chatbot/Team Chat feature. Only General App (OAuth) supports chatbots. + +### Required Credentials + +From Zoom Marketplace → Your App: + +| Credential | Location | Used By | +|------------|----------|---------| +| Client ID | App Credentials → Development | Both APIs | +| Client Secret | App Credentials → Development | Both APIs | +| Account ID | App Credentials → Development | Chatbot API | +| Bot JID | Features → Chatbot → Bot Credentials | Chatbot API | +| Secret Token | Features → Team Chat Subscriptions | Chatbot API | + +**See**: [Environment Setup Guide](../concepts/environment-setup.md) for complete configuration steps. + +## Quick Start: Team Chat API + +Send a message as a user: + +```javascript +// 1. Get access token via OAuth +const accessToken = await getOAuthToken(); // See examples/oauth-setup.md + +// 2. Send message to channel +const response = await fetch('https://api.zoom.us/v2/chat/users/me/messages', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + message: 'Hello from CI/CD pipeline!', + to_channel: 'CHANNEL_ID' + }) +}); + +const data = await response.json(); +// { "id": "msg_abc123", "date_time": "2024-01-15T10:30:00Z" } +``` + +**Complete example**: [Send Message Guide](../examples/send-message.md) + +## Quick Start: Chatbot API + +Build an interactive chatbot: + +```javascript +// 1. Get chatbot token (client_credentials) +async function getChatbotToken() { + const credentials = Buffer.from( + `${CLIENT_ID}:${CLIENT_SECRET}` + ).toString('base64'); + + const response = await fetch('https://zoom.us/oauth/token', { + method: 'POST', + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: 'grant_type=client_credentials' + }); + + return (await response.json()).access_token; +} + +// 2. Send chatbot message with buttons +const response = await fetch('https://api.zoom.us/v2/im/chat/messages', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + robot_jid: process.env.ZOOM_BOT_JID, + to_jid: payload.toJid, // From webhook + account_id: payload.accountId, // From webhook + content: { + head: { + text: 'Build Notification', + sub_head: { text: 'CI/CD Pipeline' } + }, + body: [ + { type: 'message', text: 'Deployment successful!' }, + { + type: 'fields', + items: [ + { key: 'Branch', value: 'main' }, + { key: 'Commit', value: 'abc123' } + ] + }, + { + type: 'actions', + items: [ + { text: 'View Logs', value: 'view_logs', style: 'Primary' }, + { text: 'Dismiss', value: 'dismiss', style: 'Default' } + ] + } + ] + } + }) +}); +``` + +**Complete example**: [Chatbot Setup Guide](../examples/chatbot-setup.md) + +## Key Features + +### Team Chat API + +| Feature | Description | +|---------|-------------| +| **Send Messages** | Post messages to channels or direct messages | +| **List Channels** | Get user's channels with metadata | +| **Create Channels** | Create public/private channels programmatically | +| **Threaded Replies** | Reply to specific messages in threads | +| **Edit/Delete** | Modify or remove messages | + +### Chatbot API + +| Feature | Description | +|---------|-------------| +| **Rich Message Cards** | Headers, images, fields, buttons, forms | +| **Slash Commands** | Custom `/commands` trigger webhooks | +| **Button Actions** | Interactive buttons with webhook callbacks | +| **Form Submissions** | Collect user input with forms | +| **Dropdown Selects** | Channel, member, date/time pickers | +| **LLM Integration** | Easy integration with LLM providers such as OpenAI, Cohere, Cerebras, or others | + +## Webhook Events (Chatbot API) + +| Event | Trigger | Use Case | +|-------|---------|----------| +| `bot_notification` | User messages bot or uses slash command | Process commands, integrate LLM | +| `bot_installed` | Bot added to account | Initialize bot state | +| `interactive_message_actions` | Button clicked | Handle button actions | +| `chat_message.submit` | Form submitted | Process form data | +| `app_deauthorized` | Bot removed | Cleanup | + +**See**: [Webhook Events Reference](../references/webhook-events.md) + +## Message Card Components + +Build rich interactive messages with these components: + +| Component | Description | +|-----------|-------------| +| **header** | Title and subtitle | +| **message** | Plain text | +| **fields** | Key-value pairs | +| **actions** | Buttons (Primary, Danger, Default styles) | +| **section** | Colored sidebar grouping | +| **attachments** | Images with links | +| **divider** | Horizontal line | +| **form_field** | Text input | +| **dropdown** | Select menu | +| **date_picker** | Date selection | + +**See**: [Message Cards Reference](../references/message-cards.md) for complete component catalog + +## Architecture Patterns + +### Chatbot Lifecycle + +``` +User types /command → Webhook receives bot_notification + ↓ + payload.cmd = "user's input" + ↓ + Process command + ↓ + Send response via sendChatbotMessage() +``` + +### LLM Integration Pattern + +```javascript +case 'bot_notification': { + const { toJid, cmd, accountId } = payload; + + // 1. Call your LLM + const llmResponse = await callLLM(cmd); + + // 2. Send response back + await sendChatbotMessage(toJid, accountId, { + body: [{ type: 'message', text: llmResponse }] + }); +} +``` + +**See**: [LLM Integration Guide](../examples/llm-integration.md) + +## Sample Applications + +| Sample | Description | Link | +|--------|-------------|------| +| **Chatbot Quickstart** | Official tutorial (recommended start) | [GitHub](https://github.com/zoom/chatbot-nodejs-quickstart) | +| **Unsplash Chatbot** | Image search with database | [GitHub](https://github.com/zoom/unsplash-chatbot) | +| **ERP Chatbot** | Oracle ERP with scheduled alerts | [GitHub](https://github.com/zoom/zoom-erp-chatbot-sample) | +| **Task Manager** | Full CRUD app | [GitHub](https://github.com/zoom/task-manager-sample) | + +**See**: [Sample Applications Guide](../references/samples.md) for sample analysis + +## Common Operations + +### Send Message to Channel + +```javascript +// Team Chat API +await fetch('https://api.zoom.us/v2/chat/users/me/messages', { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` }, + body: JSON.stringify({ + message: 'Hello!', + to_channel: 'CHANNEL_ID' + }) +}); +``` + +### Handle Button Click + +```javascript +// Webhook handler +case 'interactive_message_actions': { + const { actionItem, toJid, accountId } = payload; + + if (actionItem.value === 'approve') { + await sendChatbotMessage(toJid, accountId, { + body: [{ type: 'message', text: '✅ Approved!' }] + }); + } +} +``` + +### Verify Webhook Signature + +```javascript +function verifyWebhook(req) { + const message = `v0:${req.headers['x-zm-request-timestamp']}:${JSON.stringify(req.body)}`; + const hash = crypto.createHmac('sha256', process.env.ZOOM_VERIFICATION_TOKEN) + .update(message) + .digest('hex'); + return req.headers['x-zm-signature'] === `v0=${hash}`; +} +``` + +## Deployment + +### ngrok for Local Development + +```bash +# Install ngrok +npm install -g ngrok + +# Expose local server +ngrok http 4000 + +# Use HTTPS URL as Bot Endpoint URL in Zoom Marketplace +# Example: https://abc123.ngrok.io/webhook +``` + +### Production Deployment + +**See**: [Deployment Guide](../concepts/deployment.md) for: +- Nginx reverse proxy setup +- Base path configuration +- OAuth redirect URI setup + +## Limitations + +| Limit | Value | +|-------|-------| +| Message length | 4,096 characters | +| File size | 512 MB | +| Members per channel | 10,000 | +| Channels per user | 500 | + +## Security Best Practices + +1. **Verify webhook signatures** - Always validate using `x-zm-signature` header +2. **Sanitize messages** - Limit to 4096 chars, remove control characters +3. **Validate JIDs** - Check format: `user@domain` or `channel@domain` +4. **Environment variables** - Never hardcode credentials +5. **Use HTTPS** - Required for production webhooks + +**See**: [Security Best Practices](../concepts/security.md) + +## Complete Documentation Library + +### Core Concepts (Start Here!) +- **[API Selection Guide](../concepts/api-selection.md)** - Choose Team Chat API vs Chatbot API +- **[Environment Setup](../concepts/environment-setup.md)** - Complete credentials guide +- **[Authentication Flows](../concepts/authentication.md)** - OAuth vs Client Credentials +- **[Webhook Architecture](../concepts/webhooks.md)** - How webhooks work +- **[Message Card Structure](../concepts/message-structure.md)** - Card component hierarchy + +### Complete Examples +- **[OAuth Setup](../examples/oauth-setup.md)** - Full OAuth implementation +- **[Send Message](../examples/send-message.md)** - Team Chat API message sending +- **[Chatbot Setup](../examples/chatbot-setup.md)** - Complete chatbot with webhooks +- **[Button Actions](../examples/button-actions.md)** - Handle interactive buttons +- **[Form Submissions](../examples/form-submissions.md)** - Process form data +- **[Slash Commands](../examples/slash-commands.md)** - Create custom commands +- **[LLM Integration](../examples/llm-integration.md)** - LLM provider integration +- **[Scheduled Alerts](../examples/scheduled-alerts.md)** - Cron + incoming webhooks +- **[Channel Management](../examples/channel-management.md)** - Create/manage channels + +### References +- **[API Reference](../references/api-reference.md)** - All endpoints and methods +- **[Webhook Events](../references/webhook-events.md)** - Complete event reference +- **[Message Cards](../references/message-cards.md)** - All card components +- **[Sample Applications](../references/samples.md)** - Analysis of 10 official samples +- **[Error Codes](../references/error-codes.md)** - Error handling guide + +### Troubleshooting +- **[OAuth Issues](../troubleshooting/oauth-issues.md)** - Authentication failures +- **[Webhook Issues](../troubleshooting/webhook-issues.md)** - Webhook debugging +- **[Common Issues](../troubleshooting/common-issues.md)** - Quick diagnostics + +## Resources + +- **Official Docs**: https://developers.zoom.us/docs/team-chat/ +- **API Reference**: https://developers.zoom.us/docs/api/rest/reference/chatbot/ +- **Dev Forum**: https://devforum.zoom.us/ +- **App Marketplace**: https://marketplace.zoom.us/ + +--- + +**Need help?** Start with Integrated Index section below for complete navigation. + +--- + +## Integrated Index + +_This section was migrated from `SKILL.md`._ + +Complete navigation guide for the Zoom Team Chat skill. + +## Quick Start Paths + +- Start here: [Get Started](../get-started.md) +- Fast troubleshooting first: [5-Minute Runbook](../RUNBOOK.md) + +### Path 1: Team Chat API (User-Level Messaging) + +For sending messages as a user account. + +1. [API Selection Guide](../concepts/api-selection.md) - Confirm Team Chat API is right +2. [Environment Setup](../concepts/environment-setup.md) - Get credentials +3. [OAuth Setup Example](../examples/oauth-setup.md) - Implement authentication +4. [Send Message Example](../examples/send-message.md) - Send your first message + +### Path 2: Chatbot API (Interactive Bots) + +For building interactive chatbots with rich messages. + +1. [API Selection Guide](../concepts/api-selection.md) - Confirm Chatbot API is right +2. [Environment Setup](../concepts/environment-setup.md) - Get credentials (including Bot JID) +3. [Webhook Architecture](../concepts/webhooks.md) - Understand webhook events +4. [Chatbot Setup Example](../examples/chatbot-setup.md) - Build your first bot +5. [Message Cards Reference](../references/message-cards.md) - Create rich messages + +## Core Concepts + +Essential understanding for both APIs. + +| Document | Description | +|----------|-------------| +| [API Selection Guide](../concepts/api-selection.md) | Choose Team Chat API vs Chatbot API | +| [Environment Setup](../concepts/environment-setup.md) | Complete credentials and app configuration | +| [Authentication Flows](../concepts/authentication.md) | OAuth vs Client Credentials | +| [Webhook Architecture](../concepts/webhooks.md) | How webhooks work (Chatbot API) | +| [Message Card Structure](../concepts/message-structure.md) | Card component hierarchy | +| [Deployment Guide](../concepts/deployment.md) | Production deployment strategies | +| [Security Best Practices](../concepts/security.md) | Secure your integration | + +## Complete Examples + +Working code for common scenarios. + +### Authentication +| Example | Description | +|---------|-------------| +| [OAuth Setup](../examples/oauth-setup.md) | User OAuth flow implementation | +| [Token Management](../examples/token-management.md) | Refresh tokens, expiration handling | + +### Basic Operations +| Example | Description | +|---------|-------------| +| [Send Message](../examples/send-message.md) | Team Chat API message sending | +| [Chatbot Setup](../examples/chatbot-setup.md) | Complete chatbot with webhooks | +| [List Channels](../examples/channel-management.md) | Get user's channels | +| [Create Channel](../examples/channel-management.md) | Create public/private channels | + +### Interactive Features (Chatbot API) +| Example | Description | +|---------|-------------| +| [Button Actions](../examples/button-actions.md) | Handle button clicks | +| [Form Submissions](../examples/form-submissions.md) | Process form data | +| [Slash Commands](../examples/slash-commands.md) | Create custom commands | +| [Dropdown Selects](../examples/dropdown-selects.md) | Channel/member pickers | + +### Advanced Integration +| Example | Description | +|---------|-------------| +| [LLM Integration](../examples/llm-integration.md) | Integrate an LLM provider | +| [Scheduled Alerts](../examples/scheduled-alerts.md) | Cron + incoming webhooks | +| [Database Integration](../examples/database-integration.md) | Store conversation state | +| [Multi-Step Workflows](../examples/multi-step-workflows.md) | Complex user interactions | + +## References + +### API Documentation +| Reference | Description | +|-----------|-------------| +| [API Reference](../references/api-reference.md) | Pointers and common endpoints | +| [Webhook Events](../references/webhook-events.md) | Event types and handling checklist | +| [Message Cards](../references/message-cards.md) | All card components | +| [Error Codes](../references/error-codes.md) | Error handling guide | + +### Sample Applications +| Reference | Description | +|-----------|-------------| +| [Sample Applications](../references/samples.md) | Sample app index/notes | + +### Field Guides +| Reference | Description | +|-----------|-------------| +| [JID Formats](../references/jid-formats.md) | Understanding JID identifiers | +| [Scopes Reference](../references/scopes.md) | Common scopes | +| [Rate Limits](../references/rate-limits.md) | Throttling guidance | + +## Troubleshooting + +| Guide | Description | +|-------|-------------| +| [Common Issues](../troubleshooting/common-issues.md) | Quick diagnostics and solutions | +| [OAuth Issues](../troubleshooting/oauth-issues.md) | Authentication failures | +| [Webhook Issues](../troubleshooting/webhook-issues.md) | Webhook debugging | +| [Message Issues](../troubleshooting/message-issues.md) | Message sending problems | +| [Deployment Issues](../troubleshooting/deployment-issues.md) | Production problems | + +## Architecture Patterns + +### Chatbot Lifecycle + +``` +User Action → Webhook → Process → Response +``` + +### LLM Integration Pattern + +``` +User Input → Chatbot receives → Call LLM → Send response +``` + +### Approval Workflow Pattern + +``` +Request → Send card with buttons → User clicks → Update status → Notify +``` + +## Common Use Cases + +### Notifications +- CI/CD build notifications +- Server monitoring alerts +- Scheduled reports +- System health checks + +### Workflows +- Approval requests +- Task assignment +- Status updates +- Form submissions + +### Integrations +- LLM-powered assistants +- Database queries +- External API integration +- File/image sharing + +### Automation +- Scheduled messages +- Auto-responses +- Data collection +- Report generation + +## Resource Links + +### Official Documentation +- **[Team Chat Docs](https://developers.zoom.us/docs/team-chat/)** - Official overview +- **[Chatbot Docs](https://developers.zoom.us/docs/team-chat/chatbot/extend/)** - Chatbot guide +- **[API Reference](https://developers.zoom.us/docs/api/rest/reference/chatbot/)** - REST API docs +- **[App Marketplace](https://marketplace.zoom.us/)** - Create and manage apps + +### Sample Code +- **[Chatbot Quickstart](https://github.com/zoom/chatbot-nodejs-quickstart)** - Official tutorial +- **[Unsplash Chatbot](https://github.com/zoom/unsplash-chatbot)** - Image search bot +- **[ERP Chatbot](https://github.com/zoom/zoom-erp-chatbot-sample)** - Enterprise integration +- **[Task Manager](https://github.com/zoom/task-manager-sample)** - Full CRUD app + +### Tools +- **[App Card Builder](https://appssdk.zoom.us/cardbuilder/)** - Visual card designer +- **[ngrok](https://ngrok.com/)** - Local webhook testing +- **[Postman](https://www.postman.com/)** - API testing + +### Community +- **[Developer Forum](https://devforum.zoom.us/)** - Ask questions +- **[GitHub Discussions](https://github.com/zoom)** - Community support +- **[Developer Support](https://devsupport.zoom.us)** - Official support + +## Documentation Status + +### ✅ Complete +- Main skill.md entry point +- API Selection Guide +- Environment Setup +- Webhook Architecture +- Chatbot Setup Example (complete working code) +- Message Cards Reference +- Common Issues Troubleshooting + +### 📝 Pending (High Priority) +- OAuth Setup Example +- Send Message Example +- Button Actions Example +- LLM Integration Example +- Webhook Events Reference +- API Reference +- Sample Applications Analysis + +### 📋 Planned (Lower Priority) +- Form Submissions Example +- Channel Management Examples +- Database Integration Example +- Error Codes Reference +- Rate Limits Guide +- Deployment troubleshooting + +## Getting Started Checklist + +### For Team Chat API + +- [ ] Read [API Selection Guide](../concepts/api-selection.md) +- [ ] Complete [Environment Setup](../concepts/environment-setup.md) +- [ ] Obtain Client ID, Client Secret +- [ ] Add required scopes +- [ ] Implement OAuth flow +- [ ] Send first message + +### For Chatbot API + +- [ ] Read [API Selection Guide](../concepts/api-selection.md) +- [ ] Complete [Environment Setup](../concepts/environment-setup.md) +- [ ] Obtain Client ID, Client Secret, Bot JID, Secret Token, Account ID +- [ ] Enable Team Chat in Features +- [ ] Configure Bot Endpoint URL and Slash Command +- [ ] Set up ngrok for local testing +- [ ] Implement webhook handler +- [ ] Send first chatbot message + +## Version History + +- **v1.0** (2026-02-09) - Initial comprehensive documentation + - Core concepts (API selection, environment setup, webhooks) + - Complete chatbot setup example + - Message cards reference + - Common issues troubleshooting + +## Support + +Use this SKILL.md as the navigation hub for Team Chat API selection, setup, examples, and troubleshooting. + +## Environment Variables + +- See [references/environment-variables.md](../references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/plugins/zoom-developers/skills/team-chat/references/jid-formats.md b/plugins/zoom-developers/skills/team-chat/references/jid-formats.md new file mode 100644 index 00000000..1013df3e --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/references/jid-formats.md @@ -0,0 +1,10 @@ +# JID Formats (Quick Guide) + +JIDs identify users/bots in Team Chat contexts. + +## Practical Tips + +- Treat JIDs as opaque identifiers. +- Store them exactly as received. +- Don’t parse structure unless Zoom explicitly documents the format you need. + diff --git a/plugins/zoom-developers/skills/team-chat/references/message-cards.md b/plugins/zoom-developers/skills/team-chat/references/message-cards.md new file mode 100644 index 00000000..f14219a8 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/references/message-cards.md @@ -0,0 +1,343 @@ +# Message Card Components Reference + +Complete reference for building rich interactive messages in Zoom Team Chat chatbots. + +## Card Structure + +Every chatbot message has this structure: + +```javascript +{ + "content": { + "head": { // Optional header + "text": "Title", + "sub_head": { "text": "Subtitle" } + }, + "body": [ // Array of components + { "type": "message", "text": "Content" }, + { "type": "actions", "items": [...] } + // ... more components + ] + } +} +``` + +## Components Catalog + +### Text Components + +#### message +Plain text content. + +```javascript +{ + "type": "message", + "text": "Hello, this is plain text" +} +``` + +#### header +Title text with optional styling. + +```javascript +{ + "type": "header", + "text": "Main Heading", + "style": { + "bold": true, + "italic": false + } +} +``` + +#### styled_text +Text with markdown-like styling. + +```javascript +{ + "type": "styled_text", + "text": "**Bold** *italic* `code`" +} +``` + +### Interactive Components + +#### actions (Buttons) +Clickable buttons that trigger webhooks. + +```javascript +{ + "type": "actions", + "items": [ + { + "text": "Approve", + "value": "approve", + "style": "Primary" // Primary, Danger, Default + }, + { + "text": "Reject", + "value": "reject", + "style": "Danger" + } + ] +} +``` + +**Styles**: +- `Primary` - Blue button +- `Danger` - Red button +- `Default` - Gray button + +#### dropdown +Select menu with options. + +```javascript +{ + "type": "dropdown", + "select_items": [ + { "text": "Option 1", "value": "opt1" }, + { "text": "Option 2", "value": "opt2" } + ] +} +``` + +#### form_field +Text input field. + +```javascript +{ + "type": "form_field", + "editable": true, + "text": "Enter your name" +} +``` + +### Layout Components + +#### section +Group components with optional colored sidebar. + +```javascript +{ + "type": "section", + "sidebar_color": "#3b82f6", // Hex color + "sections": [ + { "type": "message", "text": "Grouped content" } + ] +} +``` + +**Common colors**: +- Success: `#10b981` (green) +- Error: `#ef4444` (red) +- Warning: `#f59e0b` (orange) +- Info: `#3b82f6` (blue) + +#### fields +Key-value pairs displayed in columns. + +```javascript +{ + "type": "fields", + "items": [ + { "key": "Status", "value": "Active" }, + { "key": "Priority", "value": "High" }, + { "key": "Assignee", "value": "John Doe" } + ] +} +``` + +#### divider +Horizontal line separator. + +```javascript +{ + "type": "divider" +} +``` + +### Media Components + +#### attachments +Image with optional link. + +```javascript +{ + "type": "attachments", + "img_url": "https://example.com/image.jpg", + "resource_url": "https://example.com/full-page", + "information": { + "title": { "text": "Image Title" }, + "description": { "text": "Click to view" } + } +} +``` + +## Complete Examples + +### Build Notification + +```javascript +{ + "content": { + "head": { + "text": "Build #123 Complete", + "sub_head": { "text": "main branch" } + }, + "body": [ + { + "type": "section", + "sidebar_color": "#10b981", + "sections": [ + { "type": "message", "text": "✅ Build completed successfully" } + ] + }, + { + "type": "fields", + "items": [ + { "key": "Branch", "value": "main" }, + { "key": "Commit", "value": "abc123" }, + { "key": "Duration", "value": "2m 34s" } + ] + }, + { + "type": "actions", + "items": [ + { "text": "View Logs", "value": "view_logs", "style": "Primary" }, + { "text": "Deploy", "value": "deploy", "style": "Default" } + ] + } + ] + } +} +``` + +### Approval Request + +```javascript +{ + "content": { + "head": { + "text": "Expense Approval Required" + }, + "body": [ + { "type": "message", "text": "John Doe submitted an expense report" }, + { + "type": "fields", + "items": [ + { "key": "Amount", "value": "$500.00" }, + { "key": "Category", "value": "Travel" }, + { "key": "Date", "value": "Feb 9, 2026" } + ] + }, + { "type": "divider" }, + { + "type": "actions", + "items": [ + { "text": "Approve", "value": "approve_500", "style": "Primary" }, + { "text": "Reject", "value": "reject_500", "style": "Danger" }, + { "text": "View Details", "value": "details_500", "style": "Default" } + ] + } + ] + } +} +``` + +### Error Notification + +```javascript +{ + "content": { + "head": { + "text": "⚠️ Service Alert" + }, + "body": [ + { + "type": "section", + "sidebar_color": "#ef4444", + "sections": [ + { "type": "message", "text": "Database connection failed" } + ] + }, + { + "type": "fields", + "items": [ + { "key": "Service", "value": "api-prod" }, + { "key": "Error", "value": "Connection timeout" }, + { "key": "Time", "value": "2026-02-09 18:30:00 UTC" } + ] + }, + { + "type": "actions", + "items": [ + { "text": "View Logs", "value": "logs", "style": "Primary" }, + { "text": "Acknowledge", "value": "ack", "style": "Default" } + ] + } + ] + } +} +``` + +## Limitations + +| Component | Limit | +|-----------|-------| +| Message text | 4,096 characters | +| Button text | 40 characters | +| Field key/value | 256 characters each | +| Dropdown options | 100 options | +| Buttons per message | 5 buttons | + +## Best Practices + +### Button Design +✅ **DO**: Use clear, action-oriented labels +- "Approve Request" +- "View Details" +- "Cancel Order" + +❌ **DON'T**: Use vague labels +- "OK" +- "Click Here" +- "Button" + +### Color Usage +✅ **DO**: Use semantic colors +- Green (`#10b981`) for success +- Red (`#ef4444`) for errors/destructive actions +- Blue (`#3b82f6`) for info +- Orange (`#f59e0b`) for warnings + +❌ **DON'T**: Use random colors without meaning + +### Field Formatting +✅ **DO**: Keep keys concise, values informative +```javascript +{ "key": "Status", "value": "Active" } +``` + +❌ **DON'T**: Make keys too long +```javascript +{ "key": "The current status of the request", "value": "Active" } +``` + +## Testing Cards + +Use the [Team Chat App Card Builder](https://appssdk.zoom.us/cardbuilder/) to: +- Preview card designs +- Test layouts +- Generate JSON + +## Next Steps + +- [Chatbot Setup](../examples/chatbot-setup.md) - Build your first bot +- [Button Actions](../examples/button-actions.md) - Handle button clicks +- [Webhook Events](webhook-events.md) - Understand webhook payloads + +## Resources + +- [Official Card Components](https://developers.zoom.us/docs/team-chat/customizing-messages/) +- [App Card Builder](https://appssdk.zoom.us/cardbuilder/) +- [Sample Chatbots](https://github.com/zoom?q=chatbot) diff --git a/plugins/zoom-developers/skills/team-chat/references/rate-limits.md b/plugins/zoom-developers/skills/team-chat/references/rate-limits.md new file mode 100644 index 00000000..4e340c94 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/references/rate-limits.md @@ -0,0 +1,8 @@ +# Rate Limits + +Rate limits vary by endpoint and account. If you get throttled: + +- add retries with exponential backoff +- batch work where possible +- avoid calling list endpoints repeatedly (cache results) + diff --git a/plugins/zoom-developers/skills/team-chat/references/sample-comparison.md b/plugins/zoom-developers/skills/team-chat/references/sample-comparison.md new file mode 100644 index 00000000..e9d35895 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/references/sample-comparison.md @@ -0,0 +1,18 @@ +# Sample Comparison + +Use this quick matrix to choose the right Team Chat or chatbot sample shape before building your own integration. + +| Sample shape | Best when | Strengths | Watch-outs | +|--------------|-----------|-----------|------------| +| Minimal webhook bot | You want to validate event flow quickly | Fastest setup, easy signature verification review | Usually in-memory state only | +| Full chatbot sample | You need slash commands, cards, and bot responses together | Shows end-to-end chat lifecycle | More moving parts than a simple receiver | +| LLM-enhanced bot | You want summarization or assistant behavior | Good reference for prompt assembly and response shaping | Requires stricter latency and fallback design | +| Multi-language sample | You need parity across runtimes | Useful for comparing auth and verification patterns | Feature coverage often drifts between languages | + +## What to Compare + +- OAuth flow type and token handling +- Webhook signature verification approach +- Message card rendering support +- Persistence model: in-memory, file, or database +- Local development strategy: tunnel, mock events, replay fixtures diff --git a/plugins/zoom-developers/skills/team-chat/references/samples.md b/plugins/zoom-developers/skills/team-chat/references/samples.md new file mode 100644 index 00000000..bceb45b4 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/references/samples.md @@ -0,0 +1,546 @@ +# Sample Applications Analysis + +Analysis of official Zoom Team Chat sample applications, extracted patterns, and best practices. + +## Sample Overview + +| Sample | Language | Complexity | Best For | +|--------|----------|------------|----------| +| [chatbot-nodejs-quickstart](https://github.com/zoom/chatbot-nodejs-quickstart) | Node.js | ⭐ Beginner | **Start here** - Tutorial series | +| LLM chatbot pattern | Node.js | ⭐⭐ Intermediate | Provider-neutral LLM integration pattern | +| [unsplash-chatbot](https://github.com/zoom/unsplash-chatbot) | Node.js | ⭐⭐ Intermediate | API integration + database | +| [zoom-erp-chatbot-sample](https://github.com/zoom/zoom-erp-chatbot-sample) | Node.js | ⭐⭐⭐ Advanced | Enterprise integration | +| [task-manager-sample](https://github.com/zoom/task-manager-sample) | Node.js | ⭐⭐⭐ Advanced | Full CRUD application | +| [zoom-cohere-chatbot-sample](https://github.com/zoom/zoom-cohere-chatbot-sample) | Node.js | ⭐⭐ Intermediate | Cohere LLM integration | +| [zoom-cerebras-chatbot-sample](https://github.com/zoom/zoom-cerebras-chatbot-sample) | Node.js | ⭐⭐ Intermediate | Cerebras LLM integration | +| [zoom-team-chat-shortcut-sample](https://github.com/zoom/zoom-team-chat-shortcut-sample) | Node.js | ⭐⭐ Intermediate | Shortcuts and UI elements | +| [zoom-teams-chat-snowflake-sample](https://github.com/zoom/zoom-teams-chat-snowflake-sample) | Node.js | ⭐⭐⭐ Advanced | Snowflake data integration | +| [rivet-javascript-sample](https://github.com/zoom/rivet-javascript-sample) | Node.js | ⭐⭐ Intermediate | Rivet SDK usage | + +## 1. chatbot-nodejs-quickstart + +**Repository**: https://github.com/zoom/chatbot-nodejs-quickstart + +**Description**: Official tutorial series covering 9 episodes from setup to advanced features. + +**Key Features**: +- Setup & Send Messages +- Handle Events +- Slash Commands +- Markdown & Emojis +- Reactions & Interactive Messages +- Threaded Replies +- Search Messages via API +- Scheduling Messages +- Zoom Workplace App Integration + +**Project Structure**: +``` +chatbot-nodejs-quickstart/ +├── routes/ +│ ├── zoom-webhookHandler.js # Webhook event handling +│ └── oauth-routes.js # OAuth flow +├── utils/ +│ ├── zoom-api.js # API helper functions +│ ├── zoom-chatbot-auth.js # Token generation +│ └── validation.js # Webhook signature verification +├── views/ # EJS templates +├── server.js # Express app +└── .env.example # Environment variables +``` + +**Key Patterns**: + +### Webhook Handler Pattern +```javascript +async function handleZoomWebhook(req, res) { + verifyZoomWebhookSignature(req); + + const { event, payload } = req.body; + + switch (event) { + case 'bot_notification': + return handleBotNotification(payload, res); + case 'interactive_message_actions': + return handleButtonClick(payload, res); + // ... more cases + } +} +``` + +### Token Generation +```javascript +async function getChatbotToken() { + const credentials = Buffer.from( + `${CLIENT_ID}:${CLIENT_SECRET}` + ).toString('base64'); + + const response = await fetch('https://zoom.us/oauth/token', { + method: 'POST', + headers: { 'Authorization': `Basic ${credentials}` }, + body: 'grant_type=client_credentials' + }); + + return (await response.json()).access_token; +} +``` + +**Best Practices**: +- ✅ Signature verification on all webhooks +- ✅ Environment variables for credentials +- ✅ Modular route structure +- ✅ Error handling with try/catch +- ✅ Immediate webhook response (200 status) + +**Recommended For**: First-time chatbot developers + +## 2. LLM chatbot pattern + +**Description**: AI-powered chatbot using an LLM provider for natural language responses. + +**Key Features**: +- LLM API integration +- Conversation history tracking +- Streaming responses (optional) +- Context management + +**LLM Integration Pattern**: +```javascript +case 'bot_notification': { + const { toJid, cmd, accountId } = payload; + + // Call your LLM provider + const response = await llmClient.responses.create({ + model: process.env.LLM_MODEL, + max_tokens: 1024, + messages: [{ role: 'user', content: cmd }] + }); + + const llmResponse = response.content[0].text; + + // Send back to Zoom + await sendChatbotMessage(toJid, accountId, { + body: [{ type: 'message', text: llmResponse }] + }); +} +``` + +**Conversation History Pattern**: +```javascript +const conversationHistory = new Map(); + +function addToHistory(userId, role, content) { + if (!conversationHistory.has(userId)) { + conversationHistory.set(userId, []); + } + conversationHistory.get(userId).push({ role, content }); +} + +// In bot_notification handler +const history = conversationHistory.get(userId) || []; +const response = await llmClient.responses.create({ + model: process.env.LLM_MODEL, + messages: history +}); +``` + +**Environment Variables**: +```bash +LLM_API_KEY=your_api_key_here +LLM_MODEL=your_model_here +ZOOM_CLIENT_ID=... +ZOOM_CLIENT_SECRET=... +ZOOM_BOT_JID=... +``` + +**Recommended For**: Building AI assistants + +## 3. unsplash-chatbot + +**Repository**: https://github.com/zoom/unsplash-chatbot + +**Description**: Image search bot integrating Unsplash API with database storage. + +**Key Features**: +- Third-party API integration (Unsplash) +- Database persistence (SQLite/PostgreSQL) +- Image search and display +- User preference storage + +**Database Schema**: +```sql +CREATE TABLE users ( + id INTEGER PRIMARY KEY, + zoom_user_id TEXT UNIQUE, + preferences TEXT +); + +CREATE TABLE searches ( + id INTEGER PRIMARY KEY, + user_id INTEGER, + query TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +); +``` + +**Image Display Pattern**: +```javascript +{ + "content": { + "head": { "text": "Image Results" }, + "body": [ + { + "type": "attachments", + "img_url": imageData.urls.regular, + "resource_url": imageData.links.html, + "information": { + "title": { "text": imageData.description }, + "description": { "text": `Photo by ${imageData.user.name}` } + } + } + ] + } +} +``` + +**Best Practices**: +- ✅ API rate limiting handling +- ✅ Error handling for external APIs +- ✅ Database connection pooling +- ✅ User data privacy + +**Recommended For**: External API integration patterns + +## 4. zoom-erp-chatbot-sample + +**Repository**: https://github.com/zoom/zoom-erp-chatbot-sample + +**Description**: Enterprise Resource Planning integration with scheduled alerts. + +**Key Features**: +- Oracle ERP API integration +- Scheduled notifications (cron) +- Approval workflows +- Threaded conversations + +**Scheduled Alerts Pattern**: +```javascript +const cron = require('node-cron'); + +// Daily report at 9 AM +cron.schedule('0 9 * * *', async () => { + const report = await getERPReport(); + + await sendChatbotMessage(channelJid, accountId, { + head: { "text": "Daily ERP Report" }, + body: [ + { "type": "fields", "items": report.fields }, + { + "type": "actions", + "items": [ + { "text": "View Details", "value": "view_report" } + ] + } + ] + }); +}); +``` + +**Approval Workflow Pattern**: +```javascript +// Send approval request +{ + "head": { "text": "Expense Approval Required" }, + "body": [ + { "type": "fields", "items": expenseFields }, + { + "type": "actions", + "items": [ + { "text": "Approve", "value": `approve_${expenseId}`, "style": "Primary" }, + { "text": "Reject", "value": `reject_${expenseId}`, "style": "Danger" } + ] + } + ] +} + +// Handle button click +case 'interactive_message_actions': { + const action = payload.actionItem.value; + const [decision, expenseId] = action.split('_'); + + await updateERPStatus(expenseId, decision); + await sendConfirmation(payload.toJid, decision); +} +``` + +**Recommended For**: Enterprise integrations, workflows + +## 5. task-manager-sample + +**Repository**: https://github.com/zoom/task-manager-sample + +**Description**: Full-featured task management application with CRUD operations. + +**Key Features**: +- Create, read, update, delete tasks +- Task assignment +- Due date tracking +- Status management +- Persistent storage + +**CRUD Pattern**: +```javascript +// CREATE +case 'bot_notification': { + if (cmd.startsWith('create task')) { + const taskData = parseTaskCommand(cmd); + const task = await db.createTask(taskData); + await sendTaskCreatedMessage(toJid, accountId, task); + } +} + +// READ +case 'interactive_message_actions': { + if (actionItem.value.startsWith('view_task')) { + const taskId = actionItem.value.split('_')[2]; + const task = await db.getTask(taskId); + await sendTaskDetails(toJid, accountId, task); + } +} + +// UPDATE +case 'interactive_message_actions': { + if (actionItem.value.startsWith('complete_task')) { + const taskId = actionItem.value.split('_')[2]; + await db.updateTaskStatus(taskId, 'completed'); + await sendStatusUpdate(toJid, accountId, taskId); + } +} + +// DELETE +case 'interactive_message_actions': { + if (actionItem.value.startsWith('delete_task')) { + const taskId = actionItem.value.split('_')[2]; + await db.deleteTask(taskId); + await sendDeletionConfirmation(toJid, accountId, taskId); + } +} +``` + +**Recommended For**: Full application architecture + +## Common Patterns Across Samples + +### 1. Environment Variable Management + +All samples use `.env` files with similar structure: + +```bash +# Authentication +ZOOM_CLIENT_ID= +ZOOM_CLIENT_SECRET= +ZOOM_BOT_JID= +ZOOM_VERIFICATION_TOKEN= +ZOOM_ACCOUNT_ID= + +# Third-party APIs (if applicable) +LLM_API_KEY= +UNSPLASH_ACCESS_KEY= + +# Server +PORT=4000 +NODE_ENV=development +``` + +### 2. Project Structure + +Common folder organization: + +``` +sample-app/ +├── routes/ +│ ├── webhook.js # Webhook handlers +│ └── oauth.js # OAuth flows (if needed) +├── utils/ +│ ├── zoom-api.js # Zoom API wrappers +│ ├── auth.js # Token management +│ └── validation.js # Input validation +├── models/ # Database models (if applicable) +├── views/ # Frontend templates (if applicable) +├── server.js # Express app +├── .env.example +└── package.json +``` + +### 3. Webhook Verification + +All samples verify webhook signatures: + +```javascript +function verifyWebhook(req) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + const message = `v0:${timestamp}:${JSON.stringify(req.body)}`; + + const hash = crypto.createHmac('sha256', SECRET_TOKEN) + .update(message) + .digest('hex'); + + return signature === `v0=${hash}`; +} +``` + +### 4. Error Handling + +Consistent error handling pattern: + +```javascript +app.post('/webhook', async (req, res) => { + try { + verifyWebhook(req); + await handleWebhook(req.body); + res.status(200).json({ success: true }); + } catch (error) { + console.error('Webhook error:', error); + + if (error.message.includes('signature')) { + return res.status(401).json({ error: 'Invalid signature' }); + } + + res.status(500).json({ error: 'Internal server error' }); + } +}); +``` + +### 5. Async Webhook Processing + +Respond immediately, process async: + +```javascript +app.post('/webhook', (req, res) => { + // Respond immediately + res.status(200).json({ success: true }); + + // Process asynchronously + processWebhookAsync(req.body).catch(error => { + console.error('Async processing error:', error); + }); +}); +``` + +## Architecture Lessons + +### Chatbot Lifecycle + +Common lifecycle across all samples: + +``` +1. User Action (slash command, button click, message) + ↓ +2. Zoom sends webhook to Bot Endpoint URL + ↓ +3. Server verifies signature + ↓ +4. Server responds 200 (immediately) + ↓ +5. Server processes request (async) + ↓ +6. Server calls external APIs if needed + ↓ +7. Server sends chatbot message back to Zoom +``` + +### State Management + +**Simple bots**: In-memory state (Map/Object) +**Production bots**: Database (PostgreSQL, MongoDB, Redis) + +```javascript +// Simple (development) +const userState = new Map(); + +// Production +const userState = { + async get(userId) { + return await db.query('SELECT * FROM user_state WHERE user_id = $1', [userId]); + }, + async set(userId, state) { + return await db.query('INSERT INTO user_state (user_id, state) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET state = $2', [userId, state]); + } +}; +``` + +## Deprecation Notes + +Some samples may use deprecated patterns: + +### ❌ Old Pattern (Don't Use) +```javascript +// Hardcoded credentials +const CLIENT_ID = 'abc123'; +``` + +### ✅ New Pattern (Use This) +```javascript +// Environment variables +const CLIENT_ID = process.env.ZOOM_CLIENT_ID; +``` + +### ❌ Old Pattern (Don't Use) +```javascript +// Synchronous webhook processing (may timeout) +app.post('/webhook', async (req, res) => { + await longRunningProcess(); + res.status(200).json({ success: true }); +}); +``` + +### ✅ New Pattern (Use This) +```javascript +// Async processing +app.post('/webhook', (req, res) => { + res.status(200).json({ success: true }); + longRunningProcess().catch(console.error); +}); +``` + +## Sample Selection Guide + +### Choose chatbot-nodejs-quickstart if: +- You're new to Zoom chatbots +- You want a tutorial series +- You need step-by-step guidance + +### Choose an LLM chatbot pattern if: +- You want to integrate an LLM +- You need conversational AI +- You want to see LLM integration patterns + +### Choose unsplash-chatbot if: +- You need to integrate external APIs +- You want database patterns +- You need user preference storage + +### Choose zoom-erp-chatbot-sample if: +- You're building enterprise integrations +- You need scheduled notifications +- You want approval workflows + +### Choose task-manager-sample if: +- You want a full CRUD application +- You need complex state management +- You want to see production architecture + +## Next Steps + +- [Chatbot Setup Example](../examples/chatbot-setup.md) - Build your own using these patterns +- [LLM Integration Example](../examples/llm-integration.md) - Integrate an LLM provider +- [Button Actions Example](../examples/button-actions.md) - Handle interactive components +- [Sample Comparison](sample-comparison.md) - Compare common sample shapes before choosing a baseline + +## Resources + +- [Official Samples GitHub Org](https://github.com/zoom?q=chatbot) +- [Chatbot Documentation](https://developers.zoom.us/docs/team-chat/chatbot/extend/) +- [Developer Forum](https://devforum.zoom.us/) diff --git a/plugins/zoom-developers/skills/team-chat/references/scopes.md b/plugins/zoom-developers/skills/team-chat/references/scopes.md new file mode 100644 index 00000000..c970bb3c --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/references/scopes.md @@ -0,0 +1,17 @@ +# Scopes Reference (Common) + +## Team Chat API + +Common scopes include: + +- `chat_message:write` +- `chat_channel:read` + +## Chatbot API + +The Chatbot feature uses bot credentials; typical setup includes enabling the feature and using the bot token. + +## Pitfall + +After adding scopes in Marketplace, users often need to reauthorize to grant them. + diff --git a/plugins/zoom-developers/skills/team-chat/references/webhook-events.md b/plugins/zoom-developers/skills/team-chat/references/webhook-events.md new file mode 100644 index 00000000..483ae8ca --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/references/webhook-events.md @@ -0,0 +1,17 @@ +# Webhook Events (Chatbot API) + +Common webhook event types you will handle: + +- `bot_notification`: user messages your bot or triggers a command +- `interactive_message_actions`: user clicks a button +- `chat_message.submit`: user submits a form +- `bot_installed`: bot added to an account +- `app_deauthorized`: bot removed / app deauthorized + +## Handler Checklist + +- Verify the request (per Zoom's verification guidance). +- Parse payload carefully (treat as untrusted input). +- Route by event type and action values. +- Respond quickly; do heavy work async if needed. + diff --git a/plugins/zoom-developers/skills/team-chat/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/team-chat/troubleshooting/common-issues.md new file mode 100644 index 00000000..37742664 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/troubleshooting/common-issues.md @@ -0,0 +1,428 @@ +# Common Issues and Solutions + +Quick diagnostics and solutions for Zoom Team Chat development. + +## Authentication Issues + +### "Invalid client_id or client_secret" + +**Cause**: Incorrect credentials or using wrong environment (dev vs production) + +**Solution**: +1. Verify credentials in `.env` match Zoom Marketplace +2. Check you're using Development credentials (not Production) +3. Regenerate Client Secret if needed + +### "Get Bot Token" returns 404 or HTML page + +**Cause**: Using wrong token endpoint. + +**Fix**: +- Use `https://zoom.us/oauth/token` for token exchange. +- Do not use `https://zoom.us/oauth/token` for chatbot token requests. + +Quick check: +```bash +curl -X POST https://zoom.us/oauth/token \ + -H "Authorization: Basic " \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" +``` + +### "Token expired" + +**Cause**: Access token has expired (1 hour for user tokens) + +**Solution**: +```javascript +// Implement token refresh +if (error.message.includes('token expired')) { + const newToken = await refreshAccessToken(refreshToken); + // Retry request with new token +} +``` + +### "Scope not authorized" + +**Cause**: Missing required scope in app configuration + +**Solution**: +1. Go to Zoom Marketplace → Your App → Scopes +2. Add missing scope (e.g., `chat_message:write`) +3. Users must re-authorize the app + +## Webhook Issues + +### "Cannot GET /webhook" (Browser) + +**Expected Behavior**: This is NORMAL + +**Explanation**: Webhooks are POST-only. Browsers send GET requests. + +**Test properly**: +```bash +WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:4000" + +# Use POST instead +curl -X POST "$WEBHOOK_BASE_URL/webhook" \ + -H "Content-Type: application/json" \ + -d '{"event":"test"}' +``` + +### "Invalid webhook signature" + +**Cause**: Mismatch between your Secret Token and Zoom's + +**Solution**: +1. Verify `ZOOM_VERIFICATION_TOKEN` in `.env` +2. Check Secret Token in Zoom Marketplace → Features → Team Chat Subscriptions +3. Ensure no extra spaces/characters in token + +**Debug**: +```javascript +console.log('Expected token:', process.env.ZOOM_VERIFICATION_TOKEN); +console.log('Signature from Zoom:', req.headers['x-zm-signature']); +``` + +### URL Validation Fails + +**Cause**: Incorrect response format + +**Correct response**: +```javascript +{ + "plainToken": "xyz123", + "encryptedToken": "hmac_sha256_hash" +} +``` + +**Incorrect**: +```javascript +{ "success": true } // Wrong! +``` + +### No Webhooks Received + +**Checklist**: +- [ ] ngrok is running: `ngrok http 4000` +- [ ] Bot Endpoint URL in Zoom Marketplace matches ngrok URL +- [ ] Server is running: `node server.js` +- [ ] Slash command configured in Zoom Marketplace +- [ ] Bot installed in your account + +**Test**: +```bash +# In Zoom Team Chat, type: +/yourbot test + +# Should see webhook in server logs +``` + +## Bot JID Issues + +### "Bot JID not found" + +**Cause**: Chatbot feature not enabled + +**Solution**: +1. Go to Zoom Marketplace → Your App → Features +2. Toggle **Chatbot** ON +3. Bot JID will appear in **Bot Credentials** section + +### "Bot JID appears but messages not sending" + +**Cause**: Wrong Bot JID format or environment mismatch + +**Solution**: +1. Verify format: `v1abc123xyz@xmpp.zoom.us` +2. Use Development Bot JID for testing +3. Check Account ID matches the bot's account + +## Message Sending Issues + +### "Messages not appearing in Team Chat" + +**Common causes**: + +1. **Wrong `to_jid`** + ```javascript + // Use toJid from webhook payload + await sendMessage(payload.toJid, accountId, content); + ``` + +2. **Missing `account_id`** + ```javascript + // Required for chatbot messages + { + "account_id": process.env.ZOOM_ACCOUNT_ID, // Don't forget! + "robot_jid": process.env.ZOOM_BOT_JID, + "to_jid": toJid + } + ``` + +3. **Incorrect content format** + ```javascript + // ❌ Wrong + { "text": "Hello" } + + // ✅ Correct + { + "content": { + "body": [ + { "type": "message", "text": "Hello" } + ] + } + } + ``` + +### "Message truncated or garbled" + +**Cause**: Special characters or exceeding 4096 char limit + +**Solution**: +```javascript +function sanitizeMessage(message) { + return message + .trim() + .replace(/[\x00-\x1F\x7F]/g, '') // Remove control chars + .substring(0, 4096); // Enforce limit +} +``` + +## Button/Form Issues + +### "Buttons not clickable" + +**Cause**: Missing `value` field + +**Incorrect**: +```javascript +{ + "type": "actions", + "items": [ + { "text": "Click Me" } // Missing value! + ] +} +``` + +**Correct**: +```javascript +{ + "type": "actions", + "items": [ + { "text": "Click Me", "value": "clicked" } + ] +} +``` + +### "Button clicks not triggering webhooks" + +**Checklist**: +- [ ] Webhook handler has `interactive_message_actions` case +- [ ] Bot Endpoint URL configured correctly +- [ ] Server responding with 200 status +- [ ] Webhook signature verification passing + +## ngrok Issues + +### "ngrok session expired" + +**Cause**: Free ngrok URLs expire after 2 hours + +**Solutions**: +1. **Short-term**: Restart ngrok, update Bot Endpoint URL +2. **Long-term**: Use ngrok paid plan or deploy to production + +### "ngrok URL changes every restart" + +**Free plan behavior**: URL changes each time + +**Solutions**: +1. Use ngrok auth token for persistent URLs (paid) +2. Use environment variable for flexibility: + ```javascript + const WEBHOOK_URL = process.env.WEBHOOK_URL || 'https://YOUR_PUBLIC_WEBHOOK_URL/webhook'; + ``` + +## Deployment Issues + +### "Works locally but not in production" + +**Common causes**: + +1. **Environment variables not set** + ```bash + # Verify all vars exist + echo $ZOOM_CLIENT_ID + echo $ZOOM_CLIENT_SECRET + echo $ZOOM_BOT_JID + ``` + +2. **HTTP instead of HTTPS** + - Production MUST use HTTPS + - Zoom rejects HTTP endpoints + +3. **Port binding issues** + ```javascript + // Use PORT from environment + const PORT = process.env.PORT || 4000; + ``` + +4. **Credentials exist, but wrong `.env` file is loaded** + - If your app keeps per-mode env files (for example `project/team-chat-api/.env` and `project/chatbot-api/.env`), make sure runtime loads those files explicitly. + - Verify loaded config via a health/config endpoint before debugging OAuth logic. + +### `404` on `/team-chat/api/channel/*` + +**Cause**: Route mismatch between old and new demo structure. + +**Fix**: +- New pages should use: + - `/team-chat/user-demo` + - `/team-chat/bot-demo` +- Keep compatibility routes in backend if older UI still calls: + - `/api/channel/list` + - `/api/channel/messages` + - `/api/channel/message` + +### Browser shows `ERR_BLOCKED_BY_CLIENT` + +**Cause**: Browser extension/adblock/privacy filter blocked a request. + +**What to do**: +- Test in Incognito or with extensions disabled for your host. +- Confirm backend route with `curl` before treating this as server failure. + +## Rate Limiting + +### "Rate limit exceeded" + +**Zoom Limits**: +- 10 requests/second per user +- 100 requests/second per app + +**Solution**: +```javascript +// Implement exponential backoff +async function retryWithBackoff(fn, maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + try { + return await fn(); + } catch (error) { + if (error.status === 429) { + const delay = Math.pow(2, i) * 1000; + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + throw error; + } + } + } + throw new Error('Max retries exceeded'); +} +``` + +## General App Issues + +### "App not appearing in Team Chat" + +**Cause**: Team Chat surface not enabled + +**Solution**: +1. Go to Zoom Marketplace → Your App → Features → Surface +2. Check **Team Chat** +3. Configure Home URL and Domain Allow List +4. Save changes + +### "Users can't install the app" + +**Cause**: App not in Local Test or not published + +**Solutions**: +1. **For testing**: Go to Local Test → Generate Authorization URL → Share with team +2. **For production**: Submit app for Zoom review and publish + +## Debugging Tools + +### Log All Webhooks + +```javascript +app.post('/webhook', (req, res) => { + console.log('=== Webhook Received ==='); + console.log('Event:', req.body.event); + console.log('Payload:', JSON.stringify(req.body.payload, null, 2)); + console.log('Headers:', req.headers); + // ... handle webhook +}); +``` + +### Test Token Generation + +```javascript +// Test script: test-token.js +require('dotenv').config(); +const { getChatbotToken } = require('./utils/auth'); + +(async () => { + try { + const token = await getChatbotToken(); + console.log('✅ Token generated successfully'); + console.log('Token:', token.substring(0, 20) + '...'); + } catch (error) { + console.error('❌ Token error:', error.message); + } +})(); +``` + +### Verify Credentials + +```javascript +// verify-setup.js +require('dotenv').config(); + +const required = [ + 'ZOOM_CLIENT_ID', + 'ZOOM_CLIENT_SECRET', + 'ZOOM_BOT_JID', + 'ZOOM_VERIFICATION_TOKEN', + 'ZOOM_ACCOUNT_ID' +]; + +console.log('=== Credential Check ==='); +required.forEach(key => { + const value = process.env[key]; + if (!value) { + console.error(`❌ Missing: ${key}`); + } else { + console.log(`✅ ${key}: ${value.substring(0, 10)}...`); + } +}); +``` + +## Getting Help + +### Before Asking for Help + +1. Check error messages in console/logs +2. Verify all credentials are correct +3. Test with curl or Postman +4. Review [official samples](https://github.com/zoom?q=chatbot) + +### Where to Get Help + +- [Zoom Developer Forum](https://devforum.zoom.us/) +- [GitHub Issues](https://github.com/zoom/chatbot-nodejs-quickstart/issues) +- [Developer Support](https://devsupport.zoom.us) + +### Include in Support Requests + +1. Zoom app type (General App OAuth) +2. Error message (full text) +3. Code snippet (sanitized - no credentials!) +4. Steps to reproduce +5. Expected vs actual behavior + +## Next Steps + +- [Webhook Architecture](../concepts/webhooks.md) - Deep dive into webhooks +- [Chatbot Setup](../examples/chatbot-setup.md) - Complete working example +- [API Reference](../references/api-reference.md) - Endpoint documentation diff --git a/plugins/zoom-developers/skills/team-chat/troubleshooting/deployment-issues.md b/plugins/zoom-developers/skills/team-chat/troubleshooting/deployment-issues.md new file mode 100644 index 00000000..c0cfa2aa --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/troubleshooting/deployment-issues.md @@ -0,0 +1,19 @@ +# Deployment Issues + +## Works Locally, Fails in Prod + +- DNS/HTTPS misconfiguration +- blocked outbound calls from your environment +- missing env vars / secrets +- wrong env file loaded at runtime (for split setups like `team-chat-api/.env` and `chatbot-api/.env`) + +## Quick Prod Checklist + +- Confirm token endpoint is `https://zoom.us/oauth/token` +- Confirm user OAuth authorize URL is `https://zoom.us/oauth/authorize` +- Confirm current UI routes are `/team-chat/user-demo` and `/team-chat/bot-demo` +- Confirm reverse proxy forwards `/team-chat/api/*` correctly + +## Webhooks Time Out + +- Respond fast and move long-running work to async jobs. diff --git a/plugins/zoom-developers/skills/team-chat/troubleshooting/message-issues.md b/plugins/zoom-developers/skills/team-chat/troubleshooting/message-issues.md new file mode 100644 index 00000000..80d8d446 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/troubleshooting/message-issues.md @@ -0,0 +1,13 @@ +# Message Issues + +## Messages Not Sending + +- Confirm you're using the correct API: + - Team Chat API uses user OAuth token + - Chatbot API uses bot token + `robot_jid` + +## Card Not Rendering + +- Validate the card JSON payload against known-good examples. +- Simplify to a minimal card and add components incrementally. + diff --git a/plugins/zoom-developers/skills/team-chat/troubleshooting/oauth-issues.md b/plugins/zoom-developers/skills/team-chat/troubleshooting/oauth-issues.md new file mode 100644 index 00000000..3bad6715 --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/troubleshooting/oauth-issues.md @@ -0,0 +1,25 @@ +# OAuth Issues (Team Chat API) + +## "Invalid redirect" / redirect mismatch + +- The redirect URL in the token exchange must exactly match what's configured in Marketplace. +- Keep endpoint split correct: + - authorize: `https://zoom.us/oauth/authorize` + - token exchange: `https://zoom.us/oauth/token` + +## "Invalid access token, does not contain scopes" + +- Add the scope in Marketplace. +- Ensure the user re-authorizes after scope changes. +- Confirm you're using the user token for Team Chat API calls. + +## Token Expired + +- Refresh access tokens using the refresh token. +- If refresh fails, the user likely needs to reauthorize. + +## Callback succeeds but app still has no token + +- Verify callback route actually exchanges `code` server-side. +- Verify `state` is validated and not expired. +- Verify token is persisted where your UI expects it (session/database/local storage for demo). diff --git a/plugins/zoom-developers/skills/team-chat/troubleshooting/webhook-issues.md b/plugins/zoom-developers/skills/team-chat/troubleshooting/webhook-issues.md new file mode 100644 index 00000000..813f89eb --- /dev/null +++ b/plugins/zoom-developers/skills/team-chat/troubleshooting/webhook-issues.md @@ -0,0 +1,13 @@ +# Webhook Issues (Chatbot API) + +## No Events Arriving + +- Ensure your endpoint is publicly reachable over HTTPS. +- Confirm the correct app/account is installed and subscribed. +- Check verification settings (secret token, validation flow). + +## Duplicate Events + +- Webhooks can be delivered more than once. +- Add idempotency (store processed event IDs if available). + diff --git a/plugins/zoom-developers/skills/ui-toolkit/RUNBOOK.md b/plugins/zoom-developers/skills/ui-toolkit/RUNBOOK.md new file mode 100644 index 00000000..3711062f --- /dev/null +++ b/plugins/zoom-developers/skills/ui-toolkit/RUNBOOK.md @@ -0,0 +1,63 @@ +# UI Toolkit 5-Minute Preflight Runbook + +Use this before deep debugging. It catches the most common UI Toolkit failures quickly. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Token Source + +- UI Toolkit still needs Video SDK JWT. +- Generate JWT server-side only (never expose SDK secret client-side). + +## 2) Confirm Basic Config + +- `videoSDKJWT`, `sessionName`, `userName` must be present. +- Verify enabled features match your expected UI behavior. + +## 3) Confirm Framework Constraints + +- Validate your installed package peer dependencies (React version mismatch is common). +- In SSR frameworks, load toolkit client-side and clean up on unmount. + +## 4) Confirm CSS and Lifecycle + +- Ensure toolkit CSS is loaded. +- Call `closeSession`/`destroy` on teardown to avoid stale UI state. + +## 5) Confirm Deployment Paths + +- In basePath/subpath deployments, verify API route URLs and asset paths. +- If API returns HTML instead of JSON, re-check route mapping/proxy. + +## 6) Quick Probes + +- Token endpoint returns JSON token. +- `joinSession` succeeds and session events fire. +- Closing session cleans up container without errors. + +### Copy/Paste Validation Commands + +```bash +# 1) Verify token endpoint responds with JSON +curl -sS -i "$UI_TOOLKIT_BASE_URL/api/token" + +# 2) Verify app route is reachable +curl -sS -i "$UI_TOOLKIT_BASE_URL" +``` + +Expected: valid JSON for token endpoint and valid HTML for app route. + +## 7) Fast Decision Tree + +- **Session won't join** -> invalid/missing JWT or bad session config. +- **UI partially broken** -> missing CSS or unsupported feature config. +- **Works local, fails prod** -> basePath/proxy mismatch. + +## 8) SDK Selection Guardrail + +- Use UI Toolkit for low-code prebuilt Video SDK UI. +- Use raw Video SDK for full custom rendering and control. diff --git a/plugins/zoom-developers/skills/ui-toolkit/SKILL.md b/plugins/zoom-developers/skills/ui-toolkit/SKILL.md new file mode 100644 index 00000000..67500d54 --- /dev/null +++ b/plugins/zoom-developers/skills/ui-toolkit/SKILL.md @@ -0,0 +1,24 @@ +--- +name: ui-toolkit +description: Use when using Zoom UI Toolkit. +--- + +# Zoom Video SDK UI Toolkit + +Use this skill when the user wants a prebuilt web UI for a custom Zoom Video SDK session. If the user needs an actual Zoom meeting, route to `build-zoom-meeting-sdk-app` instead. + +## Workflow + +1. Confirm product fit: UI Toolkit is for Video SDK custom sessions, not Meeting SDK joins. +2. Set up the required Video SDK session credentials and server-side signature generation. +3. Pick the UI Toolkit integration shape and mount it inside the app shell. +4. Configure session join, leave, media permissions, chat, captions, recordings, and theming based on product requirements. +5. Keep custom UI needs explicit; if the user needs deep media layout control, route to `build-zoom-video-sdk-app`. +6. Debug browser permissions, cross-origin isolation, token generation, package version mismatch, and unsupported paid features separately. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- Environment variables: [references/environment-variables.md](references/environment-variables.md) +- Common issues: [troubleshooting/common-issues.md](troubleshooting/common-issues.md) +- Video SDK Web: [../video-sdk/web/SKILL.md](../video-sdk/web/SKILL.md) diff --git a/plugins/zoom-developers/skills/ui-toolkit/references/environment-variables.md b/plugins/zoom-developers/skills/ui-toolkit/references/environment-variables.md new file mode 100644 index 00000000..fe5c5bef --- /dev/null +++ b/plugins/zoom-developers/skills/ui-toolkit/references/environment-variables.md @@ -0,0 +1,19 @@ +# Zoom UI Toolkit (Video SDK) Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_VIDEO_SDK_KEY` | Yes | Video SDK identity for token generation | Zoom Marketplace -> Video SDK app -> App Credentials | +| `ZOOM_VIDEO_SDK_SECRET` | Yes | Server-side token/signature generation | Zoom Marketplace -> Video SDK app -> App Credentials | +| `ZOOM_UI_TOOLKIT_BASE_URL` | Optional | App base URL used by your token service | Set to your deployed app origin | + +## Runtime-only values + +- `VIDEO_SDK_JWT` + +Generate server-side and return short-lived tokens to the browser. + +## Notes + +- UI Toolkit depends on Video SDK auth; keep `ZOOM_VIDEO_SDK_SECRET` off the client. diff --git a/plugins/zoom-developers/skills/ui-toolkit/references/full-guide.md b/plugins/zoom-developers/skills/ui-toolkit/references/full-guide.md new file mode 100644 index 00000000..294abd96 --- /dev/null +++ b/plugins/zoom-developers/skills/ui-toolkit/references/full-guide.md @@ -0,0 +1,541 @@ +# Zoom Video SDK UI Toolkit + +Background reference for the prebuilt Zoom Video SDK UI Toolkit on web. Prefer `choose-zoom-approach` first when the user might still need Meeting SDK instead. + +**Official Documentation**: https://developers.zoom.us/docs/video-sdk/web/ui-toolkit/ +**API Reference**: https://marketplacefront.zoom.us/sdk/uitoolkit/web/ +**NPM Package**: https://www.npmjs.com/package/@zoom/videosdk-zoom-ui-toolkit +**Live Demo**: https://sdk.zoom.com/videosdk-uitoolkit + +## Quick Links + +**New to UI Toolkit? Follow this path:** + +1. **Quick Start** - Get running in 5 minutes (see below) +2. **JWT Authentication** - Server-side token generation (required) +3. **Composite vs Components** - Choose your approach +4. **Framework Integration** - React, Vue, Angular, Next.js patterns +5. **Integrated Index** - see the section below in this file + +**Having issues?** +- Session not joining → Check JWT Authentication (most common issue) +- React 18 peer dependency error → See Installation section +- CSS not loading → See [Troubleshooting](../troubleshooting/common-issues.md) +- Components not showing → Check Component Lifecycle +- Start with preflight checks → [5-Minute Runbook](../RUNBOOK.md) + +## Overview + +The Zoom Video SDK UI Toolkit is a **pre-built video UI library** that renders complete video conferencing experiences with minimal code. Unlike the raw Video SDK, the UI Toolkit provides: + +- ✅ **Ready-to-use UI** - Professional video interface out of the box +- ✅ **Zero UI code** - No need to build video layouts, controls, or participant management +- ✅ **Framework agnostic** - Works with React, Vue, Angular, Next.js, vanilla JS +- ✅ **Highly customizable** - Choose which features to enable, customize themes +- ✅ **Built-in features** - Chat, screen share, settings, virtual backgrounds included + +**When to use UI Toolkit:** +- You want a complete video solution quickly +- You need Zoom-like UI consistency +- You don't want to build custom video UI +- You need standard features (chat, share, participants) + +**When to use raw Video SDK instead:** +- You need complete custom UI control +- You're building a non-standard video experience +- You need access to raw video/audio data +- You want to build your own rendering pipeline + +## Installation + +```bash +npm install @zoom/videosdk-zoom-ui-toolkit jsrsasign +npm install -D @types/jsrsasign +``` + +**Note**: React support depends on the UI Toolkit version. Check the package peer dependencies for your installed version (React 18 is commonly required). + +## Quick Start + +### Basic Usage (Vanilla JS) + +```javascript +import uitoolkit from "@zoom/videosdk-zoom-ui-toolkit"; +import "@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css"; + +const container = document.getElementById("sessionContainer"); + +const config = { + videoSDKJWT: "your_jwt_token", + sessionName: "my-session", + userName: "John Doe", + sessionPasscode: "", + features: ["video", "audio", "share", "chat", "users", "settings"], +}; + +uitoolkit.joinSession(container, config); + +uitoolkit.onSessionJoined(() => { + console.log("Session joined"); +}); + +uitoolkit.onSessionClosed(() => { + console.log("Session closed"); +}); +``` + +### Next.js / React Integration + +```typescript +'use client'; + +import { useEffect, useRef } from 'react'; + +export default function VideoSession({ jwt, sessionName, userName }) { + const containerRef = useRef(null); + const uitoolkitRef = useRef(null); + + useEffect(() => { + let isMounted = true; + + const init = async () => { + const uitoolkitModule = await import('@zoom/videosdk-zoom-ui-toolkit'); + const uitoolkit = uitoolkitModule.default; + uitoolkitRef.current = uitoolkit; + + // If TypeScript complains about CSS imports, configure your app to allow them + // (for example via a global `declare module \"*.css\";`), or import the CSS from + // a global entrypoint (Next.js layout/_app) instead of inlining here. + await import('@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css'); + + if (!isMounted || !containerRef.current) return; + + const config: any = { + videoSDKJWT: jwt, + sessionName: sessionName, + userName: userName, + sessionPasscode: '', + features: ['video', 'audio', 'share', 'chat', 'users', 'settings'], + }; + + uitoolkit.joinSession(containerRef.current, config); + uitoolkit.onSessionJoined(() => console.log('Joined')); + uitoolkit.onSessionClosed(() => console.log('Closed')); + }; + + init(); + + return () => { + isMounted = false; + if (uitoolkitRef.current && containerRef.current) { + try { + uitoolkitRef.current.closeSession(containerRef.current); + } catch (e) {} + } + }; + }, [jwt, sessionName, userName]); + + return
; +} +``` + +## Available Features + +| Feature | Description | +|---------|-------------| +| `video` | Enable video layout and send/receive video | +| `audio` | Show audio button, send/receive audio | +| `share` | Screen sharing | +| `chat` | In-session messaging | +| `users` | Participant list | +| `settings` | Device selection, virtual background | +| `preview` | Pre-join camera/mic preview | +| `recording` | Cloud recording (paid plan) | +| `leave` | Leave/end session button | + +## Troubleshooting + +- **[troubleshooting/common-issues.md](../troubleshooting/common-issues.md)** - CSS, SSR, JWT/session join, customization limits + +## JWT Token Generation (Server-Side) + +**Required**: Generate JWT tokens on your server, never expose SDK secret client-side. + +### Node.js / Next.js API Route + +```typescript +import { NextRequest, NextResponse } from 'next/server'; +import { KJUR } from 'jsrsasign'; + +const ZOOM_VIDEO_SDK_KEY = process.env.ZOOM_VIDEO_SDK_KEY; +const ZOOM_VIDEO_SDK_SECRET = process.env.ZOOM_VIDEO_SDK_SECRET; + +export async function POST(request: NextRequest) { + const { sessionName, role, userName } = await request.json(); + + if (!sessionName || role === undefined) { + return NextResponse.json({ error: 'Missing params' }, { status: 400 }); + } + + const iat = Math.floor(Date.now() / 1000); + const exp = iat + 60 * 60 * 2; // 2 hours + + const oHeader = { alg: 'HS256', typ: 'JWT' }; + const oPayload = { + app_key: ZOOM_VIDEO_SDK_KEY, + role_type: role, // 0 = participant, 1 = host + tpc: sessionName, + version: 1, + iat, + exp, + user_identity: userName || 'User', + }; + + const signature = KJUR.jws.JWS.sign( + 'HS256', + JSON.stringify(oHeader), + JSON.stringify(oPayload), + ZOOM_VIDEO_SDK_SECRET + ); + + return NextResponse.json({ signature }); +} +``` + +### JWT Payload Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `app_key` | Yes | Your Video SDK Key | +| `role_type` | Yes | 0 = participant, 1 = host | +| `tpc` | Yes | Session/topic name | +| `version` | Yes | Always 1 | +| `iat` | Yes | Issued at (Unix timestamp) | +| `exp` | Yes | Expiration (Unix timestamp) | +| `user_identity` | No | User identifier | + +## API Reference + +### Core Methods + +```javascript +uitoolkit.joinSession(container, config); +uitoolkit.closeSession(container); +``` + +### Event Listeners + +```javascript +uitoolkit.onSessionJoined(callback); +uitoolkit.onSessionClosed(callback); +uitoolkit.offSessionJoined(callback); +uitoolkit.offSessionClosed(callback); +``` + +### Component Methods + +```javascript +uitoolkit.showChatComponent(container); +uitoolkit.hideChatComponent(container); +uitoolkit.showUsersComponent(container); +uitoolkit.hideUsersComponent(container); +uitoolkit.showControlsComponent(container); +uitoolkit.hideControlsComponent(container); +uitoolkit.showSettingsComponent(container); +uitoolkit.hideSettingsComponent(container); +uitoolkit.hideAllComponents(); +``` + +## CDN Usage (No Build Step) + +```html + + + +
+ + +``` + +## Next.js with basePath + +When deploying Next.js under a subpath: + +```typescript +// next.config.ts +const nextConfig = { + basePath: "/your-app-path", + assetPrefix: "/your-app-path", +}; +``` + +Fetch API routes with full path: +```typescript +fetch('/your-app-path/api/token', { ... }) +``` + +## Prerequisites + +1. **Zoom Video SDK credentials** from [Zoom Marketplace](https://marketplace.zoom.us/) +2. **React** version compatible with your installed UI Toolkit package (check peer deps; React 18 is common) +3. **Server-side JWT generation** (never expose SDK secret) +4. **Modern browser** with WebRTC support + +## Browser Support + +| Browser | Version | +|---------|---------| +| Chrome | 78+ | +| Firefox | 76+ | +| Safari | 14.1+ | +| Edge | 79+ | + +## Common Issues + +| Issue | Solution | +|-------|----------| +| `peer react@"^18.0.0"` error | Use the React version required by the installed UI Toolkit package (check peer deps; React 18 is common) | +| CSS import TypeScript error | Configure TS/CSS handling (prefer a global `*.css` module declaration); avoid `@ts-ignore` except in throwaway demos | +| Config type error | Type config as `any` | +| API returns HTML not JSON | Check basePath in fetch URL | + +## Resources + +- **GitHub**: https://github.com/zoom/videosdk-zoom-ui-toolkit-web +- **UI Toolkit Docs**: https://developers.zoom.us/docs/video-sdk/web/ui-toolkit/ +- **Auth Endpoint Sample**: https://github.com/zoom/videosdk-auth-endpoint-sample +- **Marketplace**: https://marketplace.zoom.us/ + +--- + +## Integrated Index + +_This section was migrated from `SKILL.md`._ + +Complete navigation for all UI Toolkit documentation. + +## 📚 Start Here + +New to the UI Toolkit? Follow this learning path: + +1. **[SKILL.md](../SKILL.md)** - Main overview and quick start +2. **[5-Minute Runbook](../RUNBOOK.md)** - Preflight checks before deep debugging +3. **Quick Start Guide** - Working code in 5 minutes (see skill.md) +4. **JWT Authentication** - Server-side token generation (see skill.md) +5. **Choose Your Mode** - Composite vs Components (see skill.md) + +## 🎯 Core Concepts + +Understanding how UI Toolkit works: + +- **Composite vs Components** - Two ways to use UI Toolkit (see skill.md) +- **UI Toolkit Architecture** - How it wraps Video SDK internally +- **Feature Configuration** - Understanding featuresOptions structure +- **Session Lifecycle** - Join → Active → Leave/Close → Destroy flow + +## 📖 Complete Guides + +### Getting Started +- **Installation** - NPM install and React 18 setup (see skill.md) +- **Quick Start - Composite** - Full UI in one container (see skill.md) +- **Quick Start - Components** - Individual UI pieces (see skill.md) +- **JWT Authentication** - Server-side token generation (see skill.md) + +### Framework Integration +- **React Integration** - Hooks, useEffect patterns (see skill.md) +- **Vue.js Integration** - Composition API and Options API (see skill.md) +- **Angular Integration** - Component lifecycle (see skill.md) +- **Next.js Integration** - App Router, Server Components (see skill.md) +- **Vanilla JavaScript** - No framework usage (see skill.md) + +### Advanced Topics +- **Component Lifecycle** - Mount, unmount, cleanup patterns +- **Event Listeners** - React to session events +- **Session Management** - Programmatic control +- **Quality Statistics** - Monitor connection quality +- **Custom Themes** - Theme customization +- **Virtual Backgrounds** - Custom background images + +## 📚 API Reference + +Complete API documentation: + +- **Core Methods** (see skill.md) + - `joinSession()` - Start a video session + - `closeSession()` - End session and remove UI + - `destroy()` - Clean up UI Toolkit instance + - `leaveSession()` - Leave without destroying UI + +- **Component Methods** (see skill.md) + - `showControlsComponent()` - Display control bar + - `showChatComponent()` - Display chat panel + - `showUsersComponent()` - Display participants list + - `showSettingsComponent()` - Display settings panel + - `hideAllComponents()` - Hide all components + +- **Event Listeners** (see skill.md) + - `onSessionJoined()` - Session joined successfully + - `onSessionClosed()` - Session ended + - `onSessionDestroyed()` - UI Toolkit destroyed + - `onViewTypeChange()` - View mode changed + - `on()` - Subscribe to Video SDK events + - `off()` - Unsubscribe from events + +- **Information Methods** (see skill.md) + - `getSessionInfo()` - Get session details + - `getCurrentUserInfo()` - Get current user + - `getAllUser()` - Get all participants + - `getClient()` - Get underlying Video SDK client + - `version()` - Get version info + +- **Control Methods** (see skill.md) + - `changeViewType()` - Switch view mode + - `mirrorVideo()` - Mirror self video + - `isSupportCustomLayout()` - Check device support + +- **Statistics Methods** (see skill.md) + - `subscribeAudioStatisticData()` - Audio quality stats + - `subscribeVideoStatisticData()` - Video quality stats + - `subscribeShareStatisticData()` - Share quality stats + +## 🔧 Configuration + +- **Feature Configuration** (see skill.md) + - `featuresOptions` structure + - Audio/Video options + - Chat, Users, Settings + - Virtual Background + - Recording, Captions (paid features) + - Theme customization + - View modes + +- **Session Configuration** (see skill.md) + - Required: `videoSDKJWT`, `sessionName`, `userName` + - Optional: `sessionPasscode`, `sessionIdleTimeoutMins` + - Debug mode + - Web endpoint + - Language settings + +## ⚠️ Troubleshooting + +### Common Issues +- React 18 peer dependency error +- JWT token invalid +- CSS not loading +- Components not showing +- Session join failures + +See: **[troubleshooting/common-issues.md](../troubleshooting/common-issues.md)** + +### Framework-Specific Issues +- React: SSR, hydration, cleanup +- Vue: Reactivity, lifecycle +- Angular: Module imports, AOT +- Next.js: App Router, basePath + +### Session Issues +- Authentication failures +- Connection problems +- Video/audio not working +- Screen share issues + +## 📦 Sample Applications + +**Official Repositories**: + +| Framework | Repository | Key Features | +|-----------|------------|--------------| +| React | [videosdk-zoom-ui-toolkit-react-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-react-sample) | Hooks, TypeScript | +| Vue.js | [videosdk-zoom-ui-toolkit-vuejs-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-vuejs-sample) | Composition API | +| Angular | [videosdk-zoom-ui-toolkit-angular-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-angular-sample) | Services, Guards | +| JavaScript | [videosdk-zoom-ui-toolkit-javascript-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-javascript-sample) | Vanilla JS | +| Auth Endpoint | [videosdk-auth-endpoint-sample](https://github.com/zoom/videosdk-auth-endpoint-sample) | Node.js JWT | + +## 🌐 External Resources + +- **Official Documentation**: https://developers.zoom.us/docs/video-sdk/web/ui-toolkit/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/uitoolkit/web/ +- **NPM Package**: https://www.npmjs.com/package/@zoom/videosdk-zoom-ui-toolkit +- **Marketplace**: https://marketplace.zoom.us/ +- **Developer Forum**: https://devforum.zoom.us/ +- **Live Demo**: https://sdk.zoom.com/videosdk-uitoolkit +- **Changelog**: https://developers.zoom.us/changelog/ui-toolkit/web/ + +## 🎓 Learning Path + +### Beginner +1. Read [SKILL.md](../SKILL.md) overview +2. Follow Quick Start - Composite +3. Generate JWT on server +4. Join your first session +5. Explore available features + +### Intermediate +1. Try Component Mode +2. Add event listeners +3. Customize theme +4. Add virtual backgrounds +5. Integrate with your framework + +### Advanced +1. Access underlying Video SDK +2. Subscribe to quality statistics +3. Handle all edge cases +4. Implement custom layouts +5. Build production-ready app + +## 📋 Quick Reference Card + +### Minimal Working Example + +```javascript +import uitoolkit from "@zoom/videosdk-zoom-ui-toolkit"; +import "@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css"; + +const config = { + videoSDKJWT: "YOUR_JWT", + sessionName: "test-session", + userName: "User", + featuresOptions: { + video: { enable: true }, + audio: { enable: true } + } +}; + +uitoolkit.joinSession(document.getElementById("container"), config); +uitoolkit.onSessionJoined(() => console.log("Joined")); +uitoolkit.onSessionClosed(() => uitoolkit.destroy()); +``` + +### Must-Remember Rules + +1. ✅ **Always** generate JWT server-side +2. ✅ **Always** call `destroy()` on cleanup +3. ✅ **Always** use React 18 (not 17/19) +4. ✅ **Always** import CSS file +5. ❌ **Never** expose SDK secret client-side +6. ❌ **Never** skip `onSessionClosed` cleanup +7. ❌ **Never** call components before `joinSession` + +## 📞 Support + +- **Developer Forum**: https://devforum.zoom.us/ +- **Developer Support**: https://developers.zoom.us/support/ +- **Premier Support**: https://explore.zoom.us/en/support-plans/developer/ + +--- + +**Navigation**: [← Back to SKILL.md](../SKILL.md) + +## Environment Variables + +- See [references/environment-variables.md](../references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/plugins/zoom-developers/skills/ui-toolkit/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/ui-toolkit/troubleshooting/common-issues.md new file mode 100644 index 00000000..02f6a817 --- /dev/null +++ b/plugins/zoom-developers/skills/ui-toolkit/troubleshooting/common-issues.md @@ -0,0 +1,41 @@ +# Common Issues + +Quick diagnostics for Zoom Video SDK UI Toolkit (Web). + +## CSS Not Loading / UI Looks Unstyled + +**Fix**: +- Ensure you import the toolkit CSS: + - `@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css` +- For bundlers, verify CSS handling is enabled (Vite/Next.js). + +## SSR / Next.js Errors ("window is not defined") + +**Fix**: +- Load UI Toolkit only on the client (dynamic import in a client component). + +## Session Doesn't Join + +**Common causes**: +- Missing/invalid `videoSDKJWT` +- Expired JWT +- Using Meeting SDK credentials/signature instead of Video SDK credentials/JWT +- Session name/passcode mismatch + +**Fix**: +- Generate JWT server-side; keep TTL short; check clock skew. +- Log the join errors surfaced by the toolkit callbacks. + +## UI Renders Blank / Cropped + +**Common cause**: The container has no height (common in flex layouts). + +**Fix**: +- Ensure the container has an explicit height (for example `height: 100vh`). + +## "I Need Granular UI Components" + +**Reality**: UI Toolkit is optimized for fast, prebuilt UI. It’s not meant to expose every internal tile/control as a first-class primitive. + +**Fix**: +- If you need fully custom layout, use raw **Zoom Video SDK** and build your own UI. diff --git a/plugins/zoom-developers/skills/video-sdk/RUNBOOK.md b/plugins/zoom-developers/skills/video-sdk/RUNBOOK.md new file mode 100644 index 00000000..852c7920 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/RUNBOOK.md @@ -0,0 +1,78 @@ +# Video SDK 5-Minute Preflight Runbook + +Use this before deep debugging. It catches the most common Video SDK failures fast. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Product Choice + +- Video SDK is for custom video experiences (not Zoom Meeting UI). +- If you expect native Zoom meeting UI behavior, use Meeting SDK instead. + +## 2) Confirm Lifecycle Order + +Required order: +1. `createClient()` +2. `init()` +3. `join()` +4. `getMediaStream()` +5. `startAudio()` / `startVideo()` + +Calling stream APIs before `join()` causes silent failures. + +## 3) Confirm Token Generation + +- JWT must be generated server-side. +- Validate `app_key`, `role_type`, `tpc`, `iat`, `exp` claims. +- Ensure topic (`tpc`) matches what clients join with. + +## 4) Confirm Rendering Pattern + +- Use event-driven attach/detach flow for participant video. +- Handle user join/leave and peer video state changes. +- Do not assume remote video auto-renders. + +## 5) Confirm Delivery Method + +- npm vs CDN globals differ (`ZoomVideo` vs `WebVideoSDK.default`). +- In CDN/module setups, guard for SDK-load race conditions. + +## 6) Quick Probes + +- Signature endpoint returns valid JWT payload. +- Join succeeds for two users on same `topic`. +- Audio/video start calls return success. +- Browser logs show no mixed-content/CORS blocking. + +### Copy/Paste Validation Commands + +```bash +# 1) Verify signature/token endpoint responds +curl -sS -i "$VIDEO_SDK_BASE_URL/api/signature" + +# 2) Verify app page is reachable +curl -sS -i "$VIDEO_SDK_BASE_URL" +``` + +Expected: JSON from token endpoint and HTML from app route. + +## 7) Fast Decision Tree + +- **No media stream** -> check lifecycle order (`getMediaStream` after `join`). +- **Only local video works** -> missing event-driven remote attach flow. +- **Join auth errors** -> JWT claims mismatch or expired token. + +## 8) SDK Selection Guardrail + +- Use Video SDK for custom video sessions you design. +- Use Meeting SDK for Zoom-native meeting experience embedding. + +## 9) Wrong-Path Detector (SDK vs REST/Meeting SDK) + +- If implementation asks for `meetingNumber` or uses `join_url`, you are not in Video SDK flow. +- If implementation creates resources via `/v2/meetings` for join flow, you are on REST/Meeting path. +- For Video SDK MVP, require: Video SDK JWT + `client.join(topic, ...)` + media stream lifecycle. diff --git a/plugins/zoom-developers/skills/video-sdk/SKILL.md b/plugins/zoom-developers/skills/video-sdk/SKILL.md new file mode 100644 index 00000000..3929af04 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/SKILL.md @@ -0,0 +1,28 @@ +--- +name: build-zoom-video-sdk-app +description: Use when using Video SDK. +--- + +# Build Zoom Video SDK App + +Use this skill when the user needs a custom video session rather than a real Zoom meeting. If the user needs meeting numbers, waiting rooms, hosts, or normal Zoom meeting controls, route to Meeting SDK. + +## Workflow + +1. Confirm product fit: Video SDK is for custom sessions with app-owned UX and lifecycle. +2. Choose the target platform: web, Android, iOS, macOS, Windows, Linux, Flutter, React Native, Unity, or UI Toolkit. +3. Validate auth: generate Video SDK session tokens server-side and keep SDK credentials out of client code. +4. Implement join, media permissions, audio/video publish-subscribe, screen share, chat, and leave before advanced media features. +5. Add custom layouts, raw media, recording, live transcription, or storage integrations only after the session lifecycle is stable. +6. Debug by isolating token generation, SDK version, browser isolation, platform permissions, media device behavior, and entitlement limits. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- Web: [web/SKILL.md](web/SKILL.md) +- Android: [android/SKILL.md](android/SKILL.md) +- iOS: [ios/SKILL.md](ios/SKILL.md) +- Windows: [windows/SKILL.md](windows/SKILL.md) +- Linux: [linux/SKILL.md](linux/SKILL.md) +- UI Toolkit: [../ui-toolkit/SKILL.md](../ui-toolkit/SKILL.md) +- Session lifecycle: [references/session-lifecycle.md](references/session-lifecycle.md) diff --git a/plugins/zoom-developers/skills/video-sdk/android/RUNBOOK.md b/plugins/zoom-developers/skills/video-sdk/android/RUNBOOK.md new file mode 100644 index 00000000..4e7dce49 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/android/RUNBOOK.md @@ -0,0 +1,64 @@ +# 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/` diff --git a/plugins/zoom-developers/skills/video-sdk/android/SKILL.md b/plugins/zoom-developers/skills/video-sdk/android/SKILL.md new file mode 100644 index 00000000..f2d84d66 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/android/SKILL.md @@ -0,0 +1,32 @@ +--- +name: zoom-video-sdk-android +description: | + Zoom Video SDK for Android native apps. Use when building custom Android video experiences + with full UI control, session tokens, raw media options, and event-driven participant state. +--- + +# Zoom Video SDK (Android) + +Use this skill when building Android apps with custom real-time video sessions. + +## Start Here + +1. [android.md](android.md) +2. [concepts/lifecycle-workflow.md](concepts/lifecycle-workflow.md) +3. [concepts/architecture.md](concepts/architecture.md) +4. [examples/session-join-pattern.md](examples/session-join-pattern.md) +5. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +6. [references/android-reference-map.md](references/android-reference-map.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +9. [troubleshooting/common-issues.md](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](../SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/video-sdk/android/android.md b/plugins/zoom-developers/skills/video-sdk/android/android.md new file mode 100644 index 00000000..23b2dad9 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/android/android.md @@ -0,0 +1,32 @@ +# 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 diff --git a/plugins/zoom-developers/skills/video-sdk/android/concepts/architecture.md b/plugins/zoom-developers/skills/video-sdk/android/concepts/architecture.md new file mode 100644 index 00000000..b022223a --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/android/concepts/architecture.md @@ -0,0 +1,19 @@ +# Android Architecture Concept + +```mermaid +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 --> UI +``` + +## Design 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. diff --git a/plugins/zoom-developers/skills/video-sdk/android/concepts/lifecycle-workflow.md b/plugins/zoom-developers/skills/video-sdk/android/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..9513a117 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/android/concepts/lifecycle-workflow.md @@ -0,0 +1,21 @@ +# Android Lifecycle Workflow + +```mermaid +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. diff --git a/plugins/zoom-developers/skills/video-sdk/android/examples/session-join-pattern.md b/plugins/zoom-developers/skills/video-sdk/android/examples/session-join-pattern.md new file mode 100644 index 00000000..6470eec7 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/android/examples/session-join-pattern.md @@ -0,0 +1,27 @@ +# Android Session Join Pattern + +```kotlin +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. diff --git a/plugins/zoom-developers/skills/video-sdk/android/references/android-reference-map.md b/plugins/zoom-developers/skills/video-sdk/android/references/android-reference-map.md new file mode 100644 index 00000000..64179b5e --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/android/references/android-reference-map.md @@ -0,0 +1,19 @@ +# 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) diff --git a/plugins/zoom-developers/skills/video-sdk/android/references/environment-variables.md b/plugins/zoom-developers/skills/video-sdk/android/references/environment-variables.md new file mode 100644 index 00000000..6c299389 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/android/references/environment-variables.md @@ -0,0 +1,13 @@ +# 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_TOKEN` should be short-lived and generated server-side. diff --git a/plugins/zoom-developers/skills/video-sdk/android/references/versioning-and-compatibility.md b/plugins/zoom-developers/skills/video-sdk/android/references/versioning-and-compatibility.md new file mode 100644 index 00000000..71f785f3 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/android/references/versioning-and-compatibility.md @@ -0,0 +1,18 @@ +# 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.aar` and 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. diff --git a/plugins/zoom-developers/skills/video-sdk/android/scenarios/high-level-scenarios.md b/plugins/zoom-developers/skills/video-sdk/android/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..ce0018c7 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/android/scenarios/high-level-scenarios.md @@ -0,0 +1,26 @@ +# 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. diff --git a/plugins/zoom-developers/skills/video-sdk/android/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/video-sdk/android/troubleshooting/common-issues.md new file mode 100644 index 00000000..79b47ee6 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/android/troubleshooting/common-issues.md @@ -0,0 +1,21 @@ +# 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. diff --git a/plugins/zoom-developers/skills/video-sdk/flutter/RUNBOOK.md b/plugins/zoom-developers/skills/video-sdk/flutter/RUNBOOK.md new file mode 100644 index 00000000..ec4f8021 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/flutter/RUNBOOK.md @@ -0,0 +1,64 @@ +# 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/` diff --git a/plugins/zoom-developers/skills/video-sdk/flutter/SKILL.md b/plugins/zoom-developers/skills/video-sdk/flutter/SKILL.md new file mode 100644 index 00000000..97923531 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/flutter/SKILL.md @@ -0,0 +1,81 @@ +--- +name: zoom-video-sdk-flutter +description: | + Zoom Video SDK for Flutter. Use when building custom video session apps in Flutter with + flutter_zoom_videosdk, event-driven architecture, session lifecycle handling, and mobile + platform integration patterns. +--- + +# Zoom Video SDK (Flutter) + +Use this skill for Flutter apps that build custom real-time video session experiences with Zoom Video SDK. + +## Quick Links + +1. **[Lifecycle Workflow](concepts/lifecycle-workflow.md)** - init -> joinSession -> media/control -> leave -> cleanup +2. **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - helper-based API surface and event model +3. **[High-Level Scenarios](concepts/high-level-scenarios.md)** - common product patterns +4. **[Setup Guide](examples/setup-guide.md)** - package setup + platform prerequisites +5. **[Session Join Pattern](examples/session-join-pattern.md)** - tokenized session join flow +6. **[Event Handling Pattern](examples/event-handling-pattern.md)** - listener mapping and action routing +7. **[SKILL.md](SKILL.md)** - complete navigation + +## Core Notes + +- Video SDK sessions are custom sessions, not Zoom Meetings. +- Keep SDK credentials server-side; generate JWT token on backend. +- Integration is strongly event-driven; bind listener flows early. +- Feature support and enum names can drift by wrapper/native version. + +## References + +- [Flutter Reference Index](references/flutter-reference.md) +- [Module Map](references/module-map.md) +- [Official Sources](references/official-sources.md) +- [Deprecated and Contradictions](troubleshooting/deprecated-and-contradictions.md) + +## Related Skills + +- [zoom-video-sdk](../SKILL.md) +- [zoom-oauth](../../oauth/SKILL.md) +- [zoom-general](../../general/SKILL.md) + + +## Merged from video-sdk/flutter/SKILL.md + +# Zoom Video SDK Flutter - Documentation Index + +## Start Here + +1. [SKILL.md](SKILL.md) +2. [Lifecycle Workflow](concepts/lifecycle-workflow.md) +3. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) +4. [Setup Guide](examples/setup-guide.md) + +## Concepts + +- [Lifecycle Workflow](concepts/lifecycle-workflow.md) +- [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) +- [High-Level Scenarios](concepts/high-level-scenarios.md) + +## Examples + +- [Setup Guide](examples/setup-guide.md) +- [Session Join Pattern](examples/session-join-pattern.md) +- [Event Handling Pattern](examples/event-handling-pattern.md) + +## References + +- [Flutter Reference Index](references/flutter-reference.md) +- [Module Map](references/module-map.md) +- [Official Sources](references/official-sources.md) + +## Troubleshooting + +- [Common Issues](troubleshooting/common-issues.md) +- [Version Drift](troubleshooting/version-drift.md) +- [Deprecated and Contradictions](troubleshooting/deprecated-and-contradictions.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/video-sdk/flutter/concepts/high-level-scenarios.md b/plugins/zoom-developers/skills/video-sdk/flutter/concepts/high-level-scenarios.md new file mode 100644 index 00000000..bd2d6d92 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/flutter/concepts/high-level-scenarios.md @@ -0,0 +1,21 @@ +# 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. diff --git a/plugins/zoom-developers/skills/video-sdk/flutter/concepts/lifecycle-workflow.md b/plugins/zoom-developers/skills/video-sdk/flutter/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..8e6ac0d8 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/flutter/concepts/lifecycle-workflow.md @@ -0,0 +1,23 @@ +# 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 + +```text +Flutter App + -> initSdk(initConfig) + -> register ZoomVideoSdkEventListener + -> joinSession(joinConfig) + -> use audio/video/share/chat helpers + -> handle EventType callbacks + -> leaveSession + -> cleanup +``` diff --git a/plugins/zoom-developers/skills/video-sdk/flutter/concepts/sdk-architecture-pattern.md b/plugins/zoom-developers/skills/video-sdk/flutter/concepts/sdk-architecture-pattern.md new file mode 100644 index 00000000..6b403f90 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/flutter/concepts/sdk-architecture-pattern.md @@ -0,0 +1,23 @@ +# SDK 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 `ZoomVideoSdkEventListener` and `EventType` constants. + +## 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. diff --git a/plugins/zoom-developers/skills/video-sdk/flutter/examples/event-handling-pattern.md b/plugins/zoom-developers/skills/video-sdk/flutter/examples/event-handling-pattern.md new file mode 100644 index 00000000..e1ab865c --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/flutter/examples/event-handling-pattern.md @@ -0,0 +1,36 @@ +# 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 `onUserJoin` and `onUserLeave`, refresh participant list and rerender. +- On `onUserVideoStatusChanged` and `onUserAudioStatusChanged`, update UI state from events (avoid optimistic-only state). +- Keep one code path for participant list refresh to avoid state drift. diff --git a/plugins/zoom-developers/skills/video-sdk/flutter/examples/session-join-pattern.md b/plugins/zoom-developers/skills/video-sdk/flutter/examples/session-join-pattern.md new file mode 100644 index 00000000..7c4a5c8a --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/flutter/examples/session-join-pattern.md @@ -0,0 +1,21 @@ +# 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 + +```dart +final joinConfig = JoinSessionConfig( + sessionName: 'my-session', + token: '', + userName: 'Mobile User', + audioOptions: {'connect': true, 'mute': true}, + videoOptions: {'localVideoOn': true}, +); +await zoom.joinSession(joinConfig); +``` diff --git a/plugins/zoom-developers/skills/video-sdk/flutter/examples/setup-guide.md b/plugins/zoom-developers/skills/video-sdk/flutter/examples/setup-guide.md new file mode 100644 index 00000000..37f2b579 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/flutter/examples/setup-guide.md @@ -0,0 +1,86 @@ +# Setup Guide + +## 0. Flutter and Android tooling baseline (Windows) + +Install and verify Flutter before SDK wiring. + +```bash +# 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 -v +``` + +If Flutter is not on PATH in your shell, use the full `flutter.bat` path for all commands. + +## 1. Install package + +```yaml +dependencies: + flutter_zoom_videosdk: ^ +``` + +```bash +flutter pub get +``` + +## 2. Initialize SDK + +```dart +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 `minSdk` to at least `28` in 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: + +```kotlin +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: + +```bash +# pairing (from Wireless debugging) +adb pair + +# connect (from mDNS connect port) +adb connect +adb devices -l +``` + +Then run the app: + +```bash +flutter run -d --debug --no-resident +``` + +## 6. Security baseline + +- Never embed SDK secret in app bundle. +- Issue short-lived session tokens server-side. +- Validate all session join parameters before SDK call. diff --git a/plugins/zoom-developers/skills/video-sdk/flutter/references/flutter-reference.md b/plugins/zoom-developers/skills/video-sdk/flutter/references/flutter-reference.md new file mode 100644 index 00000000..293815e1 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/flutter/references/flutter-reference.md @@ -0,0 +1,12 @@ +# Flutter Reference Index + +Primary sources used: + +- `raw-docs/developers.zoom.us/docs/video-sdk/flutter/*.md` +- `raw-docs/marketplacefront.zoom.us/sdk/video-sdk/flutter/index.md` +- `raw-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 diff --git a/plugins/zoom-developers/skills/video-sdk/flutter/references/module-map.md b/plugins/zoom-developers/skills/video-sdk/flutter/references/module-map.md new file mode 100644 index 00000000..8cd98f69 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/flutter/references/module-map.md @@ -0,0 +1,20 @@ +# 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 diff --git a/plugins/zoom-developers/skills/video-sdk/flutter/references/official-sources.md b/plugins/zoom-developers/skills/video-sdk/flutter/references/official-sources.md new file mode 100644 index 00000000..c80690de --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/flutter/references/official-sources.md @@ -0,0 +1,13 @@ +# 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/` diff --git a/plugins/zoom-developers/skills/video-sdk/flutter/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/video-sdk/flutter/troubleshooting/common-issues.md new file mode 100644 index 00000000..9683007d --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/flutter/troubleshooting/common-issues.md @@ -0,0 +1,48 @@ +# Common Issues + +## Join fails or stalls + +- Validate JWT token generation and expiry. +- Ensure `sessionName` and join config fields are valid. +- Verify SDK init completed before `joinSession`. + +## SDK initialization fails + +- Confirm `domain: "zoom.us"` is used in `InitConfig`. +- Request runtime permissions before init/join (camera, microphone, Bluetooth connect on newer Android). +- Catch `PlatformException` from `initSdk` and log `code`, `message`, and `details`. +- 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.kts` or Gradle Groovy equivalent), even when using local plugin path dependency. +- Rebuild with `flutter clean`, `flutter pub get`, and `flutter build apk --debug`. + +## ADB connection issues (physical device) + +- If `adb devices` is empty, verify USB/Wireless debugging is enabled and phone trusts host. +- For wireless debugging, complete both steps: `adb pair` then `adb connect`. +- If connection drops, rerun `adb connect ` and verify with `adb devices -l`. diff --git a/plugins/zoom-developers/skills/video-sdk/flutter/troubleshooting/deprecated-and-contradictions.md b/plugins/zoom-developers/skills/video-sdk/flutter/troubleshooting/deprecated-and-contradictions.md new file mode 100644 index 00000000..792c89c6 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/flutter/troubleshooting/deprecated-and-contradictions.md @@ -0,0 +1,27 @@ +# 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.yaml` and changelog content show `2.3.10` references. + +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. diff --git a/plugins/zoom-developers/skills/video-sdk/flutter/troubleshooting/version-drift.md b/plugins/zoom-developers/skills/video-sdk/flutter/troubleshooting/version-drift.md new file mode 100644 index 00000000..4acfdae1 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/flutter/troubleshooting/version-drift.md @@ -0,0 +1,11 @@ +# 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. diff --git a/plugins/zoom-developers/skills/video-sdk/ios/RUNBOOK.md b/plugins/zoom-developers/skills/video-sdk/ios/RUNBOOK.md new file mode 100644 index 00000000..fd1611d7 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/ios/RUNBOOK.md @@ -0,0 +1,64 @@ +# 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/` diff --git a/plugins/zoom-developers/skills/video-sdk/ios/SKILL.md b/plugins/zoom-developers/skills/video-sdk/ios/SKILL.md new file mode 100644 index 00000000..054b5dd5 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/ios/SKILL.md @@ -0,0 +1,32 @@ +--- +name: zoom-video-sdk-ios +description: | + Zoom Video SDK for iOS native apps. Use when building custom iOS video sessions with + full UI control, token-based session auth, and event-driven media/participant flows. +--- + +# Zoom Video SDK (iOS) + +Use this skill when building custom iOS video session experiences. + +## Start Here + +1. [ios.md](ios.md) +2. [concepts/lifecycle-workflow.md](concepts/lifecycle-workflow.md) +3. [concepts/architecture.md](concepts/architecture.md) +4. [examples/session-join-pattern.md](examples/session-join-pattern.md) +5. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +6. [references/ios-reference-map.md](references/ios-reference-map.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +9. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Key Sources + +- Docs: https://developers.zoom.us/docs/video-sdk/ios/ +- API reference: https://marketplacefront.zoom.us/sdk/custom/ios/annotated.html +- Broader guide: [../SKILL.md](../SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/video-sdk/ios/concepts/architecture.md b/plugins/zoom-developers/skills/video-sdk/ios/concepts/architecture.md new file mode 100644 index 00000000..5a345df5 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/ios/concepts/architecture.md @@ -0,0 +1,17 @@ +# iOS Architecture Concept + +```mermaid +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 --> Store +``` + +## Design 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. diff --git a/plugins/zoom-developers/skills/video-sdk/ios/concepts/lifecycle-workflow.md b/plugins/zoom-developers/skills/video-sdk/ios/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..b576a15e --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/ios/concepts/lifecycle-workflow.md @@ -0,0 +1,20 @@ +# iOS Lifecycle Workflow + +```mermaid +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. diff --git a/plugins/zoom-developers/skills/video-sdk/ios/examples/session-join-pattern.md b/plugins/zoom-developers/skills/video-sdk/ios/examples/session-join-pattern.md new file mode 100644 index 00000000..772bc351 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/ios/examples/session-join-pattern.md @@ -0,0 +1,24 @@ +# iOS Session Join Pattern + +```swift +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. diff --git a/plugins/zoom-developers/skills/video-sdk/ios/ios.md b/plugins/zoom-developers/skills/video-sdk/ios/ios.md new file mode 100644 index 00000000..acf1dd4b --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/ios/ios.md @@ -0,0 +1,31 @@ +# 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 diff --git a/plugins/zoom-developers/skills/video-sdk/ios/references/environment-variables.md b/plugins/zoom-developers/skills/video-sdk/ios/references/environment-variables.md new file mode 100644 index 00000000..e70703eb --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/ios/references/environment-variables.md @@ -0,0 +1,13 @@ +# 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_TOKEN` is generated backend-side and sent to app for join. diff --git a/plugins/zoom-developers/skills/video-sdk/ios/references/ios-reference-map.md b/plugins/zoom-developers/skills/video-sdk/ios/references/ios-reference-map.md new file mode 100644 index 00000000..99afeb38 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/ios/references/ios-reference-map.md @@ -0,0 +1,18 @@ +# 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) diff --git a/plugins/zoom-developers/skills/video-sdk/ios/references/versioning-and-compatibility.md b/plugins/zoom-developers/skills/video-sdk/ios/references/versioning-and-compatibility.md new file mode 100644 index 00000000..b92f2305 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/ios/references/versioning-and-compatibility.md @@ -0,0 +1,18 @@ +# 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). diff --git a/plugins/zoom-developers/skills/video-sdk/ios/scenarios/high-level-scenarios.md b/plugins/zoom-developers/skills/video-sdk/ios/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..7a812a92 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/ios/scenarios/high-level-scenarios.md @@ -0,0 +1,26 @@ +# 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. diff --git a/plugins/zoom-developers/skills/video-sdk/ios/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/video-sdk/ios/troubleshooting/common-issues.md new file mode 100644 index 00000000..ebdda76c --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/ios/troubleshooting/common-issues.md @@ -0,0 +1,21 @@ +# 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. diff --git a/plugins/zoom-developers/skills/video-sdk/linux/RUNBOOK.md b/plugins/zoom-developers/skills/video-sdk/linux/RUNBOOK.md new file mode 100644 index 00000000..7be05460 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/RUNBOOK.md @@ -0,0 +1,64 @@ +# 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/` diff --git a/plugins/zoom-developers/skills/video-sdk/linux/SKILL.md b/plugins/zoom-developers/skills/video-sdk/linux/SKILL.md new file mode 100644 index 00000000..23327edb --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/SKILL.md @@ -0,0 +1,435 @@ +--- +name: zoom-video-sdk-linux +description: "Zoom Video SDK for Linux - C++ headless bots, raw audio/video capture/injection, Qt/GTK integration, Docker support" +--- + +# Zoom Video SDK - Linux Development + +Expert guidance for developing with the Zoom Video SDK on Linux. Build headless bots, raw media capture/injection applications, and custom UI integrations with Qt/GTK. + +**Official Documentation**: https://developers.zoom.us/docs/video-sdk/linux/ +**API Reference**: https://marketplacefront.zoom.us/sdk/custom/linux/ +**Sample Repository**: https://github.com/zoom/videosdk-linux-raw-recording-sample + +## Quick Links + +**New to Video SDK? Follow this path:** + +1. **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - Universal 3-step pattern for ANY feature +2. **[Session Join Pattern](examples/session-join-pattern.md)** - Complete working code to join a session +3. **[Raw Data vs Canvas](concepts/raw-data-vs-canvas.md)** - **CRITICAL**: Linux has NO Canvas API - raw data ONLY +4. **[Raw Video Capture](examples/raw-video-capture.md)** - Capture and process YUV420 frames + +**Reference:** +- **[Singleton Hierarchy](concepts/singleton-hierarchy.md)** - 5-level SDK navigation map +- **[API Reference](references/linux-reference.md)** - Complete API documentation +- **[Qt/GTK Integration](examples/qt-gtk-integration.md)** - UI framework patterns +- **[Troubleshooting](troubleshooting/common-issues.md)** - Quick diagnostics +- **[SKILL.md](SKILL.md)** - Complete documentation navigation + +**Having issues?** +- PulseAudio setup → [PulseAudio Guide](troubleshooting/pulseaudio-setup.md) +- Qt dependencies → [Qt Dependencies](troubleshooting/qt-dependencies.md) +- Build errors → [Build Errors Guide](troubleshooting/build-errors.md) + +## Key Differences from Windows/macOS + +| Feature | Linux | Windows/Mac | +|---------|-------|-------------| +| **Canvas API** | ❌ Not available | ✅ Available | +| **Raw Data Pipe** | ✅ **ONLY option** | ✅ Available | +| **UI Integration** | Qt, GTK, SDL2, OpenGL | Win32/WinForms/WPF, Cocoa | +| **Headless Support** | ✅ Excellent (Docker) | Limited | +| **Audio** | PulseAudio required | Native | +| **Virtual Devices** | ✅ Required for headless | Optional | + +## SDK Overview + +The Zoom Video SDK for Linux is a C++ library optimized for: +- **Headless Bots**: Docker/WSL support, no display required +- **Raw Data Access**: Capture YUV420 video, PCM audio +- **Raw Data Injection**: Virtual camera/mic for custom media +- **Screen Sharing**: Capture or inject share data +- **Cloud Recording**: Record sessions to Zoom cloud +- **Live Streaming**: Stream to RTMP endpoints +- **Live Transcription**: Real-time speech-to-text +- **Qt/GTK Integration**: Full UI framework support + +## Prerequisites + +### System Requirements + +- **OS**: Ubuntu 20.04+, Debian 11+, or compatible +- **Architecture**: x64 (recommended), ARM64 +- **Compiler**: GCC 9+, Clang 10+ +- **CMake**: 3.14 or later +- **Qt5**: Bundled with SDK (do NOT install system Qt5) + +### Dependencies + +```bash +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 + +# PulseAudio configuration (CRITICAL for audio) +mkdir -p ~/.config +echo "[General]" > ~/.config/zoomus.conf +echo "system.audio.type=default" >> ~/.config/zoomus.conf + +# Log directory +mkdir -p ~/.zoom/logs +``` + +## Quick Start + +```cpp +#include "zoom_video_sdk_api.h" +#include "zoom_video_sdk_interface.h" +#include "zoom_video_sdk_delegate_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +// 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(myDelegate); + +// 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: Virtual audio speaker +ctx.virtualAudioSpeaker = new VirtualSpeaker(); + +IZoomVideoSDKSession* session = sdk->joinSession(ctx); +``` + +See **[Session Join Pattern](examples/session-join-pattern.md)** for complete code. + +## Key Features + +| Feature | Linux Support | Guide | +|---------|---------------|-------| +| **Session Management** | ✅ Full | [Session Join](examples/session-join-pattern.md) | +| **Raw Video (YUV420)** | ✅ ONLY rendering option | [Raw Video](examples/raw-video-capture.md) | +| **Raw Audio (PCM)** | ✅ Full | [Raw Audio](examples/raw-audio-capture.md) | +| **Virtual Camera/Mic** | ✅ Full | [Virtual Devices](examples/virtual-audio-video.md) | +| **Cloud Recording** | ✅ Full | [Recording](examples/cloud-recording.md) | +| **Live Streaming** | ✅ Full | [Live Stream](examples/live-streaming.md) | +| **Live Transcription** | ✅ Full | [Transcription](examples/transcription.md) | +| **Command Channel** | ✅ Full | [Commands](examples/command-channel.md) | +| **Chat** | ✅ Full | [Chat](examples/chat.md) | +| **Qt Integration** | ✅ Recommended | [Qt/GTK](examples/qt-gtk-integration.md) | +| **GTK Integration** | ✅ Supported | [Qt/GTK](examples/qt-gtk-integration.md) | +| **Docker/Headless** | ✅ Excellent | [Virtual Devices](examples/virtual-audio-video.md) | + +## Critical Gotchas + +### ⚠️ CRITICAL #1: No Canvas API on Linux + +**Problem**: Linux SDK does NOT have Canvas API like Windows/Mac. + +**Solution**: You MUST use Raw Data Pipe and implement your own rendering. + +See: **[Raw Data vs Canvas](concepts/raw-data-vs-canvas.md)** + +### ⚠️ CRITICAL #2: PulseAudio Required for Audio + +**Problem**: SDK requires PulseAudio for raw audio functions. + +**Solution**: +```bash +sudo apt install -y pulseaudio +mkdir -p ~/.config +echo "[General]" > ~/.config/zoomus.conf +echo "system.audio.type=default" >> ~/.config/zoomus.conf +``` + +See: **[PulseAudio Setup](troubleshooting/pulseaudio-setup.md)** + +### ⚠️ CRITICAL #3: Qt5 Dependencies + +**Problem**: SDK requires Qt5 libraries (bundled, NOT system Qt5). + +**Solution**: +```bash +# Copy from SDK package +cp -r samples/qt_libs/Qt/lib/* lib/zoom_video_sdk/ + +# Create symlinks +cd lib/zoom_video_sdk +for lib in libQt5*.so.5; do ln -sf $lib ${lib%.5}; done +``` + +See: **[Qt Dependencies](troubleshooting/qt-dependencies.md)** + +### ⚠️ CRITICAL #4: Heap Memory Mode + +Always use heap mode for raw data: + +```cpp +init_params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +init_params.shareRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +init_params.audioRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +``` + +### ⚠️ CRITICAL #5: Virtual Audio for Headless + +**Problem**: Docker/headless environments have no audio devices. + +**Solution**: Use virtual audio speaker and mic. + +```cpp +session_context.virtualAudioSpeaker = new VirtualSpeaker(); +session_context.virtualAudioMic = new VirtualMic(); +``` + +See: **[Virtual Audio/Video](examples/virtual-audio-video.md)** + +## Sample Repositories + +### Official Samples + +| Repository | Description | +|-----------|-------------| +| **[raw-recording-sample](https://github.com/zoom/videosdk-linux-raw-recording-sample)** | Raw audio/video capture | +| **[qt-quickstart](https://github.com/tanchunsiong/videosdk-linux-qt-quickstart)** | Qt6 UI integration | +| **[gtk-quickstart](https://github.com/tanchunsiong/videosdk-linux-gtk-quickstart)** | GTK3 UI integration | + +### Sample Architecture + +``` +Headless Bot (Docker): +┌──────────────────────────────────┐ +│ Virtual Audio Speaker/Mic │ +├──────────────────────────────────┤ +│ Raw Data Processing │ +│ - YUV420 → File/Stream + + +## Merged from video-sdk/linux/SKILL.md + +# Zoom Video SDK Linux - Complete Documentation Index + +## Quick Start Path + +**If you're new to the SDK, follow this order:** + +1. **Read the architecture pattern** → [concepts/sdk-architecture-pattern.md](concepts/sdk-architecture-pattern.md) + - Universal formula: Singleton → Delegate → Subscribe + - Once you understand this, you can implement any feature + +2. **Understand Linux specifics** → [concepts/raw-data-vs-canvas.md](concepts/raw-data-vs-canvas.md) + - **CRITICAL**: Linux has NO Canvas API - raw data ONLY + +3. **Implement session join** → [examples/session-join-pattern.md](examples/session-join-pattern.md) + - Complete working JWT + session join code + +4. **Setup environment** → [troubleshooting/pulseaudio-setup.md](troubleshooting/pulseaudio-setup.md) + - PulseAudio configuration (required for audio) + - [troubleshooting/qt-dependencies.md](troubleshooting/qt-dependencies.md) + - Qt5 library setup (bundled with SDK) + +5. **Implement features** → Choose from examples below + +--- + +## Documentation Structure + +``` +video-sdk/linux/ +├── SKILL.md # Main skill overview +├── SKILL.md # This file - navigation guide +├── linux.md # Platform summary +│ +├── concepts/ # Core architectural patterns +│ ├── sdk-architecture-pattern.md # Universal formula for ANY feature +│ ├── singleton-hierarchy.md # 5-level navigation guide +│ └── raw-data-vs-canvas.md # Linux-specific: raw data ONLY +│ +├── examples/ # Complete working code +│ ├── session-join-pattern.md # JWT auth + session join +│ └── command-channel.md # Command channel with threading +│ +├── troubleshooting/ # Problem solving guides +│ ├── pulseaudio-setup.md # Audio configuration +│ ├── qt-dependencies.md # Qt5 library setup +│ ├── build-errors.md # Common build issues +│ └── common-issues.md # Quick diagnostic workflow +│ +└── references/ # Reference documentation + └── linux-reference.md # API hierarchy, methods, error codes +``` + +--- + +## By Use Case + +### I want to build a headless bot +1. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Understand the pattern +2. [Session Join Pattern](examples/session-join-pattern.md) - Join sessions +3. [PulseAudio Setup](troubleshooting/pulseaudio-setup.md) - Configure audio +4. [Raw Data vs Canvas](concepts/raw-data-vs-canvas.md) - Understand Linux differences + +### I'm getting build errors +1. [Build Errors Guide](troubleshooting/build-errors.md) - SDK build issues +2. [Qt Dependencies](troubleshooting/qt-dependencies.md) - Qt5 setup +3. [Common Issues](troubleshooting/common-issues.md) - Quick diagnostics + +### I'm getting runtime errors +1. [PulseAudio Setup](troubleshooting/pulseaudio-setup.md) - Audio not working +2. [Qt Dependencies](troubleshooting/qt-dependencies.md) - Library not found +3. [Common Issues](troubleshooting/common-issues.md) - Error code tables + +### I want to use command channel +1. [Command Channel](examples/command-channel.md) - Send/receive commands +2. [Common Issues](troubleshooting/common-issues.md) - Threading requirements + +### I want to implement a specific feature +1. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - **START HERE!** +2. [Singleton Hierarchy](concepts/singleton-hierarchy.md) - Navigate to the feature +3. [API Reference](references/linux-reference.md) - Method signatures + +--- + +## Most Critical Documents + +### 1. SDK Architecture Pattern (MASTER DOCUMENT) +**[concepts/sdk-architecture-pattern.md](concepts/sdk-architecture-pattern.md)** + +The universal 3-step pattern: +1. Get singleton (SDK, helpers, session, users) +2. Implement delegate (event callbacks) +3. Subscribe and use + +### 2. Raw Data vs Canvas (LINUX-SPECIFIC) +**[concepts/raw-data-vs-canvas.md](concepts/raw-data-vs-canvas.md)** + +**CRITICAL**: Unlike Windows/Mac, Linux SDK has NO Canvas API. You MUST use raw data pipe. + +### 3. PulseAudio Setup (MOST COMMON ISSUE) +**[troubleshooting/pulseaudio-setup.md](troubleshooting/pulseaudio-setup.md)** + +Audio requires PulseAudio configuration. + +### 4. Qt Dependencies +**[troubleshooting/qt-dependencies.md](troubleshooting/qt-dependencies.md)** + +SDK requires bundled Qt5 libraries, NOT system Qt5. + +--- + +## Key Learnings + +### Critical Discoveries: + +1. **Linux has NO Canvas API** + - Windows/Mac have Canvas API for SDK-rendered video + - Linux MUST use Raw Data Pipe + - See: [Raw Data vs Canvas](concepts/raw-data-vs-canvas.md) + +2. **PulseAudio is MANDATORY** + - SDK requires PulseAudio for raw audio + - Must configure ~/.config/zoomus.conf + - See: [PulseAudio Setup](troubleshooting/pulseaudio-setup.md) + +3. **Use Bundled Qt5, NOT System Qt5** + - SDK includes specific Qt5 versions + - Copy from samples/qt_libs/ + - See: [Qt Dependencies](troubleshooting/qt-dependencies.md) + +4. **Helpers Control YOUR Streams Only** + - `videoHelper->startVideo()` starts YOUR camera + - To see others, subscribe to their VideoPipe + - See: [Singleton Hierarchy](concepts/singleton-hierarchy.md) + +5. **Virtual Devices for Headless** + - Docker/headless needs virtual audio speaker/mic + - Set before joining session + - See: [Session Join Pattern](examples/session-join-pattern.md) + +6. **Always Use Heap Memory Mode** + ```cpp + init_params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; + ``` + +7. **GLib Main Loop Required** + - while/sleep loops don't dispatch SDK events + - Must use g_main_loop_run() + - See: [Common Issues](troubleshooting/common-issues.md) + +8. **All SDK Calls Must Be on Main Thread** + - Background thread SDK calls return error 2 (Internal_Error) + - Use g_idle_add() to schedule on GLib main thread + - See: [Command Channel](examples/command-channel.md) + +9. **Command Channel is Session-Scoped** + - Does NOT span across different sessions + - Both sender and receiver must be in the same session + - See: [Command Channel](examples/command-channel.md) + +--- + +## Sample Repositories + +- **[raw-recording-sample](https://github.com/zoom/videosdk-linux-raw-recording-sample)** - Official raw data sample +- **[qt-quickstart](https://github.com/tanchunsiong/videosdk-linux-qt-quickstart)** - Qt6 UI integration +- **[gtk-quickstart](https://github.com/tanchunsiong/videosdk-linux-gtk-quickstart)** - GTK3 UI integration + +--- + +## Quick Reference + +### "My code won't compile" +→ [Build Errors Guide](troubleshooting/build-errors.md) + +### "Audio not working" +→ [PulseAudio Setup](troubleshooting/pulseaudio-setup.md) + +### "Library not found" +→ [Qt Dependencies](troubleshooting/qt-dependencies.md) + +### "How do I implement [feature]?" +→ [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +### "What error code means what?" +→ [Common Issues](troubleshooting/common-issues.md) + +--- + +## Document Version + +Based on **Zoom Video SDK for Linux v2.x** + +--- + +**Happy coding!** + +Remember: The [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) is your key to unlocking the entire SDK. Read it first! + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/video-sdk/linux/concepts/raw-data-vs-canvas.md b/plugins/zoom-developers/skills/video-sdk/linux/concepts/raw-data-vs-canvas.md new file mode 100644 index 00000000..424d81b5 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/concepts/raw-data-vs-canvas.md @@ -0,0 +1,571 @@ +# 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 + +```cpp +// 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) + +```cpp +// 1. Implement virtual video source +class VirtualCamera : public IZoomVideoSDKVideoSource { + IZoomVideoSDKVideoSender* sender_; + +public: + void onInitialize(IZoomVideoSDKVideoSender* sender, + IVideoSDKVector* 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 + +```cpp +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) + +```cpp +#include + +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**: +```bash +sudo apt install -y libyuv-dev +``` + +--- + +## Rendering Options + +### Option 1: Qt (Recommended for Cross-Platform) + +```cpp +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 + +```cpp +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 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 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) + +```cpp +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) + +```cpp +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. + +```cpp +// 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 + +```cpp +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(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 + +```cpp +// 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) + +```cpp +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 diff --git a/plugins/zoom-developers/skills/video-sdk/linux/concepts/sdk-architecture-pattern.md b/plugins/zoom-developers/skills/video-sdk/linux/concepts/sdk-architecture-pattern.md new file mode 100644 index 00000000..5657dc17 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/concepts/sdk-architecture-pattern.md @@ -0,0 +1,528 @@ +# 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 & Use +``` + +This 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: + +```cpp +// 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 + +```cpp +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*) override { /* Users joined */ } + virtual void onUserLeave(IZoomVideoSDKUserHelper*, IVideoSDKVector*) override { /* Users left */ } + + // Video events + virtual void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper*, IVideoSDKVector*) override { /* Video status changed */ } + + // Audio events + virtual void onUserAudioStatusChanged(IZoomVideoSDKAudioHelper*, IVideoSDKVector*) 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: + +```cpp +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: + +```cpp +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: + +```cpp +class VirtualVideo : public IZoomVideoSDKVideoSource { +public: + virtual void onInitialize(IZoomVideoSDKVideoSender* sender, + IVideoSDKVector* 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) + +```cpp +// 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) + +```cpp +// 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) + +```cpp +// 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 + +```cpp +// 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 + +```cpp +// 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 + +```cpp +// 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 + +```cpp +// 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. + +```cpp +// 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**: +```cpp +// 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**: +```cpp +// 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 audio +- `IZoomVideoSDKVirtualAudioSpeaker` - Receive audio (headless) +- `IZoomVideoSDKVideoSource` - Send custom video +- `IZoomVideoSDKShareSource` - Send custom share + +Set these **before** joining session. + +### 3. Memory Modes + +Always use heap mode for raw data: +```cpp +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](../troubleshooting/qt-dependencies.md) + +### 5. PulseAudio for Audio + +Linux requires PulseAudio for raw audio features: +```bash +sudo apt install -y pulseaudio +mkdir -p ~/.config +echo "[General]" > ~/.config/zoomus.conf +echo "system.audio.type=default" >> ~/.config/zoomus.conf +``` + +--- + +## Complete Example + +```cpp +#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* 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 + diff --git a/plugins/zoom-developers/skills/video-sdk/linux/concepts/singleton-hierarchy.md b/plugins/zoom-developers/skills/video-sdk/linux/concepts/singleton-hierarchy.md new file mode 100644 index 00000000..263e6002 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/concepts/singleton-hierarchy.md @@ -0,0 +1,542 @@ +# 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. + +```cpp +// 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 operations +- `joinSession()` - Join or create a session +- `leaveSession(bool endSession)` - Leave session (host can end for all) +- `cleanup()` - Release SDK resources +- `addListener()` / `removeListener()` - Register event callbacks +- `isInSession()` - Check if currently in a session +- `getSessionInfo()` - Access Level 2: Session +- `get*Helper()` - Access Level 2: Feature Helpers + +--- + +## Level 2A: Session Info + +**Purpose**: Access session-level information and users. + +```cpp +IZoomVideoSDKSession* session = video_sdk_obj->getSessionInfo(); +``` + +**Key Methods**: +```cpp +// Current user +IZoomVideoSDKUser* myself = session->getMyself(); + +// All users (including self) +IVideoSDKVector* all = session->getAllUsers(); + +// Remote users only (excluding self) +IVideoSDKVector* 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). + +```cpp +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* mics = audio->getMicList(); +IVideoSDKVector* 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). + +```cpp +IZoomVideoSDKVideoHelper* video = video_sdk_obj->getVideoHelper(); + +// Start/stop your video +video->startVideo(); +video->stopVideo(); + +// Camera management +IVideoSDKVector* cameras = video->getCameraList(); +video->selectCamera(deviceID); + +// Video settings +video->rotateMyVideo(90); // 0, 90, 180, 270 +``` + +**Key Insight**: To receive others' video, subscribe to their `GetVideoPipe()`. + +### Share Helper + +**Purpose**: Control YOUR screen share. + +```cpp +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. + +```cpp +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. + +```cpp +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. + +```cpp +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. + +```cpp +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* langs = trans->getAvailableSpokenLanguages(); +trans->setSpokenLanguage(langs->GetItem(0)->getLTTLanguageID()); +``` + +**Receive messages**: Implement `onLiveTranscriptionMsgInfoReceived()` in delegate. + +### Command Channel + +**Purpose**: Custom command messaging between participants. + +```cpp +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. + +```cpp +IZoomVideoSDKPhoneHelper* phone = video_sdk_obj->getPhoneHelper(); + +// Get supported countries +IVideoSDKVector* 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. + +```cpp +// 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. + +```cpp +// Get current user +IZoomVideoSDKUser* myself = video_sdk_obj->getSessionInfo()->getMyself(); + +// Get all users +IVideoSDKVector* 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**: +```cpp +// 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 + +```cpp +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 + +```cpp +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 +```cpp +IZoomVideoSDK* sdk = CreateZoomVideoSDKObj(); +``` + +### Get Session +```cpp +IZoomVideoSDKSession* session = sdk->getSessionInfo(); +``` + +### Get Users +```cpp +IZoomVideoSDKUser* myself = session->getMyself(); +IVideoSDKVector* all = session->getAllUsers(); +IVideoSDKVector* remote = session->getRemoteUsers(); +``` + +### Get Helpers +```cpp +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 +```cpp +IZoomVideoSDKRawDataPipe* videoPipe = user->GetVideoPipe(); +IZoomVideoSDKRawDataPipe* sharePipe = user->GetSharePipe(); +``` + +--- + +## Complete Navigation Example + +```cpp +// 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* 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 diff --git a/plugins/zoom-developers/skills/video-sdk/linux/examples/chat.md b/plugins/zoom-developers/skills/video-sdk/linux/examples/chat.md new file mode 100644 index 00000000..bd81f4a7 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/examples/chat.md @@ -0,0 +1,9 @@ +# 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. diff --git a/plugins/zoom-developers/skills/video-sdk/linux/examples/cloud-recording.md b/plugins/zoom-developers/skills/video-sdk/linux/examples/cloud-recording.md new file mode 100644 index 00000000..e78adc50 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/examples/cloud-recording.md @@ -0,0 +1,8 @@ +# 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. diff --git a/plugins/zoom-developers/skills/video-sdk/linux/examples/command-channel.md b/plugins/zoom-developers/skills/video-sdk/linux/examples/command-channel.md new file mode 100644 index 00000000..11ec4496 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/examples/command-channel.md @@ -0,0 +1,321 @@ +# Command Channel + +Complete working code for custom command messaging between participants on Linux. + +**Official Sample**: [videosdk-linux-raw-recording-sample](https://github.com/zoom/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](../../windows/examples/command-channel.md) 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](../troubleshooting/common-issues.md) for details. + +--- + +## Complete Working Code + +### CommandHandler.h + +```cpp +#ifndef COMMAND_HANDLER_H +#define COMMAND_HANDLER_H + +#include "zoom_video_sdk_api.h" +#include "zoom_video_sdk_interface.h" +#include +#include +#include + +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 SetMessageHandler(MessageCallback callback) { m_callback = callback; } + +private: + IZoomVideoSDK* m_sdk; + IZoomVideoSDKCmdChannel* m_cmdChannel; + bool m_connected; + MessageCallback m_callback; +}; + +#endif // COMMAND_HANDLER_H +``` + +### CommandHandler.cpp + +```cpp +#include "CommandHandler.h" +#include + +// 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(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 + +```cpp +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()`: + +```cpp +// 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: +```cpp +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: +```cpp +// 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-join-pattern.md) - Session setup with GLib main loop +- [Common Issues](../troubleshooting/common-issues.md) - Threading and GLib requirements +- [Windows Command Channel](../../windows/examples/command-channel.md) - Windows equivalent (uses wchar_t) +- [Web Command Channel](../../web/examples/command-channel.md) - Web SDK equivalent +- [Authorization](../../references/authorization.md) - JWT roleType for host/co-host diff --git a/plugins/zoom-developers/skills/video-sdk/linux/examples/live-streaming.md b/plugins/zoom-developers/skills/video-sdk/linux/examples/live-streaming.md new file mode 100644 index 00000000..310d5e89 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/examples/live-streaming.md @@ -0,0 +1,9 @@ +# 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. diff --git a/plugins/zoom-developers/skills/video-sdk/linux/examples/qt-gtk-integration.md b/plugins/zoom-developers/skills/video-sdk/linux/examples/qt-gtk-integration.md new file mode 100644 index 00000000..00a35476 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/examples/qt-gtk-integration.md @@ -0,0 +1,9 @@ +# 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. diff --git a/plugins/zoom-developers/skills/video-sdk/linux/examples/raw-audio-capture.md b/plugins/zoom-developers/skills/video-sdk/linux/examples/raw-audio-capture.md new file mode 100644 index 00000000..95d5106b --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/examples/raw-audio-capture.md @@ -0,0 +1,9 @@ +# 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. diff --git a/plugins/zoom-developers/skills/video-sdk/linux/examples/raw-video-capture.md b/plugins/zoom-developers/skills/video-sdk/linux/examples/raw-video-capture.md new file mode 100644 index 00000000..93bc000f --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/examples/raw-video-capture.md @@ -0,0 +1,9 @@ +# 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. diff --git a/plugins/zoom-developers/skills/video-sdk/linux/examples/session-join-pattern.md b/plugins/zoom-developers/skills/video-sdk/linux/examples/session-join-pattern.md new file mode 100644 index 00000000..1a2e7f28 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/examples/session-join-pattern.md @@ -0,0 +1,642 @@ +# 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 + +```bash +# 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/logs +``` + +## JWT Token Generation + +**CRITICAL**: You need a JWT token to join sessions. Generate from your SDK credentials. + +### Using Python + +```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 + +```javascript +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 + +```cpp +#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 +#include + +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* userList) override; + virtual void onUserLeave(IZoomVideoSDKUserHelper* pUserHelper, + IVideoSDKVector* userList) override; + virtual void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* pVideoHelper, + IVideoSDKVector* userList) override; + virtual void onUserAudioStatusChanged(IZoomVideoSDKAudioHelper* pAudioHelper, + IVideoSDKVector* 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*) 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 running_; +}; + +#endif // BOT_DELEGATE_H +``` + +### Implementation File: BotDelegate.cpp + +```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* 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* 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* 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* 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](../troubleshooting/common-issues.md) for details. + +```cpp +#include "BotDelegate.h" +#include +#include +#include +#include + +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(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_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 +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 + +```bash +# 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 + +```bash +# 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](../troubleshooting/common-issues.md). + +### 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: +```cpp +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 diff --git a/plugins/zoom-developers/skills/video-sdk/linux/examples/transcription.md b/plugins/zoom-developers/skills/video-sdk/linux/examples/transcription.md new file mode 100644 index 00000000..436999b1 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/examples/transcription.md @@ -0,0 +1,9 @@ +# 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. diff --git a/plugins/zoom-developers/skills/video-sdk/linux/examples/virtual-audio-video.md b/plugins/zoom-developers/skills/video-sdk/linux/examples/virtual-audio-video.md new file mode 100644 index 00000000..48418467 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/examples/virtual-audio-video.md @@ -0,0 +1,8 @@ +# 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. diff --git a/plugins/zoom-developers/skills/video-sdk/linux/linux.md b/plugins/zoom-developers/skills/video-sdk/linux/linux.md new file mode 100644 index 00000000..592036d0 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/linux.md @@ -0,0 +1,787 @@ +--- +name: video-sdk/linux +description: "Zoom Video SDK for Linux - C++ integration for video sessions, raw audio/video capture, screen sharing, recording, and real-time communication" +--- + +# Zoom Video SDK - Linux Development + +Expert guidance for developing with the Zoom Video SDK on Linux. This SDK enables headless video conferencing bots, raw media capture/injection, cloud recording, live streaming, and real-time transcription. + +**Official Documentation**: https://developers.zoom.us/docs/video-sdk/linux/ +**API Reference**: https://marketplacefront.zoom.us/sdk/custom/linux/ +**Sample Repository**: https://github.com/zoom/videosdk-linux-raw-recording-sample + +## SDK Overview + +The Zoom Video SDK for Linux is a C++ library that provides: +- **Session Management**: Join/leave video SDK sessions +- **Raw Data Access**: Capture raw audio/video frames (YUV420, PCM) +- **Raw Data Injection**: Send custom audio/video into sessions +- **Screen Sharing**: Share screens or inject custom share sources +- **Cloud Recording**: Record sessions to Zoom cloud +- **Live Streaming**: Stream to RTMP endpoints (YouTube, etc.) +- **Chat & Commands**: In-session messaging and command channels +- **Live Transcription**: Real-time speech-to-text +- **Subsessions**: Breakout room support +- **Whiteboard**: Collaborative whiteboard features +- **Annotations**: Screen share annotations + +## Project Structure + +``` +project/ +├── CMakeLists.txt +├── config.json # Session credentials +├── src/ +│ ├── main.cpp +├── include/ +│ └── zoom_video_sdk/ # SDK headers +│ ├── zoom_video_sdk_api.h +│ ├── zoom_video_sdk_interface.h +│ ├── zoom_video_sdk_delegate_interface.h +│ ├── zoom_video_sdk_def.h +│ └── helpers/ # Feature-specific interfaces +│ ├── zoom_video_sdk_audio_helper_interface.h +│ ├── zoom_video_sdk_audio_send_rawdata_interface.h # Virtual audio +│ ├── zoom_video_sdk_video_helper_interface.h +│ ├── zoom_video_sdk_video_source_helper_interface.h # Video injection +│ └── zoom_video_sdk_user_helper_interface.h +├── lib/ +│ └── zoom_video_sdk/ # SDK + Qt5 libraries +│ ├── libvideosdk.so # Main SDK (from SDK tarball) +│ ├── libcml.so # Required +│ ├── libmpg123.so # Audio codec +│ ├── cpthost # Host binary +│ ├── libQt5Core.so.5 # Qt5 dependencies (from sdk samples/qt_libs) +│ ├── libQt5Gui.so.5 +│ ├── libQt5Network.so.5 +│ ├── libQt5Qml.so.5 +│ └── libQt5Quick.so.5 +└── bin/ # Build output +``` + +**CRITICAL**: SDK requires Qt5 libraries. Copy from SDK samples (`qt_libs/Qt/lib/`) and create symlinks: +```bash +cd lib/zoom_video_sdk +ln -sf libQt5Core.so.5 libQt5Core.so +ln -sf libQt5Gui.so.5 libQt5Gui.so +# ... repeat for all Qt5 libs +``` + +## Prerequisites + +```bash +# System dependencies +sudo apt update +sudo apt install -y build-essential gcc cmake +sudo apt install -y 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 + +# Qt5 is bundled with SDK - do NOT install system Qt5 +# Copy Qt5 libs from SDK samples/qt_libs/Qt/lib/ to your lib directory + +# For headless Linux (no soundcard) +sudo apt install -y pulseaudio +# OR use virtual audio speaker/mic in code (recommended) + +# Create log directory +mkdir -p ~/.zoom/logs +``` + +## CMakeLists.txt Template + +```cmake +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) + +# CRITICAL: Include both paths for nested SDK headers +include_directories( + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/include/zoom_video_sdk # For helpers/ relative includes + ${GLIB_INCLUDE_DIRS} +) + +link_directories(${CMAKE_SOURCE_DIR}/lib/zoom_video_sdk) + +set(SOURCES src/main.cpp src/ZoomDelegate.cpp) + +add_executable(${PROJECT_NAME} ${SOURCES}) + +target_link_libraries(${PROJECT_NAME} + videosdk + ${GLIB_LIBRARIES} + pthread + Qt5Core Qt5Gui Qt5Network Qt5Qml Qt5Quick +) + +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 +) +``` + +## Configuration + +**config.json** (place in bin/ directory): +```json +{ + "session_name": "your-session-name", + "token": "your.jwt.token", + "session_psw": "optional-password" +} +``` + +**JWT Generation**: Use Zoom Video SDK credentials from marketplace.zoom.us + +## Core Implementation Pattern + +### 1. SDK Initialization + +```cpp +#include "zoom_video_sdk_api.h" +#include "zoom_video_sdk_interface.h" +#include "zoom_video_sdk_delegate_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +IZoomVideoSDK* video_sdk_obj = CreateZoomVideoSDKObj(); + +ZoomVideoSDKInitParams init_params; +init_params.domain = "https://zoom.us"; +init_params.enableLog = true; +init_params.logFilePrefix = "my_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); +``` + +### 2. Session Join + +```cpp +ZoomVideoSDKSessionContext session_context; +session_context.sessionName = "session-name"; +session_context.sessionPassword = "password"; +session_context.userName = "Linux Bot"; +session_context.token = "jwt-token"; +session_context.sessionIdleTimeoutMins = 40; // 0 = never timeout +session_context.autoLoadMutliStream = true; +session_context.videoOption.localVideoOn = true; +session_context.audioOption.connect = true; +session_context.audioOption.mute = false; + +// For headless Linux - use virtual audio +ZoomVideoSDKVirtualAudioSpeaker* vSpeaker = new ZoomVideoSDKVirtualAudioSpeaker(); +session_context.virtualAudioSpeaker = vSpeaker; + +IZoomVideoSDKSession* session = video_sdk_obj->joinSession(session_context); +``` + +### 3. Event Delegate + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +public: + virtual void onSessionJoin() override { + printf("Joined session!\n"); + // Start audio subscription + IZoomVideoSDKAudioHelper* audioHelper = video_sdk_obj->getAudioHelper(); + if (audioHelper) { + audioHelper->startAudio(); + audioHelper->subscribe(); + } + } + + virtual void onSessionLeave() override { + printf("Left session\n"); + } + + virtual void onError(ZoomVideoSDKErrors errorCode, int detailErrorCode) override { + printf("Error: %d, Detail: %d\n", errorCode, detailErrorCode); + } + + virtual void onUserJoin(IZoomVideoSDKUserHelper* pUserHelper, + IVideoSDKVector* userList) override { + int count = userList->GetCount(); + for (int i = 0; i < count; i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + // Subscribe to user's video + user->GetVideoPipe()->subscribe(ZoomVideoSDKResolution_720P, videoDelegate); + } + } + + virtual void onMixedAudioRawDataReceived(AudioRawData* data_) override { + // Handle mixed audio (all participants) + char* buffer = data_->GetBuffer(); + unsigned int len = data_->GetBufferLen(); + unsigned int sampleRate = data_->GetSampleRate(); + // Process PCM audio + } + + virtual void onOneWayAudioRawDataReceived(AudioRawData* data_, + IZoomVideoSDKUser* pUser) override { + // Handle individual user audio + } +}; + +video_sdk_obj->addListener(new MyDelegate()); +``` + +## IZoomVideoSDKDelegate - CRITICAL Notes + +**WARNING**: `IZoomVideoSDKDelegate` has many pure virtual methods. ALL must be implemented. + +**VERSION SENSITIVITY**: The delegate interface changes between SDK versions: +- New callbacks are added, old ones may be removed +- Method signatures may change +- **ALWAYS check `zoom_video_sdk_delegate_interface.h` when upgrading SDK versions** + +### Required Includes for Full Class Definitions + +**VERSION SENSITIVITY**: Forward declarations in `zoom_video_sdk_def.h` may change. Always verify which helper headers contain full class definitions. + +```cpp +// zoom_video_sdk_def.h only forward-declares classes! +// Include helper headers for actual implementations: +#include "zoom_video_sdk/helpers/zoom_video_sdk_user_helper_interface.h" // IZoomVideoSDKUser full definition +#include "zoom_video_sdk/helpers/zoom_video_sdk_audio_send_rawdata_interface.h" // Virtual audio classes +#include "zoom_video_sdk/helpers/zoom_video_sdk_video_source_helper_interface.h" // Video source/sender +#include "zoom_video_sdk/zoom_video_sdk_chat_message_interface.h" // IZoomVideoSDKChatMessage +``` + +### SDK 2.4.12 Method Signature Examples + +**VERSION SENSITIVITY**: Method signatures change between SDK versions. These are examples from 2.4.12 - **always verify against current SDK headers**. + +| Example Change | SDK 2.4.12 Signature | +|----------------|----------------------| +| `onSessionLeave()` | `onSessionLeave(ZoomVideoSDKSessionLeaveReason reason)` (both exist) | +| `onUserShareStatusChanged(...)` | Takes `IZoomVideoSDKShareAction*` instead of `ZoomVideoSDKShareStatus, ZoomVideoSDKShareType` | +| `message->getSenderUser()` | Now `message->getSendUser()` | +| `info->getSpeaker()` | Now `info->getSpeakerName()` (returns `const zchar_t*` directly) | +| `sender->send()` | Now `sender->Send()` (capital S) | +| `user->getVideoStatus()` | Now `user->GetVideoPipe()->getVideoStatus()` | + +### Required Empty Stub Callbacks + +**VERSION SENSITIVITY**: Callbacks are added/removed between SDK versions. Check `zoom_video_sdk_delegate_interface.h` for the complete current list. + +Must implement ALL these (use empty `{}` for unused ones): +```cpp +virtual void onUserShareStatusChanged(IZoomVideoSDKShareHelper*, IZoomVideoSDKUser*, IZoomVideoSDKShareAction*) override {} +virtual void onShareContentChanged(IZoomVideoSDKShareHelper*, IZoomVideoSDKUser*, IZoomVideoSDKShareAction*) override {} +virtual void onFailedToStartShare(IZoomVideoSDKShareHelper*, IZoomVideoSDKUser*) override {} +virtual void onShareSettingChanged(ZoomVideoSDKShareSetting) override {} +virtual void onUserManagerChanged(IZoomVideoSDKUser*) override {} +virtual void onAudioLevelChanged(unsigned int, bool, IZoomVideoSDKUser*) override {} +virtual void onSpokenLanguageChanged(ILiveTranscriptionLanguage*) override {} +virtual void onChatPrivilegeChanged(IZoomVideoSDKChatHelper*, ZoomVideoSDKChatPrivilegeType) override {} +virtual void onSendFileStatus(IZoomVideoSDKSendFile*, const FileTransferStatus&) override {} +virtual void onReceiveFileStatus(IZoomVideoSDKReceiveFile*, const FileTransferStatus&) override {} +virtual void onShareNetworkStatusChanged(ZoomVideoSDKNetworkStatus, bool) override {} +virtual void onUserNetworkStatusChanged(ZoomVideoSDKDataType, ZoomVideoSDKNetworkStatus, IZoomVideoSDKUser*) override {} +virtual void onUserOverallNetworkStatusChanged(ZoomVideoSDKNetworkStatus, IZoomVideoSDKUser*) override {} +virtual void onAnnotationHelperCleanUp(IZoomVideoSDKAnnotationHelper*) override {} +virtual void onAnnotationPrivilegeChange(IZoomVideoSDKUser*, IZoomVideoSDKShareAction*) override {} +virtual void onAnnotationHelperActived(void*) override {} +virtual void onAnnotationToolTypeChanged(IZoomVideoSDKAnnotationHelper*, void*, ZoomVideoSDKAnnotationToolType) override {} +virtual void onVideoAlphaChannelStatusChanged(bool) override {} +virtual void onSpotlightVideoChanged(IZoomVideoSDKVideoHelper*, IVideoSDKVector*) override {} +// ... and many more for live stream, subsession, broadcast, etc. +``` + +See `zoom_video_sdk_delegate_interface.h` for complete list (~90 methods). + +### IZoomVideoSDKRawDataPipeDelegate Requirements + +**VERSION SENSITIVITY**: Interface methods may be added/removed between SDK versions. + +```cpp +class VideoDelegate : public IZoomVideoSDKRawDataPipeDelegate { + virtual void onRawDataFrameReceived(YUVRawDataI420* data) override { /* handle */ } + virtual void onRawDataStatusChanged(RawDataStatus status) override { /* handle */ } + virtual void onShareCursorDataReceived(ZoomVideoSDKShareCursorData info) override {} // REQUIRED in 2.4.12 +}; +``` + +## Raw Data Capture + +### Video Raw Data (YUV I420) + +```cpp +class VideoEncoder : public IZoomVideoSDKRawDataPipeDelegate { +public: + virtual void onRawDataFrameReceived(YUVRawDataI420* data) override { + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + char* yBuffer = data->GetYBuffer(); + char* uBuffer = data->GetUBuffer(); + char* vBuffer = data->GetVBuffer(); + char* alphaBuffer = data->GetAlphaBuffer(); // Optional + + unsigned long long timestamp = data->GetTimeStamp(); + int rotation = data->GetRotation(); // 0, 90, 180, 270 + bool isLimited = data->IsLimitedI420(); + + // Reference counting for async processing + if (data->CanAddRef()) { + data->AddRef(); + // Process in background thread + data->Release(); + } + } + + virtual void onRawDataStatusChanged(RawDataStatus status) override { + // RawData_On or RawData_Off + } +}; + +// Subscribe to user's video +user->GetVideoPipe()->subscribe(ZoomVideoSDKResolution_720P, new VideoEncoder()); +``` + +### Audio Raw Data (PCM) + +```cpp +// Subscribe to audio in onSessionJoin or onUserAudioStatusChanged +IZoomVideoSDKAudioHelper* audioHelper = video_sdk_obj->getAudioHelper(); +audioHelper->subscribe(); + +// Receive in callbacks: +virtual void onMixedAudioRawDataReceived(AudioRawData* data_) override { + char* buffer = data_->GetBuffer(); // PCM 16-bit + unsigned int len = data_->GetBufferLen(); + unsigned int sampleRate = data_->GetSampleRate(); + unsigned int channels = data_->GetChannelNum(); + unsigned long long timestamp = data_->GetTimeStamp(); +} + +// Convert PCM to MP3: +// ffmpeg -f s16le -ar 32000 -i output.pcm output.mp3 +``` + +## Raw Data Injection + +### Virtual Audio Mic + +```cpp +class MyAudioMic : public IZoomVideoSDKVirtualAudioMic { +public: + virtual void onMicInitialize(IZoomVideoSDKAudioSender* sender) override { + audio_sender_ = sender; + } + + virtual void onMicStartSend() override { + // Start sending audio frames + } + + virtual void onMicStopSend() override {} + virtual void onMicUninitialized() override { audio_sender_ = nullptr; } + + void SendAudio(char* data, unsigned int len, int sampleRate) { + if (audio_sender_) { + audio_sender_->Send(data, len, sampleRate); + } + } + +private: + IZoomVideoSDKAudioSender* audio_sender_ = nullptr; +}; + +// Set before joining +session_context.virtualAudioMic = new MyAudioMic(); +session_context.audioOption.connect = true; +session_context.audioOption.mute = false; +``` + +### Virtual Video Source + +```cpp +class MyVideoSource : public IZoomVideoSDKVideoSource { +public: + virtual void onInitialize(IZoomVideoSDKVideoSender* sender, + IVideoSDKVector* caps, + VideoSourceCapability& suggest) override { + video_sender_ = sender; + } + + virtual void onStartSend() override { + // Start sending video frames + } + + virtual void onStopSend() override {} + virtual void onPropertyChange(...) override {} + + void SendFrame(char* y, char* u, char* v, int w, int h, int rotation) { + if (video_sender_) { + video_sender_->sendVideoFrame(y, u, v, w, h, 0, rotation); + } + } + +private: + IZoomVideoSDKVideoSender* video_sender_ = nullptr; +}; + +// Set before joining +session_context.externalVideoSource = new MyVideoSource(); +``` + +## Helper Interfaces + +### Audio Helper +```cpp +IZoomVideoSDKAudioHelper* audio = video_sdk_obj->getAudioHelper(); +audio->startAudio(); +audio->stopAudio(); +audio->muteAudio(true); +audio->subscribe(); // For raw data callbacks +audio->unSubscribe(); +``` + +### Video Helper +```cpp +IZoomVideoSDKVideoHelper* video = video_sdk_obj->getVideoHelper(); +video->startVideo(); +video->stopVideo(); +video->rotateMyVideo(90); +IVideoSDKVector* cameras = video->getCameraList(); +``` + +### Share Helper +```cpp +IZoomVideoSDKShareHelper* share = video_sdk_obj->getShareHelper(); +share->startShare(); +share->startSharingExternalSource(shareSource); +share->stopShare(); +bool isSharing = share->isSharingOut(); +``` + +### Chat Helper +```cpp +IZoomVideoSDKChatHelper* chat = video_sdk_obj->getChatHelper(); +chat->sendChatToAll("Hello!"); +chat->sendChatToUser(user, "Private message"); +``` + +### Recording Helper +```cpp +IZoomVideoSDKRecordingHelper* rec = video_sdk_obj->getRecordingHelper(); +if (rec->canStartRecording() == ZoomVideoSDKErrors_Success) { + rec->startCloudRecording(); +} +``` + +### Live Stream Helper +```cpp +IZoomVideoSDKLiveStreamHelper* ls = video_sdk_obj->getLiveStreamHelper(); +if (ls->canStartLiveStream() == ZoomVideoSDKErrors_Success) { + ls->startLiveStream("rtmp://...", "stream-key", "broadcast-url"); +} +``` + +### Live Transcription Helper + +**Note**: The session host can start/stop live transcription via the SDK object. Participants receive transcription messages via callbacks. + +```cpp +IZoomVideoSDKLiveTranscriptionHelper* ltt = video_sdk_obj->getLiveTranscriptionHelper(); + +// Check if transcription can be started (host privilege required) +if (ltt->canStartLiveTranscription()) { + ltt->startLiveTranscription(); +} + +// Set spoken language (optional) +IVideoSDKVector* languages = ltt->getAvailableSpokenLanguages(); +if (languages && languages->GetCount() > 0) { + ltt->setSpokenLanguage(languages->GetItem(0)->getLTTLanguageID()); +} + +// Receive transcription in callback: +virtual void onLiveTranscriptionMsgInfoReceived(ILiveTranscriptionMessageInfo* info) override { + const char* speaker = info->getSpeakerName(); + const char* content = info->getMessageContent(); + printf("[%s]: %s\n", speaker, content); +} + +// Monitor transcription status changes: +virtual void onLiveTranscriptionStatus(ZoomVideoSDKLiveTranscriptionStatus status) override { + // ZoomVideoSDKLiveTranscription_Status_Start or _Stop +} +``` + +## CMakeLists.txt Template + +```cmake +cmake_minimum_required(VERSION 3.10) +project(ZoomVideoSDKBot) + +set(CMAKE_CXX_STANDARD 14) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GLIB REQUIRED glib-2.0) + +include_directories( + ${CMAKE_SOURCE_DIR}/src/include + ${GLIB_INCLUDE_DIRS} +) + +link_directories(${CMAKE_SOURCE_DIR}/src/lib/zoom_video_sdk) + +add_executable(${PROJECT_NAME} + src/main.cpp + src/ZoomVideoSDKRawDataPipeDelegate.cpp + src/ZoomVideoSDKVirtualAudioMic.cpp + src/ZoomVideoSDKVirtualAudioSpeaker.cpp +) + +target_link_libraries(${PROJECT_NAME} + videosdk + ${GLIB_LIBRARIES} + pthread + z + lzma +) +``` + +## Build & Run + +```bash +cmake -B build +cd build && make + +# Run from bin directory +cd bin +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../src/lib/zoom_video_sdk +./ZoomVideoSDKBot +``` + +## Headless Linux (Docker/WSL) + +```bash +# Install PulseAudio +sudo apt install -y pulseaudio +pulseaudio --start + +# Create virtual devices +pactl load-module module-null-sink sink_name=virtual_speaker +pactl load-module module-null-source source_name=virtual_mic + +# Configure +mkdir -p ~/.config +cat > ~/.config/zoomus.conf << EOF +[General] +system.audio.type=default +EOF +``` + +Or use `IZoomVideoSDKVirtualAudioSpeaker` and `IZoomVideoSDKVirtualAudioMic` interfaces. + +## Thread Safety + +- SDK callbacks are on SDK's internal threads +- Don't perform heavy operations in callbacks +- Use message queues for async processing +- Don't call `cleanup()` inside callbacks + +```cpp +// Reference counting for async use +if (data->CanAddRef()) { + data->AddRef(); + // Queue for processing + data->Release(); // When done +} +``` + +## Custom UI Frameworks + +For applications requiring a graphical user interface, use these quickstart templates: + +### Qt Framework (Recommended) + +**Repository**: https://github.com/tanchunsiong/videosdk-linux-qt-quickstart + +```bash +# Prerequisites +sudo apt install -y qt6-base-dev qt6-tools-dev libasound2-dev cmake + +# Build +cd videosdk-linux-qt-quickstart/src +mkdir build && cd build +export Qt6_DIR=/usr/lib/x86_64-linux-gnu/cmake/Qt6 +cmake .. && make -j$(nproc) + +# Run +./run_qt_demo.sh +``` + +**Features**: +- Qt6/Qt5 cross-platform GUI +- Video rendering with QPainter + QImage +- YUV-to-RGB conversion (ITU-R BT.601) +- ALSA audio playback +- Device selection (camera, mic, speaker) +- Session management UI + +**Key Components**: +- `QtMainWindow` - Main window with controls +- `QtVideoWidget` - Video display widget +- `QtVideoRenderer` - Video rendering logic +- `QtPreviewVideoHandler` - Self video preview +- `QtRemoteVideoHandler` - Remote video streams + +**Qt Platform Options**: +```bash +# Desktop +./run_qt_demo.sh + +# Headless/Server +QT_QPA_PLATFORM=offscreen ./run_qt_demo.sh + +# Wayland +QT_QPA_PLATFORM=wayland ./run_qt_demo.sh + +# X11 with SSH forwarding +ssh -X user@host +export DISPLAY=:10.0 +./run_qt_demo.sh +``` + +### GTK Framework + +**Repository**: https://github.com/tanchunsiong/videosdk-linux-gtk-quickstart + +```bash +# Prerequisites +sudo apt install -y libgtkmm-3.0-dev libsdl2-dev libasound2-dev libcurl4-openssl-dev cmake + +# Build +cd videosdk-linux-gtk-quickstart/src +mkdir build && cd build +cmake .. && make + +# Run +cd ../bin && ./SkeletonDemo +``` + +**Features**: +- GTKmm 3.0 native Linux GUI +- SDL2 + Cairo video rendering +- ALSA audio integration +- Device hot-swapping +- Separate self/remote video panels +- Chat system + +**Key Components**: +- `VideoRenderer` - SDL2-based rendering engine +- `VideoDisplayBridge` - SDK to renderer connection +- `PreviewVideoHandler` - Camera preview +- `RemoteVideoRawDataHandler` - Remote participant video + +**Architecture**: +``` +┌─────────────────────────────────────────────────────────┐ +│ GTK User Interface │ +├─────────────────────────────────────────────────────────┤ +│ Session Controls │ Device Selection │ Video Controls │ +├─────────────────────────────────────────────────────────┤ +│ Self Video │ Status Area │ Remote Video │ +│ (Left Panel) │ │ (Right Panel) │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Video Processing Layer │ +│ VideoRenderer ←→ VideoDisplayBridge ←→ Device Manager │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Zoom Video SDK │ +└─────────────────────────────────────────────────────────┘ +``` + +### Comparison + +| Feature | Qt | GTK | +|---------|----|----| +| Cross-platform | Yes (Windows, Mac, Linux) | Linux-focused | +| Video Rendering | QPainter + QImage | SDL2 + Cairo | +| Threading | Qt signals/slots | Glib main loop | +| Modern C++ | Better support | Good support | +| Performance | Slightly better | Good | +| Learning Curve | Moderate | Moderate | + +**Choose Qt** for: Cross-platform apps, modern C++ features, better tooling +**Choose GTK** for: Native Linux look, lightweight, GNOME integration + +## Runtime Setup + +### Required Symlinks + +```bash +cd lib/zoom_video_sdk + +# SDK library versioned symlink +ln -sf libvideosdk.so libvideosdk.so.1 + +# Qt5 library symlinks (if using bundled Qt) +ln -sf libQt5Core.so.5 libQt5Core.so +ln -sf libQt5Gui.so.5 libQt5Gui.so +ln -sf libQt5Network.so.5 libQt5Network.so +ln -sf libQt5Qml.so.5 libQt5Qml.so +ln -sf libQt5Quick.so.5 libQt5Quick.so +``` + +### Output Directories + +Create directories for raw data capture (relative to binary location): +```bash +mkdir -p bin/output/audio bin/output/video +``` + +### Raw File Playback + +```bash +# Audio (PCM 16-bit, 32kHz, Mono) +ffplay -f s16le -ar 32000 -ac 1 output/audio/mixed_audio.pcm + +# Video (YUV420P) - adjust resolution as needed +ffplay -f rawvideo -pixel_format yuv420p -video_size 640x360 output/video/video.yuv + +# Convert audio to MP3 +ffmpeg -f s16le -ar 32000 -ac 1 -i audio.pcm output.mp3 + +# Convert video to MP4 +ffmpeg -f rawvideo -pixel_format yuv420p -video_size 640x360 -framerate 30 -i video.yuv -c:v libx264 output.mp4 +``` + +## Error Codes + +| Code | Name | Description | +|------|------|-------------| +| 0 | Success | Operation succeeded | +| 1001 | Auth_Error | Authentication failed | +| 1002 | Auth_Empty_Token | No token provided | +| 1003 | Auth_Wrong_Token | Invalid token | +| 1004 | Auth_Expired_Token | Token expired | +| 3001 | Session_Join_Failed | Failed to join | +| 3008 | Session_Need_Password | Password required | +| 3009 | Session_Password_Wrong | Wrong password | + +## Reference Files + + + +See `linux-reference.md` for complete API documentation. diff --git a/plugins/zoom-developers/skills/video-sdk/linux/references/linux-reference.md b/plugins/zoom-developers/skills/video-sdk/linux/references/linux-reference.md new file mode 100644 index 00000000..3d901c03 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/references/linux-reference.md @@ -0,0 +1,800 @@ +# Zoom Video SDK Linux - API Reference + +**Source**: https://marketplacefront.zoom.us/sdk/custom/linux/ + +## Complete Class List + +### Core SDK + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDK` | Main singleton object - session creation, callbacks, features | +| `IZoomVideoSDKSession` | Session information interface | +| `IZoomVideoSDKDelegate` | Event callbacks for session events | +| `IZoomVideoSDKUser` | User object interface | +| `IZoomVideoSDKUserHelper` | User management helper | + +### Raw Data Interfaces + +| Class | Description | +|-------|-------------| +| `AudioRawData` | Audio raw data handler (PCM) | +| `YUVRawDataI420` | YUV raw data handler (I420 format) | +| `YUVProcessDataI420` | YUV processing data | +| `IZoomVideoSDKRawDataPipe` | Video/share raw data pipe | +| `IZoomVideoSDKRawDataPipeDelegate` | Video/share raw data sink | +| `IYUVRawDataI420Converter` | I420 YUV converter | + +### Virtual Devices + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKVirtualAudioMic` | Virtual audio microphone for injection | +| `IZoomVideoSDKVirtualAudioSpeaker` | Virtual audio speaker for headless | +| `IZoomVideoSDKVideoSource` | Video source for injection | +| `IZoomVideoSDKVideoSourcePreProcessor` | Video preprocessing | +| `IZoomVideoSDKShareSource` | Share source for injection | +| `IZoomVideoSDKSharePreprocessor` | Share preprocessing | + +### Senders + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKAudioSender` | Audio raw data sender | +| `IZoomVideoSDKVideoSender` | Video raw data sender | +| `IZoomVideoSDKShareSender` | Share raw data sender | +| `IZoomVideoSDKShareAudioSender` | Share audio sender | +| `IZoomVideoSDKShareAudioSource` | Share audio source | + +### Helpers + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKAudioHelper` | Audio controls | +| `IZoomVideoSDKVideoHelper` | Video/camera controls | +| `IZoomVideoSDKShareHelper` | Screen sharing | +| `IZoomVideoSDKChatHelper` | Chat messaging | +| `IZoomVideoSDKRecordingHelper` | Cloud recording | +| `IZoomVideoSDKLiveStreamHelper` | RTMP live streaming | +| `IZoomVideoSDKLiveTranscriptionHelper` | Live transcription | +| `IZoomVideoSDKPhoneHelper` | Phone dial-out | +| `IZoomVideoSDKCmdChannel` | Command channel | +| `IZoomVideoSDKCRCHelper` | CRC helper | +| `IZoomVideoSDKWhiteboardHelper` | Whiteboard | +| `IZoomVideoSDKAnnotationHelper` | Annotations | +| `IZoomVideoSDKNetworkConnectionHelper` | Network connection | +| `IZoomVideoSDKSubSessionHelper` | Subsession helper | +| `IZoomVideoSDKSubSessionManager` | Subsession manager | + +### Settings Helpers + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKAudioSettingHelper` | Audio settings | +| `IZoomVideoSDKVideoSettingHelper` | Video settings | +| `IZoomVideoSDKShareSettingHelper` | Share settings | +| `IZoomVideoSDKTestAudioDeviceHelper` | Audio device testing | + +### Devices + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKCameraDevice` | Camera device | +| `IZoomVideoSDKMicDevice` | Microphone device | +| `IZoomVideoSDKSpeakerDevice` | Speaker device | +| `IVirtualBackgroundItem` | Virtual background item | + +### Streaming + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKIncomingLiveStreamHelper` | Incoming live stream | +| `IZoomVideoSDKBroadcastStreamingController` | Broadcast controller | +| `IZoomVideoSDKBroadcastStreamingViewer` | Broadcast viewer | +| `IZoomVideoSDKBroadcastStreamingAudioCallback` | Broadcast audio callback | +| `IZoomVideoSDKBroadcastStreamingVideoCallback` | Broadcast video callback | + +### Session & Messages + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKChatMessage` | Chat message | +| `ILiveTranscriptionLanguage` | Transcription language | +| `ILiveTranscriptionMessageInfo` | Transcription message | +| `IZoomVideoSDKSessionDialInNumberInfo` | Dial-in info | +| `IZoomVideoSDKPhoneSupportCountryInfo` | Phone country info | + +### Subsessions + +| Class | Description | +|-------|-------------| +| `ISubSessionKit` | Subsession kit | +| `ISubSessionUser` | Subsession user | +| `ISubSessionUserHelpRequestHandler` | Help request handler | +| `IZoomVideoSDKSubSessionParticipant` | Subsession participant | + +### File Transfer + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKFileTransferBaseInfo` | File transfer base info | +| `IZoomVideoSDKSendFile` | Send file interface | +| `IZoomVideoSDKReceiveFile` | Receive file interface | + +### Handlers + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKPasswordHandler` | Password handler | +| `IZoomVideoSDKRecordingConsentHandler` | Recording consent | +| `IZoomVideoSDKCameraControlRequestHandler` | Camera control requests | +| `IZoomVideoSDKRemoteCameraControlHelper` | Remote camera control | +| `IZoomVideoSDKProxySettingHandler` | Proxy settings | +| `IZoomVideoSDKSSLCertificateInfo` | SSL certificate info | + +### Canvas & Actions + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKCanvas` | Video/share canvas | +| `IZoomVideoSDKShareAction` | Share action | +| `IMonitorListBuilder` | Monitor list builder | + +### Utilities + +| Class | Description | +|-------|-------------| +| `IVideoSDKVector` | SDK vector collection | + +--- + +## Structures + +### Initialization + +```cpp +struct ZoomVideoSDKInitParams { + const zchar_t* domain; // Required: "https://zoom.us" + bool enableLog; // Enable logging + const zchar_t* logFilePrefix; // Log file prefix + ZoomVideoSDKRawDataMemoryMode videoRawDataMemoryMode; + ZoomVideoSDKRawDataMemoryMode shareRawDataMemoryMode; + ZoomVideoSDKRawDataMemoryMode audioRawDataMemoryMode; + bool enableIndirectRawdata; // Indirect raw data access + ZoomVideoSDKExtendParams* extendParams; // Extended parameters +}; + +struct ZoomVideoSDKExtendParams { + const zchar_t* speakerTestFilePath; + // Additional extended parameters +}; +``` + +### Session Context + +```cpp +struct ZoomVideoSDKSessionContext { + const zchar_t* sessionName; // Required + const zchar_t* sessionPassword; // Optional + const zchar_t* userName; // Required + const zchar_t* token; // Required: JWT token + unsigned int sessionIdleTimeoutMins; // 0 = never timeout, default 40 + bool autoLoadMutliStream; // Auto-load multiple streams + + ZoomVideoSDKVideoOption videoOption; + ZoomVideoSDKAudioOption audioOption; + + IZoomVideoSDKVideoSource* externalVideoSource; + IZoomVideoSDKVirtualAudioMic* virtualAudioMic; + IZoomVideoSDKVirtualAudioSpeaker* virtualAudioSpeaker; + IZoomVideoSDKVideoSourcePreProcessor* preProcessor; +}; + +struct ZoomVideoSDKVideoOption { + bool localVideoOn; // Start with video on +}; + +struct ZoomVideoSDKAudioOption { + bool connect; // Connect to audio + bool mute; // Start muted +}; +``` + +### Statistics + +```cpp +struct ZoomVideoSDKSessionAudioStatisticInfo { + int frequency; + int latency; + int Jitter; + float packetLossAvg; + float packetLossMax; +}; + +struct ZoomVideoSDKSessionASVStatisticInfo { + int frame_width; + int frame_height; + int fps; + int latency; + int Jitter; + float packetLossAvg; + float packetLossMax; +}; + +// Alias +typedef ZoomVideoSDKSessionASVStatisticInfo _SessionASVStatisticInfo; +typedef ZoomVideoSDKSessionAudioStatisticInfo _SessionAudioStatisticInfo; + +struct ZoomVideoSDKVideoStatisticInfo { + int width; + int height; + int fps; + int bps; +}; + +struct ZoomVideoSDKShareStatisticInfo { + int width; + int height; + int fps; + int bps; +}; +``` + +### Video/Audio Status + +```cpp +struct ZoomVideoSDKVideoStatus { + bool isOn; + bool hasSource; +}; + +struct ZoomVideoSDKAudioStatus { + bool isMuted; + bool isAudioConnected; + ZoomVideoSDKAudioType audioType; +}; + +struct VideoSourceCapability { + unsigned int width; + unsigned int height; + unsigned int frame; // FPS +}; +``` + +### Live Streaming + +```cpp +struct ZoomVideoSDKLiveStreamParams { + const zchar_t* streamUrl; // RTMP URL + const zchar_t* streamKey; // Stream key + const zchar_t* broadcastUrl; // Broadcast URL +}; + +struct ZoomVideoSDKLiveStreamSetting { + // Live stream settings +}; + +struct IncomingLiveStreamStatus { + // Incoming stream status +}; +``` + +### Share + +```cpp +struct ZoomVideoSDKShareOption { + bool isWithDeviceAudio; + bool isOptimizeForSharedVideo; +}; + +struct ZoomVideoSDKShareCursorData { + int x; + int y; + // Cursor information +}; + +struct ZoomVideoSDKSharePreprocessParam { + // Preprocessing parameters +}; +``` + +### File Transfer + +```cpp +struct FileTransferProgress { + unsigned long long transferredSize; + unsigned long long totalSize; + float percentage; +}; + +struct ZoomVideoSDKFileStatus { + // File transfer status +}; +``` + +### Misc + +```cpp +struct ZoomVideoSDKViewSize { + int width; + int height; +}; + +struct tagVideoPreferenceSetting { + // Video preference settings +}; + +struct tagProxySettings { + // Proxy configuration +}; + +struct InvitePhoneUserInfo { + const zchar_t* countryCode; + const zchar_t* phoneNumber; + const zchar_t* displayName; +}; + +struct ZoomVideoSDKSteamingJoinContext { + // Streaming join context +}; +``` + +--- + +## IZoomVideoSDK (Main Interface) + +```cpp +class IZoomVideoSDK { +public: + // Lifecycle + virtual ZoomVideoSDKErrors initialize(ZoomVideoSDKInitParams& params) = 0; + virtual ZoomVideoSDKErrors cleanup() = 0; + + // Session management + virtual IZoomVideoSDKSession* joinSession(ZoomVideoSDKSessionContext& params) = 0; + virtual ZoomVideoSDKErrors leaveSession(bool end) = 0; + virtual IZoomVideoSDKSession* getSessionInfo() = 0; + virtual bool isInSession() = 0; + + // Listeners + virtual void addListener(IZoomVideoSDKDelegate* listener) = 0; + virtual void removeListener(IZoomVideoSDKDelegate* listener) = 0; + + // Helpers + virtual IZoomVideoSDKAudioHelper* getAudioHelper() = 0; + virtual IZoomVideoSDKVideoHelper* getVideoHelper() = 0; + virtual IZoomVideoSDKUserHelper* getUserHelper() = 0; + virtual IZoomVideoSDKShareHelper* getShareHelper() = 0; + virtual IZoomVideoSDKRecordingHelper* getRecordingHelper() = 0; + virtual IZoomVideoSDKLiveStreamHelper* getLiveStreamHelper() = 0; + virtual IZoomVideoSDKChatHelper* getChatHelper() = 0; + virtual IZoomVideoSDKCmdChannel* getCmdChannel() = 0; + virtual IZoomVideoSDKPhoneHelper* getPhoneHelper() = 0; + virtual IZoomVideoSDKLiveTranscriptionHelper* getLiveTranscriptionHelper() = 0; + virtual IZoomVideoSDKCRCHelper* getCRCHelper() = 0; + virtual IZoomVideoSDKWhiteboardHelper* getWhiteboardHelper() = 0; + virtual IZoomVideoSDKSubSessionHelper* getSubSessionHelper() = 0; + virtual IZoomVideoSDKIncomingLiveStreamHelper* getIncomingLiveStreamHelper() = 0; + + // Settings + virtual IZoomVideoSDKAudioSettingHelper* getAudioSettingHelper() = 0; + virtual IZoomVideoSDKVideoSettingHelper* getVideoSettingHelper() = 0; + virtual IZoomVideoSDKShareSettingHelper* getShareSettingHelper() = 0; + virtual IZoomVideoSDKTestAudioDeviceHelper* GetAudioDeviceTestHelper() = 0; + virtual IZoomVideoSDKNetworkConnectionHelper* getNetworkConnectionHelper() = 0; + + // Utilities + virtual const zchar_t* getSDKVersion() = 0; + virtual const zchar_t* exportLog() = 0; + virtual ZoomVideoSDKErrors cleanAllExportedLogs() = 0; +}; +``` + +--- + +## IZoomVideoSDKDelegate (Callbacks) + +```cpp +class IZoomVideoSDKDelegate { +public: + // Session + virtual void onSessionJoin() = 0; + virtual void onSessionLeave() = 0; + virtual void onError(ZoomVideoSDKErrors errorCode, int detailErrorCode) = 0; + virtual void onSessionNeedPassword(IZoomVideoSDKPasswordHandler* handler) = 0; + virtual void onSessionPasswordWrong(IZoomVideoSDKPasswordHandler* handler) = 0; + + // Users + virtual void onUserJoin(IZoomVideoSDKUserHelper* pUserHelper, + IVideoSDKVector* userList) = 0; + virtual void onUserLeave(IZoomVideoSDKUserHelper* pUserHelper, + IVideoSDKVector* userList) = 0; + virtual void onUserHostChanged(IZoomVideoSDKUserHelper* pUserHelper, + IZoomVideoSDKUser* pUser) = 0; + virtual void onUserManagerChanged(IZoomVideoSDKUser* pUser) = 0; + virtual void onUserNameChanged(IZoomVideoSDKUser* pUser) = 0; + + // Video + virtual void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* pVideoHelper, + IVideoSDKVector* userList) = 0; + + // Audio + virtual void onUserAudioStatusChanged(IZoomVideoSDKAudioHelper* pAudioHelper, + IVideoSDKVector* userList) = 0; + virtual void onUserActiveAudioChanged(IZoomVideoSDKAudioHelper* pAudioHelper, + IVideoSDKVector* list) = 0; + + // Raw Audio + virtual void onMixedAudioRawDataReceived(AudioRawData* data_) = 0; + virtual void onOneWayAudioRawDataReceived(AudioRawData* data_, + IZoomVideoSDKUser* pUser) = 0; + virtual void onSharedAudioRawDataReceived(AudioRawData* data_) = 0; + + // Virtual Speaker + virtual void onVirtualSpeakerMixedAudioReceived(AudioRawData* data_) = 0; + virtual void onVirtualSpeakerOneWayAudioReceived(AudioRawData* data_, + IZoomVideoSDKUser* pUser) = 0; + virtual void onVirtualSpeakerSharedAudioReceived(AudioRawData* data_) = 0; + + // Share + virtual void onUserShareStatusChanged(IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) = 0; + + // Chat + virtual void onChatNewMessageNotify(IZoomVideoSDKChatHelper* pChatHelper, + IZoomVideoSDKChatMessage* messageItem) = 0; + virtual void onChatMsgDeleteNotification(IZoomVideoSDKChatHelper* pChatHelper, + const zchar_t* msgID, + ZoomVideoSDKChatMessageDeleteType deleteBy) = 0; + virtual void onChatPrivilegeChanged(IZoomVideoSDKChatHelper* pChatHelper, + ZoomVideoSDKChatPrivilegeType privilege) = 0; + + // Commands + virtual void onCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* strCmd) = 0; + virtual void onCommandChannelConnectResult(bool isSuccess) = 0; + + // Recording + virtual void onCloudRecordingStatus(RecordingStatus status, + IZoomVideoSDKRecordingConsentHandler* pHandler) = 0; + virtual void onUserRecordingConsent(IZoomVideoSDKUser* pUser) = 0; + virtual void onHostAskUnmute() = 0; + + // Live Stream + virtual void onLiveStreamStatusChanged(IZoomVideoSDKLiveStreamHelper* pLiveStreamHelper, + ZoomVideoSDKLiveStreamStatus status) = 0; + + // Live Transcription + virtual void onLiveTranscriptionStatus(ZoomVideoSDKLiveTranscriptionStatus status) = 0; + virtual void onLiveTranscriptionMsgReceived(const zchar_t* ltMsg, + IZoomVideoSDKUser* pUser, + ZoomVideoSDKLiveTranscriptionOperationType type) = 0; + virtual void onLiveTranscriptionMsgInfoReceived(ILiveTranscriptionMessageInfo* messageInfo) = 0; + virtual void onLiveTranscriptionMsgError(ILiveTranscriptionLanguage* spokenLanguage, + ILiveTranscriptionLanguage* transcriptLanguage) = 0; + virtual void onOriginalLanguageMsgReceived(ILiveTranscriptionMessageInfo* messageInfo) = 0; + + // Phone + virtual void onInviteByPhoneStatus(PhoneStatus status, PhoneFailedReason reason) = 0; + virtual void onCalloutJoinSuccess(IZoomVideoSDKUser* pUser, const zchar_t* phoneNumber) = 0; + + // Camera Control + virtual void onCameraControlRequestResult(IZoomVideoSDKUser* pUser, bool isApproved) = 0; + virtual void onCameraControlRequestReceived(IZoomVideoSDKUser* pUser, + ZoomVideoSDKCameraControlRequestType requestType, + IZoomVideoSDKCameraControlRequestHandler* handler) = 0; + + // Multi-Camera + virtual void onMultiCameraStreamStatusChanged(ZoomVideoSDKMultiCameraStreamStatus status, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKRawDataPipe* pVideoPipe) = 0; + + // Devices + virtual void onMicSpeakerVolumeChanged(unsigned int micVolume, + unsigned int speakerVolume) = 0; + virtual void onAudioDeviceStatusChanged(ZoomVideoSDKAudioDeviceType type, + ZoomVideoSDKAudioDeviceStatus status) = 0; + virtual void onTestMicStatusChanged(ZoomVideoSDK_TESTMIC_STATUS status) = 0; + virtual void onSelectedAudioDeviceChanged() = 0; + virtual void onCameraListChanged() = 0; + + // Network + virtual void onUserVideoNetworkStatusChanged(ZoomVideoSDKNetworkStatus status, + IZoomVideoSDKUser* pUser) = 0; + virtual void onProxyDetectComplete() = 0; + virtual void onProxySettingNotification(IZoomVideoSDKProxySettingHandler* handler) = 0; + virtual void onSSLCertVerifiedFailNotification(IZoomVideoSDKSSLCertificateInfo* info) = 0; + + // CRC + virtual void onCallCRCDeviceStatusChanged(ZoomVideoSDKCRCCallStatus status) = 0; + + // Canvas + virtual void onVideoCanvasSubscribeFail(ZoomVideoSDKSubscribeFailReason fail_reason, + IZoomVideoSDKUser* pUser, void* handle) = 0; + virtual void onShareCanvasSubscribeFail(ZoomVideoSDKSubscribeFailReason fail_reason, + IZoomVideoSDKUser* pUser, void* handle) = 0; + + // Annotations + virtual void onAnnotationHelperCleanUp(IZoomVideoSDKAnnotationHelper* helper) = 0; + virtual void onAnnotationPrivilegeChange(IZoomVideoSDKUser* pUser, bool enable) = 0; + virtual void onAnnotationHelperActived(void* handle) = 0; + + // Video Alpha Channel + virtual void onVideoAlphaChannelStatusChanged(bool isAlphaModeOn) = 0; + + // File Transfer + virtual void onSendFileStatus(IZoomVideoSDKSendFile* file, + const FileTransferStatus& status) = 0; + virtual void onReceiveFileStatus(IZoomVideoSDKReceiveFile* file, + const FileTransferStatus& status) = 0; +}; +``` + +--- + +## Raw Data Interfaces + +### AudioRawData + +```cpp +class AudioRawData { +public: + virtual char* GetBuffer() = 0; // PCM 16-bit buffer + virtual unsigned int GetBufferLen() = 0; // Buffer length in bytes + virtual unsigned int GetSampleRate() = 0;// Sample rate (Hz) + virtual unsigned int GetChannelNum() = 0;// Channels (1=mono, 2=stereo) + virtual unsigned long long GetTimeStamp() = 0; +}; +``` + +### YUVRawDataI420 + +```cpp +class YUVRawDataI420 { +public: + virtual char* GetYBuffer() = 0; + virtual char* GetUBuffer() = 0; + virtual char* GetVBuffer() = 0; + virtual char* GetAlphaBuffer() = 0; // Optional alpha channel + virtual char* GetBuffer() = 0; // Full YUV buffer + virtual unsigned int GetBufferLen() = 0; + virtual unsigned int GetStreamWidth() = 0; + virtual unsigned int GetStreamHeight() = 0; + virtual unsigned int GetRotation() = 0; // 0, 90, 180, 270 + virtual unsigned long long GetSourceID() = 0; + virtual unsigned long long GetTimeStamp() = 0; + virtual bool IsLimitedI420() = 0; + + // Reference counting + virtual bool CanAddRef() = 0; + virtual bool AddRef() = 0; + virtual int Release() = 0; +}; +``` + +### IZoomVideoSDKRawDataPipeDelegate + +```cpp +class IZoomVideoSDKRawDataPipeDelegate { +public: + virtual void onRawDataFrameReceived(YUVRawDataI420* data) = 0; + virtual void onRawDataStatusChanged(RawDataStatus status) = 0; +}; + +enum RawDataStatus { + RawData_On, + RawData_Off +}; +``` + +--- + +## Virtual Device Interfaces + +### IZoomVideoSDKVirtualAudioMic + +```cpp +class IZoomVideoSDKVirtualAudioMic { +public: + virtual void onMicInitialize(IZoomVideoSDKAudioSender* sender) = 0; + virtual void onMicStartSend() = 0; + virtual void onMicStopSend() = 0; + virtual void onMicUninitialized() = 0; +}; + +class IZoomVideoSDKAudioSender { +public: + virtual ZoomVideoSDKErrors Send(char* data, + unsigned int dataLength, + int sampleRate) = 0; +}; +``` + +### IZoomVideoSDKVirtualAudioSpeaker + +```cpp +class IZoomVideoSDKVirtualAudioSpeaker { +public: + virtual void onVirtualSpeakerMixedAudioReceived(AudioRawData* data_) = 0; + virtual void onVirtualSpeakerOneWayAudioReceived(AudioRawData* data_, + IZoomVideoSDKUser* pUser) = 0; + virtual void onVirtualSpeakerSharedAudioReceived(AudioRawData* data_) = 0; +}; +``` + +### IZoomVideoSDKVideoSource + +```cpp +class IZoomVideoSDKVideoSource { +public: + virtual void onInitialize(IZoomVideoSDKVideoSender* sender, + IVideoSDKVector* supportCapList, + VideoSourceCapability& suggestCap) = 0; + virtual void onPropertyChange(IVideoSDKVector* supportCapList, + VideoSourceCapability suggestCap) = 0; + virtual void onStartSend() = 0; + virtual void onStopSend() = 0; + virtual void onUninitialized() = 0; +}; + +class IZoomVideoSDKVideoSender { +public: + virtual ZoomVideoSDKErrors sendVideoFrame(char* frameBuffer, + int width, int height, + int frameLength, + int rotation) = 0; + // Alternative with Y, U, V planes + virtual ZoomVideoSDKErrors sendVideoFrame(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height, + int frameLength, + int rotation) = 0; +}; +``` + +### IZoomVideoSDKShareSource + +```cpp +class IZoomVideoSDKShareSource { +public: + virtual void onShareSendStarted(IZoomVideoSDKShareSender* pSender) = 0; + virtual void onShareSendStopped() = 0; +}; + +class IZoomVideoSDKShareSender { +public: + virtual ZoomVideoSDKErrors sendShareFrame(char* frameBuffer, + int width, int height, + int frameLength) = 0; + virtual ZoomVideoSDKErrors sendShareFrame(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height, + int frameLength, + int rotation) = 0; +}; +``` + +--- + +## Enumerations + +### ZoomVideoSDKErrors + +```cpp +enum ZoomVideoSDKErrors { + ZoomVideoSDKErrors_Success = 0, + ZoomVideoSDKErrors_Wrong_Usage = 1, + ZoomVideoSDKErrors_Internal_Error = 2, + ZoomVideoSDKErrors_Uninitialize = 3, + ZoomVideoSDKErrors_Memory_Error = 4, + ZoomVideoSDKErrors_Load_Module_Error = 5, + ZoomVideoSDKErrors_UnLoad_Module_Error = 6, + ZoomVideoSDKErrors_Invalid_Parameter = 7, + ZoomVideoSDKErrors_Call_Too_Frequently = 8, + ZoomVideoSDKErrors_No_Impl = 9, + ZoomVideoSDKErrors_Dont_Support_Feature = 10, + ZoomVideoSDKErrors_Unknown = 100, + + // Auth errors (1000+) + ZoomVideoSDKErrors_Auth_Error = 1001, + ZoomVideoSDKErrors_Auth_Empty_Key_or_Secret = 1002, + ZoomVideoSDKErrors_Auth_Wrong_Key_or_Secret = 1003, + ZoomVideoSDKErrors_Auth_DoesNot_Support_SDK = 1004, + ZoomVideoSDKErrors_Auth_Disable_SDK = 1005, + + // Session errors (3000+) + ZoomVideoSDKErrors_Session_Join_Failed = 3001, + ZoomVideoSDKErrors_Session_No_Rights = 3002, + ZoomVideoSDKErrors_Session_Already_In_Progress = 3003, + ZoomVideoSDKErrors_Session_Dont_Support_SessionType = 3004, + ZoomVideoSDKErrors_Session_Reconnecting = 3005, + ZoomVideoSDKErrors_Session_Disconnecting = 3006, + ZoomVideoSDKErrors_Session_Not_Started = 3007, + ZoomVideoSDKErrors_Session_Need_Password = 3008, + ZoomVideoSDKErrors_Session_Password_Wrong = 3009, + ZoomVideoSDKErrors_Session_Remote_DB_Error = 3010, + ZoomVideoSDKErrors_Session_Invalid_Param = 3011, + + // Audio/Video errors + ZoomVideoSDKErrors_Session_Audio_Error = 4001, + ZoomVideoSDKErrors_Session_Audio_No_Microphone = 4002, + ZoomVideoSDKErrors_Session_Video_Error = 5001, + ZoomVideoSDKErrors_Session_Video_Device_Error = 5002, + + // Share errors + ZoomVideoSDKErrors_Session_Share_Error = 6001, + ZoomVideoSDKErrors_Session_Share_Module_Not_Ready = 6002, + ZoomVideoSDKErrors_Session_Share_You_Are_Not_Sharing = 6003, + ZoomVideoSDKErrors_Session_Share_Type_Is_Not_Support = 6004, + ZoomVideoSDKErrors_Session_Share_Internal_Error = 6005, + + ZoomVideoSDKErrors_Dont_Support_Multi_Stream_Video_User = 7001, +}; +``` + +### Resolution Options + +```cpp +enum ZoomVideoSDKResolution { + ZoomVideoSDKResolution_90P, + ZoomVideoSDKResolution_180P, + ZoomVideoSDKResolution_360P, + ZoomVideoSDKResolution_720P, + ZoomVideoSDKResolution_1080P +}; +``` + +### Recording Status + +```cpp +enum RecordingStatus { + Recording_Start, + Recording_Stop, + Recording_Pause, + Recording_Connecting, + Recording_DiskFull +}; +``` + +### Memory Mode + +```cpp +enum ZoomVideoSDKRawDataMemoryMode { + ZoomVideoSDKRawDataMemoryModeStack, + ZoomVideoSDKRawDataMemoryModeHeap +}; +``` + +--- + +## Essential Headers + +```cpp +#include "zoom_video_sdk_api.h" // CreateZoomVideoSDKObj, DestroyZoomVideoSDKObj +#include "zoom_video_sdk_interface.h" // IZoomVideoSDK +#include "zoom_video_sdk_delegate_interface.h" // IZoomVideoSDKDelegate +#include "zoom_video_sdk_def.h" // Structures, enums +#include "zoom_video_sdk_platform.h" // Platform definitions +#include "zoom_sdk_raw_data_def.h" // Raw data types + +// Helpers +#include "helpers/zoom_video_sdk_user_helper_interface.h" +#include "helpers/zoom_video_sdk_audio_helper_interface.h" +#include "helpers/zoom_video_sdk_video_helper_interface.h" +#include "helpers/zoom_video_sdk_share_helper_interface.h" +#include "helpers/zoom_video_sdk_chat_helper_interface.h" +#include "helpers/zoom_video_sdk_recording_helper_interface.h" +#include "helpers/zoom_video_sdk_livestream_helper_interface.h" +#include "helpers/zoom_video_sdk_livetranscription_helper_interface.h" +#include "helpers/zoom_video_sdk_cmd_channel_interface.h" +#include "helpers/zoom_video_sdk_phone_helper_interface.h" +#include "helpers/zoom_video_sdk_audio_send_rawdata_interface.h" +#include "helpers/zoom_video_sdk_video_source_helper_interface.h" +``` + +--- + +## Additional Resources + +- **Official Docs**: https://developers.zoom.us/docs/video-sdk/linux/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/custom/linux/ +- **Sample Code**: https://github.com/zoom/videosdk-linux-raw-recording-sample +- **Dev Forum**: https://devforum.zoom.us diff --git a/plugins/zoom-developers/skills/video-sdk/linux/troubleshooting/build-errors.md b/plugins/zoom-developers/skills/video-sdk/linux/troubleshooting/build-errors.md new file mode 100644 index 00000000..616c8739 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/troubleshooting/build-errors.md @@ -0,0 +1,96 @@ +# Common Build Errors + +## CMake Errors + +### Error: "Could not find glib-2.0" + +```bash +sudo apt install -y libglib2.0-dev +``` + +### Error: "CMake version too old" + +```bash +# 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 cmake +``` + +## Linker Errors + +### Error: "undefined reference to `CreateZoomVideoSDKObj'" + +**Cause**: Not linking libvideosdk.so + +**Solution**: +```cmake +link_directories(${CMAKE_SOURCE_DIR}/lib/zoom_video_sdk) +target_link_libraries(${PROJECT_NAME} videosdk) +``` + +### Error: Missing Qt symbols + +**Solution**: Link Qt5 libraries: +```cmake +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**: +```cmake +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: +```cmake +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**: +```bash +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/lib/zoom_video_sdk +``` + +Or set RPATH: +```cmake +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: +```cmake +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). diff --git a/plugins/zoom-developers/skills/video-sdk/linux/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/video-sdk/linux/troubleshooting/common-issues.md new file mode 100644 index 00000000..ffdd644a --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/troubleshooting/common-issues.md @@ -0,0 +1,192 @@ +# 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**: +```python +# 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**: +```cpp +session_context.sessionPassword = "password"; +``` + +## Audio Issues + +### Issue: No audio callbacks + +**Cause**: PulseAudio not configured. + +**Solution**: See [PulseAudio Setup](pulseaudio-setup.md). + +### Issue: Audio on headless Linux + +**Solution**: Use virtual audio: +```cpp +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](../concepts/raw-data-vs-canvas.md). + +### Issue: Video frames not received + +**Cause**: Not subscribing to user's video pipe. + +**Solution**: +```cpp +void onUserVideoStatusChanged(..., IVideoSDKVector* 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](qt-dependencies.md). + +### Issue: Undefined reference to SDK symbols + +**Solution**: See [Build Errors](build-errors.md). + +## Memory Issues + +### Issue: Crashes with large video frames + +**Solution**: Use heap memory mode: +```cpp +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](pulseaudio-setup.md). +3. **Missing `~/.config/zoomus.conf`** - Must exist with content: + ``` + [General] + system.audio.type=default + ``` + +## GLib 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: + +```cpp +#include + +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](../examples/session-join-pattern.md) 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: + +```cpp +struct CallContext { + IZoomVideoSDK* sdk; + std::string data; +}; + +static gboolean executeOnMainThread(gpointer data) { + auto* ctx = static_cast(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](../examples/command-channel.md) 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 `tpc` claim +- [ ] 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](https://developers.zoom.us/docs/video-sdk/linux/) +2. Search [Dev Forum](https://devforum.zoom.us/) +3. Review [GitHub Samples](https://github.com/zoom/videosdk-linux-raw-recording-sample) diff --git a/plugins/zoom-developers/skills/video-sdk/linux/troubleshooting/pulseaudio-setup.md b/plugins/zoom-developers/skills/video-sdk/linux/troubleshooting/pulseaudio-setup.md new file mode 100644 index 00000000..87f9df6a --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/troubleshooting/pulseaudio-setup.md @@ -0,0 +1,97 @@ +# 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 + +```bash +sudo apt update +sudo apt install -y pulseaudio +``` + +## Configuration + +### 1. Create Configuration File + +```bash +mkdir -p ~/.config +cat > ~/.config/zoomus.conf << 'CONFIG' +[General] +system.audio.type=default +CONFIG +``` + +### 2. Start PulseAudio (if not running) + +```bash +pulseaudio --check || pulseaudio --start +``` + +## Docker/Headless Setup + +For Docker or headless environments: + +```bash +# 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.conf +``` + +**Better Approach**: Use virtual audio speaker/mic in SDK: + +```cpp +session_context.virtualAudioSpeaker = new VirtualSpeaker(); +session_context.virtualAudioMic = new VirtualMic(); +``` + +## Verification + +```bash +# 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.conf +``` + +## Common Issues + +### Issue: PulseAudio not starting + +```bash +# Kill existing instance +pulseaudio --kill + +# Start fresh +pulseaudio --start + +# Check status +pulseaudio --check +``` + +### Issue: Permission denied + +```bash +# Add user to audio group +sudo usermod -aG audio $USER + +# Logout and login again +``` + +### Issue: No audio in Docker + +**Solution**: Use virtual audio devices in SDK instead of system audio. diff --git a/plugins/zoom-developers/skills/video-sdk/linux/troubleshooting/qt-dependencies.md b/plugins/zoom-developers/skills/video-sdk/linux/troubleshooting/qt-dependencies.md new file mode 100644 index 00000000..a88ee9df --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/linux/troubleshooting/qt-dependencies.md @@ -0,0 +1,108 @@ +# 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 + +```bash +tar -xf zoom-video-sdk-linux_x86_64.tar.xz +cd zoom-video-sdk-linux_x86_64 +``` + +### 2. Copy Qt5 Libraries + +```bash +# Qt5 libs are in SDK samples +cp -r samples/qt_libs/Qt/lib/* lib/zoom_video_sdk/ +``` + +### 3. Create Symlinks + +```bash +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*.so +``` + +Should see: +``` +libQt5Core.so -> libQt5Core.so.5 +libQt5Core.so.5 +libQt5Gui.so -> libQt5Gui.so.5 +libQt5Gui.so.5 +... +``` + +### 4. Set Library Path + +```bash +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/lib/zoom_video_sdk +``` + +## CMakeLists.txt Configuration + +```cmake +# 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**: +```bash +# 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**: +```bash +cd lib/zoom_video_sdk +for lib in libQt5*.so.5; do ln -sf $lib ${lib%.5}; done +``` diff --git a/plugins/zoom-developers/skills/video-sdk/macos/RUNBOOK.md b/plugins/zoom-developers/skills/video-sdk/macos/RUNBOOK.md new file mode 100644 index 00000000..2cc4a51c --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/macos/RUNBOOK.md @@ -0,0 +1,64 @@ +# Video SDK macOS 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 macOS (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/macos/ +- https://marketplacefront.zoom.us/sdk/custom/macos/annotated.html + +### Raw docs in repo + +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/video-sdk/macos/` +- `tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/video-sdk/macos/` diff --git a/plugins/zoom-developers/skills/video-sdk/macos/SKILL.md b/plugins/zoom-developers/skills/video-sdk/macos/SKILL.md new file mode 100644 index 00000000..17ee1373 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/macos/SKILL.md @@ -0,0 +1,32 @@ +--- +name: zoom-video-sdk-macos +description: | + Zoom Video SDK for macOS native desktop apps. Use when building custom macOS video sessions + with native UI control, tokenized join, and desktop-oriented media/device workflows. +--- + +# Zoom Video SDK (macOS) + +Use this skill when building custom macOS desktop video session apps. + +## Start Here + +1. [macos.md](macos.md) +2. [concepts/lifecycle-workflow.md](concepts/lifecycle-workflow.md) +3. [concepts/architecture.md](concepts/architecture.md) +4. [examples/session-join-pattern.md](examples/session-join-pattern.md) +5. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +6. [references/macos-reference-map.md](references/macos-reference-map.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +9. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Key Sources + +- Docs: https://developers.zoom.us/docs/video-sdk/macos/ +- API reference: https://marketplacefront.zoom.us/sdk/custom/macos/annotated.html +- Broader guide: [../SKILL.md](../SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/video-sdk/macos/concepts/architecture.md b/plugins/zoom-developers/skills/video-sdk/macos/concepts/architecture.md new file mode 100644 index 00000000..9e5f462c --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/macos/concepts/architecture.md @@ -0,0 +1,17 @@ +# macOS Architecture Concept + +```mermaid +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. diff --git a/plugins/zoom-developers/skills/video-sdk/macos/concepts/lifecycle-workflow.md b/plugins/zoom-developers/skills/video-sdk/macos/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..28e78613 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/macos/concepts/lifecycle-workflow.md @@ -0,0 +1,21 @@ +# macOS Lifecycle Workflow + +```mermaid +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. diff --git a/plugins/zoom-developers/skills/video-sdk/macos/examples/session-join-pattern.md b/plugins/zoom-developers/skills/video-sdk/macos/examples/session-join-pattern.md new file mode 100644 index 00000000..5a604658 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/macos/examples/session-join-pattern.md @@ -0,0 +1,24 @@ +# macOS Session Join Pattern + +```swift +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. diff --git a/plugins/zoom-developers/skills/video-sdk/macos/macos.md b/plugins/zoom-developers/skills/video-sdk/macos/macos.md new file mode 100644 index 00000000..afa852cc --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/macos/macos.md @@ -0,0 +1,31 @@ +# 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 diff --git a/plugins/zoom-developers/skills/video-sdk/macos/references/environment-variables.md b/plugins/zoom-developers/skills/video-sdk/macos/references/environment-variables.md new file mode 100644 index 00000000..a37dcda0 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/macos/references/environment-variables.md @@ -0,0 +1,13 @@ +# 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_TOKEN` is server-generated and short-lived. diff --git a/plugins/zoom-developers/skills/video-sdk/macos/references/macos-reference-map.md b/plugins/zoom-developers/skills/video-sdk/macos/references/macos-reference-map.md new file mode 100644 index 00000000..df291092 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/macos/references/macos-reference-map.md @@ -0,0 +1,18 @@ +# 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) diff --git a/plugins/zoom-developers/skills/video-sdk/macos/references/versioning-and-compatibility.md b/plugins/zoom-developers/skills/video-sdk/macos/references/versioning-and-compatibility.md new file mode 100644 index 00000000..0a044da0 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/macos/references/versioning-and-compatibility.md @@ -0,0 +1,18 @@ +# macOS Versioning and Compatibility + +## Package evidence + +- SDK package: `zoom-video-sdk-macos-2.5.0.zip` +- Internal version file: `v2.5.0 (75746)` +- Changelog link is external to package contents. + +## Compatibility notes + +- Keep desktop framework integration and signing settings stable per release. +- Revalidate framework linkage and app entitlements after upgrades. +- Track deprecated APIs from reference pages before jumping versions. + +## Contradictions or drift to watch + +- Changelog details are hosted externally; package only provides pointer links. +- Large framework bundles may include unrelated symbols; keep your surface area minimal. diff --git a/plugins/zoom-developers/skills/video-sdk/macos/scenarios/high-level-scenarios.md b/plugins/zoom-developers/skills/video-sdk/macos/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..69468d66 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/macos/scenarios/high-level-scenarios.md @@ -0,0 +1,26 @@ +# macOS High-Level Scenarios + +## 1. Pro desktop collaboration client + +- Multi-pane participant layout with advanced controls. +- Desktop-focused UX for share-heavy sessions. + +## 2. Broadcast control workstation + +- Host dashboard for session moderation and layout control. +- Optional command channels for runtime operator commands. + +## 3. Support command center + +- Agent desktop app for high-volume escalation sessions. +- Integrates with internal case/ticket panels. + +## 4. Creative studio review rooms + +- High-quality desktop video previews and remote collaboration. +- Role-based controls for speakers, producers, and reviewers. + +## 5. Internal operations and runbooks + +- Controlled sessions for incident response and executive comms. +- Detailed telemetry and recovery procedures integrated. diff --git a/plugins/zoom-developers/skills/video-sdk/macos/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/video-sdk/macos/troubleshooting/common-issues.md new file mode 100644 index 00000000..3ff14df7 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/macos/troubleshooting/common-issues.md @@ -0,0 +1,21 @@ +# macOS Common Issues + +## Session join fails + +- Verify token validity and expiration. +- Confirm app/backend use matching Video SDK credentials. + +## Camera/mic/share unavailable + +- Check macOS privacy permission prompts and app entitlements. +- Confirm device selection and active session state. + +## Render or participant state mismatch + +- Reconcile UI updates with delegate event order. +- Handle reconnect and window lifecycle transitions explicitly. + +## Framework/load issues + +- Validate embed/sign settings in Xcode target. +- Check architecture and minimum macOS version compatibility. diff --git a/plugins/zoom-developers/skills/video-sdk/react-native/RUNBOOK.md b/plugins/zoom-developers/skills/video-sdk/react-native/RUNBOOK.md new file mode 100644 index 00000000..eda4b971 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/react-native/RUNBOOK.md @@ -0,0 +1,64 @@ +# Video SDK React Native 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 React Native (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/react-native/ +- https://marketplacefront.zoom.us/sdk/custom/reactnative/index.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/video-sdk/react-native/` +- `raw-docs/marketplacefront.zoom.us/sdk/video-sdk/reactnative/` diff --git a/plugins/zoom-developers/skills/video-sdk/react-native/SKILL.md b/plugins/zoom-developers/skills/video-sdk/react-native/SKILL.md new file mode 100644 index 00000000..8572087f --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/react-native/SKILL.md @@ -0,0 +1,77 @@ +--- +name: zoom-video-sdk-react-native +description: | + Zoom Video SDK for React Native. Use when building custom mobile video session experiences + with @zoom/react-native-videosdk, event listeners, helper-based APIs, and backend JWT token flows. +--- + +# Zoom Video SDK (React Native) + +Use this skill for React Native apps that need fully custom video session experiences using Zoom Video SDK. + +## Quick Links + +1. **[Lifecycle Workflow](concepts/lifecycle-workflow.md)** - init -> listeners -> join -> helpers -> leave -> cleanup +2. **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - provider + helper model +3. **[High-Level Scenarios](concepts/high-level-scenarios.md)** - common mobile product patterns +4. **[Setup Guide](examples/setup-guide.md)** - package + platform setup baseline +5. **[Session Join Pattern](examples/session-join-pattern.md)** - tokenized join flow +6. **[Event Handling Pattern](examples/event-handling-pattern.md)** - event listener to state routing +7. **[SKILL.md](SKILL.md)** - complete navigation + +## Core Notes + +- Video SDK sessions are not Zoom Meetings and use session tokens. +- JWT generation must stay backend-side. +- Wrapper is helper-heavy (audio/video/chat/share/recording/transcription, etc.). +- Event-driven design is required for robust UI state. + +## References + +- [React Native Reference Index](references/react-native-reference.md) +- [Module Map](references/module-map.md) +- [Official Sources](references/official-sources.md) +- [Deprecated and Contradictions](troubleshooting/deprecated-and-contradictions.md) + +## Related Skills + +- [zoom-video-sdk](../SKILL.md) +- [zoom-oauth](../../oauth/SKILL.md) +- [zoom-general](../../general/SKILL.md) + +## Documentation Index + +### Start Here + +1. [SKILL.md](SKILL.md) +2. [Lifecycle Workflow](concepts/lifecycle-workflow.md) +3. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) +4. [Setup Guide](examples/setup-guide.md) + +### Concepts + +- [Lifecycle Workflow](concepts/lifecycle-workflow.md) +- [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) +- [High-Level Scenarios](concepts/high-level-scenarios.md) + +### Examples + +- [Setup Guide](examples/setup-guide.md) +- [Session Join Pattern](examples/session-join-pattern.md) +- [Event Handling Pattern](examples/event-handling-pattern.md) + +### References + +- [React Native Reference Index](references/react-native-reference.md) +- [Module Map](references/module-map.md) +- [Official Sources](references/official-sources.md) + +### Troubleshooting + +- [Common Issues](troubleshooting/common-issues.md) +- [Version Drift](troubleshooting/version-drift.md) +- [Deprecated and Contradictions](troubleshooting/deprecated-and-contradictions.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/video-sdk/react-native/concepts/high-level-scenarios.md b/plugins/zoom-developers/skills/video-sdk/react-native/concepts/high-level-scenarios.md new file mode 100644 index 00000000..3adac3cb --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/react-native/concepts/high-level-scenarios.md @@ -0,0 +1,21 @@ +# High-Level Scenarios + +## 1. Branded mobile collaboration app + +- Build custom session UI/UX with your own controls. +- Use chat + command channel for in-session interactions. + +## 2. Telehealth/support app + +- Join secured sessions from mobile clients. +- Apply strict camera/mic permission and consent handling. + +## 3. Creator/live event companion + +- Audience or host clients join custom sessions. +- Enable selective features: transcription, recording, share, moderation. + +## 4. Enterprise field collaboration + +- Operators join task sessions on mobile. +- Use helper modules for audio/video controls and remote assistance patterns. diff --git a/plugins/zoom-developers/skills/video-sdk/react-native/concepts/lifecycle-workflow.md b/plugins/zoom-developers/skills/video-sdk/react-native/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..52b0b2ef --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/react-native/concepts/lifecycle-workflow.md @@ -0,0 +1,22 @@ +# Lifecycle Workflow + +Recommended flow for React Native Video SDK integrations: + +1. Initialize provider/SDK config. +2. Register SDK event listeners. +3. Join session using backend-signed JWT token. +4. Drive media/features via helpers. +5. React to event callbacks for UI/session state. +6. Leave session and cleanup SDK resources. + +## Sequence + +```text +React Native app + -> initSdk + -> addListener(EventType...) + -> joinSession(joinConfig) + -> helper operations + -> leaveSession + -> cleanup +``` diff --git a/plugins/zoom-developers/skills/video-sdk/react-native/concepts/sdk-architecture-pattern.md b/plugins/zoom-developers/skills/video-sdk/react-native/concepts/sdk-architecture-pattern.md new file mode 100644 index 00000000..633932ef --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/react-native/concepts/sdk-architecture-pattern.md @@ -0,0 +1,23 @@ +# SDK Architecture Pattern + +The wrapper uses provider/context + helper objects. + +## Structure + +- `ZoomVideoSdkProvider` bootstraps SDK lifecycle. +- `useZoom()` / handler exposes helper modules. +- Helpers include session, user, audio, video, share, chat, recording, transcription, phone, CRC, annotation, and subsession. +- `useSdkEventListener` + `EventType` power event-driven state updates. + +## Pattern + +1. Resolve helper from context. +2. Call helper API. +3. Handle event callback. +4. Update UI/store. + +## Guidance + +- Centralize event handling logic. +- Treat helper return codes as versioned contracts. +- Guard advanced helpers behind capability checks where possible. diff --git a/plugins/zoom-developers/skills/video-sdk/react-native/examples/event-handling-pattern.md b/plugins/zoom-developers/skills/video-sdk/react-native/examples/event-handling-pattern.md new file mode 100644 index 00000000..1f3a1064 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/react-native/examples/event-handling-pattern.md @@ -0,0 +1,28 @@ +# Event Handling Pattern + +Attach listener(s) early and route all event types through a centralized state handler. + +## Prioritized events + +- session join/leave and error events +- user/video/audio/share status changes +- chat + command channel events +- recording/transcription status events + +## Pattern + +1. Register listeners once near app/session root. +2. Map event payloads to typed state updates. +3. Keep cleanup to remove listeners on unmount/leave. + +## Practical custom UI baseline + +For a production-like test surface, implement a single custom session screen with: + +- Join / leave actions +- Local controls: mute-unmute, video on-off +- Device controls: camera switch, speaker toggle +- Remote participant video tiles +- Timestamped event log panel + +This minimal UI gives fast validation of core media and event paths without relying on sample navigation complexity. diff --git a/plugins/zoom-developers/skills/video-sdk/react-native/examples/session-join-pattern.md b/plugins/zoom-developers/skills/video-sdk/react-native/examples/session-join-pattern.md new file mode 100644 index 00000000..05e1f777 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/react-native/examples/session-join-pattern.md @@ -0,0 +1,21 @@ +# Session Join Pattern + +## Flow + +1. Backend signs Video SDK JWT. +2. App builds join config. +3. App calls `joinSession`. +4. UI state is driven by event callbacks. + +## Minimal shape + +```ts +await zoom.joinSession({ + sessionName: 'my-session', + token: '', + userName: 'Mobile User', + audioOptions: { connect: true, mute: false }, + videoOptions: { localVideoOn: true }, + sessionIdleTimeoutMins: 40, +}); +``` diff --git a/plugins/zoom-developers/skills/video-sdk/react-native/examples/setup-guide.md b/plugins/zoom-developers/skills/video-sdk/react-native/examples/setup-guide.md new file mode 100644 index 00000000..7a1495a7 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/react-native/examples/setup-guide.md @@ -0,0 +1,56 @@ +# Setup Guide + +## 1. Install package + +```bash +npm install @zoom/react-native-videosdk +``` + +## 2. Wrap app with provider + +```tsx +', + }} +> + + +``` + +## 3. Platform baseline + +- Configure Android/iOS native prerequisites from package docs. +- Ensure React Native/toolchain compatibility with wrapper version. +- Implement backend JWT signing endpoint before session join. + +## 4. Android build stability on Windows + +- Prefer a short workspace path for Android builds (example: `C:\temp\rn-video-sdk-example`). +- Deep nested paths can trigger native CMake/Gradle instability (for example reanimated build trees and repeated `build.ninja` regeneration). + +## 5. Android SDK wiring for CLI runs + +- Ensure `android/local.properties` points to your SDK dir: + +```properties +sdk.dir=C\:\\Users\\\\AppData\\Local\\Android\\Sdk +``` + +- For shell-driven runs, export/set Android env vars before `react-native run-android`: + - `ANDROID_HOME` + - `ANDROID_SDK_ROOT` + - `PATH` includes `platform-tools` + +## 6. Dependency pinning for copied sample projects + +- If project was copied from SDK sources, do not keep local relative dependency (`"@zoom/react-native-videosdk": "../"`) unless parent package exists at that exact location. +- Pin/install a concrete version instead (example `@zoom/react-native-videosdk@2.4.5`) to avoid Metro resolution failures. + +## 7. Security baseline + +- Keep Video SDK secret out of mobile app. +- Use short-lived JWTs per session/user context. +- Validate incoming session inputs server-side. diff --git a/plugins/zoom-developers/skills/video-sdk/react-native/references/module-map.md b/plugins/zoom-developers/skills/video-sdk/react-native/references/module-map.md new file mode 100644 index 00000000..8f293599 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/react-native/references/module-map.md @@ -0,0 +1,20 @@ +# Module Map + +## Core wrapper + +- `native/ZoomVideoSdk` +- provider/hooks modules (`ZoomVideoSdkProvider`, `hooks/useZoom`, `hooks/useSdkHandler`, `hooks/useSdkEventListener`) + +## Helpers + +- session/user helpers +- audio/video/share helpers +- chat + command channel +- recording/live stream/live transcription +- phone/CRC/annotation/subsession helpers + +## Types and enums + +- join/init config types +- event enum/type map +- error/status/permission/resolution enums diff --git a/plugins/zoom-developers/skills/video-sdk/react-native/references/official-sources.md b/plugins/zoom-developers/skills/video-sdk/react-native/references/official-sources.md new file mode 100644 index 00000000..9af3fa3f --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/react-native/references/official-sources.md @@ -0,0 +1,13 @@ +# Official Sources + +Primary sources used for this skill: + +- Zoom docs: https://developers.zoom.us/docs/video-sdk/react-native/ +- Zoom reference: https://marketplacefront.zoom.us/sdk/custom/reactnative/index.html +- Zoom quickstart repo: https://github.com/zoom/videosdk-reactnative-quickstart +- Local package archive analysis (react-native video sdk zip) + +Crawled snapshots: + +- `skills/raw-docs/developers.zoom.us/docs/video-sdk/react-native/` +- `skills/raw-docs/marketplacefront.zoom.us/sdk/video-sdk/reactnative/` diff --git a/plugins/zoom-developers/skills/video-sdk/react-native/references/react-native-reference.md b/plugins/zoom-developers/skills/video-sdk/react-native/references/react-native-reference.md new file mode 100644 index 00000000..ce8b390d --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/react-native/references/react-native-reference.md @@ -0,0 +1,15 @@ +# React Native Reference Index + +Primary source groups: + +- `raw-docs/developers.zoom.us/docs/video-sdk/react-native/*.md` +- `raw-docs/marketplacefront.zoom.us/sdk/video-sdk/reactnative/index.md` +- `raw-docs/marketplacefront.zoom.us/sdk/video-sdk/reactnative/modules*.md` +- `raw-docs/marketplacefront.zoom.us/sdk/video-sdk/reactnative/classes/*.md` +- `raw-docs/marketplacefront.zoom.us/sdk/video-sdk/reactnative/types/*.md` +- `raw-docs/marketplacefront.zoom.us/sdk/video-sdk/reactnative/enums/*.md` +- `raw-docs/marketplacefront.zoom.us/sdk/video-sdk/reactnative/functions/*.md` + +Reference base URL: + +- https://marketplacefront.zoom.us/sdk/custom/reactnative/index.html diff --git a/plugins/zoom-developers/skills/video-sdk/react-native/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/video-sdk/react-native/troubleshooting/common-issues.md new file mode 100644 index 00000000..5b8aa192 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/react-native/troubleshooting/common-issues.md @@ -0,0 +1,56 @@ +# Common Issues + +## Session join fails + +- Validate JWT token format/expiry and claims. +- Ensure SDK init/provider setup completed before join. +- Validate join config fields and naming (`userName` vs `username` mismatch by version). + +## Event callbacks appear inconsistent + +- Register listeners before join where possible. +- Avoid duplicate listeners in multiple screens. +- Ensure cleanup/unsubscribe on unmount. + +## Helper methods return errors unexpectedly + +- Confirm in-session preconditions for helper calls. +- Re-check permission state on mobile OS. +- Map and log error enums for diagnosis. + +## Platform build/runtime issues + +- Verify React Native + package version compatibility. +- Reinstall pods/gradle dependencies after package changes. +- Validate native bridge files and linked binaries. + +## Android native build fails on Windows deep paths + +Symptom: + +- CMake/Gradle failures in native modules (for example repeated `build.ninja` dirty/regeneration failures). + +Fix: + +- Move project to a short path (example `C:\temp\rn-video-sdk-example`). +- Clean Android artifacts and rebuild from the shorter path. + +## Metro error: cannot resolve `@zoom/react-native-videosdk` + +Symptom: + +- Red screen in `src/App.tsx` or startup bundle: module cannot be found. + +Root cause: + +- Dependency points to a relative local link (`../`) that does not resolve in copied project layout. + +Fix: + +- Pin/install published package version (example `@zoom/react-native-videosdk@2.4.5`). +- Reinstall node modules and restart Metro. + +## Android SDK/adb not detected by RN CLI + +- Add `android/local.properties` with valid `sdk.dir`. +- Ensure `ANDROID_HOME`, `ANDROID_SDK_ROOT`, and PATH (`platform-tools`) are set in the shell running `react-native run-android`. diff --git a/plugins/zoom-developers/skills/video-sdk/react-native/troubleshooting/deprecated-and-contradictions.md b/plugins/zoom-developers/skills/video-sdk/react-native/troubleshooting/deprecated-and-contradictions.md new file mode 100644 index 00000000..75275a5e --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/react-native/troubleshooting/deprecated-and-contradictions.md @@ -0,0 +1,33 @@ +# Deprecated and Contradictions + +Observed from crawled docs/reference and local package analysis: + +## 1. Naming inconsistency in examples + +- Some samples/docs use `username`; current typed wrappers often use `userName`. +- Action: normalize to the exact type definitions of the installed wrapper version. + +## 2. README duplication/quality issues + +- Package README includes duplicated `videoOptions` block in join example. +- Action: trust typed API references over README snippets when inconsistent. + +## 3. Deprecated annotation helper surface in reference + +- Crawled reference marks deprecation signals on annotation helper type/class pages. +- Action: treat annotation helper APIs as version-sensitive and verify current support before new feature adoption. + +## 4. \"Not supported\" notes in share docs + +- Crawled React Native share docs include explicit not-supported caveats for some share paths/features. +- Action: gate share features by runtime capability checks and fallback UI. + +## 5. Large API surface drift risk + +- React Native reference includes many helpers/enums/types that evolve over releases. +- Action: pin versions and keep app-level adapter boundaries around helper/event APIs. + +## 6. External quickstart verification constraint + +- Direct remote quickstart verification can fail in restricted DNS/network environments. +- Action: prioritize local package + crawled docs/reference as baseline and verify quickstart when network is available. diff --git a/plugins/zoom-developers/skills/video-sdk/react-native/troubleshooting/version-drift.md b/plugins/zoom-developers/skills/video-sdk/react-native/troubleshooting/version-drift.md new file mode 100644 index 00000000..74ba2fc8 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/react-native/troubleshooting/version-drift.md @@ -0,0 +1,11 @@ +# Version Drift + +React Native wrapper, native Video SDKs, and docs may drift independently. + +## Upgrade checklist + +1. Compare wrapper package version and exported type signatures. +2. Re-check join config/property names and enum constants. +3. Re-test full lifecycle: init -> join -> media/helper calls -> leave -> cleanup. +4. Re-validate event types used in app listeners. +5. Re-run smoke tests on both iOS and Android. diff --git a/plugins/zoom-developers/skills/video-sdk/references/authorization.md b/plugins/zoom-developers/skills/video-sdk/references/authorization.md new file mode 100644 index 00000000..5b9bcb3f --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/references/authorization.md @@ -0,0 +1,121 @@ +# Video SDK - Authorization + +Generate JWT signatures for Video SDK authentication. + +## Overview + +Video SDK uses JWT (JSON Web Token) signatures to authenticate users joining video sessions. Signatures must be generated server-side to protect your SDK Secret. + +**Important**: Unlike Zoom Meetings, Video SDK sessions are NOT pre-created. The `tpc` (topic) in your JWT can be any string you choose - the session is created when the first participant joins with that topic. + +## Prerequisites + +- Video SDK Key and Secret from [Marketplace](https://marketplace.zoom.us/) (sign-in required) +- Server-side code to generate signatures + +## JWT Structure + +| Claim | Description | +|-------|-------------| +| `app_key` | Your SDK Key | +| `tpc` | Topic (session name) - **any string you choose** | +| `role_type` | 0 = participant, 1 = host | +| `user_identity` | (Optional) Unique user identifier | +| `iat` | Issued at timestamp | +| `exp` | Expiration timestamp | + +**Note on `tpc` (Topic)**: +- Can be any string (e.g., `"room-123"`, `"consultation-abc"`, `"game-lobby-5"`) +- All users joining with the same `tpc` value join the same session +- Session is created automatically when first user joins +- No API call needed to create the session beforehand + +## Host and Co-Host via roleType + +In Video SDK, host/co-host status is determined entirely by `role_type` in the JWT — not by runtime API calls. + +| role_type | First to Join | Subsequent Joiners | +|-----------|--------------|-------------------| +| 1 | Host | Co-host | +| 0 | Participant | Participant | + +- Only host or co-host can call `client.leave(true)` to end session for all +- A participant calling `leave(true)` only leaves themselves +- No need for `makeHost()` or `makeManager()` API calls — use JWT roleType instead + +### Session Creation Pattern (Bot as Host) + +For scenarios where you need a bot to create and manage the session: + +1. **Bot** joins with `role_type: 1` → becomes host (creates session) +2. **User A** joins with `role_type: 1` → becomes co-host +3. **Bot leaves** → User A remains as co-host, can end session +4. **User B** joins with `role_type: 0` → participant + +This pattern avoids race conditions from runtime host assignment. + +```javascript +// JWT generation examples +generateJWT(key, secret, sessionName, 1, 'SessionBot'); // Bot: host +generateJWT(key, secret, sessionName, 1, 'Advisor'); // Advisor: co-host +generateJWT(key, secret, sessionName, 0, 'Customer'); // Customer: participant +``` + +## Signature Generation Best Practices + +### Short-Lived Tokens (Recommended) + +For security, generate tokens with short expiry: + +```javascript +const iat = Math.floor(Date.now() / 1000) - 7200; // 2 hours in the past +const exp = Math.floor(Date.now() / 1000) + 10; // 10 seconds from now + +const payload = { + app_key: SDK_KEY, + tpc: topic, + role_type: role, + user_identity: userIdentity, + iat: iat, + exp: exp +}; +``` + +**Why this works:** +- `exp` is only 10 seconds after generation (short-lived for security) +- `iat` is set 2 hours in the past to satisfy Zoom's requirement that `exp - iat >= 2 hours` +- Token is generated just before joining, so 10 second window is sufficient + +### Server-Side Example (Node.js) + +```javascript +const jwt = require('jsonwebtoken'); + +function generateSignature(sdkKey, sdkSecret, topic, role, userIdentity) { + const iat = Math.floor(Date.now() / 1000) - 7200; // 2 hours ago + const exp = Math.floor(Date.now() / 1000) + 10; // 10 seconds from now + + const payload = { + app_key: sdkKey, + tpc: topic, + role_type: role, + user_identity: userIdentity || '', + iat: iat, + exp: exp + }; + + return jwt.sign(payload, sdkSecret, { algorithm: 'HS256' }); +} +``` + +## Security Guidelines + +| Do | Don't | +|----|-------| +| Generate signatures server-side | Expose SDK Secret in client code | +| Use short expiry times | Use long-lived tokens | +| Validate user before generating | Generate for unauthenticated users | + +## Resources + +- **Auth docs**: https://developers.zoom.us/docs/video-sdk/auth/ diff --git a/plugins/zoom-developers/skills/video-sdk/references/environment-variables.md b/plugins/zoom-developers/skills/video-sdk/references/environment-variables.md new file mode 100644 index 00000000..4d25c6ff --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/references/environment-variables.md @@ -0,0 +1,25 @@ +# Zoom Video SDK Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_VIDEO_SDK_KEY` | Yes | Video SDK app identity | Zoom Marketplace -> Video SDK app -> App Credentials | +| `ZOOM_VIDEO_SDK_SECRET` | Yes (server only) | Video SDK token/signature generation | Zoom Marketplace -> Video SDK app -> App Credentials | +| `VIDEO_SDK_TOKEN_ENDPOINT` | Yes | URL your clients call to fetch short-lived Video SDK token | Your backend deployment config | +| `VIDEO_SDK_BASE_URL` | Optional | Base URL for your app backend | Set to your deployed environment | +| `VIDEO_SDK_SESSION_NAME` | Runtime | Session/topic identifier | Created by your app workflow | +| `VIDEO_SDK_USER_NAME` | Runtime | Display name in session | App user profile/context | + +## Runtime-only values + +- `VIDEO_SDK_TOKEN` / `VIDEO_SDK_JWT` (short-lived; generated server-side) + +## Notes + +- Some older samples use `ZOOM_SDK_KEY`/`ZOOM_SDK_SECRET`; prefer explicit `ZOOM_VIDEO_SDK_*` names. +- Platform-specific variants: + - [Android env vars](../android/references/environment-variables.md) + - [iOS env vars](../ios/references/environment-variables.md) + - [macOS env vars](../macos/references/environment-variables.md) + - [Unity env vars](../unity/references/environment-variables.md) diff --git a/plugins/zoom-developers/skills/video-sdk/references/forum-top-questions.md b/plugins/zoom-developers/skills/video-sdk/references/forum-top-questions.md new file mode 100644 index 00000000..298d7c75 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/references/forum-top-questions.md @@ -0,0 +1,64 @@ +--- +title: "Forum-Derived Top Questions (Video SDK)" +--- + +# Forum-Derived Top Questions (Video SDK) + +Use this as a checklist of what developers repeatedly ask about the **Zoom Video SDK**. + +## Fast Routing Questions (Ask First) + +- Platform: **Web** vs **React Native** vs **Flutter** vs **Linux** +- UI style: **UI Toolkit** vs **Custom UI** +- What you are building: 1:1 call, group session, audio-only room, screen share, recording, etc. +- Inputs you have: `sdkKey`, `sdkSecret` (server only), `topic`, `userName`, `password`, signature + +## Signatures (Common Failure Point) + +- Signatures must be generated server-side (do not ship `sdkSecret` to clients). +- Confirm signature expiry and clock skew if joins fail. + +## Lifecycle Ordering (Very Common Bug) + +Developers frequently call APIs out of order. + +Required order: +1. `client = ZoomVideo.createClient()` +2. `await client.init(...)` +3. `await client.join(...)` +4. `stream = client.getMediaStream()` (only after join) +5. `await stream.startVideo()` / `await stream.startAudio()` + +## Rendering and Events + +Common asks: +- “Why is remote video not showing?” +- “How do I render self-view / peer video?” + +Answer pattern: +- Emphasize event-driven rendering (listen for `user-added`, `peer-video-state-change`, etc.) +- Prefer `attachVideo()` / `detachVideo()` where supported (Web) + +## UI Toolkit Questions + +Common asks: +- retrieving errors/logs from UI Toolkit +- capturing images/snapshots in Toolkit +- detecting screen sharing when using Toolkit + +Answer pattern: +- Confirm the UI Toolkit version + SDK version +- Clarify which surfaces the Toolkit abstracts vs what requires underlying SDK APIs + +## React Native / Flutter + +Common asks: +- permissions (camera/mic) +- native build issues +- background/foreground behavior +- audio routing and device disconnects + +Answer pattern: +- Ask for platform version + SDK wrapper version +- Ask for minimal reproduction steps and logs + diff --git a/plugins/zoom-developers/skills/video-sdk/references/full-guide.md b/plugins/zoom-developers/skills/video-sdk/references/full-guide.md new file mode 100644 index 00000000..9e638d08 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/references/full-guide.md @@ -0,0 +1,352 @@ +# /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. + +Build custom video experiences powered by Zoom's infrastructure. + +## Hard Routing Guardrail (Read First) + +- If the user asks for custom real-time video app behavior (topic/session join, custom rendering, attach/detach), route to Video SDK. +- 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`). + +## Meeting SDK vs Video SDK + +| Feature | Meeting SDK | Video SDK | +|---------|-------------|-----------| +| UI | Default Zoom UI or Custom UI | **Fully custom UI** (you build it) | +| Experience | Zoom meetings | Video sessions | +| Branding | Limited customization | **Full branding control** | +| Features | Full Zoom features | Core video features | + +## UI Options (Web) + +Video SDK gives you **full control over the UI**: + +| Option | Description | +|--------|-------------| +| **UI Toolkit** | Pre-built React components (low-code) | +| **Custom UI** | Build your own UI using the SDK APIs | + +## Prerequisites + +- Zoom Video SDK credentials from Marketplace +- SDK Key and Secret +- Web development environment + +> **Need help with OAuth or signatures?** See the **[zoom-oauth](../../oauth/SKILL.md)** skill for authentication flows. + +> **Need pre-join diagnostics on web?** Use **[probe-sdk](../../probe-sdk/SKILL.md)** before Video SDK `join()` to reduce first-minute failures. + +> **Start troubleshooting fast:** Use the **[5-Minute Runbook](../RUNBOOK.md)** before deep debugging. + +## Quick Start (Web) + +### NPM Usage (Bundler like Vite/Webpack) + +```javascript +import ZoomVideo from '@zoom/videosdk'; + +const client = ZoomVideo.createClient(); +await client.init('en-US', 'Global', { patchJsMedia: true }); +await client.join(topic, signature, userName, password); + +// IMPORTANT: getMediaStream() ONLY works AFTER join() +const stream = client.getMediaStream(); +await stream.startVideo(); +await stream.startAudio(); +``` + +### CDN Usage (No Bundler) + +> **WARNING: Ad blockers block `source.zoom.us`**. Self-host the SDK to avoid issues. + +```bash +# Download SDK locally +curl "https://source.zoom.us/videosdk/zoom-video-1.12.0.min.js" -o js/zoom-video-sdk.min.js +``` + +```html + +``` + +```javascript +// CDN exports as WebVideoSDK, NOT ZoomVideo +// Must use .default property +const ZoomVideo = WebVideoSDK.default; +const client = ZoomVideo.createClient(); + +await client.init('en-US', 'Global', { patchJsMedia: true }); +await client.join(topic, signature, userName, password); + +// IMPORTANT: getMediaStream() ONLY works AFTER join() +const stream = client.getMediaStream(); +await stream.startVideo(); +await stream.startAudio(); +``` + +### ES Module with CDN (Race Condition Fix) + +When using ` + +``` + +### Angular + +Add CSS to `angular.json`: +```json +"styles": [ + "node_modules/@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css" +] +``` + +## Quick Start + +```html +
+``` + +```javascript +import uitoolkit from "@zoom/videosdk-zoom-ui-toolkit"; +import "@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css"; + +const config = { + videoSDKJWT: "your-jwt-token", + sessionName: "SessionA", + userName: "UserA", + sessionPasscode: "abc123", + featuresOptions: { + preview: true, + video: true, + audio: true, + share: true, + chat: true, + users: true, + settings: true, + leave: true, + }, +}; + +const sessionContainer = document.getElementById("sessionContainer"); +uitoolkit.joinSession(sessionContainer, config); +``` + +## Feature Components + +Toggle components on/off via `featuresOptions`: + +| Component | Description | Paid Plan? | +|-----------|-------------|------------| +| `preview` | Pre-session device selection, virtual background | No | +| `video` | Video layout for sending/receiving | No | +| `audio` | Audio button and controls | No | +| `share` | Screen sharing | No | +| `chat` | In-session chat | No | +| `users` | Participant list | No | +| `settings` | Device config, virtual background, stats | No | +| `viewMode` | Grid/speaker view options | No | +| `leave` | Leave/end session button | No | +| `invite` | Invite via link | No | +| `theme` | Theme color selection | No | +| `feedback` | Session feedback form | No | +| `troubleshoot` | Zoom Probe SDK troubleshooting | No | +| `subsession` | Breakout rooms button | No | +| `playback` | Media file playback | No | +| `recording` | Cloud recording | **Yes** | +| `phone` | Join by phone audio | **Yes** | +| `caption` | Live translations | **Yes** | + +## React Example + +```jsx +import uitoolkit from "@zoom/videosdk-zoom-ui-toolkit"; +import "@zoom/videosdk-ui-toolkit/dist/videosdk-zoom-ui-toolkit.css"; +import { useEffect, useRef } from "react"; + +function VideoSession({ jwt, sessionName, userName }) { + const containerRef = useRef(null); + + useEffect(() => { + const config = { + videoSDKJWT: jwt, + sessionName, + userName, + sessionPasscode: "abc123", + featuresOptions: { + video: true, + audio: true, + chat: true, + users: true, + leave: true, + }, + }; + + if (containerRef.current) { + uitoolkit.joinSession(containerRef.current, config); + } + + return () => { + if (containerRef.current) { + uitoolkit.closeSession(containerRef.current); + } + }; + }, [jwt, sessionName, userName]); + + return
; +} +``` + +## Event Listeners + +```javascript +// Session events +uitoolkit.onSessionJoined(() => { + console.log("Session joined"); +}); + +uitoolkit.onSessionClosed(() => { + console.log("Session closed"); +}); + +// Unsubscribe +uitoolkit.offSessionJoined(callback); +uitoolkit.offSessionClosed(callback); +``` + +## Component Visibility Control + +```javascript +// Hide all components +uitoolkit.hideAllComponents(); + +// Show/hide individual components +uitoolkit.showControlsComponent(container); +uitoolkit.hideControlsComponent(container); + +uitoolkit.showChatComponent(container); +uitoolkit.hideChatComponent(container); + +uitoolkit.showUsersComponent(container); +uitoolkit.hideUsersComponent(container); + +uitoolkit.showSettingsComponent(container); +uitoolkit.hideSettingsComponent(container); +``` + +## Close Session + +```javascript +uitoolkit.closeSession(sessionContainer); +``` + +## Live Demo + +- **With SharedArrayBuffer**: https://videosdk.dev/uitoolkit/ +- **Without SharedArrayBuffer**: https://videosdk.dev/uitoolkit-no-sab/ + +## Sample Repositories + +| Framework | Repository | +|-----------|------------| +| React | [zoom/videosdk-zoom-ui-toolkit-react-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-react-sample) | +| Angular | [zoom/videosdk-zoom-ui-toolkit-angular-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-angular-sample) | +| Vue.js | [zoom/videosdk-zoom-ui-toolkit-vuejs-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-vuejs-sample) | +| Vanilla JS | [zoom/videosdk-zoom-ui-toolkit-javascript-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-javascript-sample) | + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/video-sdk/web/ui-toolkit/ +- **npm package**: https://www.npmjs.com/package/@zoom/videosdk-zoom-ui-toolkit +- **GitHub**: https://github.com/zoom/videosdk-zoom-ui-toolkit-web diff --git a/plugins/zoom-developers/skills/video-sdk/unity/RUNBOOK.md b/plugins/zoom-developers/skills/video-sdk/unity/RUNBOOK.md new file mode 100644 index 00000000..4301886d --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/unity/RUNBOOK.md @@ -0,0 +1,64 @@ +# Video SDK Unity 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 Unity (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/unity/ +- https://marketplacefront.zoom.us/sdk/custom/unity/index.html + +### Raw docs in repo + +- `tools/zoom-crawler/raw-docs/developers.zoom.us/docs/video-sdk/unity/` +- `tools/zoom-crawler/raw-docs/marketplacefront.zoom.us/sdk/video-sdk/unity/` diff --git a/plugins/zoom-developers/skills/video-sdk/unity/SKILL.md b/plugins/zoom-developers/skills/video-sdk/unity/SKILL.md new file mode 100644 index 00000000..8e35e74b --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/unity/SKILL.md @@ -0,0 +1,32 @@ +--- +name: zoom-video-sdk-unity +description: | + Zoom Video SDK for Unity wrapper integrations. Use when building custom Unity-based + video session experiences and mapping Unity scene/UI state to Video SDK events. +--- + +# Zoom Video SDK (Unity) + +Use this skill when building Unity apps that integrate Zoom Video SDK wrapper APIs. + +## Start Here + +1. [unity.md](unity.md) +2. [concepts/lifecycle-workflow.md](concepts/lifecycle-workflow.md) +3. [concepts/architecture.md](concepts/architecture.md) +4. [examples/session-join-pattern.md](examples/session-join-pattern.md) +5. [scenarios/high-level-scenarios.md](scenarios/high-level-scenarios.md) +6. [references/unity-reference-map.md](references/unity-reference-map.md) +7. [references/environment-variables.md](references/environment-variables.md) +8. [references/versioning-and-compatibility.md](references/versioning-and-compatibility.md) +9. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Key Sources + +- Docs: https://developers.zoom.us/docs/video-sdk/unity/ +- API reference: https://marketplacefront.zoom.us/sdk/custom/unity/index.html +- Broader guide: [../SKILL.md](../SKILL.md) + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/video-sdk/unity/concepts/architecture.md b/plugins/zoom-developers/skills/video-sdk/unity/concepts/architecture.md new file mode 100644 index 00000000..48f6d101 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/unity/concepts/architecture.md @@ -0,0 +1,18 @@ +# Unity Architecture Concept + +```mermaid +flowchart LR + Scene[Unity Scene + UI] --> SessionMgr[Session Manager Script] + SessionMgr --> Wrapper[Zoom Video SDK Unity Wrapper] + SessionMgr --> TokenAPI[Token API] + TokenAPI --> Signer[Server JWT Signer] + Wrapper --> Events[SDK Event Callbacks] + Events --> SessionMgr + SessionMgr --> Scene +``` + +## Design guidance + +- Keep wrapper calls behind one Session Manager abstraction. +- Convert SDK callbacks into explicit Unity state updates. +- Avoid direct credential logic in Unity client. diff --git a/plugins/zoom-developers/skills/video-sdk/unity/concepts/lifecycle-workflow.md b/plugins/zoom-developers/skills/video-sdk/unity/concepts/lifecycle-workflow.md new file mode 100644 index 00000000..7310e742 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/unity/concepts/lifecycle-workflow.md @@ -0,0 +1,21 @@ +# Unity Lifecycle Workflow + +```mermaid +flowchart TD + A[Get token from backend] --> B[Initialize Unity wrapper] + B --> C[Bind SDK events] + C --> D[Join session] + D --> E[Map state to Unity scene/UI] + E --> F[Handle media and participant updates] + F --> G[Leave session] + G --> H[Dispose wrapper/resources] +``` + +## Operational sequence + +1. Request token from backend. +2. Initialize SDK wrapper and event handlers. +3. Join session with topic/session name and display identity. +4. Start/stop local media via wrapper APIs. +5. Apply participant/media updates to Unity scene objects. +6. Cleanly dispose resources on leave or scene switch. diff --git a/plugins/zoom-developers/skills/video-sdk/unity/examples/session-join-pattern.md b/plugins/zoom-developers/skills/video-sdk/unity/examples/session-join-pattern.md new file mode 100644 index 00000000..d0c5f766 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/unity/examples/session-join-pattern.md @@ -0,0 +1,21 @@ +# Unity Session Join Pattern + +```csharp +public async Task JoinVideoSession(string sessionName, string userName) +{ + var token = await tokenClient.FetchVideoToken(sessionName, userName); + + zoomSdk.Initialize(initParams); + zoomSdk.OnSessionJoin += HandleSessionJoin; + + zoomSdk.JoinSession(sessionName, userName, token); + + zoomSdk.GetAudioHelper().StartAudio(); + zoomSdk.GetVideoHelper().StartVideo(); +} +``` + +## Notes + +- Bind/unbind Unity event handlers during scene lifecycle. +- Guard against stale handlers when reloading scenes. diff --git a/plugins/zoom-developers/skills/video-sdk/unity/references/environment-variables.md b/plugins/zoom-developers/skills/video-sdk/unity/references/environment-variables.md new file mode 100644 index 00000000..aed4f384 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/unity/references/environment-variables.md @@ -0,0 +1,13 @@ +# Unity Environment Variables + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_VIDEO_SDK_KEY` | Yes | Video SDK credential pair (server use) | Zoom Marketplace -> Video SDK app -> App Credentials | +| `ZOOM_VIDEO_SDK_SECRET` | Yes (server only) | Token signing secret | Zoom Marketplace -> Video SDK app -> App Credentials | +| `VIDEO_SDK_TOKEN_ENDPOINT` | Yes | Unity app token fetch URL | Your backend deployment config | +| `VIDEO_SDK_SESSION_NAME` | Runtime | Session/topic identifier | Generated by game/app workflow | +| `VIDEO_SDK_USER_NAME` | Runtime | Participant display name | Game/app profile identity | + +## Runtime-only values + +- `VIDEO_SDK_TOKEN` should be short-lived and backend-generated. diff --git a/plugins/zoom-developers/skills/video-sdk/unity/references/unity-reference-map.md b/plugins/zoom-developers/skills/video-sdk/unity/references/unity-reference-map.md new file mode 100644 index 00000000..5025bef3 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/unity/references/unity-reference-map.md @@ -0,0 +1,18 @@ +# Unity Reference Map + +## Docs anchors + +- Integration docs: https://developers.zoom.us/docs/video-sdk/unity/ +- API reference index: https://marketplacefront.zoom.us/sdk/custom/unity/index.html + +## API areas to focus on + +- Core SDK wrapper classes +- Session context/init params +- Audio/video helper wrappers +- Delegate/event interfaces for participant/media state + +## Crawl summary + +- Reference pages crawled: 98 +- Docs pages crawled: 6 (5 markdown files persisted) diff --git a/plugins/zoom-developers/skills/video-sdk/unity/references/versioning-and-compatibility.md b/plugins/zoom-developers/skills/video-sdk/unity/references/versioning-and-compatibility.md new file mode 100644 index 00000000..60329698 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/unity/references/versioning-and-compatibility.md @@ -0,0 +1,17 @@ +# Unity Versioning and Compatibility + +## Package evidence + +- Wrapper package: `unity-zoom-video-sdk-0.0.2-beta.zip` +- Contains `ZoomVideoSDK.unitypackage` wrapper bundle. + +## Compatibility notes + +- Unity wrapper versioning is independent from native Android/iOS/macOS SDK streams. +- Validate each API/event used in wrapper reference docs before implementation. +- Treat wrapper as potentially partial feature surface versus native SDKs. + +## Contradictions or drift to watch + +- Wrapper release (`0.0.2-beta`) is substantially behind native 2.5.0 package family. +- Some docs and feature names may differ between wrapper and native platform references. diff --git a/plugins/zoom-developers/skills/video-sdk/unity/scenarios/high-level-scenarios.md b/plugins/zoom-developers/skills/video-sdk/unity/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..bf4b0c5d --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/unity/scenarios/high-level-scenarios.md @@ -0,0 +1,26 @@ +# Unity High-Level Scenarios + +## 1. Interactive virtual events + +- Unity scene-based sessions with branded interaction elements. +- Runtime participant controls and custom overlays. + +## 2. Training simulators with live instructor + +- Instructor-led sessions embedded into simulation environments. +- Tokenized joins mapped to training cohorts. + +## 3. Support walkthrough spaces + +- Agent + customer co-presence in guided Unity environments. +- Scene state tied to session participant roles. + +## 4. Product demo theaters + +- Host-controlled interactive demo rooms. +- Event callbacks drive stage, spotlight, and audience states. + +## 5. Experimental mixed-reality collaboration + +- Session media integrated with custom rendering pipelines. +- Explicit fallback when wrapper feature parity is incomplete. diff --git a/plugins/zoom-developers/skills/video-sdk/unity/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/video-sdk/unity/troubleshooting/common-issues.md new file mode 100644 index 00000000..5660dde1 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/unity/troubleshooting/common-issues.md @@ -0,0 +1,21 @@ +# Unity Common Issues + +## Missing API from native docs + +- Confirm API exists in Unity wrapper reference, not just native SDK docs. +- Implement fallback logic for unsupported wrapper features. + +## Join/session event issues + +- Verify token validity and wrapper event binding order. +- Ensure scene lifecycle does not drop active handlers. + +## Platform build permission problems + +- Confirm microphone/camera permissions for target platform build settings. +- Re-test on clean devices with fresh permission prompts. + +## Rendering/state mismatch + +- Keep scene state synchronized from SDK events. +- Reset participant objects on leave/rejoin transitions. diff --git a/plugins/zoom-developers/skills/video-sdk/unity/unity.md b/plugins/zoom-developers/skills/video-sdk/unity/unity.md new file mode 100644 index 00000000..86205a5d --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/unity/unity.md @@ -0,0 +1,31 @@ +# Unity Video SDK Overview + +## What this platform skill is for + +- Integrating Zoom Video SDK wrapper into Unity projects +- Building custom in-scene video interaction experiences +- Mapping session lifecycle and participant events into Unity game loop/UI + +## Primary implementation path + +1. Backend generates short-lived Video SDK token. +2. Unity initializes wrapper objects and event bindings. +3. Unity joins session with session name and user identity. +4. Unity updates scene/UI based on SDK callbacks. +5. Unity leaves session and disposes wrapper resources cleanly. + +## Prerequisites + +- Unity project with packaged `ZoomVideoSDK.unitypackage` +- Backend token endpoint +- Platform-specific permissions for mic/camera on target builds + +## Important notes + +- Unity wrapper version may lag native SDK versions. +- Validate feature availability before promising parity with native platforms. + +## Source links + +- Docs: https://developers.zoom.us/docs/video-sdk/unity/ +- API reference: https://marketplacefront.zoom.us/sdk/custom/unity/index.html diff --git a/plugins/zoom-developers/skills/video-sdk/web/RUNBOOK.md b/plugins/zoom-developers/skills/video-sdk/web/RUNBOOK.md new file mode 100644 index 00000000..659e5f39 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/web/RUNBOOK.md @@ -0,0 +1,69 @@ +# Video SDK Web 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 Web (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 + +## 9) Field Pitfalls (Custom Flows) + +- For waiting-room -> main-session transfers and browser/CSP edge cases, review: + - `troubleshooting/common-issues.md` (section: "Real-World Integration Pitfalls (Custom Waiting Room Flows)") + +### Official docs + +- https://developers.zoom.us/docs/video-sdk/web/ +- https://marketplacefront.zoom.us/sdk/custom/web/modules.html + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/video-sdk/web/` +- `raw-docs/marketplacefront.zoom.us/sdk/video-sdk/web/` diff --git a/plugins/zoom-developers/skills/video-sdk/web/SKILL.md b/plugins/zoom-developers/skills/video-sdk/web/SKILL.md new file mode 100644 index 00000000..08a05372 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/web/SKILL.md @@ -0,0 +1,814 @@ +--- +name: zoom-video-sdk-web +description: "Zoom Video SDK for Web - JavaScript/TypeScript integration for browser-based video sessions, real-time communication, screen sharing, recording, and live transcription" +--- + +# Zoom Video SDK - Web Development + +Expert guidance for developing with the Zoom Video SDK on Web. This SDK enables custom video applications in the browser with real-time video/audio, screen sharing, cloud recording, live streaming, chat, and live transcription. + +This skill is for **custom video sessions**, not embedded Zoom meetings. +If the user wants a custom UI for a real Zoom meeting, route to +[../../meeting-sdk/web/component-view/SKILL.md](../../meeting-sdk/web/component-view/SKILL.md). + +**Official Documentation**: https://developers.zoom.us/docs/video-sdk/web/ +**API Reference**: https://marketplacefront.zoom.us/sdk/custom/web/modules.html +**Sample Repository**: https://github.com/zoom/videosdk-web-sample + +## Quick Links + +**New to Video SDK? Follow this path:** + +1. **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - Universal 3-step pattern for ANY feature +2. **[Session Join Pattern](examples/session-join-pattern.md)** - Complete working code to join a session +3. **[Video Rendering](examples/video-rendering.md)** - Display video with attachVideo() +4. **[Event Handling](examples/event-handling.md)** - Required events for video/audio + +**Reference:** +- **[Singleton Hierarchy](concepts/singleton-hierarchy.md)** - 4-level SDK navigation map +- **[API Reference](references/web-reference.md)** - Methods, events, error codes +- **[SKILL.md](SKILL.md)** - Complete documentation navigation +- **[../../probe-sdk/SKILL.md](../../probe-sdk/SKILL.md)** - Optional browser/device/network readiness diagnostics before join + +**Having issues?** +- Video not showing → [Video Rendering](examples/video-rendering.md) (use attachVideo, not renderVideo) +- getMediaStream() returns undefined → Call AFTER join() completes +- Quick diagnostics → [Common Issues](troubleshooting/common-issues.md) + +## SDK Overview + +The Zoom Video SDK for Web is a JavaScript library that provides: +- **Session Management**: Join/leave video SDK sessions +- **Video/Audio**: Start/stop camera and microphone +- **Screen Sharing**: Share screens or browser tabs +- **Cloud Recording**: Record sessions to Zoom cloud +- **Live Streaming**: Stream to RTMP endpoints +- **Chat**: In-session messaging +- **Command Channel**: Custom command messaging +- **Live Transcription**: Real-time speech-to-text +- **Subsessions**: Breakout room support +- **Whiteboard**: Collaborative whiteboard features +- **Virtual Background**: Blur or custom image backgrounds + +## Prerequisites + +### System Requirements + +- **Modern Browser**: Chrome 80+, Firefox 75+, Safari 14+, Edge 80+ +- **Video SDK Credentials**: SDK Key and Secret from [Marketplace](https://marketplace.zoom.us/) +- **JWT Token**: Server-side generated signature + +### Browser Feature Requirements + +```javascript +// Check browser compatibility before init +const compatibility = ZoomVideo.checkSystemRequirements(); +console.log('Audio:', compatibility.audio); +console.log('Video:', compatibility.video); +console.log('Screen:', compatibility.screen); + +// Check feature support +const features = ZoomVideo.checkFeatureRequirements(); +console.log('Supported:', features.supportFeatures); +console.log('Unsupported:', features.unSupportFeatures); +``` + +### Optional Pre-Join Diagnostics (Recommended for Reliability) + +Use Probe SDK as a readiness gate before `client.join(...)` when you need to reduce failed starts: + +1. Run diagnostics with [../../probe-sdk/SKILL.md](../../probe-sdk/SKILL.md). +2. Evaluate policy (`allow`, `warn`, `block`). +3. Start Video SDK join only when policy allows. + +Cross-skill flow: [../../general/use-cases/probe-sdk-preflight-readiness-gate.md](../../general/use-cases/probe-sdk-preflight-readiness-gate.md) + +## Installation + +### NPM (Recommended) + +```bash +npm install @zoom/videosdk +``` + +```javascript +import ZoomVideo from '@zoom/videosdk'; +``` + +### CDN (Fallback Strategy Recommended) + +> **Note**: Some networks/ad blockers can block `source.zoom.us`. If you see flaky loads, first try allowlisting the domain in your environment. If needed, consider a fallback (mirror/self-host) only if it's permitted for your use case and you can keep versions in sync. + +```bash +# Download SDK locally +curl "https://source.zoom.us/videosdk/zoom-video-2.3.12.min.js" -o public/js/zoom-video-sdk.min.js +``` + +```html + + +``` + +```javascript +// CDN exports as WebVideoSDK, NOT ZoomVideo +const ZoomVideo = WebVideoSDK.default; +``` + +## Quick Start + +```javascript +import ZoomVideo from '@zoom/videosdk'; + +// 1. Create client (singleton - returns same instance) +const client = ZoomVideo.createClient(); + +// 2. Initialize SDK +await client.init('en-US', 'Global', { patchJsMedia: true }); + +// 3. Join session +await client.join(topic, signature, userName, password); + +// 4. CRITICAL: Get stream AFTER join +const stream = client.getMediaStream(); + +// 5. Start media +await stream.startVideo(); +await stream.startAudio(); + +// 6. Attach video to DOM +const videoElement = await stream.attachVideo(userId, VideoQuality.Video_360P); +document.getElementById('video-container').appendChild(videoElement); +``` + +## SDK Lifecycle (CRITICAL ORDER) + +The SDK has a strict lifecycle. Violating it causes **silent failures**. + +``` +1. Create client: client = ZoomVideo.createClient() +2. Initialize: await client.init('en-US', 'Global', options) +3. Join session: await client.join(topic, signature, userName, password) +4. Get stream: stream = client.getMediaStream() ← ONLY AFTER JOIN +5. Start media: await stream.startVideo() / await stream.startAudio() +``` + +**Common Mistake:** + +```javascript +// WRONG: Getting stream before joining +const stream = client.getMediaStream(); // Returns undefined! +await client.join(...); + +// CORRECT: Get stream after joining +await client.join(...); +const stream = client.getMediaStream(); // Works! +``` + +## Critical Gotchas and Best Practices + +### getMediaStream() ONLY Works After join() + +The #1 issue that causes video/audio to fail: + +```javascript +// WRONG +const stream = client.getMediaStream(); // undefined! +await client.join(...); + +// CORRECT +await client.join(...); +const stream = client.getMediaStream(); // Works +``` + +### Use attachVideo() NOT renderVideo() + +`renderVideo()` is **deprecated**. Use `attachVideo()` which returns a VideoPlayer element: + +```javascript +import { VideoQuality } from '@zoom/videosdk'; + +// CORRECT: attachVideo returns element to append +const videoElement = await stream.attachVideo(userId, VideoQuality.Video_360P); +document.getElementById('video-container').appendChild(videoElement); + +// WRONG: renderVideo is deprecated +await stream.renderVideo(canvas, userId, ...); // Don't use! +``` + +### Video Rendering is Event-Driven (CRITICAL) + +You MUST listen for events to properly render participant videos: + +```javascript +// When another participant's video state changes +client.on('peer-video-state-change', async (payload) => { + const { action, userId } = payload; + + if (action === 'Start') { + // Participant turned on video - attach it + const element = await stream.attachVideo(userId, VideoQuality.Video_360P); + container.appendChild(element); + } else if (action === 'Stop') { + // Participant turned off video - detach it + await stream.detachVideo(userId); + } +}); + +// When participants join/leave +client.on('user-added', (payload) => { + // New participant joined - check if their video is on + const users = client.getAllUser(); + // Render videos for users with bVideoOn === true +}); + +client.on('user-removed', (payload) => { + // Participant left - clean up their video element + stream.detachVideo(payload[0].userId); +}); +``` + +### Peer Video on Mid-Session Join + +**Existing participants' videos won't auto-render when you join mid-session.** + +```javascript +// After joining, render existing participants' videos +const renderExistingVideos = async () => { + await new Promise(resolve => setTimeout(resolve, 500)); + + const users = client.getAllUser(); + const currentUserId = client.getCurrentUserInfo().userId; + + for (const user of users) { + if (user.bVideoOn && user.userId !== currentUserId) { + const element = await stream.attachVideo(user.userId, VideoQuality.Video_360P); + document.getElementById(`video-${user.userId}`).appendChild(element); + } + } +}; +``` + +### CDN Race Condition with ES Modules + +When using ` + + +``` + +### Nuxt 3 Plugin (Client-Only) + +```typescript +// plugins/videosdk.client.ts +import ZoomVideo from '@zoom/videosdk'; + +export default defineNuxtPlugin(() => { + return { + provide: { + zoomVideo: ZoomVideo, + }, + }; +}); +``` + +```vue + + +``` + +--- + +## Zoom For Government (ZFG) + +If using Zoom For Government, you need a separate SDK key from [marketplace.zoomgov.com](https://marketplace.zoomgov.com/). + +### Option 1: ZFG-Specific Package Version + +```json +{ + "dependencies": { + "@zoom/videosdk": "1.11.0-zfg" + } +} +``` + +```javascript +client.init('en-US', 'Global'); +``` + +### Option 2: Custom WebEndpoint + +```javascript +client.init('en-US', 'https://source.zoomgov.com/videosdk/1.11.0/lib', { + webEndpoint: 'www.zoomgov.com', +}); +``` + +--- + +## Official Sample Repositories + +| Framework | Repository | Branch | +|-----------|------------|--------| +| Vanilla JS/TS | [videosdk-web-sample](https://github.com/zoom/videosdk-web-sample) | master | +| React | [videosdk-react](https://github.com/zoom/videosdk-react) | main | +| Next.js (App) | [videosdk-nextjs-quickstart](https://github.com/zoom/videosdk-nextjs-quickstart) | app-router | +| Next.js (Pages) | [videosdk-nextjs-quickstart](https://github.com/zoom/videosdk-nextjs-quickstart) | pages-router | +| Vue/Nuxt | [videosdk-vue-nuxt-quickstart](https://github.com/zoom/videosdk-vue-nuxt-quickstart) | main | +| UI Toolkit (React) | [videosdk-zoom-ui-toolkit-react-sample](https://github.com/zoom/videosdk-zoom-ui-toolkit-react-sample) | main | +| Auth Endpoint | [videosdk-auth-endpoint-sample](https://github.com/zoom/videosdk-auth-endpoint-sample) | main | + +--- + +## Common Patterns Across Frameworks + +### 1. Client-Side Only + +The SDK must run client-side. All frameworks need to handle this: + +| Framework | Solution | +|-----------|----------| +| Next.js | `'use client'` or `dynamic(..., { ssr: false })` | +| Nuxt 3 | `.client.ts` plugin or `` | +| Vue SPA | No special handling needed | + +### 2. Lifecycle Management + +```typescript +// Always follow this order: +const client = ZoomVideo.createClient(); +await client.init(...); +await client.join(...); +const stream = client.getMediaStream(); // ONLY after join() + +// Cleanup on unmount +await client.leave(); +ZoomVideo.destroyClient(); +``` + +### 3. Event-Driven Video Rendering + +```typescript +// All frameworks should use this pattern +client.on('peer-video-state-change', async ({ action, userId }) => { + if (action === 'Start') { + const el = await stream.attachVideo(userId, VideoQuality.Video_360P); + container.appendChild(el); + } else { + await stream.detachVideo(userId); + } +}); +``` + +## Related Documentation + +- [React Hooks](react-hooks.md) - @zoom/videosdk-react +- [Session Join](session-join-pattern.md) - Core SDK patterns +- [Video Rendering](video-rendering.md) - attachVideo() details diff --git a/plugins/zoom-developers/skills/video-sdk/web/examples/react-hooks.md b/plugins/zoom-developers/skills/video-sdk/web/examples/react-hooks.md new file mode 100644 index 00000000..f89ae97f --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/web/examples/react-hooks.md @@ -0,0 +1,411 @@ +# React Hooks (@zoom/videosdk-react) + +Official React SDK that provides custom hooks and components for integrating Zoom Video SDK into React apps. + +## Installation + +```bash +npm install @zoom/videosdk +npm install https://github.com/zoom/videosdk-react/releases/download/v0.0.1/zoom-videosdk-react-0.0.1.tgz +``` + +**Prerequisites:** +- React 18+ +- Zoom Video SDK account and credentials + +## Quick Start + +```tsx +import { + useSession, + useSessionUsers, + VideoPlayerComponent, + VideoPlayerContainerComponent +} from '@zoom/videosdk-react'; + +function VideoChat() { + const { isInSession, isLoading, isError } = useSession( + "session123", + "your_jwt_token", + "User Name" + ); + + const participants = useSessionUsers(); + + if (isLoading) return
Joining session...
; + if (isError) return
Error joining session
; + + return ( +
+ {isInSession && ( + + {participants.map(participant => ( + + ))} + + )} +
+ ); +} +``` + +## Available Hooks + +### useSession + +Manages the complete lifecycle of a Zoom video session. + +```tsx +const { isInSession, isLoading, isError, error } = useSession( + topic, // Session topic/ID + token, // JWT authentication token + userName, // Display name + sessionPassword, // Optional session password + sessionIdleTimeoutMins, // Optional idle timeout + { + disableVideo: false, + disableAudio: false, + language: "en-US", + dependentAssets: "Global", + waitBeforeJoining: 0, // Delay before auto-joining + endSessionOnLeave: false, // End session when host leaves + } +); +``` + +**Return values:** +| Field | Type | Description | +|-------|------|-------------| +| `isInSession` | boolean | Currently in session | +| `isLoading` | boolean | Session join in progress | +| `isError` | boolean | Error occurred | +| `error` | Error | Error object if any | + +### useSessionUsers + +Provides real-time access to all session participants with reference stability. + +```tsx +const participants = useSessionUsers(); + +// participants is an array of Participant objects +participants.map(p => ( +
+ {p.displayName} - {p.bVideoOn ? 'Video On' : 'Video Off'} +
+)); +``` + +### useMyself + +Access the local user in the current session. + +```tsx +const myself = useMyself(); + +return ( +
+ {myself.userName} - {myself.bVideoOn ? 'Video On' : 'Video Off'} +
+); +``` + +### useScreenShareUsers + +Get users who are currently sharing their screen. + +```tsx +const screenshareusers = useScreenShareUsers(); + + + {screenshareusers.map(userId => ( + + ))} + +``` + +### useVideoState + +Manages video capture state and controls. + +```tsx +const { isVideoOn, toggleVideo, setVideo } = useVideoState(); + +// Toggle video on/off + + +// Set video state explicitly + +``` + +### useAudioState + +Comprehensive audio state management. + +```tsx +const { + isAudioMuted, + isCapturingAudio, + toggleMute, + toggleCapture, + setMute, + setCapture +} = useAudioState(); + +// Toggle mute + + +// Toggle audio capture + +``` + +### useScreenshare + +Manages screen sharing functionality. + +```tsx +const { ScreenshareRef, startScreenshare } = useScreenshare(); + +return ( +
+ + +
+); +``` + +## Components + +### VideoPlayerContainerComponent + +**Required container** for video players. Must wrap all `VideoPlayerComponent` instances. + +```tsx + + {participants.map(participant => ( + + ))} + +``` + +### VideoPlayerComponent + +Renders individual participant video streams. + +```tsx +const participants = useSessionUsers(); + + +``` + +### ScreenShareContainerComponent + +**Required container** for screen share players. + +```tsx + + {screenshareusers.map(userId => ( + + ))} + +``` + +### ScreenSharePlayerComponent + +Renders screen share streams. + +```tsx + +``` + +## Complete Example + +```tsx +import React from 'react'; +import { + useSession, + useSessionUsers, + useMyself, + useVideoState, + useAudioState, + useScreenshare, + useScreenShareUsers, + VideoPlayerComponent, + VideoPlayerContainerComponent, + ScreenSharePlayerComponent, + ScreenShareContainerComponent, + LocalScreenShareComponent +} from '@zoom/videosdk-react'; + +interface VideoCallProps { + topic: string; + token: string; + userName: string; +} + +export const VideoCall: React.FC = ({ topic, token, userName }) => { + // Session management + const { isInSession, isLoading, isError, error } = useSession( + topic, + token, + userName, + undefined, // no password + undefined, // default idle timeout + { + disableVideo: false, + disableAudio: false, + } + ); + + // Participants + const participants = useSessionUsers(); + const myself = useMyself(); + const screenshareUsers = useScreenShareUsers(); + + // Media controls + const { isVideoOn, toggleVideo } = useVideoState(); + const { isAudioMuted, isCapturingAudio, toggleMute, toggleCapture } = useAudioState(); + const { ScreenshareRef, startScreenshare } = useScreenshare(); + + // Loading state + if (isLoading) { + return
Joining session...
; + } + + // Error state + if (isError) { + return
Error: {error?.message}
; + } + + // Not in session + if (!isInSession) { + return
Not in session
; + } + + return ( +
+ {/* Video Grid */} + + {participants.map(participant => ( +
+ +
{participant.displayName}
+
+ ))} +
+ + {/* Screen Share */} + {screenshareUsers.length > 0 && ( + + {screenshareUsers.map(userId => ( + + ))} + + )} + + {/* Local Screen Share Preview */} + + + {/* Controls */} +
+ {/* Audio */} + {!isCapturingAudio ? ( + + ) : ( + + )} + + {/* Video */} + + + {/* Screen Share */} + +
+ + {/* Participant Info */} +
+

Logged in as: {myself?.userName}

+

Participants: {participants.length}

+
+
+ ); +}; +``` + +## Interoperability with @zoom/videosdk + +The React SDK is designed to work alongside the core `@zoom/videosdk`. You can use both: + +```tsx +import ZoomVideo from '@zoom/videosdk'; +import { useSession, useSessionUsers } from '@zoom/videosdk-react'; + +// Use React hooks for common patterns +const { isInSession } = useSession(topic, token, userName); +const participants = useSessionUsers(); + +// Access the underlying client for advanced features +const client = ZoomVideo.createClient(); +const chatClient = client.getChatClient(); +const recordingClient = client.getRecordingClient(); +``` + +## Project Structure + +``` +src/ +├── components/ # React components +│ ├── VideoPlayerComponent +│ ├── VideoPlayerContainerComponent +│ ├── ScreenSharePlayerComponent +│ ├── ScreenShareContainerComponent +│ └── LocalScreenShareComponent +├── hooks/ # Custom React hooks +│ ├── useSession +│ ├── useSessionUsers +│ ├── useMyself +│ ├── useVideoState +│ ├── useAudioState +│ ├── useScreenshare +│ └── useScreenShareUsers +└── index.ts # Main exports +``` + +## Key Benefits + +| Benefit | Description | +|---------|-------------| +| **Simplified State** | Automatic participant state management | +| **Reference Stability** | Hooks maintain stable references | +| **TypeScript Support** | Full type definitions included | +| **Flexible** | Use alongside core SDK | +| **Customizable** | Components accept standard React props | + +## Official Repository + +- **GitHub**: [zoom/videosdk-react](https://github.com/zoom/videosdk-react) + +## Related Documentation + +- [Session Join Pattern](session-join-pattern.md) - Manual SDK usage +- [Video Rendering](video-rendering.md) - Manual attachVideo() patterns +- [Event Handling](event-handling.md) - Event patterns diff --git a/plugins/zoom-developers/skills/video-sdk/web/examples/recording.md b/plugins/zoom-developers/skills/video-sdk/web/examples/recording.md new file mode 100644 index 00000000..9881b1f5 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/web/examples/recording.md @@ -0,0 +1,435 @@ +# Cloud Recording + +Complete guide to controlling cloud recording in the Zoom Video SDK for Web. + +## Prerequisites + +- Cloud recording must be enabled on your Zoom account +- User must have recording privileges (typically host/manager) + +## Getting the Recording Client + +```javascript +// Get recording client after joining session +const recordingClient = client.getRecordingClient(); +``` + +## Basic Operations + +### Check Recording Capability + +```javascript +// Check if cloud recording is available +const canRecord = recordingClient.canStartRecording(); + +if (!canRecord) { + console.log('Cloud recording not enabled for this session'); +} +``` + +### Start Recording + +```javascript +try { + await recordingClient.startCloudRecording(); + console.log('Recording started'); +} catch (error) { + console.error('Failed to start recording:', error); +} +``` + +### Stop Recording + +```javascript +try { + await recordingClient.stopCloudRecording(); + console.log('Recording stopped'); +} catch (error) { + console.error('Failed to stop recording:', error); +} +``` + +### Pause/Resume Recording + +```javascript +// Pause +await recordingClient.pauseCloudRecording(); + +// Resume +await recordingClient.resumeCloudRecording(); +``` + +### Get Recording Status + +```javascript +import { RecordingStatus } from '@zoom/videosdk'; + +const status = recordingClient.getCloudRecordingStatus(); + +switch (status) { + case RecordingStatus.Recording: + console.log('Currently recording'); + break; + case RecordingStatus.Paused: + console.log('Recording paused'); + break; + case RecordingStatus.Stopped: + console.log('Not recording'); + break; +} +``` + +## Recording Events + +### Recording State Changes + +```javascript +client.on('recording-change', (payload) => { + const { state } = payload; + + switch (state) { + case 'Recording': + showRecordingIndicator(); + break; + case 'Paused': + showPausedIndicator(); + break; + case 'Stopped': + hideRecordingIndicator(); + break; + } +}); +``` + +### Individual Recording Consent + +When individual recording is enabled, participants must consent: + +```javascript +client.on('individual-recording-change', (payload) => { + const { state, userId } = payload; + + switch (state) { + case 'Ask': + // Host is asking you to accept individual recording + showRecordingConsentDialog(); + break; + case 'Accept': + // User accepted recording + console.log('User', userId, 'accepted recording'); + break; + case 'Decline': + // User declined recording + console.log('User', userId, 'declined recording'); + break; + } +}); +``` + +### Respond to Recording Consent + +```javascript +// Accept individual recording +await recordingClient.acceptIndividualRecording(); + +// Decline individual recording +await recordingClient.declineIndividualRecording(); +``` + +## Complete Recording Manager + +```javascript +import { RecordingStatus } from '@zoom/videosdk'; + +class RecordingManager { + constructor(client) { + this.client = client; + this.recordingClient = null; + this.currentStatus = RecordingStatus.Stopped; + this.onStatusChange = null; + this.onConsentRequired = null; + } + + init() { + this.recordingClient = this.client.getRecordingClient(); + this.currentStatus = this.recordingClient.getCloudRecordingStatus(); + this.setupEventListeners(); + } + + setupEventListeners() { + // Recording state changes + this.client.on('recording-change', (payload) => { + this.currentStatus = payload.state; + + if (this.onStatusChange) { + this.onStatusChange(payload.state); + } + }); + + // Individual recording consent + this.client.on('individual-recording-change', (payload) => { + if (payload.state === 'Ask' && this.onConsentRequired) { + this.onConsentRequired(); + } + }); + } + + canStartRecording() { + return this.recordingClient.canStartRecording(); + } + + isRecording() { + return this.currentStatus === 'Recording' || + this.currentStatus === RecordingStatus.Recording; + } + + isPaused() { + return this.currentStatus === 'Paused' || + this.currentStatus === RecordingStatus.Paused; + } + + async start() { + if (!this.canStartRecording()) { + throw new Error('Cloud recording not available'); + } + + if (this.isRecording()) { + throw new Error('Already recording'); + } + + await this.recordingClient.startCloudRecording(); + } + + async stop() { + if (!this.isRecording() && !this.isPaused()) { + throw new Error('Not currently recording'); + } + + await this.recordingClient.stopCloudRecording(); + } + + async pause() { + if (!this.isRecording()) { + throw new Error('Not currently recording'); + } + + await this.recordingClient.pauseCloudRecording(); + } + + async resume() { + if (!this.isPaused()) { + throw new Error('Recording not paused'); + } + + await this.recordingClient.resumeCloudRecording(); + } + + async acceptConsent() { + await this.recordingClient.acceptIndividualRecording(); + } + + async declineConsent() { + await this.recordingClient.declineIndividualRecording(); + } + + getStatus() { + return this.recordingClient.getCloudRecordingStatus(); + } +} + +// Usage +const recordingManager = new RecordingManager(client); +recordingManager.init(); + +recordingManager.onStatusChange = (status) => { + updateRecordingUI(status); +}; + +recordingManager.onConsentRequired = () => { + showConsentDialog({ + onAccept: () => recordingManager.acceptConsent(), + onDecline: () => recordingManager.declineConsent(), + }); +}; + +// UI handlers +document.getElementById('start-recording').onclick = async () => { + try { + await recordingManager.start(); + } catch (error) { + alert(error.message); + } +}; + +document.getElementById('stop-recording').onclick = async () => { + try { + await recordingManager.stop(); + } catch (error) { + alert(error.message); + } +}; +``` + +## React Component + +```typescript +import React, { useState, useEffect } from 'react'; +import { VideoClient, RecordingStatus } from '@zoom/videosdk'; + +interface RecordingControlsProps { + client: typeof VideoClient; + isHost: boolean; +} + +export const RecordingControls: React.FC = ({ + client, + isHost +}) => { + const [status, setStatus] = useState(RecordingStatus.Stopped); + const [canRecord, setCanRecord] = useState(false); + const [showConsent, setShowConsent] = useState(false); + + const recordingClient = client.getRecordingClient(); + + useEffect(() => { + // Initial state + setStatus(recordingClient.getCloudRecordingStatus()); + setCanRecord(recordingClient.canStartRecording()); + + // Listen for changes + const handleRecordingChange = (payload: { state: RecordingStatus }) => { + setStatus(payload.state); + }; + + const handleIndividualRecording = (payload: { state: string }) => { + if (payload.state === 'Ask') { + setShowConsent(true); + } + }; + + client.on('recording-change', handleRecordingChange); + client.on('individual-recording-change', handleIndividualRecording); + + return () => { + client.off('recording-change', handleRecordingChange); + client.off('individual-recording-change', handleIndividualRecording); + }; + }, [client, recordingClient]); + + const startRecording = async () => { + try { + await recordingClient.startCloudRecording(); + } catch (error) { + console.error('Start recording failed:', error); + } + }; + + const stopRecording = async () => { + try { + await recordingClient.stopCloudRecording(); + } catch (error) { + console.error('Stop recording failed:', error); + } + }; + + const pauseRecording = async () => { + try { + await recordingClient.pauseCloudRecording(); + } catch (error) { + console.error('Pause recording failed:', error); + } + }; + + const resumeRecording = async () => { + try { + await recordingClient.resumeCloudRecording(); + } catch (error) { + console.error('Resume recording failed:', error); + } + }; + + const acceptConsent = async () => { + await recordingClient.acceptIndividualRecording(); + setShowConsent(false); + }; + + const declineConsent = async () => { + await recordingClient.declineIndividualRecording(); + setShowConsent(false); + }; + + const isRecording = status === RecordingStatus.Recording; + const isPaused = status === RecordingStatus.Paused; + + return ( +
+ {/* Recording indicator */} + {(isRecording || isPaused) && ( +
+ + {isPaused ? 'Recording Paused' : 'Recording'} +
+ )} + + {/* Host controls */} + {isHost && canRecord && ( +
+ {!isRecording && !isPaused && ( + + )} + + {isRecording && ( + <> + + + + )} + + {isPaused && ( + <> + + + + )} +
+ )} + + {/* Consent dialog */} + {showConsent && ( +
+

The host would like to record this session. Do you consent?

+ + +
+ )} +
+ ); +}; +``` + +## Key Points + +1. **Check `canStartRecording()` first** - Recording must be enabled on the account +2. **Only host/manager can control recording** - Regular participants can only consent +3. **Handle consent for individual recording** - Listen to `individual-recording-change` +4. **Show recording indicator** - Let participants know they're being recorded +5. **Recordings are available in Zoom portal** - After session ends, recordings appear in account + +## Error Handling + +```javascript +try { + await recordingClient.startCloudRecording(); +} catch (error) { + if (error.type === 'INVALID_OPERATION') { + // Recording not available or already recording + } else if (error.type === 'INSUFFICIENT_PRIVILEGES') { + // User doesn't have permission to record + } +} +``` + +## Related Documentation + +- [Event Handling](event-handling.md) - Recording events +- [API Reference](../references/web-reference.md) - Full RecordingClient API diff --git a/plugins/zoom-developers/skills/video-sdk/web/examples/screen-share.md b/plugins/zoom-developers/skills/video-sdk/web/examples/screen-share.md new file mode 100644 index 00000000..c19bcdb7 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/web/examples/screen-share.md @@ -0,0 +1,433 @@ +# Screen Share + +Complete guide to sending and receiving screen shares in the Zoom Video SDK for Web. + +## Overview + +Screen sharing involves two distinct operations: +1. **Sending** - Sharing your screen to others (`startShareScreen`) +2. **Receiving** - Viewing others' shared screens (`startShareView` or `attachShareView`) + +## Sending Screen Share + +### Basic Screen Share + +```javascript +// Get the media stream +const stream = client.getMediaStream(); + +// Create a canvas or video element to render the share preview +const shareCanvas = document.getElementById('share-canvas'); + +// Start sharing +await stream.startShareScreen(shareCanvas); +``` + +### With Options + +```javascript +await stream.startShareScreen(shareCanvas, { + displaySurface: 'monitor', // 'monitor' | 'window' | 'browser' + audio: true, // Share system/tab audio + optimizedForSharedVideo: true, // Optimize for video content +}); +``` + +### Stop Sharing + +```javascript +await stream.stopShareScreen(); +``` + +### Pause/Resume + +```javascript +// Pause +await stream.pauseShareScreen(); + +// Resume +await stream.resumeShareScreen(); +``` + +## Receiving Screen Share + +### Method 1: Canvas-based (Legacy) + +```javascript +// Create a canvas for rendering +const receiveCanvas = document.getElementById('share-view-canvas'); + +// Listen for active share changes +client.on('active-share-change', async (payload) => { + const { state, userId } = payload; + + if (state === 'Active') { + // Someone started sharing - render it + await stream.startShareView(receiveCanvas, userId); + } else if (state === 'Inactive') { + // Sharing stopped + await stream.stopShareView(); + } +}); +``` + +### Method 2: VideoPlayer-based (Recommended - SDK 2.2.10+) + +```javascript +// Listen for active share changes +client.on('active-share-change', async (payload) => { + const { state, userId } = payload; + + if (state === 'Active') { + // Attach share view - returns a VideoPlayer element + const element = await stream.attachShareView(userId); + document.getElementById('share-container').appendChild(element); + } else if (state === 'Inactive') { + // Detach share view + const elements = await stream.detachShareView(userId); + if (Array.isArray(elements)) { + elements.forEach(e => e.remove()); + } else if (elements) { + elements.remove(); + } + } +}); +``` + +## Complete Example + +```javascript +import ZoomVideo from '@zoom/videosdk'; + +class ScreenShareManager { + constructor(client) { + this.client = client; + this.stream = null; + this.isSharing = false; + this.activeShareUserId = null; + } + + init() { + this.stream = this.client.getMediaStream(); + this.setupEventListeners(); + } + + setupEventListeners() { + // When someone starts/stops sharing + this.client.on('active-share-change', async (payload) => { + const { state, userId } = payload; + console.log('Share change:', state, 'from user:', userId); + + if (state === 'Active') { + this.activeShareUserId = userId; + await this.renderReceivedShare(userId); + } else { + await this.stopRenderingShare(); + this.activeShareUserId = null; + } + }); + + // When share content changes (e.g., user switches windows) + this.client.on('share-content-change', (payload) => { + console.log('Share content changed for user:', payload.userId); + }); + + // When share dimension changes + this.client.on('share-content-dimension-change', (payload) => { + console.log('Share dimension:', payload.width, 'x', payload.height); + // Resize container if needed + }); + + // When you're forced to stop sharing + this.client.on('passively-stop-share', (payload) => { + console.log('Forced to stop sharing. Reason:', payload); + this.isSharing = false; + }); + + // Share privilege changes + this.client.on('share-privilege-change', (payload) => { + console.log('Share privilege:', payload.privilege); + }); + } + + async startSharing() { + // Check privilege first + const privilege = this.stream.getSharePrivilege(); + if (privilege === 0) { + throw new Error('Sharing not allowed - host has disabled it'); + } + + // Check if share is locked + if (this.stream.isShareLocked()) { + throw new Error('Sharing is locked by host'); + } + + const canvas = document.getElementById('share-preview-canvas'); + + try { + await this.stream.startShareScreen(canvas, { + audio: true, + }); + this.isSharing = true; + console.log('Screen share started'); + } catch (error) { + console.error('Failed to start screen share:', error); + + if (error.type === 'INVALID_OPERATION' && error.reason?.includes('extension')) { + // Chrome extension required (legacy browsers) + console.log('Install extension from:', error.extensionUrl); + } + + throw error; + } + } + + async stopSharing() { + if (!this.isSharing) return; + + await this.stream.stopShareScreen(); + this.isSharing = false; + console.log('Screen share stopped'); + } + + async renderReceivedShare(userId) { + const container = document.getElementById('share-view-container'); + + try { + // Use attachShareView for VideoPlayer-based rendering (SDK 2.2.10+) + const element = await this.stream.attachShareView(userId); + container.innerHTML = ''; + container.appendChild(element); + container.style.display = 'block'; + } catch (error) { + console.error('Failed to render share view:', error); + } + } + + async stopRenderingShare() { + if (!this.activeShareUserId) return; + + const container = document.getElementById('share-view-container'); + container.style.display = 'none'; + + try { + await this.stream.detachShareView(this.activeShareUserId); + container.innerHTML = ''; + } catch (error) { + console.error('Failed to detach share view:', error); + } + } + + // Get who is currently sharing + getShareUserList() { + return this.stream.getShareUserList(); + } + + // Get active sharer's user ID + getActiveShareUserId() { + return this.stream.getActiveShareUserId(); + } +} + +// Usage +const shareManager = new ScreenShareManager(client); +shareManager.init(); + +// Start sharing +document.getElementById('share-btn').onclick = () => shareManager.startSharing(); +document.getElementById('stop-share-btn').onclick = () => shareManager.stopSharing(); +``` + +## Share with Audio + +```javascript +// Share screen with system/tab audio +await stream.startShareScreen(canvas, { + audio: true, +}); + +// Check share audio status +const audioStatus = stream.getShareAudioStatus(); +console.log('Share audio:', audioStatus); + +// Mute/unmute share audio +await stream.muteShareAudio(); // Mute +await stream.unmuteShareAudio(); // Unmute +``` + +## Share Privilege Management (Host Only) + +```javascript +import { SharePrivilege } from '@zoom/videosdk'; + +// Get current privilege +const privilege = stream.getSharePrivilege(); + +// Set privilege (host only) +await stream.setSharePrivilege(SharePrivilege.Unlocked); // Anyone can share +await stream.setSharePrivilege(SharePrivilege.Locked); // Only host can share +await stream.setSharePrivilege(SharePrivilege.OneParticipant); // One at a time + +// Lock/unlock share (host only) +await stream.lockShare(true); // Only host can share +await stream.lockShare(false); // Anyone can share +``` + +## Multiple Share Views (SDK 2.2.10+) + +```javascript +// Check max renderable share views +const maxViews = stream.getMaxRenderableShareViews(); +console.log('Can render up to', maxViews, 'share views'); + +// Get list of users who are sharing +const sharers = stream.getShareUserList(); + +// Render multiple share views +for (const sharer of sharers) { + const element = await stream.attachShareView(sharer.userId); + document.getElementById('multi-share-container').appendChild(element); +} +``` + +## Share Quality Optimization + +```javascript +// Optimize for video content (movies, animations) +await stream.enableOptimizeForSharedVideo(true); + +// Check if optimized +const isOptimized = stream.isOptimizeForSharedVideoEnabled(); + +// Check if optimization is supported +const isSupported = stream.isSupportOptimizedForSharedVideo(); + +// Update shared video quality +await stream.updateSharedVideoQuality(VideoQuality.Video_720P); +``` + +## Screenshot Share View + +```javascript +// Take screenshot of active share +const blob = await stream.screenshotShareView(); + +// Convert to image +const url = URL.createObjectURL(blob); +const img = document.createElement('img'); +img.src = url; +document.body.appendChild(img); +``` + +## React Component + +```typescript +import React, { useEffect, useRef, useState } from 'react'; +import { VideoClient, Stream } from '@zoom/videosdk'; + +interface ScreenShareProps { + client: typeof VideoClient; + stream: typeof Stream; +} + +export const ScreenShare: React.FC = ({ client, stream }) => { + const containerRef = useRef(null); + const [isSharing, setIsSharing] = useState(false); + const [activeShareUserId, setActiveShareUserId] = useState(null); + + useEffect(() => { + const handleShareChange = async (payload: { state: string; userId: number }) => { + if (payload.state === 'Active') { + setActiveShareUserId(payload.userId); + + if (containerRef.current) { + const element = await stream.attachShareView(payload.userId); + containerRef.current.innerHTML = ''; + containerRef.current.appendChild(element); + } + } else { + if (activeShareUserId) { + await stream.detachShareView(activeShareUserId); + } + setActiveShareUserId(null); + if (containerRef.current) { + containerRef.current.innerHTML = ''; + } + } + }; + + const handlePassiveStop = () => { + setIsSharing(false); + }; + + client.on('active-share-change', handleShareChange); + client.on('passively-stop-share', handlePassiveStop); + + return () => { + client.off('active-share-change', handleShareChange); + client.off('passively-stop-share', handlePassiveStop); + }; + }, [client, stream, activeShareUserId]); + + const startShare = async () => { + try { + const canvas = document.createElement('canvas'); + await stream.startShareScreen(canvas); + setIsSharing(true); + } catch (error) { + console.error('Share failed:', error); + } + }; + + const stopShare = async () => { + await stream.stopShareScreen(); + setIsSharing(false); + }; + + return ( +
+
+ {isSharing ? ( + + ) : ( + + )} +
+ + {activeShareUserId && ( +
+ )} +
+ ); +}; +``` + +## Key Events + +| Event | When | Payload | +|-------|------|---------| +| `active-share-change` | Someone starts/stops sharing | `{ state: 'Active' \| 'Inactive', userId }` | +| `share-content-change` | Sharer switches window/tab | `{ userId }` | +| `share-content-dimension-change` | Share resolution changes | `{ width, height, type }` | +| `passively-stop-share` | You're forced to stop | `PassiveStopShareReason` enum | +| `share-privilege-change` | Host changes share settings | `{ privilege }` | +| `peer-share-state-change` | Peer share state changes | `{ action: 'Start' \| 'Stop', userId }` | +| `share-audio-change` | Share audio state changes | `{ state: 'on' \| 'off' }` | + +## Key Points + +1. **Two methods for receiving**: Canvas-based (`startShareView`) or VideoPlayer-based (`attachShareView`) +2. **Check privileges first**: Use `getSharePrivilege()` and `isShareLocked()` before attempting to share +3. **Handle `passively-stop-share`**: You may be forced to stop sharing by the host +4. **Share audio requires user gesture**: Browsers require user interaction to share audio +5. **Different from video**: Screen share uses separate canvas/container from video + +## Related Documentation + +- [Video Rendering](video-rendering.md) - Video handling +- [Event Handling](event-handling.md) - All events +- [Common Issues](../troubleshooting/common-issues.md) - Troubleshooting diff --git a/plugins/zoom-developers/skills/video-sdk/web/examples/session-join-pattern.md b/plugins/zoom-developers/skills/video-sdk/web/examples/session-join-pattern.md new file mode 100644 index 00000000..13f8aaf4 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/web/examples/session-join-pattern.md @@ -0,0 +1,389 @@ +# Session Join Pattern + +Complete working example for joining a Zoom Video SDK session. + +## Prerequisites + +1. Video SDK credentials from [Marketplace](https://marketplace.zoom.us/) +2. JWT token generated server-side +3. Modern browser (Chrome 80+, Firefox 75+, Safari 14+, Edge 80+) + +## Complete Example + +```javascript +import ZoomVideo, { VideoQuality } from '@zoom/videosdk'; + +// State +let client = null; +let stream = null; + +// Configuration +const config = { + language: 'en-US', + dependentAssets: 'Global', // or 'CDN', 'CN', or custom path + options: { + patchJsMedia: true, + // webrtc: true, // Enable for HD video + } +}; + +/** + * Initialize the SDK (call once) + */ +async function initializeSDK() { + // Step 1: Create client (singleton) + client = ZoomVideo.createClient(); + + // Optional: Check compatibility first + const compatibility = ZoomVideo.checkSystemRequirements(); + if (!compatibility.video || !compatibility.audio) { + throw new Error('Browser not compatible'); + } + + // Step 2: Initialize + await client.init(config.language, config.dependentAssets, config.options); + + console.log('SDK initialized successfully'); + return client; +} + +/** + * Join a session + * @param {string} topic - Session name (must match JWT tpc) + * @param {string} signature - Video SDK JWT from server + * @param {string} userName - Display name + * @param {string} password - Optional session password + */ +async function joinSession(topic, signature, userName, password = '') { + try { + // Step 3: Join session + await client.join(topic, signature, userName, password); + + // Step 4: Get stream (ONLY AFTER JOIN!) + stream = client.getMediaStream(); + + // Step 5: Set up event listeners + setupEventListeners(); + + // Step 6: Get current user info + const currentUser = client.getCurrentUserInfo(); + console.log('Joined as:', currentUser.displayName, 'ID:', currentUser.userId); + + return { client, stream, currentUser }; + } catch (error) { + handleJoinError(error); + throw error; + } +} + +/** + * Set up all required event listeners + */ +function setupEventListeners() { + // Connection state changes + client.on('connection-change', (payload) => { + console.log('Connection state:', payload.state); + + if (payload.state === 'Closed') { + console.log('Disconnected. Reason:', payload.reason); + cleanup(); + } + + if (payload.state === 'Reconnecting') { + console.log('Reconnecting...', payload.reason); + } + }); + + // New participant joined + client.on('user-added', (payload) => { + console.log('User(s) joined:', payload.map(u => u.displayName)); + // Create UI containers for new users + }); + + // Participant left + client.on('user-removed', (payload) => { + console.log('User(s) left:', payload.map(u => u.displayName)); + // Clean up UI for users + payload.forEach(user => { + stream.detachVideo(user.userId); + }); + }); + + // Participant updated (name, mute, etc.) + client.on('user-updated', (payload) => { + console.log('User(s) updated:', payload); + // Update UI state + }); + + // Peer video state change (CRITICAL for rendering) + client.on('peer-video-state-change', async (payload) => { + const { action, userId } = payload; + console.log(`User ${userId} video: ${action}`); + + if (action === 'Start') { + const element = await stream.attachVideo(userId, VideoQuality.Video_360P); + document.getElementById(`video-${userId}`)?.appendChild(element); + } else { + await stream.detachVideo(userId); + } + }); + + // Audio changes + client.on('current-audio-change', (payload) => { + console.log('Audio change:', payload.action, payload.type); + }); + + // Active speaker + client.on('active-speaker', (payload) => { + if (payload.length > 0) { + console.log('Active speaker:', payload[0].userId); + } + }); +} + +/** + * Start media after joining + */ +async function startMedia() { + // Start video + try { + await stream.startVideo(); + console.log('Video started'); + + // Attach own video + const currentUser = client.getCurrentUserInfo(); + const myVideoElement = await stream.attachVideo( + currentUser.userId, + VideoQuality.Video_360P + ); + document.getElementById('my-video')?.appendChild(myVideoElement); + } catch (error) { + console.error('Failed to start video:', error); + } + + // Start audio + try { + await stream.startAudio(); + console.log('Audio started'); + } catch (error) { + console.error('Failed to start audio:', error); + } + + // Render existing participants (for mid-session join) + await renderExistingParticipants(); +} + +/** + * Render videos of participants who joined before us + */ +async function renderExistingParticipants() { + // Small delay to ensure all participants are loaded + await new Promise(resolve => setTimeout(resolve, 500)); + + const users = client.getAllUser(); + const currentUserId = client.getCurrentUserInfo().userId; + + for (const user of users) { + if (user.bVideoOn && user.userId !== currentUserId) { + const element = await stream.attachVideo(user.userId, VideoQuality.Video_360P); + document.getElementById(`video-${user.userId}`)?.appendChild(element); + } + } +} + +/** + * Handle join errors + */ +function handleJoinError(error) { + console.error('Join error:', error); + + const errorMessage = error.reason || error.message || 'Unknown error'; + + if (errorMessage.includes('signature')) { + console.error('Invalid signature - regenerate JWT'); + } else if (errorMessage.includes('Session')) { + console.error('Session not found - host may not have started'); + } else if (errorMessage.includes('password')) { + console.error('Invalid password'); + } else if (errorMessage.includes('Permission')) { + console.error('Permission denied - check camera/mic access'); + } +} + +/** + * Leave the session + * @param {boolean} end - If true, ends session for all (host only) + */ +async function leaveSession(end = false) { + try { + await client.leave(end); + console.log(end ? 'Session ended' : 'Left session'); + cleanup(); + } catch (error) { + console.error('Leave error:', error); + } +} + +/** + * Clean up resources + */ +function cleanup() { + stream = null; + // Remove event listeners, clear UI, etc. +} + +// Usage +async function main() { + await initializeSDK(); + + const { currentUser } = await joinSession( + 'my-session-topic', + 'YOUR_JWT_TOKEN', + 'User Name' + ); + + await startMedia(); + + console.log('Ready! User:', currentUser.displayName); +} + +main().catch(console.error); +``` + +## CDN Version + +```html + + + + Zoom Video SDK + + + + +
+
+ + + + +``` + +## React Example + +```typescript +import React, { useEffect, useRef, useState } from 'react'; +import ZoomVideo, { VideoClient, Stream, VideoQuality } from '@zoom/videosdk'; + +interface Props { + topic: string; + signature: string; + userName: string; +} + +export const VideoSession: React.FC = ({ topic, signature, userName }) => { + const [client, setClient] = useState(null); + const [stream, setStream] = useState(null); + const [isJoined, setIsJoined] = useState(false); + const videoContainerRef = useRef(null); + + // Initialize SDK + useEffect(() => { + const init = async () => { + const zmClient = ZoomVideo.createClient(); + await zmClient.init('en-US', 'Global', { patchJsMedia: true }); + setClient(zmClient); + }; + init(); + + return () => { + ZoomVideo.destroyClient(); + }; + }, []); + + // Join session + useEffect(() => { + if (!client || isJoined) return; + + const join = async () => { + await client.join(topic, signature, userName); + const mediaStream = client.getMediaStream(); + setStream(mediaStream); + setIsJoined(true); + }; + join(); + }, [client, topic, signature, userName, isJoined]); + + // Set up event listeners + useEffect(() => { + if (!client || !stream) return; + + const handleVideoChange = async (payload: { action: string; userId: number }) => { + const { action, userId } = payload; + if (action === 'Start') { + const element = await stream.attachVideo(userId, VideoQuality.Video_360P); + videoContainerRef.current?.appendChild(element); + } else { + await stream.detachVideo(userId); + } + }; + + client.on('peer-video-state-change', handleVideoChange); + + return () => { + client.off('peer-video-state-change', handleVideoChange); + }; + }, [client, stream]); + + // Start own video + useEffect(() => { + if (!stream) return; + + const startVideo = async () => { + await stream.startVideo(); + const currentUser = client!.getCurrentUserInfo(); + const element = await stream.attachVideo(currentUser.userId, VideoQuality.Video_360P); + videoContainerRef.current?.appendChild(element); + }; + startVideo(); + }, [stream, client]); + + return ( +
+
+ +
+ ); +}; +``` + +## Key Points + +1. **Lifecycle order**: `createClient() → init() → join() → getMediaStream()` +2. **Stream timing**: ONLY call `getMediaStream()` after `join()` completes +3. **Event-driven**: Set up event listeners to handle participant video +4. **Mid-session join**: Manually render existing participants' videos +5. **Error handling**: Handle join errors gracefully + +## Related Documentation + +- [Video Rendering](video-rendering.md) - attachVideo patterns +- [Event Handling](event-handling.md) - Required events +- [Common Issues](../troubleshooting/common-issues.md) - Troubleshooting diff --git a/plugins/zoom-developers/skills/video-sdk/web/examples/transcription.md b/plugins/zoom-developers/skills/video-sdk/web/examples/transcription.md new file mode 100644 index 00000000..08c1a6d3 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/web/examples/transcription.md @@ -0,0 +1,531 @@ +# Live Transcription + +Complete guide to implementing live transcription in the Zoom Video SDK for Web. + +## Prerequisites + +- Live transcription must be enabled on your Zoom account +- Session must have live transcription feature available + +## Getting the Transcription Client + +```javascript +// Get transcription client after joining session +const transcriptionClient = client.getLiveTranscriptionClient(); +``` + +## Basic Operations + +### Start Live Transcription + +```javascript +try { + await transcriptionClient.startLiveTranscription(); + console.log('Live transcription started'); +} catch (error) { + console.error('Failed to start transcription:', error); +} +``` + +### Check Transcription Status + +```javascript +const status = transcriptionClient.getLiveTranscriptionStatus(); + +console.log('Transcription enabled:', status.isLiveTranscriptionEnabled); +console.log('Caption language:', status.captionLanguage); +console.log('Translation settings:', status.translatedSetting); +``` + +### Set Speaking Language + +```javascript +import { LiveTranscriptionLanguage } from '@zoom/videosdk'; + +// Set the language you're speaking +await transcriptionClient.setSpeakingLanguage( + LiveTranscriptionLanguage.English +); +``` + +### Set Translation Language + +```javascript +// Translate transcriptions to another language +await transcriptionClient.setTranslationLanguage( + LiveTranscriptionLanguage.Spanish +); + +// Disable translation +await transcriptionClient.setTranslationLanguage(); // No argument +``` + +## Receiving Transcription Messages + +```javascript +// Listen for transcription text +client.on('caption-message', (payload) => { + const { + msgId, // Message ID + text, // Transcribed text + userId, // Speaker's user ID + displayName, // Speaker's display name + source, // 'caption' or 'translation' + language, // Language code + done, // true = final, false = interim + timestamp, // Unix timestamp + } = payload; + + if (done) { + // Final transcription - display permanently + addFinalTranscription(displayName, text); + } else { + // Interim result - update in place + updateInterimTranscription(displayName, text); + } +}); +``` + +## Transcription Events + +### Caption Status Changes + +```javascript +client.on('caption-status', (payload) => { + const { + autoCaption, // Auto-captioning enabled + language, // Caption language name + lang, // Language code + sessionLanguage, // Session's transcription language + translationStarted, // Translation active + } = payload; + + console.log('Caption status:', payload); +}); +``` + +### Captions Enabled/Disabled + +```javascript +client.on('caption-enable', (isEnabled) => { + if (isEnabled) { + showCaptionUI(); + } else { + hideCaptionUI(); + } +}); +``` + +### Caption Language Locked + +```javascript +client.on('caption-language-lock', (isLocked) => { + if (isLocked) { + // Host has locked the transcription language + disableLanguageSelector(); + } else { + enableLanguageSelector(); + } +}); +``` + +### Host Disabled Captions + +```javascript +client.on('caption-host-disable', (isDisabled) => { + if (isDisabled) { + console.log('Host has disabled captions'); + hideCaptionUI(); + } +}); +``` + +## Get Transcription History + +```javascript +// Get full transcription history +const history = transcriptionClient.getFullTranscriptionHistory(); + +// May return Promise for large histories (100,000+ records) +if (history instanceof Promise) { + const records = await history; + displayHistory(records); +} else { + displayHistory(history); +} + +// Get latest transcription +const latest = transcriptionClient.getLatestTranscription(); +console.log('Latest:', latest); + +// Get latest translation +const latestTranslation = transcriptionClient.getLatestTranslation(); +console.log('Latest translation:', latestTranslation); +``` + +## Get Current Languages + +```javascript +// Get current transcription language +const transcriptionLang = transcriptionClient.getCurrentTranscriptionLanguage(); +console.log('Transcription language:', transcriptionLang); + +// Get current translation language +const translationLang = transcriptionClient.getCurrentTranslationLanguage(); +console.log('Translation language:', translationLang); +``` + +## Host Controls + +### Lock Transcription Language + +```javascript +// Host can lock the transcription language +await transcriptionClient.lockTranscriptionLanguage(true); // Lock +await transcriptionClient.lockTranscriptionLanguage(false); // Unlock +``` + +### Disable Captions + +```javascript +// Host can disable captions for everyone +await transcriptionClient.disableCaptions(true); // Disable +await transcriptionClient.disableCaptions(false); // Enable +``` + +## Complete Transcription Manager + +```javascript +import { LiveTranscriptionLanguage } from '@zoom/videosdk'; + +class TranscriptionManager { + constructor(client) { + this.client = client; + this.transcriptionClient = null; + this.transcriptions = []; + this.interimMap = new Map(); // Track interim results by speaker + this.onTranscriptionUpdate = null; + this.onStatusChange = null; + } + + init() { + this.transcriptionClient = this.client.getLiveTranscriptionClient(); + this.setupEventListeners(); + } + + setupEventListeners() { + // Transcription messages + this.client.on('caption-message', (payload) => { + this.handleCaptionMessage(payload); + }); + + // Status changes + this.client.on('caption-status', (payload) => { + if (this.onStatusChange) { + this.onStatusChange(payload); + } + }); + + // Captions enabled/disabled + this.client.on('caption-enable', (isEnabled) => { + if (this.onStatusChange) { + this.onStatusChange({ enabled: isEnabled }); + } + }); + } + + handleCaptionMessage(payload) { + const { userId, displayName, text, done, source, timestamp } = payload; + const key = `${userId}-${source}`; + + if (done) { + // Final result - move from interim to final + this.interimMap.delete(key); + this.transcriptions.push({ + userId, + displayName, + text, + source, + timestamp, + isFinal: true, + }); + } else { + // Interim result - update in place + this.interimMap.set(key, { + userId, + displayName, + text, + source, + timestamp, + isFinal: false, + }); + } + + if (this.onTranscriptionUpdate) { + this.onTranscriptionUpdate(this.getAllTranscriptions()); + } + } + + getAllTranscriptions() { + // Combine final transcriptions with current interim results + const interim = Array.from(this.interimMap.values()); + return [...this.transcriptions, ...interim]; + } + + async start() { + await this.transcriptionClient.startLiveTranscription(); + } + + async setSpeakingLanguage(language) { + await this.transcriptionClient.setSpeakingLanguage(language); + } + + async setTranslationLanguage(language) { + await this.transcriptionClient.setTranslationLanguage(language); + } + + async disableTranslation() { + await this.transcriptionClient.setTranslationLanguage(); + } + + getStatus() { + return this.transcriptionClient.getLiveTranscriptionStatus(); + } + + async getHistory() { + const history = this.transcriptionClient.getFullTranscriptionHistory(); + if (history instanceof Promise) { + return await history; + } + return history; + } +} + +// Usage +const transcriptionManager = new TranscriptionManager(client); +transcriptionManager.init(); + +transcriptionManager.onTranscriptionUpdate = (transcriptions) => { + renderTranscriptions(transcriptions); +}; + +// Start transcription +document.getElementById('start-captions').onclick = async () => { + await transcriptionManager.start(); +}; + +// Change language +document.getElementById('language-select').onchange = async (e) => { + await transcriptionManager.setSpeakingLanguage(e.target.value); +}; +``` + +## React Component + +```typescript +import React, { useState, useEffect, useRef } from 'react'; +import { VideoClient, LiveTranscriptionLanguage } from '@zoom/videosdk'; + +interface TranscriptionEntry { + displayName: string; + text: string; + timestamp: number; + isFinal: boolean; + source: string; +} + +interface TranscriptionProps { + client: typeof VideoClient; + isHost: boolean; +} + +export const LiveTranscription: React.FC = ({ + client, + isHost +}) => { + const [transcriptions, setTranscriptions] = useState([]); + const [interimMap] = useState(new Map()); + const [isEnabled, setIsEnabled] = useState(false); + const [speakingLanguage, setSpeakingLanguage] = useState(''); + const containerRef = useRef(null); + + const transcriptionClient = client.getLiveTranscriptionClient(); + + useEffect(() => { + // Get initial status + const status = transcriptionClient.getLiveTranscriptionStatus(); + setIsEnabled(status.isLiveTranscriptionEnabled); + + // Handle caption messages + const handleCaption = (payload: any) => { + const { userId, displayName, text, done, source, timestamp } = payload; + const key = `${userId}-${source}`; + + if (done) { + interimMap.delete(key); + setTranscriptions(prev => [...prev, { + displayName, + text, + timestamp, + isFinal: true, + source, + }]); + } else { + interimMap.set(key, { + displayName, + text, + timestamp, + isFinal: false, + source, + }); + // Force re-render + setTranscriptions(prev => [...prev]); + } + }; + + const handleEnable = (enabled: boolean) => { + setIsEnabled(enabled); + }; + + client.on('caption-message', handleCaption); + client.on('caption-enable', handleEnable); + + return () => { + client.off('caption-message', handleCaption); + client.off('caption-enable', handleEnable); + }; + }, [client, transcriptionClient, interimMap]); + + // Auto-scroll + useEffect(() => { + if (containerRef.current) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + } + }, [transcriptions]); + + const startTranscription = async () => { + await transcriptionClient.startLiveTranscription(); + }; + + const handleLanguageChange = async (e: React.ChangeEvent) => { + const language = e.target.value as LiveTranscriptionLanguage; + setSpeakingLanguage(language); + await transcriptionClient.setSpeakingLanguage(language); + }; + + // Combine final and interim transcriptions for display + const allTranscriptions = [ + ...transcriptions, + ...Array.from(interimMap.values()), + ].sort((a, b) => a.timestamp - b.timestamp); + + return ( +
+
+

Live Transcription

+ + {!isEnabled && ( + + )} + + +
+ +
+ {allTranscriptions.map((entry, index) => ( +
+ {entry.displayName}: + {entry.text} +
+ ))} +
+ + +
+ ); +}; +``` + +## Available Languages + +```typescript +// LiveTranscriptionLanguage enum +LiveTranscriptionLanguage.English +LiveTranscriptionLanguage.Spanish +LiveTranscriptionLanguage.French +LiveTranscriptionLanguage.German +LiveTranscriptionLanguage.Italian +LiveTranscriptionLanguage.Portuguese +LiveTranscriptionLanguage.Russian +LiveTranscriptionLanguage.Chinese +LiveTranscriptionLanguage.Japanese +LiveTranscriptionLanguage.Korean +// ... and more +``` + +## Key Points + +1. **Interim vs Final** - `done=false` is interim (updating), `done=true` is final +2. **Start transcription** - Call `startLiveTranscription()` to enable +3. **Set speaking language** - Tell the system what language you're speaking +4. **Translation is optional** - Use `setTranslationLanguage()` if needed +5. **Handle large histories** - `getFullTranscriptionHistory()` may return Promise + +## Key Events + +| Event | When | Payload | +|-------|------|---------| +| `caption-message` | Transcription text received | Text, speaker, done flag | +| `caption-status` | Status changes | Language, enabled state | +| `caption-enable` | Captions enabled/disabled | Boolean | +| `caption-language-lock` | Language locked by host | Boolean | +| `caption-host-disable` | Host disabled captions | Boolean | + +## Related Documentation + +- [Event Handling](event-handling.md) - Transcription events +- [API Reference](../references/web-reference.md) - Full LiveTranscriptionClient API diff --git a/plugins/zoom-developers/skills/video-sdk/web/examples/video-rendering.md b/plugins/zoom-developers/skills/video-sdk/web/examples/video-rendering.md new file mode 100644 index 00000000..9c972306 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/web/examples/video-rendering.md @@ -0,0 +1,384 @@ +# Video Rendering + +Complete guide to rendering video using `attachVideo()` in the Zoom Video SDK for Web. + +## Critical Rule + +**NEVER use `renderVideo()` - it's deprecated. Always use `attachVideo()`.** + +## VideoQuality Enum + +```typescript +import { VideoQuality } from '@zoom/videosdk'; + +// Available quality levels (value = numeric enum) +VideoQuality.Video_90P // 0 - Thumbnail +VideoQuality.Video_180P // 1 - Low quality +VideoQuality.Video_360P // 2 - Standard (recommended default) +VideoQuality.Video_720P // 3 - HD +VideoQuality.Video_1080P // 4 - Full HD (requires webrtc mode) +``` + +## Basic Video Rendering + +### Start and Render Own Video + +```javascript +import ZoomVideo, { VideoQuality } from '@zoom/videosdk'; + +const client = ZoomVideo.createClient(); +let stream; + +async function startOwnVideo() { + // Ensure you're joined first + stream = client.getMediaStream(); + + // Step 1: Start capturing video + await stream.startVideo(); + + // Step 2: Get current user ID + const currentUser = client.getCurrentUserInfo(); + + // Step 3: Attach video to DOM + const videoElement = await stream.attachVideo( + currentUser.userId, + VideoQuality.Video_360P + ); + + // Step 4: Add to container + document.getElementById('my-video-container').appendChild(videoElement); +} +``` + +### Render Remote Participant Video + +```javascript +// Listen for remote video state changes +client.on('peer-video-state-change', async (payload) => { + const { action, userId } = payload; + + if (action === 'Start') { + // Remote user started video - render it + const element = await stream.attachVideo(userId, VideoQuality.Video_360P); + + // Add to their video container + const container = document.getElementById(`video-${userId}`); + if (container) { + container.appendChild(element); + } + } else if (action === 'Stop') { + // Remote user stopped video - remove element + await stream.detachVideo(userId); + + // Clean up DOM + const container = document.getElementById(`video-${userId}`); + if (container) { + container.innerHTML = ''; + } + } +}); +``` + +## Mid-Session Join: Rendering Existing Participants + +When you join a session that already has participants with video on, you won't receive `peer-video-state-change` events for them. You must manually render their videos. + +```javascript +async function renderExistingParticipants() { + // Wait a moment for participant list to populate + await new Promise(resolve => setTimeout(resolve, 500)); + + const participants = client.getAllUser(); + const currentUserId = client.getCurrentUserInfo().userId; + + for (const participant of participants) { + // Skip self + if (participant.userId === currentUserId) continue; + + // Check if they have video on + if (participant.bVideoOn) { + const element = await stream.attachVideo( + participant.userId, + VideoQuality.Video_360P + ); + + const container = document.getElementById(`video-${participant.userId}`); + if (container) { + container.appendChild(element); + } + } + } +} + +// Call after joining and starting your own video +await startOwnVideo(); +await renderExistingParticipants(); +``` + +## Quality Selection Strategy + +```javascript +// Determine quality based on use case +function getQualityForLayout(totalParticipants, isSpotlight = false) { + if (isSpotlight) { + // Spotlighted/active speaker - highest quality + return VideoQuality.Video_720P; + } + + if (totalParticipants <= 4) { + // Small meeting - good quality for all + return VideoQuality.Video_360P; + } + + if (totalParticipants <= 9) { + // Medium meeting - balanced quality + return VideoQuality.Video_180P; + } + + // Large meeting - thumbnails + return VideoQuality.Video_90P; +} + +// Dynamic quality adjustment +async function updateVideoQualities() { + const participants = client.getAllUser().filter(p => p.bVideoOn); + const quality = getQualityForLayout(participants.length); + + for (const participant of participants) { + // Re-attach with new quality + await stream.detachVideo(participant.userId); + const element = await stream.attachVideo(participant.userId, quality); + + const container = document.getElementById(`video-${participant.userId}`); + if (container) { + container.innerHTML = ''; + container.appendChild(element); + } + } +} +``` + +## HD Video (720P/1080P) + +To use HD video, enable WebRTC mode during initialization: + +```javascript +await client.init('en-US', 'Global', { + patchJsMedia: true, + webrtc: true, // Required for HD video +}); + +// Now you can use higher qualities +const element = await stream.attachVideo(userId, VideoQuality.Video_720P); + +// Check if HD is supported on this device +if (stream.isSupportHDVideo()) { + const hdElement = await stream.attachVideo(userId, VideoQuality.Video_1080P); +} +``` + +## Multiple Video Rendering + +Check device capability for rendering multiple videos: + +```javascript +// Check max renderable videos +const maxVideos = stream.getMaxRenderableVideos(); +console.log('Can render up to', maxVideos, 'videos'); + +// Check if multiple video rendering is supported +if (stream.isSupportMultipleVideos()) { + // Can render multiple participant videos +} else { + // Limited to fewer simultaneous videos + // Consider using active speaker mode +} +``` + +## Detaching Video + +```javascript +// Detach specific user's video +const elements = await stream.detachVideo(userId); + +// elements can be a single element or array +if (Array.isArray(elements)) { + elements.forEach(el => el.remove()); +} else { + elements.remove(); +} + +// Detach from specific element +const specificElement = document.querySelector(`#video-${userId} video-player`); +await stream.detachVideo(userId, specificElement); +``` + +## Mirror Self Video + +```javascript +// Mirror your own video (selfie mode) +await stream.mirrorVideo(true); + +// Check current mirror state +const isMirrored = stream.isVideoMirrored(); +``` + +## Stop Video + +```javascript +// Stop capturing video (turns off camera) +await stream.stopVideo(); + +// The attached video element will show black/placeholder +// Detach to clean up +const currentUser = client.getCurrentUserInfo(); +await stream.detachVideo(currentUser.userId); +``` + +## Complete React Component + +```typescript +import React, { useEffect, useRef, useState } from 'react'; +import ZoomVideo, { VideoClient, Stream, VideoQuality } from '@zoom/videosdk'; + +interface VideoTileProps { + userId: number; + stream: typeof Stream; + quality?: VideoQuality; +} + +export const VideoTile: React.FC = ({ + userId, + stream, + quality = VideoQuality.Video_360P +}) => { + const containerRef = useRef(null); + const [isAttached, setIsAttached] = useState(false); + + useEffect(() => { + let mounted = true; + + const attachVideo = async () => { + if (!containerRef.current || !stream) return; + + try { + const element = await stream.attachVideo(userId, quality); + if (mounted && containerRef.current) { + containerRef.current.innerHTML = ''; + containerRef.current.appendChild(element); + setIsAttached(true); + } + } catch (error) { + console.error('Failed to attach video:', error); + } + }; + + attachVideo(); + + return () => { + mounted = false; + if (isAttached) { + stream.detachVideo(userId).catch(console.error); + } + }; + }, [userId, stream, quality, isAttached]); + + return ( +
+ ); +}; + +// Usage in parent component +export const VideoGrid: React.FC<{ client: typeof VideoClient; stream: typeof Stream }> = ({ + client, + stream +}) => { + const [participants, setParticipants] = useState([]); + + useEffect(() => { + // Initial load + setParticipants(client.getAllUser().filter(p => p.bVideoOn)); + + // Listen for changes + const handleVideoChange = async (payload: { action: string; userId: number }) => { + if (payload.action === 'Start') { + setParticipants(prev => { + const user = client.getUser(payload.userId); + if (user && !prev.find(p => p.userId === payload.userId)) { + return [...prev, user]; + } + return prev; + }); + } else { + setParticipants(prev => prev.filter(p => p.userId !== payload.userId)); + } + }; + + client.on('peer-video-state-change', handleVideoChange); + + return () => { + client.off('peer-video-state-change', handleVideoChange); + }; + }, [client]); + + const quality = participants.length <= 4 + ? VideoQuality.Video_360P + : VideoQuality.Video_180P; + + return ( +
+ {participants.map(p => ( + + ))} +
+ ); +}; +``` + +## Error Handling + +```javascript +async function safeAttachVideo(userId, quality) { + try { + const element = await stream.attachVideo(userId, quality); + return element; + } catch (error) { + console.error('attachVideo failed:', error); + + if (error.type === 'INVALID_OPERATION') { + // User may have stopped video, or not in session + console.log('User video not available'); + } else if (error.type === 'INTERNAL_ERROR') { + // SDK internal error - may need to retry + await new Promise(r => setTimeout(r, 1000)); + return stream.attachVideo(userId, quality); + } + + return null; + } +} +``` + +## Key Points + +1. **Use `attachVideo()`, NOT `renderVideo()`** - `renderVideo()` is deprecated +2. **Listen to `peer-video-state-change`** - Required for remote video +3. **Handle mid-session join** - Manually render existing participants +4. **Detach before re-attaching** - When changing quality +5. **Check device capabilities** - `getMaxRenderableVideos()`, `isSupportMultipleVideos()` +6. **Enable WebRTC for HD** - Required for 720P/1080P + +## Related Documentation + +- [Session Join](session-join-pattern.md) - Initial setup +- [Event Handling](event-handling.md) - All video events +- [Common Issues](../troubleshooting/common-issues.md) - Troubleshooting diff --git a/plugins/zoom-developers/skills/video-sdk/web/references/events-reference.md b/plugins/zoom-developers/skills/video-sdk/web/references/events-reference.md new file mode 100644 index 00000000..1b91722a --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/web/references/events-reference.md @@ -0,0 +1,696 @@ +# Events Reference + +Complete reference for all events in the Zoom Video SDK for Web. + +## Event Registration + +```javascript +// Register +client.on('event-name', (payload) => { /* handle */ }); + +// Unregister +client.off('event-name', handler); +``` + +--- + +## Session Events + +### connection-change + +Fired when connection state changes. + +```typescript +client.on('connection-change', (payload: { + state: 'Connected' | 'Connecting' | 'Reconnecting' | 'Closed' | 'Fail'; + reason?: string; +}) => {}); +``` + +| State | Description | +|-------|-------------| +| `Connected` | Successfully connected to session | +| `Connecting` | Attempting to connect | +| `Reconnecting` | Lost connection, attempting reconnect | +| `Closed` | Disconnected from session | +| `Fail` | Connection failed | + +--- + +## Participant Events + +### user-added + +Fired when participant(s) join the session. + +```typescript +client.on('user-added', (payload: Participant[]) => {}); + +interface Participant { + userId: number; + displayName: string; + avatar?: string; + isHost: boolean; + isManager: boolean; + bVideoOn: boolean; + muted: boolean; + // ... more properties +} +``` + +### user-removed + +Fired when participant(s) leave the session. + +```typescript +client.on('user-removed', (payload: Participant[]) => {}); +``` + +### user-updated + +Fired when participant properties change (name, mute, video, etc.). + +```typescript +client.on('user-updated', (payload: Participant[]) => {}); +``` + +--- + +## Video Events + +### peer-video-state-change + +Fired when a remote participant starts or stops video. + +```typescript +client.on('peer-video-state-change', (payload: { + action: 'Start' | 'Stop'; + userId: number; +}) => {}); +``` + +**Critical**: Use this to render/remove remote videos. + +### video-capturing-change + +Fired when local video capture state changes. + +```typescript +client.on('video-capturing-change', (payload: { + state: 'Started' | 'Stopped' | 'Failed'; +}) => {}); +``` + +### video-active-change + +Fired when video becomes active/inactive. + +```typescript +client.on('video-active-change', (payload: { + state: VideoActiveState; + userId: number; +}) => {}); +``` + +### video-dimension-change + +Fired when received video dimensions change. + +```typescript +client.on('video-dimension-change', (payload: { + width: number; + height: number; + type: 'received'; +}) => {}); +``` + +### video-detailed-data-change + +Fired when video quality details change. + +```typescript +client.on('video-detailed-data-change', (payload: { + userId: number; + width?: number; + height?: number; + fps?: number; + quality?: VideoQuality; +}) => {}); +``` + +### video-spotlight-change + +Fired when spotlight list changes. + +```typescript +client.on('video-spotlight-change', (payload: { + spotlightList: { userId: number }[]; +}) => {}); +``` + +--- + +## Audio Events + +### current-audio-change + +Fired when local audio state changes. + +```typescript +client.on('current-audio-change', (payload: { + action: 'join' | 'leave' | 'muted' | 'unmuted'; + type?: 'computer' | 'phone'; + source?: MutedSource | LeaveAudioSource; +}) => {}); +``` + +| Action | Description | +|--------|-------------| +| `join` | Joined audio (computer or phone) | +| `leave` | Left audio | +| `muted` | Audio muted | +| `unmuted` | Audio unmuted | + +| Source | Description | +|--------|-------------| +| `active` | User action | +| `passive(mute all)` | Host muted all | +| `passive(mute one)` | Host muted you | +| `passive` | Host unmuted you | + +### host-ask-unmute-audio + +Fired when host asks you to unmute. + +```typescript +client.on('host-ask-unmute-audio', (payload: { + reason: 'Unmute' | 'Spotlight' | 'Allow to talk'; +}) => {}); +``` + +### active-speaker + +Fired when active speakers change. + +```typescript +client.on('active-speaker', (payload: { + userId: number; + displayName: string; +}[]) => {}); +``` + +Array is sorted by volume (loudest first). + +### auto-play-audio-failed + +Fired when browser blocks audio autoplay. + +```typescript +client.on('auto-play-audio-failed', () => {}); +``` + +### current-audio-level-change + +Fired when local audio level changes. + +```typescript +client.on('current-audio-level-change', (payload: { + level: number; +}) => {}); +``` + +### speaking-while-muted + +Fired when speaking while muted is detected. + +```typescript +client.on('speaking-while-muted', () => {}); +``` + +--- + +## Screen Share Events + +### active-share-change + +Fired when someone starts/stops sharing. + +```typescript +client.on('active-share-change', (payload: { + state: 'Active' | 'Inactive'; + userId: number; +}) => {}); +``` + +### peer-share-state-change + +Fired when peer share state changes. + +```typescript +client.on('peer-share-state-change', (payload: { + action: 'Start' | 'Stop'; + userId: number; +}) => {}); +``` + +### passively-stop-share + +Fired when you're forced to stop sharing. + +```typescript +client.on('passively-stop-share', (payload: PassiveStopShareReason) => {}); + +// Reasons: 'PrivilegeChange', 'AnotherShareStarted', etc. +``` + +### share-content-change + +Fired when sharer switches windows/tabs. + +```typescript +client.on('share-content-change', (payload: { + userId: number; +}) => {}); +``` + +### share-content-dimension-change + +Fired when share dimensions change. + +```typescript +client.on('share-content-dimension-change', (payload: { + width: number; + height: number; + type: 'received' | 'sended'; +}) => {}); +``` + +### share-privilege-change + +Fired when share privilege changes. + +```typescript +client.on('share-privilege-change', (payload: { + privilege: SharePrivilege; +}) => {}); +``` + +### share-audio-change + +Fired when share audio state changes. + +```typescript +client.on('share-audio-change', (payload: { + state: 'on' | 'off'; +}) => {}); +``` + +--- + +## Chat Events + +### chat-on-message + +Fired when a chat message is received. + +```typescript +client.on('chat-on-message', (payload: { + id: string; + message: string; + sender: { userId: number; name: string; avatar?: string }; + receiver: { userId: number; name: string } | 'everyone'; + timestamp: number; + isPrivate: boolean; +}) => {}); +``` + +### chat-privilege-change + +Fired when chat privilege changes. + +```typescript +client.on('chat-privilege-change', (payload: { + chatPrivilege: ChatPrivilege; +}) => {}); +``` + +### chat-file-upload-progress + +Fired during file upload. + +```typescript +client.on('chat-file-upload-progress', (payload: { + fileName: string; + fileSize: number; + progress: number; + status: ChatFileUploadStatus; + receiverId: number; + retryToken?: string; +}) => {}); +``` + +### chat-file-download-progress + +Fired during file download. + +```typescript +client.on('chat-file-download-progress', (payload: { + fileName: string; + fileSize: number; + progress: number; + status: ChatFileDownloadStatus; + fileUrl: string; + fileBlob?: Blob; + senderId: number; +}) => {}); +``` + +--- + +## Recording Events + +### recording-change + +Fired when cloud recording state changes. + +```typescript +client.on('recording-change', (payload: { + state: RecordingStatus; +}) => {}); +``` + +| Status | Description | +|--------|-------------| +| `Recording` | Recording in progress | +| `Paused` | Recording paused | +| `Stopped` | Recording stopped | + +### individual-recording-change + +Fired for individual recording consent. + +```typescript +client.on('individual-recording-change', (payload: { + state: RecordingStatus | 'Ask' | 'Accept' | 'Decline'; + userId?: number; +}) => {}); +``` + +--- + +## Live Transcription Events + +### caption-message + +Fired when transcription text is received. + +```typescript +client.on('caption-message', (payload: { + msgId: string; + text: string; + userId: number; + displayName: string; + source: 'caption' | 'translation'; + language: string; + done: boolean; // true = final, false = interim + timestamp: number; +}) => {}); +``` + +### caption-status + +Fired when caption status changes. + +```typescript +client.on('caption-status', (payload: { + autoCaption: boolean; + language?: string; + lang?: number; + sessionLanguage?: string; + translationStarted?: boolean; +}) => {}); +``` + +### caption-enable + +Fired when captions are enabled/disabled. + +```typescript +client.on('caption-enable', (isEnabled: boolean) => {}); +``` + +### caption-language-lock + +Fired when language is locked/unlocked. + +```typescript +client.on('caption-language-lock', (isLocked: boolean) => {}); +``` + +### caption-host-disable + +Fired when host disables captions. + +```typescript +client.on('caption-host-disable', (isDisabled: boolean) => {}); +``` + +--- + +## Device Events + +### device-change + +Fired when devices are added/removed. + +```typescript +client.on('device-change', () => {}); +``` + +### device-permission-change + +Fired when device permissions change. + +```typescript +client.on('device-permission-change', (payload: { + name: 'microphone' | 'camera'; + state: 'granted' | 'denied' | 'prompt'; +}) => {}); +``` + +--- + +## Network & Quality Events + +### network-quality-change + +Fired when network quality changes. + +```typescript +client.on('network-quality-change', (payload: { + userId: number; + type: 'uplink' | 'downlink'; + level: number; // 0-5 (0=unknown, 1=bad, 5=excellent) +}) => {}); +``` + +### video-statistic-data-change + +Fired when video statistics change. + +```typescript +client.on('video-statistic-data-change', (payload: { + type: 'VIDEO_QOS_DATA'; + data: { + encoding: boolean; // true=send, false=receive + width: number; + height: number; + fps: number; + bitrate: number; + avg_loss: number; + max_loss: number; + jitter: number; + rtt: number; + }; +}) => {}); +``` + +### audio-statistic-data-change + +Fired when audio statistics change. + +```typescript +client.on('audio-statistic-data-change', (payload: { + type: 'AUDIO_QOS_DATA'; + data: { + encoding: boolean; + bitrate: number; + avg_loss: number; + max_loss: number; + jitter: number; + rtt: number; + sample_rate: number; + }; +}) => {}); +``` + +--- + +## Command Channel Events + +### command-channel-status + +Fired when command channel status changes. + +```typescript +client.on('command-channel-status', (payload: ConnectionState) => {}); +``` + +### command-channel-message + +Fired when command channel message is received. + +```typescript +client.on('command-channel-message', (payload: { + msgid: string; + senderId: string; + senderName: string; + text: string; + timestamp: number; +}) => {}); +``` + +--- + +## Live Stream Events + +### live-stream-status + +Fired when live stream status changes. + +```typescript +client.on('live-stream-status', (status: LiveStreamStatus) => {}); +``` + +--- + +## Far End Camera Control Events + +### far-end-camera-request-control + +Fired when someone requests camera control. + +```typescript +client.on('far-end-camera-request-control', (payload: { + userId: number; + displayName: string; + currentControllingUserId?: number; + currentControllingDisplayName?: string; +}) => {}); +``` + +### far-end-camera-response-control + +Fired with camera control response. + +```typescript +client.on('far-end-camera-response-control', (payload: { + userId: number; + displayName: string; + isApproved: boolean; + reason?: FarEndCameraControlDeclinedReason; +}) => {}); +``` + +### far-end-camera-capability-change + +Fired when camera capabilities change. + +```typescript +client.on('far-end-camera-capability-change', (payload: { + userId: number; + ptz: PTZCameraCapability; +}) => {}); +``` + +--- + +## Subsession Events + +### subsession-invite-to-join + +Fired when invited to join subsession. + +```typescript +client.on('subsession-invite-to-join', (payload: { + subsessionId: string; + subsessionName: string; +}) => {}); +``` + +### subsession-broadcast-message + +Fired when broadcast message received. + +```typescript +client.on('subsession-broadcast-message', (payload: { + message: string; +}) => {}); +``` + +### subsession-state-change + +Fired when subsession state changes. + +```typescript +client.on('subsession-state-change', (payload: { + status: SubsessionStatus; +}) => {}); +``` + +--- + +## Whiteboard Events + +### whiteboard-status-change + +Fired when whiteboard status changes. + +```typescript +client.on('whiteboard-status-change', (status: WhiteboardStatus) => {}); +``` + +### peer-whiteboard-state-change + +Fired when peer whiteboard state changes. + +```typescript +client.on('peer-whiteboard-state-change', (payload: { + action: 'Start' | 'Stop'; + userId: number; +}) => {}); +``` + +--- + +## Event Categories Quick Reference + +| Category | Events | +|----------|--------| +| **Session** | `connection-change` | +| **Participants** | `user-added`, `user-removed`, `user-updated` | +| **Video** | `peer-video-state-change`, `video-capturing-change`, `video-dimension-change`, `video-active-change` | +| **Audio** | `current-audio-change`, `host-ask-unmute-audio`, `active-speaker`, `auto-play-audio-failed` | +| **Share** | `active-share-change`, `passively-stop-share`, `share-privilege-change`, `share-content-change` | +| **Chat** | `chat-on-message`, `chat-privilege-change`, `chat-file-*-progress` | +| **Recording** | `recording-change`, `individual-recording-change` | +| **Transcription** | `caption-message`, `caption-status`, `caption-enable` | +| **Device** | `device-change`, `device-permission-change` | +| **Network** | `network-quality-change`, `*-statistic-data-change` | + +--- + +## Related Documentation + +- [Event Handling Examples](../examples/event-handling.md) +- [API Reference](web-reference.md) diff --git a/plugins/zoom-developers/skills/video-sdk/web/references/web-reference.md b/plugins/zoom-developers/skills/video-sdk/web/references/web-reference.md new file mode 100644 index 00000000..e1c27a3d --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/web/references/web-reference.md @@ -0,0 +1,411 @@ +# Zoom Video SDK Web - API Reference + +## Overview + +This reference provides the complete API for the Zoom Video SDK for Web. The SDK follows a hierarchical pattern: + +``` +ZoomVideo (module) + └── VideoClient (singleton) + ├── Stream (media operations) + └── Feature Clients (chat, recording, etc.) +``` + +**Official API Reference**: https://marketplacefront.zoom.us/sdk/custom/web/modules.html + +--- + +## Level 0: ZoomVideo Module + +### Static Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `createClient()` | `VideoClient` | Creates/returns the singleton client | +| `destroyClient()` | `Promise` | Destroys the client instance | +| `checkSystemRequirements()` | `MediaCompatibility` | Check browser compatibility | +| `checkFeatureRequirements()` | `SupportFeatures` | Check feature support | +| `getDevices(skip?)` | `Promise` | Enumerate media devices | +| `preloadDependentAssets(path?)` | `void` | Preload SDK assets | +| `createLocalVideoTrack(id?)` | `LocalVideoTrack` | Create local video track for preview | +| `createLocalAudioTrack(id?)` | `LocalAudioTrack` | Create local audio track for preview | +| `VERSION` | `string` | SDK version | + +### MediaCompatibility Interface + +```typescript +interface MediaCompatibility { + audio: boolean; // Audio support + video: boolean; // Video support + screen: boolean; // Screen share support +} +``` + +### SupportFeatures Interface + +```typescript +interface SupportFeatures { + platform: string; // Browser/platform info + supportFeatures: string[]; // Supported features + unSupportFeatures: string[]; // Unsupported features +} +``` + +--- + +## Level 1: VideoClient + +### Session Lifecycle + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `init` | `(language, dependentAssets, options?)` | `ExecutedResult` | Initialize SDK | +| `join` | `(topic, token, userName, password?, timeout?)` | `ExecutedResult` | Join session | +| `leave` | `(end?)` | `ExecutedResult` | Leave/end session | +| `on` | `(event, callback)` | `void` | Subscribe to events | +| `off` | `(event, callback)` | `void` | Unsubscribe from events | + +### InitOptions Interface + +```typescript +interface InitOptions { + patchJsMedia?: boolean; // Patch JS media (recommended: true) + webrtc?: boolean; // Enable WebRTC mode for HD + enforceMultipleVideos?: boolean; // Force multi-video mode + stayAwake?: boolean; // Prevent screen sleep +} +``` + +### Participant Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `getAllUser()` | `Participant[]` | Get all participants | +| `getCurrentUserInfo()` | `Participant` | Get current user | +| `getUser(userId)` | `Participant \| undefined` | Get user by ID | +| `getSessionHost()` | `Participant \| undefined` | Get session host | +| `getSessionInfo()` | `SessionInfo` | Get session info | +| `isHost()` | `boolean` | Is current user host | +| `isManager()` | `boolean` | Is current user manager | +| `isOriginalHost()` | `boolean` | Is current user original host | + +### Host Controls + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `makeHost` | `(userId)` | `ExecutedResult` | Make user host | +| `makeManager` | `(userId)` | `ExecutedResult` | Make user manager | +| `revokeManager` | `(userId)` | `ExecutedResult` | Remove manager | +| `removeUser` | `(userId)` | `ExecutedResult` | Remove user from session | +| `changeName` | `(name, userId?)` | `ExecutedResult` | Change display name | +| `reclaimHost` | `()` | `ExecutedResult` | Reclaim host (original host only) | + +### Feature Client Getters + +| Method | Returns | Description | +|--------|---------|-------------| +| `getMediaStream()` | `Stream` | Get media stream (AFTER join!) | +| `getChatClient()` | `ChatClient` | Get chat client | +| `getCommandClient()` | `CommandChannel` | Get command channel | +| `getRecordingClient()` | `RecordingClient` | Get recording client | +| `getLiveTranscriptionClient()` | `LiveTranscriptionClient` | Get transcription client | +| `getLiveStreamClient()` | `LiveStreamClient` | Get live stream client | +| `getSubsessionClient()` | `SubsessionClient` | Get subsession client | +| `getWhiteboardClient()` | `WhiteboardClient` | Get whiteboard client | +| `getBroadcastStreamingClient()` | `BroadcastStreamingClient` | Get broadcast client | +| `getRealTimeMediaStreamsClient()` | `RealTimeMediaStreamsClient` | Get RTMS client | +| `getLoggerClient(options?)` | `LoggerClient` | Get logger client | + +### Participant Interface + +```typescript +interface Participant { + userId: number; // Unique user ID + displayName: string; // Display name + bVideoOn: boolean; // Is video on + muted: boolean; // Is audio muted + audio: '' | 'computer' | 'phone'; // Audio type + sharerOn: boolean; // Is sharing screen + bShareAudioOn: boolean; // Is sharing audio + isHost: boolean; // Is host +} +``` + +--- + +## Level 2: Stream (Media Operations) + +### Video Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `startVideo` | `(options?)` | `ExecutedResult` | Start camera | +| `stopVideo` | `()` | `ExecutedResult` | Stop camera | +| `attachVideo` | `(userId, quality, element?)` | `Promise` | Attach video to DOM | +| `detachVideo` | `(userId, element?)` | `Promise` | Detach video | +| `switchCamera` | `(cameraId)` | `ExecutedResult` | Switch camera | +| `getCameraList` | `()` | `MediaDevice[]` | Get cameras | +| `getActiveCamera` | `()` | `string` | Get active camera ID | +| `mirrorVideo` | `(enable)` | `ExecutedResult` | Mirror video | +| `spotlightVideo` | `(userId)` | `ExecutedResult` | Spotlight user | +| `screenshotVideo` | `(userId?)` | `Promise` | Screenshot video | + +### Video Capability Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `isSupportHDVideo()` | `boolean` | Is HD video supported | +| `getVideoMaxQuality()` | `VideoQuality` | Get max video quality | +| `getMaxRenderableVideos()` | `number` | Max renderable videos | +| `isSupportMultipleVideos()` | `boolean` | Multiple videos support | +| `isSupportVirtualBackground()` | `boolean` | Virtual BG support | +| `isCapturingVideo()` | `boolean` | Is capturing video | + +### VideoQuality Enum + +```typescript +enum VideoQuality { + Video_90P = 0, + Video_180P = 1, + Video_360P = 2, + Video_720P = 3, + Video_1080P = 4 +} +``` + +### Virtual Background Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `updateVirtualBackgroundImage` | `(image)` | `ExecutedResult` | Set virtual background | +| `previewVirtualBackground` | `(canvas, image)` | `ExecutedResult` | Preview virtual BG | +| `stopPreviewVirtualBackground` | `()` | `ExecutedResult` | Stop preview | +| `getVirtualbackgroundStatus` | `()` | `VirtualBackgroundStatus` | Get VB status | + +**Virtual Background Options:** +- `'blur'`: Blur background +- `'https://example.com/image.jpg'`: Custom image URL +- `undefined`: Remove virtual background + +### Audio Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `startAudio` | `(options?)` | `ExecutedResult` | Start audio | +| `stopAudio` | `()` | `ExecutedResult` | Stop audio | +| `muteAudio` | `(userId?)` | `ExecutedResult` | Mute audio | +| `unmuteAudio` | `(userId?)` | `ExecutedResult` | Unmute audio | +| `muteAllAudio` | `()` | `ExecutedResult` | Mute all (host) | +| `unmuteAllAudio` | `()` | `ExecutedResult` | Unmute all (host) | +| `switchMicrophone` | `(micId)` | `ExecutedResult` | Switch microphone | +| `switchSpeaker` | `(speakerId)` | `ExecutedResult` | Switch speaker | +| `getMicList` | `()` | `MediaDevice[]` | Get microphones | +| `getSpeakerList` | `()` | `MediaDevice[]` | Get speakers | +| `getActiveMicrophone` | `()` | `string` | Get active mic ID | +| `getActiveSpeaker` | `()` | `string` | Get active speaker ID | +| `isAudioMuted` | `(userId?)` | `boolean` | Is audio muted | + +### Screen Share Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `startShareScreen` | `(canvas, options?)` | `ExecutedResult` | Start sharing | +| `stopShareScreen` | `()` | `ExecutedResult` | Stop sharing | +| `startShareView` | `(canvas, userId)` | `ExecutedResult` | View share | +| `stopShareView` | `()` | `ExecutedResult` | Stop viewing | +| `attachShareView` | `(userId, element?)` | `Promise` | Attach share view | +| `detachShareView` | `(userId, element?)` | `Promise` | Detach share view | +| `pauseShareScreen` | `()` | `ExecutedResult` | Pause share | +| `resumeShareScreen` | `()` | `ExecutedResult` | Resume share | +| `getActiveShareUserId` | `()` | `number` | Get sharer user ID | +| `getShareStatus` | `()` | `ShareStatus` | Get share status | +| `getShareUserList` | `()` | `Participant[]` | Get sharers | +| `lockShare` | `(isLocked)` | `ExecutedResult` | Lock share (host) | +| `setSharePrivilege` | `(privilege)` | `ExecutedResult` | Set share privilege | +| `isStartShareScreenWithVideoElement` | `()` | `boolean` | Use video or canvas | + +### ScreenShareOption Interface + +```typescript +interface ScreenShareOption { + requestReadReceipt?: boolean; // Request read receipt + secondaryAudio?: boolean; // Share with audio + optimizedForVideo?: boolean; // Optimize for video +} +``` + +### ShareStatus Enum + +```typescript +enum ShareStatus { + Sharing = 'Sharing', + Paused = 'Paused', + End = 'End' +} +``` + +### Processor Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `createProcessor` | `(params)` | `Promise` | Create processor | +| `addProcessor` | `(processor)` | `Promise<"">` | Add processor | +| `removeProcessor` | `(processor)` | `Promise<"">` | Remove processor | +| `isSupportVideoProcessor` | `()` | `boolean` | Video processor support | +| `isSupportAudioProcessor` | `()` | `boolean` | Audio processor support | +| `isSupportShareProcessor` | `()` | `boolean` | Share processor support | + +--- + +## Level 2: Feature Clients + +### ChatClient + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `send` | `(message)` | `ExecutedResult` | Send to all | +| `sendToUser` | `(userId, message)` | `ExecutedResult` | Send to user | +| `sendFile` | `(file, receiverId)` | `ExecutedResult` | Send file | +| `downloadFile` | `(fileUrl, options)` | `ExecutedResult` | Download file | + +### CommandChannel + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `send` | `(text)` | `ExecutedResult` | Send to all | +| `sendToUser` | `(userId, text)` | `ExecutedResult` | Send to user | + +### RecordingClient + +| Method | Returns | Description | +|--------|---------|-------------| +| `startCloudRecording()` | `ExecutedResult` | Start recording (host) | +| `stopCloudRecording()` | `ExecutedResult` | Stop recording | +| `pauseCloudRecording()` | `ExecutedResult` | Pause recording | +| `resumeCloudRecording()` | `ExecutedResult` | Resume recording | + +### LiveTranscriptionClient + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `startLiveTranscription` | `()` | `ExecutedResult` | Start transcription | +| `stopLiveTranscription` | `()` | `ExecutedResult` | Stop transcription | +| `enableReceivingCaption` | `(enable)` | `ExecutedResult` | Enable/disable captions | +| `setSpokenLanguage` | `(language)` | `ExecutedResult` | Set spoken language | + +### LiveStreamClient + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `startLiveStream` | `(url, key)` | `ExecutedResult` | Start streaming | +| `stopLiveStream` | `()` | `ExecutedResult` | Stop streaming | + +### SubsessionClient + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `createSubsessions` | `(names)` | `ExecutedResult` | Create subsessions | +| `openSubsessions` | `(rooms)` | `ExecutedResult` | Open subsessions | +| `closeAllSubsessions` | `()` | `ExecutedResult` | Close all | +| `broadcast` | `(message)` | `ExecutedResult` | Broadcast message | +| `getSubsessionList` | `()` | `Subsession[]` | Get subsessions | + +--- + +## Events Reference + +### Session Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `connection-change` | `ConnectionChangePayload` | Connection state changed | +| `user-added` | `ParticipantPropertiesPayload[]` | Participant joined | +| `user-removed` | `ParticipantPropertiesPayload[]` | Participant left | +| `user-updated` | `ParticipantPropertiesPayload[]` | Participant updated | + +### Video Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `peer-video-state-change` | `{action: 'Start'\|'Stop', userId}` | Peer video on/off | +| `video-active-change` | `{state: VideoActiveState, userId}` | Video stream changed | +| `video-capturing-change` | `{state: VideoCapturingState}` | Capture state changed | +| `video-dimension-change` | `{width, height, type}` | Video dimensions changed | + +### Audio Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `current-audio-change` | `{action, source?, type?}` | Audio state changed | +| `active-speaker` | `ActiveSpeaker[]` | Active speakers | +| `host-ask-unmute-audio` | `{reason}` | Host asks unmute | +| `auto-play-audio-failed` | (none) | Auto-play blocked | + +### Screen Share Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `active-share-change` | `{state: 'Active'\|'Inactive', userId}` | Share active state | +| `peer-share-state-change` | `{action: 'Start'\|'Stop', userId}` | Peer share changed | +| `passively-stop-share` | `PassiveStopShareReason` | Share stopped passively | +| `share-content-dimension-change` | `{width, height, type}` | Share size changed | + +### Chat Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `chat-on-message` | `ChatMessage` | Message received | +| `chat-privilege-change` | `{chatPrivilege}` | Privilege changed | + +### Command Channel Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `command-channel-message` | `{senderId, senderName, text, timestamp}` | Command received | +| `command-channel-status` | `ConnectionState` | Channel status | + +### Recording Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `recording-change` | `{state: RecordingStatus}` | Recording state changed | +| `individual-recording-change` | `{state, userId?}` | Individual recording | + +### Transcription Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `caption-message` | `LiveTranscriptionMessage` | Caption received | +| `caption-status` | `{autoCaption, lang?, ...}` | Caption status | +| `caption-enable` | `boolean` | Caption enabled/disabled | + +### Media Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `device-change` | (none) | Device added/removed | +| `device-permission-change` | `{name, state}` | Permission changed | +| `network-quality-change` | `{level, type, userId}` | Network quality | + +--- + +## Error Types + +```typescript +type ErrorTypes = + | 'INVALID_OPERATION' // Duplicated operation + | 'INTERNAL_ERROR' // Service unavailable + | 'OPERATION_TIMEOUT' // Timed out + | 'INSUFFICIENT_PRIVILEGES' // Need host/manager + | 'IMPROPER_MEETING_STATE' // Not in meeting + | 'INVALID_PARAMETERS' // Wrong params + | 'OPERATION_LOCKED'; // Property locked +``` + +--- + +## Related Documentation + +- [Singleton Hierarchy](../concepts/singleton-hierarchy.md) - Navigation guide +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Universal pattern +- [SKILL.md](../SKILL.md) - Main skill overview diff --git a/plugins/zoom-developers/skills/video-sdk/web/references/web.md b/plugins/zoom-developers/skills/video-sdk/web/references/web.md new file mode 100644 index 00000000..e3a6e548 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/web/references/web.md @@ -0,0 +1,1017 @@ +# Video SDK - Web + +Build custom video experiences in the browser with Zoom Video SDK. + +## Overview + +The Zoom Video SDK for Web enables fully customized video applications using Zoom's infrastructure. You control the UI, branding, and user experience. + +## Prerequisites + +- Video SDK credentials from [Marketplace](https://marketplace.zoom.us/) (sign-in required) +- SDK Key and Secret +- Modern browser (Chrome, Firefox, Safari, Edge) + +## Installation + +### NPM (Recommended) + +```bash +npm install @zoom/videosdk +``` + +### CDN (Fallback Strategy) + +> **Note**: Some networks/ad blockers can block `source.zoom.us`. Prefer allowlisting the domain in managed environments. If you need a fallback, consider mirroring/self-hosting only if permitted and you can keep versions in sync. + +```bash +# Download SDK locally +curl "https://source.zoom.us/videosdk/zoom-video-1.12.0.min.js" -o public/js/zoom-video-sdk.min.js +``` + +```html + + +``` + +**Note:** Remember to update your local copy when new SDK versions are released. + +## Quick Start + +### NPM Usage (Bundler) + +```javascript +import ZoomVideo from '@zoom/videosdk'; + +const client = ZoomVideo.createClient(); +await client.init('en-US', 'Global', { patchJsMedia: true }); +await client.join(topic, signature, userName, password); + +// CRITICAL: getMediaStream() ONLY works AFTER join() +const stream = client.getMediaStream(); +await stream.startVideo(); +await stream.startAudio(); +``` + +### CDN Usage (No Bundler) + +```javascript +// CDN exports as WebVideoSDK, NOT ZoomVideo +// Must use .default property +const ZoomVideo = WebVideoSDK.default; +const client = ZoomVideo.createClient(); + +await client.init('en-US', 'Global', { patchJsMedia: true }); +await client.join(topic, signature, userName, password); + +// CRITICAL: getMediaStream() ONLY works AFTER join() +const stream = client.getMediaStream(); +await stream.startVideo(); +await stream.startAudio(); +``` + +### ES Module with CDN (Race Condition) + +When using ` +``` + +**Solution 2 - Wait for SDK to load**: +```javascript +function waitForSDK(timeout = 10000) { + return new Promise((resolve, reject) => { + if (typeof WebVideoSDK !== 'undefined') { + resolve(); + return; + } + const start = Date.now(); + const check = setInterval(() => { + if (typeof WebVideoSDK !== 'undefined') { + clearInterval(check); + resolve(); + } else if (Date.now() - start > timeout) { + clearInterval(check); + reject(new Error('SDK failed to load')); + } + }, 100); + }); +} + +await waitForSDK(); +const ZoomVideo = WebVideoSDK.default; +``` + +### 5. CDN exports WebVideoSDK.default, not ZoomVideo + +**Symptom**: `ZoomVideo.createClient()` fails with CDN + +**Cause**: CDN exports as `WebVideoSDK`, not `ZoomVideo` + +**Solution**: +```javascript +// NPM +import ZoomVideo from '@zoom/videosdk'; + +// CDN +const ZoomVideo = WebVideoSDK.default; // Note: .default! + +const client = ZoomVideo.createClient(); +``` + +### 6. Join Fails with "Invalid signature" + +**Symptom**: `join()` throws error about invalid signature + +**Causes**: +1. JWT expired (check `exp` claim) +2. JWT malformed +3. Wrong SDK key/secret +4. Topic doesn't match JWT `tpc` claim + +**Solution**: +1. Generate JWT on server side +2. Check JWT expiration (typically 24h) +3. Verify topic matches JWT `tpc` value +4. Verify SDK key is correct + +### 7. Camera/Microphone Permission Denied + +**Symptom**: `startVideo()` or `startAudio()` fails + +**Cause**: Browser permission denied + +**Solution**: +```javascript +// Check permissions before starting +try { + await stream.startVideo(); +} catch (error) { + if (error.type === 'INSUFFICIENT_PRIVILEGES') { + // Permission denied - guide user + alert('Please allow camera access in browser settings'); + } +} +``` + +### 8. HD Video Not Working + +**Symptom**: Video quality stays at 360p despite `{ hd: true }` + +**Causes**: +1. SharedArrayBuffer not available +2. Browser doesn't support HD +3. Network conditions + +**Solution**: +```javascript +// Check HD support +if (stream.isSupportHDVideo()) { + await stream.startVideo({ hd: true }); +} else { + console.warn('HD not supported'); + await stream.startVideo(); +} + +// Check SharedArrayBuffer +const sabAvailable = typeof SharedArrayBuffer === 'function'; +if (!sabAvailable) { + console.warn('SharedArrayBuffer not available - add COOP/COEP headers'); +} +``` + +**Server Headers for SharedArrayBuffer**: +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +### 9. Screen Share Element Type Error + +**Symptom**: `startShareScreen()` fails or shows nothing + +**Cause**: Using wrong element type (video vs canvas) + +**Solution**: +```javascript +// Check which element type to use +if (stream.isStartShareScreenWithVideoElement()) { + const video = document.getElementById('share-video'); + await stream.startShareScreen(video as unknown as HTMLCanvasElement); +} else { + const canvas = document.getElementById('share-canvas'); + await stream.startShareScreen(canvas); +} +``` + +### 10. CORS Error to log-external-gateway.zoom.us + +**Symptom**: Console shows CORS errors to Zoom telemetry + +**Cause**: COOP/COEP headers blocking telemetry + +**Impact**: None - harmless. SDK works fine. + +**Solution**: Ignore these errors. They're telemetry-related and don't affect functionality. + +--- + +## Error Types Reference + +| Error Type | Meaning | Common Cause | +|------------|---------|--------------| +| `INVALID_OPERATION` | Duplicated operation | Calling same method twice | +| `INTERNAL_ERROR` | Service unavailable | Network issues | +| `OPERATION_TIMEOUT` | Timed out | Slow connection | +| `INSUFFICIENT_PRIVILEGES` | Need host/manager | Not authorized | +| `IMPROPER_MEETING_STATE` | Not in meeting | Wrong lifecycle stage | +| `INVALID_PARAMETERS` | Wrong params | Bad user ID, etc. | +| `OPERATION_LOCKED` | Property locked | Feature disabled | + +--- + +## Browser-Specific Issues + +### Safari + +| Issue | Solution | +|-------|----------| +| Virtual background not supported | Use alternative (blur not available) | +| Screen sharing requires macOS 15+ | Use Chrome/Firefox | +| Some audio issues | Enable `patchJsMedia: true` | + +### Firefox + +| Issue | Solution | +|-------|----------| +| Virtual background requires 90+ | Update Firefox | +| Some WebRTC issues | Use Chrome if critical | + +### Mobile Browsers + +| Issue | Solution | +|-------|----------| +| Limited screen share | Use desktop for sharing | +| Performance issues | Lower video quality | +| Camera switching | Use `MobileVideoFacingMode` enum | + +--- + +## Debugging Tips + +### 1. Enable SDK Logging + +```javascript +const loggerClient = client.getLoggerClient({ + level: 'debug' +}); +``` + +### 2. Check Event Flow + +```javascript +// Log all events +['connection-change', 'user-added', 'user-removed', 'peer-video-state-change'].forEach(event => { + client.on(event, (payload) => { + console.log(`Event: ${event}`, payload); + }); +}); +``` + +### 3. Check Participant State + +```javascript +const users = client.getAllUser(); +console.table(users.map(u => ({ + userId: u.userId, + name: u.displayName, + videoOn: u.bVideoOn, + muted: u.muted, + audio: u.audio +}))); +``` + +### 4. Check Stream State + +```javascript +console.log('Active camera:', stream.getActiveCamera()); +console.log('Active mic:', stream.getActiveMicrophone()); +console.log('Capturing video:', stream.isCapturingVideo()); +console.log('Audio muted:', stream.isAudioMuted()); +console.log('HD supported:', stream.isSupportHDVideo()); +console.log('Max quality:', stream.getVideoMaxQuality()); +``` + +--- + +## Real-World Integration Pitfalls (Custom Waiting Room Flows) + +These came up in production-style waiting-room to main-session transfers. + +### A) Joined, but no audio/video works on Firefox + +**Symptom**: Session joins, but media pipeline is flaky or blank. + +**Cause**: CSP blocks WebAssembly execution used by `js_media.min.js`. + +**Fix**: Ensure CSP `script-src` includes: + +```text +'wasm-unsafe-eval' 'unsafe-eval' +``` + +Also keep required Zoom domains in `script-src` and allow `worker-src blob:`. + +### B) Transfer works, but customer remote video never appears + +**Symptom**: Customer reaches main session but does not see advisor video. + +**Likely causes**: +1. Advisor is not publishing video (`bVideoOn` is false) +2. Event listener race during waiting->main rejoin +3. Attach attempted too early during stream readiness + +**Fix pattern**: +- Bind listeners once and gate logic by current session mode. +- On main join, do both: + - immediate `getAllUser()` render pass + - short retry/poll window for late stream availability +- Handle `peer-video-state-change`, `user-added`, and `user-updated`. + +### C) Self video appears at wrong page position + +**Symptom**: Self video renders far down the page instead of in tile. + +**Cause**: Container CSS/DOM mismatch for SDK inserted elements. + +**Fix**: +- Use `video-player-container` for SDK video mounts. +- Ensure child elements are explicitly sized: + +```css +video-player-container video-player, +video-player-container canvas, +video-player-container video { + width: 100%; + height: 100%; + display: block; +} +``` + +### D) Command channel transfer message is "missed" + +**Symptom**: Admit clicked, but customer does not transfer. + +**Cause**: Command channel does not replay history. If customer wasn't fully in waiting session yet, message is missed. + +**Fix**: +- Keep backend transfer state and allow customer to fetch transfer details after join. +- Consider one-time transfer lookup on customer waiting join as race guard. + +### E) Repeated CORS errors to `log-external-gateway.zoom.us` + +**Symptom**: Console spam with CORS 531 errors. + +**Impact**: Usually telemetry-only; does not block core session/media. + +**Action**: Treat as noise unless accompanied by actual join or media API failures. + +--- + +## Related Documentation + +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Lifecycle order +- [Video Rendering](../examples/video-rendering.md) - attachVideo patterns +- [Event Handling](../examples/event-handling.md) - Required events +- [SKILL.md](../SKILL.md) - Quick reference diff --git a/plugins/zoom-developers/skills/video-sdk/windows/RUNBOOK.md b/plugins/zoom-developers/skills/video-sdk/windows/RUNBOOK.md new file mode 100644 index 00000000..adbc167d --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/RUNBOOK.md @@ -0,0 +1,64 @@ +# Video SDK Windows 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 Windows (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/windows/ +- https://marketplacefront.zoom.us/sdk/custom/windows/ + +### Raw docs in repo + +- `raw-docs/developers.zoom.us/docs/video-sdk/windows/` +- `raw-docs/marketplacefront.zoom.us/sdk/video-sdk/windows/` diff --git a/plugins/zoom-developers/skills/video-sdk/windows/SKILL.md b/plugins/zoom-developers/skills/video-sdk/windows/SKILL.md new file mode 100644 index 00000000..821826e9 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/SKILL.md @@ -0,0 +1,1012 @@ +--- +name: zoom-video-sdk-windows +description: "Zoom Video SDK for Windows - C++ integration for video sessions, raw audio/video capture, screen sharing, recording, and real-time communication" +--- + +# Zoom Video SDK - Windows Development + +Expert guidance for developing with the Zoom Video SDK on Windows. This SDK enables custom video applications, raw media capture/injection, cloud recording, live streaming, and real-time transcription on Windows platforms. + +**Official Documentation**: https://developers.zoom.us/docs/video-sdk/windows/ +**API Reference**: https://marketplacefront.zoom.us/sdk/custom/windows/ +**Sample Repository**: https://github.com/zoom/videosdk-windows-rawdata-sample + +## Quick Links + +**New to Video SDK? Follow this path:** + +1. **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - Universal 3-step pattern for ANY feature +2. **[Session Join Pattern](examples/session-join-pattern.md)** - Complete working code to join a session +3. **[Windows Message Loop](troubleshooting/windows-message-loop.md)** - **CRITICAL**: Fix callbacks not firing +4. **[Video Rendering](examples/video-rendering.md)** - Display video with Canvas API + +**Reference:** +- **[Singleton Hierarchy](concepts/singleton-hierarchy.md)** - 5-level SDK navigation map +- **[API Reference](references/windows-reference.md)** - Methods, error codes, timing rules +- **[Delegate Methods](references/delegate-methods.md)** - All 80+ callback methods +- **[Sample Applications](references/samples.md)** - Official samples guide +- **[windows.md](windows.md)** - Secondary overview doc (pointer-style) +- **[SKILL.md](SKILL.md)** - Complete documentation navigation + +**Having issues?** +- Callbacks not firing → [Windows Message Loop](troubleshooting/windows-message-loop.md) +- Build errors → [Build Errors Guide](troubleshooting/build-errors.md) +- Video subscribe fails → [Video Rendering](examples/video-rendering.md) (subscribe in `onUserVideoStatusChanged`) +- Quick diagnostics → [Common Issues](troubleshooting/common-issues.md) + +**Building a Custom UI?** +- [Canvas vs Raw Data](concepts/canvas-vs-raw-data.md) - Choose your rendering approach +- [Raw Video Capture](examples/raw-video-capture.md) - YUV420 frame processing + +## SDK Overview + +The Zoom Video SDK for Windows is a C++ library that provides: +- **Session Management**: Join/leave video SDK sessions +- **Raw Data Access**: Capture raw audio/video frames (YUV420, PCM) +- **Raw Data Injection**: Send custom audio/video into sessions +- **Screen Sharing**: Share screens or inject custom share sources +- **Cloud Recording**: Record sessions to Zoom cloud +- **Live Streaming**: Stream to RTMP endpoints (YouTube, etc.) +- **Chat & Commands**: In-session messaging and command channels +- **Live Transcription**: Real-time speech-to-text +- **Subsessions**: Breakout room support +- **Whiteboard**: Collaborative whiteboard features +- **Annotations**: Screen share annotations +- **C# Integration**: C++/CLI wrapper for .NET applications + +## Prerequisites + +### System Requirements + +- **OS**: Windows 10 (1903 or later) or Windows 11 +- **Architecture**: x64 (recommended), x86, or ARM64 +- **Visual Studio**: 2019 or 2022 (Community, Professional, or Enterprise) +- **Windows SDK**: 10.0.19041.0 or later +- **.NET Framework**: 4.8 or later (for C# applications) + +### Visual Studio Workloads + +Install these workloads via Visual Studio Installer: + +1. **Desktop development with C++** + - MSVC v142 or v143 compiler + - Windows 10/11 SDK + - C++ CMake tools (optional) + +2. **.NET desktop development** (for C# applications) + - .NET Framework 4.8 targeting pack + - C++/CLI support + +## Quick Start + +### C++ Application + +```cpp +#include +#include "zoom_video_sdk_api.h" +#include "zoom_video_sdk_interface.h" +#include "zoom_video_sdk_delegate_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +// 1. Create SDK object +IZoomVideoSDK* video_sdk_obj = CreateZoomVideoSDKObj(); + +// 2. Initialize +ZoomVideoSDKInitParams init_params; +init_params.domain = L"https://zoom.us"; +init_params.enableLog = true; +init_params.logFilePrefix = L"zoom_win_video"; +init_params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +init_params.shareRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +init_params.audioRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; + +ZoomVideoSDKErrors err = video_sdk_obj->initialize(init_params); + +// 3. Add event listener +video_sdk_obj->addListener(myDelegate); + +// 4. Join session (IMPORTANT: set audioOption.connect = false) +ZoomVideoSDKSessionContext session_context; +session_context.sessionName = L"my-session"; +session_context.userName = L"Windows User"; +session_context.token = L"your-jwt-token"; +session_context.videoOption.localVideoOn = false; +session_context.audioOption.connect = false; // Connect audio after join +session_context.audioOption.mute = true; + +IZoomVideoSDKSession* session = video_sdk_obj->joinSession(session_context); + +// 5. CRITICAL: Add Windows message pump for callbacks to work +bool running = true; +while (running) { + // Process Windows messages (required for SDK callbacks) + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Your application logic here + Sleep(10); +} +``` + +### C# Application + +```csharp +using ZoomVideoSDK; + +var sdkManager = new ZoomSDKManager(); +sdkManager.Initialize(); +sdkManager.JoinSession("my-session", "jwt-token", "User Name", ""); +``` + +## Key Features + +| Feature | Description | +|---------|-------------| +| **Session Management** | Join, leave, and manage video sessions | +| **Raw Video (YUV I420)** | Capture and inject raw video frames | +| **Raw Audio (PCM)** | Capture and inject raw audio data | +| **Screen Sharing** | Share screens or custom content | +| **Cloud Recording** | Record sessions to Zoom cloud | +| **Live Streaming** | Stream to RTMP endpoints | +| **Chat** | Send/receive chat messages | +| **Command Channel** | Custom command messaging | +| **Live Transcription** | Real-time speech-to-text | +| **C# Support** | Full .NET Framework integration | + +## Sample Applications + +**Official Repository**: https://github.com/zoom/videosdk-windows-rawdata-sample + +| Sample | Description | +|--------|-------------| +| VSDK_SkeletonDemo | Minimal session join - **start here** | +| VSDK_getRawVideo | Capture YUV420 video frames | +| VSDK_getRawAudio | Capture PCM audio | +| VSDK_sendRawVideo | Inject custom video (virtual camera) | +| VSDK_sendRawAudio | Inject custom audio (virtual mic) | +| VSDK_CloudRecording | Cloud recording control | +| VSDK_CommandChannel | Custom command messaging | +| VSDK_TranscriptionAndTranslation | Live captions | + +**See complete guide**: [Sample Applications Reference](references/samples.md) + +## Critical Gotchas and Best Practices + +### ⚠️ CRITICAL: Windows Message Pump Required + +**The #1 issue that causes session joins to hang with no callbacks:** + +All Windows applications using the Zoom SDK **MUST** process Windows messages. The SDK uses Windows messages to deliver callbacks like `onSessionJoin()`, `onError()`, etc. + +**Problem**: Without a message pump, `joinSession()` appears to succeed but callbacks never fire. + +**Solution**: Add this to your main loop: + +```cpp +while (running) { + // REQUIRED: Process Windows messages + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Your application logic + Sleep(10); +} +``` + +**Applies to**: +- Console applications (no automatic message pump) +- Custom main loops +- Applications that don't use standard WinMain/WndProc + +**GUI applications** using WinMain with standard message loop already have this. + +### Audio Connection Strategy + +**Best Practice**: Set `audioOption.connect = false` when joining, then connect audio in the `onSessionJoin()` callback. + +```cpp +// During join +session_context.audioOption.connect = false; // Don't connect yet +session_context.audioOption.mute = true; + +// In onSessionJoin() callback +void onSessionJoin() override { + IZoomVideoSDKAudioHelper* audioHelper = video_sdk_obj->getAudioHelper(); + if (audioHelper) { + audioHelper->startAudio(); // Connect now + } +} +``` + +**Why**: This pattern is used in all official Zoom samples. It separates session join from audio initialization for better reliability and error handling. + +### All Delegate Callbacks Must Be Implemented + +The `IZoomVideoSDKDelegate` interface has 70+ pure virtual methods. **ALL must be implemented**, even if empty: + +```cpp +// Required even if you don't use them +void onProxyDetectComplete() override {} +void onUserWhiteboardShareStatusChanged(IZoomVideoSDKUser*, IZoomVideoSDKWhiteboardHelper*) override {} +// ... etc +``` + +**Tip**: Check the SDK version's `zoom_video_sdk_delegate_interface.h` for the complete list. The interface changes between SDK versions. + +### Memory Mode for Raw Data + +Always use heap mode for raw data memory: + +```cpp +init_params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +init_params.shareRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +init_params.audioRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +``` + +Stack mode can cause issues with large video frames. + +### Thread Safety + +SDK callbacks execute on SDK threads, not your main thread: +- Don't perform heavy operations in callbacks +- Don't call `cleanup()` from within callbacks +- Use thread-safe queues for passing data to UI thread +- Use mutexes when accessing shared state + +### Consult Official Samples First + +When SDK behavior is unexpected, **always check the official samples** before troubleshooting: + +**Local samples**: +- `C:\tempsdk\Zoom_VideoSDK_Windows_RawDataDemos\VSDK_SkeletonDemo\` (simplest) +- `C:\tempsdk\sdksamples\zoom-video-sdk-windows-2.4.12\Sample-Libs\x64\demo\` + +Official samples show correct patterns for: +- Message pump implementation ✓ +- Audio connection strategy ✓ +- Error handling ✓ +- Memory management ✓ + +## Video Rendering - Two Approaches + +The Zoom SDK provides **two different ways** to render video. Choose based on your needs. + +### 🎯 Canvas API (Recommended for Most Use Cases) + +**Best for**: Standard applications, clean video quality, ease of implementation + +The SDK renders video directly to your HWND. **No YUV conversion needed**. + +```cpp +// Subscribe to a user's video with Canvas API +IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); +if (canvas) { + ZoomVideoSDKErrors ret = canvas->subscribeWithView( + hwnd, // Your window handle + ZoomVideoSDKVideoAspect_PanAndScan, // Fit to window, may crop + ZoomVideoSDKResolution_Auto // Let SDK choose best resolution + ); + + if (ret == ZoomVideoSDKErrors_Success) { + // SDK is now rendering directly to your window! + } +} + +// Unsubscribe when done +canvas->unSubscribeWithView(hwnd); +``` + +**Advantages**: +- ✅ **Best quality** - SDK uses optimized, hardware-accelerated rendering +- ✅ **No artifacts** - Professional video quality +- ✅ **Simple code** - 3 lines to subscribe +- ✅ **Better performance** - No CPU-intensive YUV conversion +- ✅ **Automatic scaling** - SDK handles window resizing +- ✅ **Aspect ratio** - Built-in aspect ratio handling + +**Example from official .NET sample**: +```cpp +// Self video preview +IZoomVideoSDKCanvas* canvas = myself->GetVideoCanvas(); +canvas->subscribeWithView(selfVideoHwnd, aspect, resolution); + +// Remote user video +IZoomVideoSDKCanvas* remoteCanvas = remoteUser->GetVideoCanvas(); +remoteCanvas->subscribeWithView(remoteVideoHwnd, aspect, resolution); +``` + +**Video Aspect Options**: +- `ZoomVideoSDKVideoAspect_Original` - Letterbox/pillarbox, no cropping +- `ZoomVideoSDKVideoAspect_FullFilled` - Fill window, may crop edges +- `ZoomVideoSDKVideoAspect_PanAndScan` - Smart crop to fill window +- `ZoomVideoSDKVideoAspect_LetterBox` - Show full video with black bars + +**Resolution Options**: +- `ZoomVideoSDKResolution_90P` +- `ZoomVideoSDKResolution_180P` +- `ZoomVideoSDKResolution_360P` - Good balance +- `ZoomVideoSDKResolution_720P` - HD quality +- `ZoomVideoSDKResolution_1080P` +- `ZoomVideoSDKResolution_Auto` - Let SDK decide (recommended) + +### 🔧 Raw Data Pipe (Advanced Use Cases) + +**Best for**: Custom video processing, effects, recording, computer vision + +You receive raw YUV420 frames and handle rendering yourself. + +```cpp +// 1. Create a delegate to receive frames +class VideoRenderer : public IZoomVideoSDKRawDataPipeDelegate { +public: + void onRawDataFrameReceived(YUVRawDataI420* data) override { + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + char* yBuffer = data->GetYBuffer(); + char* uBuffer = data->GetUBuffer(); + char* vBuffer = data->GetVBuffer(); + + // Convert YUV420 to RGB and render + ConvertYUVToRGB(yBuffer, uBuffer, vBuffer, width, height); + RenderToWindow(rgbBuffer, width, height); + } + + void onRawDataStatusChanged(RawDataStatus status) override { + // Handle video on/off + } +}; + +// 2. Subscribe to raw data +IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); +VideoRenderer* renderer = new VideoRenderer(); +pipe->subscribe(ZoomVideoSDKResolution_720P, renderer); +``` + +**YUV420 to RGB Conversion** (ITU-R BT.601): +```cpp +void ConvertYUV420ToRGB(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height) { + 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 = (R < 0) ? 0 : (R > 255) ? 255 : R; + G = (G < 0) ? 0 : (G > 255) ? 255 : G; + B = (B < 0) ? 0 : (B > 255) ? 255 : B; + + // Store RGB (BGR format for Windows) + rgbBuffer[yIndex * 3 + 0] = (unsigned char)B; + rgbBuffer[yIndex * 3 + 1] = (unsigned char)G; + rgbBuffer[yIndex * 3 + 2] = (unsigned char)R; + } + } +} +``` + +**Render with GDI**: +```cpp +void RenderToWindow(unsigned char* rgbBuffer, int width, int height) { + HDC hdc = GetDC(hwnd); + + BITMAPINFO bmi = {}; + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = width; + bmi.bmiHeader.biHeight = -height; // Negative for top-down + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 24; // 24-bit RGB + bmi.bmiHeader.biCompression = BI_RGB; + + RECT rect; + GetClientRect(hwnd, &rect); + + StretchDIBits(hdc, + 0, 0, rect.right, rect.bottom, // Destination + 0, 0, width, height, // Source + rgbBuffer, &bmi, + DIB_RGB_COLORS, SRCCOPY); + + ReleaseDC(hwnd, hdc); +} +``` + +**Disadvantages**: +- ⚠️ **CPU intensive** - YUV conversion can cause frame drops +- ⚠️ **Artifacts** - Manual rendering may show tearing/artifacts +- ⚠️ **Complex** - More code to maintain +- ⚠️ **Performance** - Slower than Canvas API + +**Use Raw Data When**: +- Adding video filters/effects +- Recording to custom formats +- Computer vision processing +- Custom compositing +- Streaming to non-standard outputs + +### Self Video vs Remote Users + +**Self Video** (your own camera): + +**Option A: Canvas API** +```cpp +IZoomVideoSDKSession* session = sdk->getSessionInfo(); +IZoomVideoSDKUser* myself = session->getMyself(); +IZoomVideoSDKCanvas* canvas = myself->GetVideoCanvas(); +canvas->subscribeWithView(selfVideoHwnd, aspect, resolution); +``` + +**Option B: Video Preview** (for self only) +```cpp +IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper(); +videoHelper->startVideo(); // Start transmission + +// For preview rendering +videoHelper->startVideoCanvasPreview(selfVideoHwnd, aspect, resolution); +``` + +**Remote Users** (other participants): + +**Canvas API** (recommended): +```cpp +// In onUserJoin callback +void onUserJoin(IZoomVideoSDKUserHelper*, IVideoSDKVector* userList) { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); + canvas->subscribeWithView(userVideoHwnd, aspect, resolution); + } +} +``` + +### Event-Driven Subscription Pattern + +⚠️ **CRITICAL**: Video subscription must be **event-driven** and **manual**. + +**Key Events**: + +1. **`onSessionJoin`** - Subscribe to self video +2. **`onUserJoin`** - Subscribe to new remote users +3. **`onUserVideoStatusChanged`** - Re-subscribe when video turns on/off +4. **`onUserLeave`** - Unsubscribe and cleanup + +**Complete Pattern**: + +```cpp +class MainFrame : public IZoomVideoSDKDelegate { +private: + std::map subscribedUsers_; + HWND videoWindow_; + +public: + void onSessionJoin() override { + // Start your own video + IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper(); + videoHelper->startVideo(); + + // Subscribe to self video + IZoomVideoSDKUser* myself = sdk->getSessionInfo()->getMyself(); + SubscribeToUser(myself); + } + + void onUserJoin(IZoomVideoSDKUserHelper*, + IVideoSDKVector* userList) override { + // Get current user to exclude self + IZoomVideoSDKUser* myself = sdk->getSessionInfo()->getMyself(); + + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + + // IMPORTANT: Only subscribe to REMOTE users! + if (user != myself) { + SubscribeToUser(user); + } + } + } + + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper*, + IVideoSDKVector* userList) override { + IZoomVideoSDKUser* myself = sdk->getSessionInfo()->getMyself(); + + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + if (user != myself) { + // Re-subscribe when video status changes + SubscribeToUser(user); + } + } + } + + void onUserLeave(IZoomVideoSDKUserHelper*, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + UnsubscribeFromUser(user); + } + } + + void onSessionLeave() override { + // Cleanup all subscriptions + for (auto& pair : subscribedUsers_) { + IZoomVideoSDKCanvas* canvas = pair.second; + if (canvas) { + canvas->unSubscribeWithView(videoWindow_); + } + } + subscribedUsers_.clear(); + } + +private: + void SubscribeToUser(IZoomVideoSDKUser* user) { + if (!user || subscribedUsers_.find(user) != subscribedUsers_.end()) + return; + + IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); + if (canvas) { + ZoomVideoSDKErrors ret = canvas->subscribeWithView( + videoWindow_, + ZoomVideoSDKVideoAspect_PanAndScan, + ZoomVideoSDKResolution_Auto + ); + + if (ret == ZoomVideoSDKErrors_Success) { + subscribedUsers_[user] = canvas; + } + } + } + + void UnsubscribeFromUser(IZoomVideoSDKUser* user) { + auto it = subscribedUsers_.find(user); + if (it != subscribedUsers_.end()) { + IZoomVideoSDKCanvas* canvas = it->second; + if (canvas) { + canvas->unSubscribeWithView(videoWindow_); + } + subscribedUsers_.erase(it); + } + } +}; +``` + +**Key Points**: +- ✅ Subscribe in response to events (onUserJoin, onUserVideoStatusChanged) +- ✅ Always exclude current user from remote subscriptions +- ✅ Unsubscribe on onUserLeave +- ✅ Clean up all subscriptions on onSessionLeave +- ✅ Track subscriptions in a map for lifecycle management + +### ⚠️ Screen Share Subscription (DIFFERENT from Video!) + +**CRITICAL**: Screen share subscription uses `IZoomVideoSDKShareAction` from the callback, NOT `user->GetShareCanvas()`! + +```cpp +// WRONG - This won't work for remote screen shares! +user->GetShareCanvas()->subscribeWithView(hwnd, ...); + +// CORRECT - Use IZoomVideoSDKShareAction from onUserShareStatusChanged callback +void onUserShareStatusChanged(IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) { + if (!pShareAction) return; + + ZoomVideoSDKShareStatus status = pShareAction->getShareStatus(); + + if (status == ZoomVideoSDKShareStatus_Start || + status == ZoomVideoSDKShareStatus_Resume) { + // Subscribe to the share using Canvas API + IZoomVideoSDKCanvas* shareCanvas = pShareAction->getShareCanvas(); + if (shareCanvas) { + shareCanvas->subscribeWithView(shareWindow_, + ZoomVideoSDKVideoAspect_Original); + } + } + else if (status == ZoomVideoSDKShareStatus_Stop) { + // Unsubscribe when share stops + IZoomVideoSDKCanvas* shareCanvas = pShareAction->getShareCanvas(); + if (shareCanvas) { + shareCanvas->unSubscribeWithView(shareWindow_); + } + } +} +``` + +**Why is share different from video?** +- **Video**: Each user has one video stream → use `user->GetVideoCanvas()` +- **Share**: A user can have multiple share actions (multi-share) → use `IZoomVideoSDKShareAction*` from callback +- The `IZoomVideoSDKShareAction` object represents a specific share stream and contains the share status, type, and rendering interfaces + +**See also**: [Screen Share Subscription Example](examples/screen-share-subscription.md) + +### Multi-User Video Layout + +For multiple participants, you need **one HWND per user**: + +```cpp +// Create separate windows/panels for each user +HWND selfVideoWindow = CreateWindow(...); // Your video +HWND user1Window = CreateWindow(...); // User 1's video +HWND user2Window = CreateWindow(...); // User 2's video + +// Subscribe each user to their own window +myself->GetVideoCanvas()->subscribeWithView(selfVideoWindow, ...); +user1->GetVideoCanvas()->subscribeWithView(user1Window, ...); +user2->GetVideoCanvas()->subscribeWithView(user2Window, ...); +``` + +**Layout Strategies**: +- Grid layout (2x2, 3x3) +- Gallery view (scrollable) +- Active speaker (large) + thumbnails +- Picture-in-picture + +### Common Video Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| Video not showing | Not calling `startVideo()` | Call `videoHelper->startVideo()` in `onSessionJoin` | +| Artifacts/tearing | Using Raw Data Pipe | Switch to Canvas API | +| Poor performance | YUV conversion on UI thread | Use Canvas API or move conversion to worker thread | +| Video freezes | Not processing Windows messages | Add message pump to main loop | +| Can't see self | Subscribing to wrong user | Use `session->getMyself()` for self video | +| Seeing self in remote list | Not excluding self | Check `if (user != myself)` before subscribing | + +## Complete Documentation Library + +This skill includes comprehensive guides organized by category: + +### Core Concepts (Start Here!) +- **[SDK Architecture Pattern](concepts/sdk-architecture-pattern.md)** - Universal 3-step pattern for ANY feature +- **[Singleton Hierarchy](concepts/singleton-hierarchy.md)** - 5-level navigation guide +- **[Canvas vs Raw Data](concepts/canvas-vs-raw-data.md)** - Choose your rendering approach + +### Complete Examples +- **[Session Join Pattern](examples/session-join-pattern.md)** - JWT auth + session join with full code +- **[Video Rendering](examples/video-rendering.md)** - Canvas API video display +- **[Screen Share Subscription](examples/screen-share-subscription.md)** - View remote screen shares (DIFFERENT from video!) +- **[Raw Video Capture](examples/raw-video-capture.md)** - YUV420 frame capture +- **[Raw Audio Capture](examples/raw-audio-capture.md)** - PCM audio capture +- **[Send Raw Video](examples/send-raw-video.md)** - Virtual camera (inject custom video) +- **[Send Raw Audio](examples/send-raw-audio.md)** - Virtual mic (inject custom audio) +- **[Cloud Recording](examples/cloud-recording.md)** - Cloud recording control +- **[Command Channel](examples/command-channel.md)** - Custom command messaging +- **[Transcription](examples/transcription.md)** - Live transcription/captions + +### UI Framework Integration +- **[Win32 Native](examples/dotnet-winforms/guide.md#option-1-win32-native-c---direct-sdk)** - Direct SDK usage with Canvas API (best performance) +- **[WinForms (.NET)](examples/dotnet-winforms/guide.md#option-2-winforms-c--ccli-wrapper)** - C++/CLI wrapper + Raw Data Pipe +- **[WPF (.NET)](examples/dotnet-winforms/guide.md#option-3-wpf-c--ccli-wrapper)** - C++/CLI wrapper + BitmapSource conversion +- **[Production Quality Guidelines](examples/dotnet-winforms/guide.md#production-quality-review)** - Checklist and common issues + +### C++/CLI Wrapper Patterns (Wrapping ANY Native Library) +- **[Complete Guide](examples/dotnet-winforms/guide.md#ccli-wrapper-patterns-for-net-integration)** - 8 patterns for native→.NET interop +- **[Pattern 1: Basic Structure](examples/dotnet-winforms/guide.md#pattern-1-basic-wrapper-structure)** - Project setup, class layout +- **[Pattern 2: void* Pointers](examples/dotnet-winforms/guide.md#pattern-2-opaque-void-pointers)** - Hide native types +- **[Pattern 3: gcroot Callbacks](examples/dotnet-winforms/guide.md#pattern-3-gcrootT-for-nativemanaged-callbacks)** - Native→Managed events +- **[Pattern 4: IDisposable](examples/dotnet-winforms/guide.md#pattern-4-destructor--finalizer-idisposable)** - Cleanup pattern +- **[Pattern 5: Strings](examples/dotnet-winforms/guide.md#pattern-5-string-conversion)** - String^ ↔ wstring/string +- **[Pattern 6: Arrays](examples/dotnet-winforms/guide.md#pattern-6-arraybuffer-conversion)** - pin_ptr, Marshal::Copy +- **[Pattern 7: Threading](examples/dotnet-winforms/guide.md#pattern-7-thread-marshaling-native-thread--ui-thread)** - UI thread dispatch +- **[Pattern 8: LockBits](examples/dotnet-winforms/guide.md#pattern-8-lockbits-for-fast-image-manipulation)** - Fast image conversion +- **[Common Errors](examples/dotnet-winforms/guide.md#common-wrapper-errors)** - Troubleshooting + +### Troubleshooting +- **[Windows Message Loop](troubleshooting/windows-message-loop.md)** - **CRITICAL**: Why callbacks don't fire +- **[Build Errors](troubleshooting/build-errors.md)** - SDK header dependency fixes +- **[Common Issues](troubleshooting/common-issues.md)** - Quick diagnostics & error codes + +### References +- **[API Reference](references/windows-reference.md)** - 5-level API hierarchy, methods, error codes +- **[Delegate Methods](references/delegate-methods.md)** - All 80+ callback methods +- **[SKILL.md](SKILL.md)** - Complete navigation guide + +### Most Critical Issues (From Real Debugging) + +1. **Callbacks not firing** → Missing Windows message loop (99% of issues) + - See: [Windows Message Loop Guide](troubleshooting/windows-message-loop.md) + +2. **Video subscribe returns error 2** → Subscribing too early + - See: [Video Rendering](examples/video-rendering.md) - Subscribe in `onUserVideoStatusChanged` + +3. **Abstract class errors** → Missing virtual method implementations + - See: [Delegate Methods](references/delegate-methods.md) + +### Key Insight + +**Once you learn the 3-step pattern, you can implement ANY feature:** +1. Get singleton → 2. Implement delegate → 3. Subscribe & use + +See: [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +## Resources + +- **Official Docs**: https://developers.zoom.us/docs/video-sdk/windows/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/custom/windows/ +- **Dev Forum**: https://devforum.zoom.us/ +- **GitHub Samples**: https://github.com/zoom/videosdk-windows-rawdata-sample +- **Working Sample**: `C:\tempsdk\zoom-video-sdk-windows-sample\` (complete implementation) + +--- + +**Need help?** Start with [SKILL.md](SKILL.md) for complete navigation. + + +## Merged from video-sdk/windows/SKILL.md + +# Zoom Video SDK Windows - Complete Documentation Index + +## Quick Start Path + +**If you're new to the SDK, follow this order:** + +0. **Overview** → [windows.md](windows.md) +1. **Read the architecture pattern** → [concepts/sdk-architecture-pattern.md](concepts/sdk-architecture-pattern.md) + - Universal formula: Singleton → Delegate → Subscribe + - Once you understand this, you can implement any feature + +2. **Fix build errors** → [troubleshooting/build-errors.md](troubleshooting/build-errors.md) + - SDK header dependencies + - Required include order + +3. **Implement session join** → [examples/session-join-pattern.md](examples/session-join-pattern.md) + - Complete working JWT + session join code + +4. **Fix callback issues** → [troubleshooting/windows-message-loop.md](troubleshooting/windows-message-loop.md) + - **CRITICAL**: Why callbacks don't fire without Windows message loop + +5. **Implement video** → [examples/video-rendering.md](examples/video-rendering.md) + - Canvas API (SDK-rendered) vs Raw Data Pipe + +6. **Troubleshoot any issues** → [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + - Quick diagnostic checklist + - Error code tables + +--- + +## Documentation Structure + +``` +video-sdk/windows/ +├── SKILL.md # Main skill overview +├── SKILL.md # This file - navigation guide +├── windows.md # Secondary overview doc (pointer-style) +│ +├── concepts/ # Core architectural patterns +│ ├── sdk-architecture-pattern.md # Universal formula for ANY feature +│ ├── singleton-hierarchy.md # 5-level navigation guide +│ └── canvas-vs-raw-data.md # SDK-rendered vs self-rendered choice +│ +├── examples/ # Complete working code +│ ├── session-join-pattern.md # JWT auth + session join +│ ├── video-rendering.md # Canvas API video display +│ ├── screen-share-subscription.md # View remote screen shares +│ ├── raw-video-capture.md # YUV420 raw frame capture +│ ├── raw-audio-capture.md # PCM audio capture +│ ├── send-raw-video.md # Virtual camera (inject video) +│ ├── send-raw-audio.md # Virtual mic (inject audio) +│ ├── cloud-recording.md # Cloud recording control +│ ├── command-channel.md # Custom command messaging +│ ├── transcription.md # Live transcription/captions +│ └── dotnet-winforms/ # UI Framework integration +│ └── README.md # Win32, WinForms, WPF patterns +│ # C++/CLI wrapper patterns +│ # Production quality guidelines +│ +├── troubleshooting/ # Problem solving guides +│ ├── windows-message-loop.md # CRITICAL - Why callbacks fail +│ ├── build-errors.md # Header dependency fixes +│ └── common-issues.md # Quick diagnostic workflow +│ +└── references/ # Reference documentation + ├── windows-reference.md # API hierarchy, methods, error codes + ├── delegate-methods.md # All 80+ callback methods + └── samples.md # Official samples guide +``` + +--- + +## By Use Case + +### I want to build a video app +1. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Understand the pattern +2. [Session Join Pattern](examples/session-join-pattern.md) - Join sessions +3. [Video Rendering](examples/video-rendering.md) - Display video +4. [Windows Message Loop](troubleshooting/windows-message-loop.md) - Fix callback issues + +### I'm getting build errors +1. [Build Errors Guide](troubleshooting/build-errors.md) - SDK header dependencies +2. [Delegate Methods](references/delegate-methods.md) - Abstract class errors +3. [Common Issues](troubleshooting/common-issues.md) - Linker errors + +### I'm getting runtime errors +1. [Windows Message Loop](troubleshooting/windows-message-loop.md) - Callbacks not firing +2. [Common Issues](troubleshooting/common-issues.md) - Error code tables + +### I want to view screen shares +1. [Screen Share Subscription](examples/screen-share-subscription.md) - **DIFFERENT from video!** +2. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Event-driven pattern +3. [Video Rendering](examples/video-rendering.md) - Compare with video subscription + +### I want to capture raw video/audio +1. [Canvas vs Raw Data](concepts/canvas-vs-raw-data.md) - Choose your approach +2. [Raw Video Capture](examples/raw-video-capture.md) - YUV420 frame capture +3. [Raw Audio Capture](examples/raw-audio-capture.md) - PCM audio capture +4. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - Subscription pattern + +### I want to send custom video/audio (virtual camera/mic) +1. [Send Raw Video](examples/send-raw-video.md) - Inject custom video frames +2. [Send Raw Audio](examples/send-raw-audio.md) - Inject custom audio +3. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - External source pattern + +### I want to record sessions +1. [Cloud Recording](examples/cloud-recording.md) - Start/stop cloud recording +2. [API Reference](references/windows-reference.md) - Recording helper methods + +### I want to use live transcription +1. [Transcription](examples/transcription.md) - Enable live captions +2. [Delegate Methods](references/delegate-methods.md) - Transcription callbacks + +### I want custom messaging between participants +1. [Command Channel](examples/command-channel.md) - Send custom commands +2. [API Reference](references/windows-reference.md) - Command channel methods + +### I want to build a Win32 native app +1. [Win32 Integration](examples/dotnet-winforms/guide.md#option-1-win32-native-c---direct-sdk) - Direct SDK + Canvas API +2. [Video Rendering](examples/video-rendering.md) - Canvas API patterns +3. [Production Guidelines](examples/dotnet-winforms/guide.md#production-quality-review) - Best practices + +### I want to build a WinForms (.NET) app +1. [WinForms Integration](examples/dotnet-winforms/guide.md#option-2-winforms-c--ccli-wrapper) - C++/CLI wrapper + Raw Data +2. [C++/CLI Patterns](examples/dotnet-winforms/guide.md#ccli-wrapper-patterns-for-net-integration) - gcroot, Finalizer, LockBits +3. [Production Guidelines](examples/dotnet-winforms/guide.md#production-quality-review) - IDisposable, thread safety + +### I want to build a WPF (.NET) app +1. [WPF Integration](examples/dotnet-winforms/guide.md#option-3-wpf-c--ccli-wrapper) - C++/CLI + BitmapSource +2. [Bitmap Conversion](examples/dotnet-winforms/guide.md#2-bitmap--bitmapsource-conversion) - Freeze(), Dispatcher +3. [Production Guidelines](examples/dotnet-winforms/guide.md#production-quality-review) - Performance optimization + +### I want to use C# / .NET Framework (general) +1. [.NET Integration Overview](examples/dotnet-winforms/guide.md) - **Complete C++/CLI wrapper guide** +2. [Raw Video Capture](examples/raw-video-capture.md) - YUV→RGB conversion patterns +3. [Session Join Pattern](examples/session-join-pattern.md) - SDK initialization flow + +### I want to wrap ANY native C++ library for .NET +1. [C++/CLI Wrapper Patterns](examples/dotnet-winforms/guide.md#ccli-wrapper-patterns-for-net-integration) - **Complete 8-pattern guide** +2. [Pattern 1: Basic Structure](examples/dotnet-winforms/guide.md#pattern-1-basic-wrapper-structure) - Project setup + class layout +3. [Pattern 3: gcroot Callbacks](examples/dotnet-winforms/guide.md#pattern-3-gcrootT-for-nativemanaged-callbacks) - Native→Managed events +4. [Pattern 4: IDisposable](examples/dotnet-winforms/guide.md#pattern-4-destructor--finalizer-idisposable) - Cleanup pattern +5. [Common Errors](examples/dotnet-winforms/guide.md#common-wrapper-errors) - Troubleshooting + +### I want to implement a specific feature +1. [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) - **START HERE!** +2. [Singleton Hierarchy](concepts/singleton-hierarchy.md) - Navigate to the feature +3. [API Reference](references/windows-reference.md) - Method signatures + +--- + +## Most Critical Documents + +### 1. SDK Architecture Pattern (MASTER DOCUMENT) +**[concepts/sdk-architecture-pattern.md](concepts/sdk-architecture-pattern.md)** + +The universal 3-step pattern: +1. Get singleton (SDK, helpers, session, users) +2. Implement delegate (event callbacks) +3. Subscribe and use + +### 2. Windows Message Loop (MOST COMMON ISSUE) +**[troubleshooting/windows-message-loop.md](troubleshooting/windows-message-loop.md)** + +99% of "callbacks not firing" issues are caused by missing Windows message loop. + +### 3. Singleton Hierarchy (NAVIGATION MAP) +**[concepts/singleton-hierarchy.md](concepts/singleton-hierarchy.md)** + +5-level deep navigation showing how to reach every feature. + +--- + +## Key Learnings + +### Critical Discoveries: + +1. **Windows Message Loop is MANDATORY** + - SDK uses Windows message pump for callbacks + - Without it, callbacks are queued but never fire + - See: [Windows Message Loop Guide](troubleshooting/windows-message-loop.md) + +2. **Subscribe in onUserVideoStatusChanged, NOT onUserJoin** + - Video may not be ready when user joins + - Wait for video status change callback + - See: [Video Rendering](examples/video-rendering.md) + +3. **Two Rendering Paths** + - Canvas API: SDK renders to your HWND (recommended) + - Raw Data Pipe: You receive YUV frames (advanced) + - See: [Canvas vs Raw Data](concepts/canvas-vs-raw-data.md) + +4. **Helpers Control YOUR Streams Only** + - `videoHelper->startVideo()` starts YOUR camera + - To see others, subscribe to their Canvas/Pipe + - See: [Singleton Hierarchy](concepts/singleton-hierarchy.md) + +5. **UI Framework Integration Differs by Platform** + - **Win32**: Direct SDK, Canvas API (SDK renders to HWND) - best performance + - **WinForms**: C++/CLI wrapper, Raw Data Pipe, YUV→Bitmap, InvokeRequired + - **WPF**: Same wrapper + Bitmap→BitmapSource, Dispatcher, Freeze() + - See: [UI Framework Integration](examples/dotnet-winforms/guide.md) + +6. **C++/CLI Wrapper Patterns (for ANY native library → .NET)** + - `void*` pointers - hide native types from managed headers + - `gcroot` - prevent GC from collecting managed references in native code + - Finalizer + Destructor - `~Class()` and `!Class()` for IDisposable cleanup + - `pin_ptr` + `Marshal::Copy` - array/buffer conversion + - `LockBits` - 100x faster than SetPixel for image manipulation + - Thread marshaling - InvokeRequired (WinForms) / Dispatcher (WPF) + - See: [C++/CLI Wrapper Guide](examples/dotnet-winforms/guide.md#ccli-wrapper-patterns-for-net-integration) + +7. **Audio Connection Timing** + - Set `audioOption.connect = false` during join + - Call `startAudio()` in `onSessionJoin` callback + - See: [Production Guidelines](examples/dotnet-winforms/guide.md#production-quality-review) + +--- + +## Quick Reference + +### "My code won't compile" +→ [Build Errors Guide](troubleshooting/build-errors.md) + +### "Callbacks never fire" +→ [Windows Message Loop](troubleshooting/windows-message-loop.md) + +### "Video subscription returns error 2" +→ [Video Rendering](examples/video-rendering.md) - Subscribe in onUserVideoStatusChanged + +### "Abstract class error" +→ [Delegate Methods](references/delegate-methods.md) + +### "How do I implement [feature]?" +→ [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) + +### "How do I navigate to [controller]?" +→ [Singleton Hierarchy](concepts/singleton-hierarchy.md) + +### "What error code means what?" +→ [Common Issues](troubleshooting/common-issues.md) + +--- + +## Document Version + +Based on **Zoom Video SDK for Windows v2.x** + +--- + +**Happy coding!** + +Remember: The [SDK Architecture Pattern](concepts/sdk-architecture-pattern.md) is your key to unlocking the entire SDK. Read it first! + +## Operations + +- [RUNBOOK.md](RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/video-sdk/windows/concepts/canvas-vs-raw-data.md b/plugins/zoom-developers/skills/video-sdk/windows/concepts/canvas-vs-raw-data.md new file mode 100644 index 00000000..8db88da7 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/concepts/canvas-vs-raw-data.md @@ -0,0 +1,327 @@ +# Canvas API vs Raw Data Pipe + +## The Two Rendering Paths + +The Zoom Video SDK provides **two distinct ways** to render video. Your choice affects quality, performance, and capabilities. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ RENDERING DECISION │ +├─────────────────────────────────────────────────────────────────┤ +│ Canvas API (SDK-Rendered) │ Raw Data Pipe (Self-Rendered) │ +│ ─────────────────────────────│──────────────────────────────── │ +│ SDK renders to your HWND │ You receive YUV420 frames │ +│ Best quality, zero effort │ Full control, more work │ +│ Standard video apps │ AI, effects, recording │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Quick Decision Guide + +| Use Case | Recommended Approach | +|----------|---------------------| +| Standard video conferencing UI | **Canvas API** | +| Video grid/gallery layout | **Canvas API** | +| Simple video display | **Canvas API** | +| Custom video effects/filters | Raw Data Pipe | +| AI/ML video processing | Raw Data Pipe | +| Custom recording format | Raw Data Pipe | +| Video compositing | Raw Data Pipe | + +**Default recommendation: Canvas API** unless you need frame-level access. + +--- + +## Canvas API (SDK-Rendered) + +### How It Works + +``` +IZoomVideoSDKUser + └── GetVideoCanvas() + └── IZoomVideoSDKCanvas + └── subscribeWithView(HWND, aspect, resolution) + └── SDK renders directly to your window + └── Hardware-accelerated, optimized +``` + +### Code Example + +```cpp +// Get user's canvas +IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); + +// Subscribe - SDK renders to your window +ZoomVideoSDKErrors err = canvas->subscribeWithView( + hwnd, // Your window handle + ZoomVideoSDKVideoAspect_PanAndScan, // Aspect ratio handling + ZoomVideoSDKResolution_Auto // Let SDK choose +); + +if (err == ZoomVideoSDKErrors_Success) { + // Done! SDK is now rendering to your window +} + +// To stop +canvas->unSubscribeWithView(hwnd); +``` + +### Aspect Ratio Options + +| Option | Behavior | +|--------|----------| +| `ZoomVideoSDKVideoAspect_Original` | Letterbox/pillarbox, no cropping | +| `ZoomVideoSDKVideoAspect_FullFilled` | Fill window, may crop edges | +| `ZoomVideoSDKVideoAspect_PanAndScan` | Smart crop to fill window | +| `ZoomVideoSDKVideoAspect_LetterBox` | Show full video with black bars | + +### Resolution Options + +| Option | Resolution | +|--------|------------| +| `ZoomVideoSDKResolution_90P` | 160x90 | +| `ZoomVideoSDKResolution_180P` | 320x180 | +| `ZoomVideoSDKResolution_360P` | 640x360 | +| `ZoomVideoSDKResolution_720P` | 1280x720 | +| `ZoomVideoSDKResolution_1080P` | 1920x1080 | +| `ZoomVideoSDKResolution_Auto` | SDK chooses (recommended) | + +### Advantages + +- **Best quality** - Hardware-accelerated rendering +- **No artifacts** - Professional video quality +- **Simple code** - 3 lines to subscribe +- **Better performance** - No CPU-intensive YUV conversion +- **Automatic scaling** - SDK handles window resizing +- **Aspect ratio** - Built-in handling + +### Disadvantages + +- No access to raw frames +- Can't apply custom effects +- Can't process video for AI/ML + +--- + +## Raw Data Pipe (Self-Rendered) + +### How It Works + +``` +IZoomVideoSDKUser + └── GetVideoPipe() + └── IZoomVideoSDKRawDataPipe + └── subscribe(resolution, delegate) + └── Your IZoomVideoSDKRawDataPipeDelegate + └── onRawDataFrameReceived(YUVRawDataI420*) + └── You convert YUV→RGB and render +``` + +### Code Example + +```cpp +// Implement delegate to receive frames +class VideoRenderer : public IZoomVideoSDKRawDataPipeDelegate { +public: + void onRawDataFrameReceived(YUVRawDataI420* data) override { + if (!data) return; + + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + char* yBuffer = data->GetYBuffer(); + char* uBuffer = data->GetUBuffer(); + char* vBuffer = data->GetVBuffer(); + + // Convert YUV420 to RGB + ConvertYUVToRGB(yBuffer, uBuffer, vBuffer, width, height); + + // Render to window (GDI, DirectX, OpenGL, etc.) + RenderToWindow(rgbBuffer, width, height); + } + + void onRawDataStatusChanged(RawDataStatus status) override { + if (status == RawData_On) { + std::cout << "Video started" << std::endl; + } else { + std::cout << "Video stopped" << std::endl; + } + } +}; + +// Subscribe to raw frames +IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); +VideoRenderer* renderer = new VideoRenderer(); +pipe->subscribe(ZoomVideoSDKResolution_720P, renderer); + +// To stop +pipe->unSubscribe(renderer); +``` + +### YUV420 to RGB Conversion + +```cpp +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]; + + // ITU-R BT.601 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 = (R < 0) ? 0 : (R > 255) ? 255 : R; + G = (G < 0) ? 0 : (G > 255) ? 255 : G; + B = (B < 0) ? 0 : (B > 255) ? 255 : B; + + // Store as BGR (Windows format) + rgbBuffer[yIndex * 3 + 0] = (unsigned char)B; + rgbBuffer[yIndex * 3 + 1] = (unsigned char)G; + rgbBuffer[yIndex * 3 + 2] = (unsigned char)R; + } + } +} +``` + +### GDI Rendering + +```cpp +void RenderToWindow(unsigned char* rgbBuffer, int width, int height, HWND hwnd) { + HDC hdc = GetDC(hwnd); + + BITMAPINFO bmi = {}; + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = width; + bmi.bmiHeader.biHeight = -height; // Negative for top-down + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 24; + bmi.bmiHeader.biCompression = BI_RGB; + + RECT rect; + GetClientRect(hwnd, &rect); + + StretchDIBits(hdc, + 0, 0, rect.right, rect.bottom, // Destination + 0, 0, width, height, // Source + rgbBuffer, &bmi, + DIB_RGB_COLORS, SRCCOPY); + + ReleaseDC(hwnd, hdc); +} +``` + +### Advantages + +- Full access to raw YUV frames +- Can apply custom video effects +- Can process for AI/ML +- Can record to custom formats +- Can composite multiple streams + +### Disadvantages + +- **CPU intensive** - YUV conversion can cause frame drops +- **Artifacts** - Manual rendering may show tearing +- **Complex** - More code to maintain +- **Performance** - Slower than Canvas API + +--- + +## Comparison Table + +| Aspect | Canvas API | Raw Data Pipe | +|--------|------------|---------------| +| **Complexity** | 3 lines of code | 50+ lines of code | +| **Performance** | Hardware-accelerated | CPU-bound | +| **Quality** | Professional | Depends on implementation | +| **Frame Access** | No | Yes (YUV420) | +| **Custom Effects** | No | Yes | +| **AI Processing** | No | Yes | +| **Recording** | No | Yes | +| **Recommended For** | Standard apps | Advanced processing | + +--- + +## Hybrid Approach + +You can use **both** simultaneously: + +```cpp +// Use Canvas for display +user->GetVideoCanvas()->subscribeWithView(displayHwnd, aspect, resolution); + +// Use Raw Data for processing (different delegate) +user->GetVideoPipe()->subscribe(ZoomVideoSDKResolution_360P, processingDelegate); +``` + +**Tip**: Use lower resolution for processing to reduce CPU load. + +--- + +## Performance Considerations + +### Canvas API +- Zero CPU overhead for rendering +- SDK handles all optimization +- Scales automatically with window resize + +### Raw Data Pipe +- YUV conversion: ~5-10ms per frame at 720p +- Memory allocation: Consider pre-allocated buffers +- Threading: Move conversion off UI thread +- Frame drops: Expect some at high resolutions + +### Optimization Tips for Raw Data + +```cpp +// Pre-allocate buffers +class OptimizedRenderer : public IZoomVideoSDKRawDataPipeDelegate { + unsigned char* rgbBuffer = nullptr; + int bufferWidth = 0; + int bufferHeight = 0; + + void onRawDataFrameReceived(YUVRawDataI420* data) override { + int w = data->GetStreamWidth(); + int h = data->GetStreamHeight(); + + // Reallocate only if resolution changed + if (w != bufferWidth || h != bufferHeight) { + delete[] rgbBuffer; + rgbBuffer = new unsigned char[w * h * 3]; + bufferWidth = w; + bufferHeight = h; + } + + // Convert and render + ConvertYUV420ToRGB(..., rgbBuffer); + RenderToWindow(rgbBuffer, w, h); + } +}; +``` + +--- + +## Related Documentation + +- [Video Rendering Example](../examples/video-rendering.md) - Canvas API code +- [Raw Video Capture Example](../examples/raw-video-capture.md) - Raw Data code +- [Singleton Hierarchy](singleton-hierarchy.md) - Canvas/Pipe navigation +- [API Reference](../references/windows-reference.md) - Method details + +--- + +**TL;DR**: Use Canvas API for standard video display. Use Raw Data Pipe only when you need frame-level access for AI, effects, or custom recording. diff --git a/plugins/zoom-developers/skills/video-sdk/windows/concepts/sdk-architecture-pattern.md b/plugins/zoom-developers/skills/video-sdk/windows/concepts/sdk-architecture-pattern.md new file mode 100644 index 00000000..0e0dfb69 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/concepts/sdk-architecture-pattern.md @@ -0,0 +1,298 @@ +# SDK Architecture Pattern + +## The Universal Formula + +The Zoom Video SDK follows a **perfectly consistent architecture**. Every feature works the same way: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ UNIVERSAL 3-STEP PATTERN │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. GET SINGLETON → SDK, helpers, session, users │ +│ 2. IMPLEMENT DELEGATE → Event callbacks (IZoomVideoSDKDelegate)│ +│ 3. SUBSCRIBE & USE → Call methods, receive events │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Once you understand this pattern, you can implement ANY feature.** + +--- + +## Step 1: Get Singleton + +The SDK is a tree of singleton objects. You navigate to what you need: + +```cpp +// Root singleton +IZoomVideoSDK* sdk = CreateZoomVideoSDKObj(); + +// Level 1: Helpers (control YOUR streams) +IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper(); +IZoomVideoSDKAudioHelper* audioHelper = sdk->getAudioHelper(); +IZoomVideoSDKShareHelper* shareHelper = sdk->getShareHelper(); +IZoomVideoSDKChatHelper* chatHelper = sdk->getChatHelper(); + +// Level 2: Session +IZoomVideoSDKSession* session = sdk->getSessionInfo(); + +// Level 3: Users +IZoomVideoSDKUser* myself = session->getMyself(); +IVideoSDKVector* remoteUsers = session->getRemoteUsers(); + +// Level 4: Canvas/Pipe (per user) +IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); +IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); +``` + +**Key insight**: You don't construct these objects. You navigate to them. + +--- + +## Step 2: Implement Delegate + +The SDK uses **observer pattern** for events. Implement `IZoomVideoSDKDelegate`: + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +public: + // Session lifecycle + void onSessionJoin() override { + std::cout << "Joined session!" << std::endl; + // Safe to start video, subscribe to users, etc. + } + + void onSessionLeave() override { + std::cout << "Left session" << std::endl; + } + + // User events + void onUserJoin(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + // New users joined - but don't subscribe to video yet! + } + + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + // NOW subscribe to video - it's ready + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + if (user->GetVideoPipe()->getVideoStatus().isOn) { + user->GetVideoCanvas()->subscribeWithView(hwnd, aspect, resolution); + } + } + } + + // Chat events + void onChatNewMessageNotify(IZoomVideoSDKChatHelper* helper, + IZoomVideoSDKChatMessage* msg) override { + std::wcout << L"Chat: " << msg->getContent() << std::endl; + } + + // Share events + void onUserShareStatusChanged(IZoomVideoSDKShareHelper* helper, + IZoomVideoSDKUser* user, + IZoomVideoSDKShareAction* shareAction) override { + // Subscribe to remote user's screen share + shareAction->subscribeWithView(shareHwnd, ZoomVideoSDKVideoAspect_Original); + } + + // ... 80+ more callbacks (implement as empty if not needed) + void onError(ZoomVideoSDKErrors errorCode, int detailErrorCode) override {} + void onUserLeave(IZoomVideoSDKUserHelper*, IVideoSDKVector*) override {} + // etc. +}; +``` + +**Key insight**: All 80+ methods must be implemented (even if empty). + +--- + +## Step 3: Subscribe & Use + +Register your delegate and call methods: + +```cpp +// Register delegate BEFORE joining +sdk->addListener(new MyDelegate()); + +// Initialize +ZoomVideoSDKInitParams initParams; +initParams.domain = L"https://zoom.us"; +initParams.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +sdk->initialize(initParams); + +// Join session +ZoomVideoSDKSessionContext context; +context.sessionName = L"my-session"; +context.userName = L"Bot"; +context.token = L"your-jwt-token"; +context.audioOption.connect = false; // Connect audio in onSessionJoin +sdk->joinSession(context); + +// In onSessionJoin callback: +void onSessionJoin() override { + // Start audio + sdk->getAudioHelper()->startAudio(); + + // Start video + sdk->getVideoHelper()->startVideo(); + + // Subscribe to self video + IZoomVideoSDKUser* myself = sdk->getSessionInfo()->getMyself(); + myself->GetVideoCanvas()->subscribeWithView(selfHwnd, aspect, resolution); +} +``` + +--- + +## Pattern Applied to Every Feature + +### Audio + +```cpp +// Get singleton +IZoomVideoSDKAudioHelper* audioHelper = sdk->getAudioHelper(); + +// Use +audioHelper->startAudio(); +audioHelper->muteAudio(user); +audioHelper->unmuteAudio(user); + +// Events arrive in delegate +void onUserAudioStatusChanged(...) override { } +``` + +### Video + +```cpp +// Get singleton +IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper(); + +// Use +videoHelper->startVideo(); +videoHelper->stopVideo(); +videoHelper->switchCamera(deviceId); + +// Subscribe to user's video +user->GetVideoCanvas()->subscribeWithView(hwnd, aspect, resolution); + +// Events arrive in delegate +void onUserVideoStatusChanged(...) override { } +``` + +### Chat + +```cpp +// Get singleton +IZoomVideoSDKChatHelper* chatHelper = sdk->getChatHelper(); + +// Use +chatHelper->sendChatToAll(L"Hello everyone!"); +chatHelper->sendChatToUser(user, L"Private message"); + +// Events arrive in delegate +void onChatNewMessageNotify(...) override { } +``` + +### Screen Share + +```cpp +// Get singleton +IZoomVideoSDKShareHelper* shareHelper = sdk->getShareHelper(); + +// Use (start YOUR share) +shareHelper->startShareScreen(monitorId); +shareHelper->stopShare(); + +// Subscribe to REMOTE share (in callback) +void onUserShareStatusChanged(..., IZoomVideoSDKShareAction* shareAction) override { + shareAction->subscribeWithView(hwnd, aspect); +} +``` + +### Command Channel + +```cpp +// Get singleton +IZoomVideoSDKCmdChannel* cmdChannel = sdk->getCmdChannel(); + +// Use +cmdChannel->sendCommandToAll(L"custom-data"); +cmdChannel->sendCommand(user, L"private-data"); + +// Events arrive in delegate +void onCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* cmd) override { } +``` + +--- + +## Why This Pattern Works + +| Aspect | Design Choice | Benefit | +|--------|---------------|---------| +| **Singletons** | One instance per feature | No object lifecycle management | +| **Observer** | Delegate callbacks | Decoupled, event-driven code | +| **Navigation** | Tree structure | Predictable access patterns | +| **Consistency** | Same pattern everywhere | Learn once, apply everywhere | + +--- + +## Common Mistakes + +### Mistake 1: Subscribing Too Early + +```cpp +// WRONG - video not ready +void onUserJoin(...) { + user->GetVideoCanvas()->subscribeWithView(hwnd, ...); // Error! +} + +// CORRECT - wait for video status +void onUserVideoStatusChanged(...) { + if (user->GetVideoPipe()->getVideoStatus().isOn) { + user->GetVideoCanvas()->subscribeWithView(hwnd, ...); + } +} +``` + +### Mistake 2: Missing Message Loop + +```cpp +// WRONG - callbacks never fire +sdk->joinSession(context); +while (true) { Sleep(100); } // No message pump! + +// CORRECT - process Windows messages +while (!done) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Sleep(10); +} +``` + +### Mistake 3: Using Helpers for Remote Users + +```cpp +// WRONG - helpers control YOUR streams only +sdk->getVideoHelper()->startVideo(); // Starts YOUR camera +sdk->getVideoHelper()->stopVideo(); // Stops YOUR camera + +// CORRECT - subscribe to remote users via their Canvas +remoteUser->GetVideoCanvas()->subscribeWithView(hwnd, ...); +``` + +--- + +## Related Documentation + +- [Singleton Hierarchy](singleton-hierarchy.md) - Complete navigation tree +- [Canvas vs Raw Data](canvas-vs-raw-data.md) - Choose rendering approach +- [Delegate Methods](../references/delegate-methods.md) - All 80+ callbacks +- [Session Join Pattern](../examples/session-join-pattern.md) - Working code + +--- + +**TL;DR**: Get singleton → Implement delegate → Subscribe & use. This works for every feature. diff --git a/plugins/zoom-developers/skills/video-sdk/windows/concepts/singleton-hierarchy.md b/plugins/zoom-developers/skills/video-sdk/windows/concepts/singleton-hierarchy.md new file mode 100644 index 00000000..d4d82728 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/concepts/singleton-hierarchy.md @@ -0,0 +1,492 @@ +# Singleton Hierarchy: Navigation Guide + +## Overview + +The Zoom Video SDK uses a **service locator pattern** - a tree of singletons where you navigate from the root SDK object down to specific features. You don't construct objects; you traverse to them. + +``` +You want to... You navigate to... +───────────────────────────────────────────────────── +Start your camera IZoomVideoSDK → IZoomVideoSDKVideoHelper +Mute a user IZoomVideoSDK → IZoomVideoSDKAudioHelper +Subscribe to video IZoomVideoSDKUser → IZoomVideoSDKCanvas +Get raw YUV frames IZoomVideoSDKUser → IZoomVideoSDKRawDataPipe +Send chat message IZoomVideoSDK → IZoomVideoSDKChatHelper +Start screen share IZoomVideoSDK → IZoomVideoSDKShareHelper +Subscribe to remote share IZoomVideoSDKShareAction → subscribeWithView() +``` + +--- + +## Complete Hierarchy (5 Levels Deep) + +``` +Level 0: Global Factory Function +│ +└─► CreateZoomVideoSDKObj() ──────────────────────────────────► IZoomVideoSDK* + │ + ├─► Level 1: Session & Lifecycle + │ ├── initialize(params) → ZoomVideoSDKErrors + │ ├── joinSession(context) → IZoomVideoSDKSession* + │ ├── leaveSession(end) → ZoomVideoSDKErrors + │ ├── addListener(delegate) → void + │ ├── getSessionInfo() → IZoomVideoSDKSession* + │ └── isInSession() → bool + │ + ├─► Level 1: Core Helpers (Control YOUR streams) + │ ├── getVideoHelper() → IZoomVideoSDKVideoHelper* + │ │ ├── startVideo() / stopVideo() + │ │ ├── switchCamera(deviceId) + │ │ ├── getCameraList() → IVideoSDKVector* + │ │ │ └─► Level 4: IZoomVideoSDKCameraDevice + │ │ │ ├── getDeviceId() + │ │ │ ├── getDeviceName() + │ │ │ └── isSelectedDevice() + │ │ └── startVideoCanvasPreview(hwnd, aspect, resolution) + │ │ + │ ├── getAudioHelper() → IZoomVideoSDKAudioHelper* + │ │ ├── startAudio() / stopAudio() + │ │ ├── muteAudio(user) / unmuteAudio(user) + │ │ ├── getMicList() → IVideoSDKVector* + │ │ │ └─► Level 4: IZoomVideoSDKMicDevice + │ │ ├── getSpeakerList() → IVideoSDKVector* + │ │ │ └─► Level 4: IZoomVideoSDKSpeakerDevice + │ │ └── selectMic() / selectSpeaker() + │ │ + │ ├── getShareHelper() → IZoomVideoSDKShareHelper* + │ │ ├── startShareScreen(monitorId) + │ │ ├── startShareView(hwnd) + │ │ ├── startShareComputerAudio() + │ │ ├── startSharingExternalSource(source) + │ │ ├── stopShare() + │ │ ├── isOtherSharing() / isSharingOut() + │ │ ├── lockShare(lock) / isShareLocked() + │ │ ├── enableMultiShare(enable) + │ │ └── getWhiteboardHelper() → IZoomVideoSDKWhiteboardHelper* + │ │ + │ ├── getChatHelper() → IZoomVideoSDKChatHelper* + │ │ ├── sendChatToAll(message) + │ │ └── sendChatToUser(user, message) + │ │ + │ ├── getUserHelper() → IZoomVideoSDKUserHelper* + │ │ ├── removeUser(user) + │ │ ├── makeHost(user) + │ │ ├── makeManager(user) + │ │ └── changeName(user, name) + │ │ + │ ├── getRecordingHelper() → IZoomVideoSDKRecordingHelper* + │ │ + │ ├── getCmdChannel() → IZoomVideoSDKCmdChannel* + │ │ ├── sendCommand(user, cmd) + │ │ └── sendCommandToAll(cmd) + │ │ + │ ├── getLiveStreamHelper() → IZoomVideoSDKLiveStreamHelper* + │ │ + │ ├── getPhoneHelper() → IZoomVideoSDKPhoneHelper* + │ │ + │ └── getLiveTranscriptionHelper() → IZoomVideoSDKLiveTranscriptionHelper* + │ + ├─► Level 1: Settings Helpers (Configure devices & behavior) + │ ├── getAudioSettingHelper() → IZoomVideoSDKAudioSettingHelper* + │ │ ├── enableAutoAdjustMicVolume() + │ │ ├── enableStereoAudio() + │ │ └── setEchoCancellationLevel() + │ │ + │ ├── GetAudioDeviceTestHelper() → IZoomVideoSDKTestAudioDeviceHelper* + │ │ ├── startMicTest() / stopMicTest() + │ │ ├── startSpeakerTest() / stopSpeakerTest() + │ │ └── playMicTest() + │ │ + │ ├── getVideoSettingHelper() → IZoomVideoSDKVideoSettingHelper* + │ │ ├── enableHDVideo() + │ │ ├── enableMirrorEffect() + │ │ └── setVideoQualityPreference() + │ │ + │ └── getShareSettingHelper() → IZoomVideoSDKShareSettingHelper* + │ ├── enableGreenBorderWhenSharing() + │ └── setShareScreenSetting() + │ + ├─► Level 1: Advanced Helpers (Special features) + │ ├── getNetworkConnectionHelper() → IZoomVideoSDKNetworkConnectionHelper* + │ │ └── getNetworkType() + │ │ + │ ├── getCRCHelper() → IZoomVideoSDKCRCHelper* + │ │ └── callCRCDevice(address, protocol) + │ │ + │ ├── getSubSessionHelper() → IZoomVideoSDKSubSessionHelper* + │ │ ├── createSubSession(name) + │ │ ├── joinSubSession(id) + │ │ ├── leaveSubSession() + │ │ └── getSubSessionList() + │ │ + │ ├── getIncomingLiveStreamHelper()→ IZoomVideoSDKIncomingLiveStreamHelper* + │ │ ├── bindIncomingLiveStream(streamKeyId) + │ │ ├── unbindIncomingLiveStream(streamKeyId) + │ │ └── startIncomingLiveStream(streamKeyId) + │ │ + │ ├── getBroadcastStreamingController() → IZoomVideoSDKBroadcastStreamingController* + │ │ ├── startBroadcast() + │ │ └── stopBroadcast() + │ │ + │ ├── getBroadcastStreamingViewer()→ IZoomVideoSDKBroadcastStreamingViewer* + │ │ └── joinBroadcast(channelId) + │ │ + │ └── getRealTimeMediaStreamsHelper() → IZoomVideoSDKRTMSHelper* (RTMS) + │ ├── startRealTimeMediaStream() + │ └── stopRealTimeMediaStream() + │ + └─► Level 1: Session Object + │ + └── getSessionInfo() → IZoomVideoSDKSession* + ├── getSessionName() + ├── getSessionID() + ├── getSessionHost() → IZoomVideoSDKUser* + ├── getMyself() → IZoomVideoSDKUser* + │ │ + │ └─► Level 3: IZoomVideoSDKUser (LOCAL - yourself) + │ ├── getUserID() / getUserName() + │ ├── isHost() / isManager() + │ ├── getVideoStatus() → ZoomVideoSDKVideoStatus + │ ├── getAudioStatus() → ZoomVideoSDKAudioStatus + │ │ + │ ├── GetVideoCanvas() → IZoomVideoSDKCanvas* [SDK RENDERING] + │ │ └─► Level 4: Canvas API + │ │ ├── subscribeWithView(hwnd, aspect, resolution) + │ │ ├── unSubscribeWithView(hwnd) + │ │ ├── setAspectMode(aspect) + │ │ └── setResolution(resolution) + │ │ + │ ├── GetVideoPipe() → IZoomVideoSDKRawDataPipe* [RAW DATA] + │ │ └─► Level 4: Raw Data Pipe + │ │ ├── subscribe(resolution, delegate) + │ │ ├── unSubscribe(delegate) + │ │ ├── getVideoStatus() + │ │ │ + │ │ └─► Level 5: IZoomVideoSDKRawDataPipeDelegate (your callback) + │ │ ├── onRawDataFrameReceived(YUVRawDataI420*) + │ │ └── onRawDataStatusChanged(status) + │ │ + │ ├── getShareActionList() → IVideoSDKVector* + │ │ └─► Level 4: IZoomVideoSDKShareAction (for share subscription) + │ │ ├── getShareCanvas() → IZoomVideoSDKCanvas* + │ │ ├── getSharePipe() → IZoomVideoSDKRawDataPipe* + │ │ ├── getShareStatus() → ZoomVideoSDKShareStatus + │ │ ├── getShareType() → ZoomVideoSDKShareType + │ │ ├── getShareSourceId() + │ │ ├── isAnnotationPrivilegeEnabled() + │ │ └── getRemoteControlHelper() → IZoomVideoSDKRemoteControlHelper* (Win/Mac) + │ │ + │ └── getRemoteCameraControlHelper() → IZoomVideoSDKRemoteCameraControlHelper* + │ + └── getRemoteUsers() → IVideoSDKVector* + │ + └─► Level 3: IZoomVideoSDKUser (REMOTE - other participants) + ├── [Same methods as local user] + ├── GetVideoCanvas() → Subscribe to their video + └── GetVideoPipe() → Get their raw frames + + ┌─────────────────────────────────────────────────────────────────────────┐ + │ CALLBACK PATH (from IZoomVideoSDKDelegate) │ + │ │ + │ ⚠️ CRITICAL: Share subscription uses IZoomVideoSDKShareAction from │ + │ callback, NOT user->GetShareCanvas()! │ + │ │ + │ onUserShareStatusChanged(pShareHelper, pUser, pShareAction) │ + │ │ │ + │ └─► IZoomVideoSDKShareAction* (received in callback) │ + │ ├── getShareCanvas() → IZoomVideoSDKCanvas* │ + │ │ └── subscribeWithView(hwnd, aspect) │ + │ │ └── unSubscribeWithView(hwnd) │ + │ ├── getSharePipe() → IZoomVideoSDKRawDataPipe* │ + │ │ └── subscribe(resolution, delegate) │ + │ │ └── unSubscribe(delegate) │ + │ ├── getShareStatus() → ZoomVideoSDKShareStatus │ + │ ├── getShareType() → ZoomVideoSDKShareType │ + │ ├── getShareSourceId() │ + │ ├── getShareSourceContentSize() → ZoomVideoSDKViewSize │ + │ ├── isAnnotationPrivilegeEnabled() │ + │ └── getRemoteControlHelper() → IZoomVideoSDKRemoteControlHelper│ + │ │ + │ Pattern: Subscribe to share in onUserShareStatusChanged callback │ + │ void onUserShareStatusChanged(..., IZoomVideoSDKShareAction* action) { │ + │ if (action->getShareStatus() == ZoomVideoSDKShareStatus_Start) { │ + │ action->getShareCanvas()->subscribeWithView(hwnd, aspect); │ + │ } │ + │ } │ + └─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Key Difference from Meeting SDK + +| Aspect | Meeting SDK | Video SDK | +|--------|-------------|-----------| +| **Root Object** | `IMeetingService` | `IZoomVideoSDK` | +| **Feature Access** | Controllers (`GetMeetingAudioController()`) | Helpers (`getAudioHelper()`) | +| **Video Subscription** | Per-user renderers | Per-user Canvas/Pipe | +| **Share Subscription** | Via callback's `IZoomVideoSDKShareAction` | Via callback's `IZoomVideoSDKShareAction` | +| **Depth** | 4 levels max | 5 levels max | + +--- + +## When to Use Each Level + +| Level | When | Example | +|-------|------|---------| +| **Level 1** | After SDK init, control YOUR streams | `sdk->getVideoHelper()->startVideo()` | +| **Level 2** | After session join, get session info | `sdk->getSessionInfo()->getMyself()` | +| **Level 3** | Get user objects | `session->getRemoteUsers()` | +| **Level 4** | Subscribe to video/share | `user->GetVideoCanvas()->subscribeWithView()` | +| **Level 5** | Receive raw frames | `pipe->subscribe(res, myDelegate)` | + +--- + +## Two Rendering Paths + +The SDK provides **two distinct paths** for video rendering: + +### Path A: Canvas API (SDK-Rendered) + +``` +IZoomVideoSDKUser + └── GetVideoCanvas() + └── IZoomVideoSDKCanvas + └── subscribeWithView(HWND, aspect, resolution) + └── SDK renders directly to your window +``` + +**Pros**: Best quality, no CPU overhead, automatic scaling +**Use for**: Standard video conferencing UI + +### Path B: Raw Data Pipe (Self-Rendered) + +``` +IZoomVideoSDKUser + └── GetVideoPipe() + └── IZoomVideoSDKRawDataPipe + └── subscribe(resolution, delegate) + └── Your IZoomVideoSDKRawDataPipeDelegate + └── onRawDataFrameReceived(YUVRawDataI420*) + └── You convert YUV→RGB and render +``` + +**Pros**: Full control over frames, can process/filter/record +**Use for**: Custom effects, AI processing, recording + +--- + +## Universal Pattern (3 Steps) + +Every feature follows the **same pattern**: + +```cpp +// Step 1: Navigate to the helper (singleton) +IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper(); + +// Step 2: Use it +videoHelper->startVideo(); + +// For subscriptions, get user first: +IZoomVideoSDKUser* user = sdk->getSessionInfo()->getMyself(); +IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); +canvas->subscribeWithView(hwnd, aspect, resolution); +``` + +For **event-driven features**, implement `IZoomVideoSDKDelegate`: + +```cpp +// Step 1: Implement delegate +class MyDelegate : public IZoomVideoSDKDelegate { + void onUserVideoStatusChanged(...) override { + // React to video status changes + } + // ... all 80+ callbacks +}; + +// Step 2: Register +sdk->addListener(new MyDelegate()); + +// Step 3: Events arrive automatically +``` + +--- + +## Navigation by Feature + +| Feature | Navigation Path | +|---------|-----------------| +| **Start camera** | `sdk->getVideoHelper()->startVideo()` | +| **Stop camera** | `sdk->getVideoHelper()->stopVideo()` | +| **Switch camera** | `sdk->getVideoHelper()->switchCamera(deviceId)` | +| **Camera list** | `sdk->getVideoHelper()->getCameraList()` | +| **Mute audio** | `sdk->getAudioHelper()->muteAudio(user)` | +| **Unmute audio** | `sdk->getAudioHelper()->unmuteAudio(user)` | +| **Start audio** | `sdk->getAudioHelper()->startAudio()` | +| **Mic list** | `sdk->getAudioHelper()->getMicList()` | +| **Speaker list** | `sdk->getAudioHelper()->getSpeakerList()` | +| **Test mic** | `sdk->GetAudioDeviceTestHelper()->startMicTest()` | +| **Test speaker** | `sdk->GetAudioDeviceTestHelper()->startSpeakerTest()` | +| **Send chat** | `sdk->getChatHelper()->sendChatToAll(msg)` | +| **Start share** | `sdk->getShareHelper()->startShareScreen(monitorId)` | +| **Stop share** | `sdk->getShareHelper()->stopShare()` | +| **Subscribe video** | `user->GetVideoCanvas()->subscribeWithView(hwnd, ...)` | +| **Get raw frames** | `user->GetVideoPipe()->subscribe(res, delegate)` | +| **Subscribe share** | `shareAction->getShareCanvas()->subscribeWithView(hwnd, aspect)` ⚠️ From callback! | +| **Get raw share** | `shareAction->getSharePipe()->subscribe(res, delegate)` ⚠️ From callback! | +| **Kick user** | `sdk->getUserHelper()->removeUser(user)` | +| **Make host** | `sdk->getUserHelper()->makeHost(user)` | +| **Send command** | `sdk->getCmdChannel()->sendCommandToAll(cmd)` | +| **Get myself** | `sdk->getSessionInfo()->getMyself()` | +| **Get remote users** | `sdk->getSessionInfo()->getRemoteUsers()` | +| **Join subsession** | `sdk->getSubSessionHelper()->joinSubSession(id)` | +| **Start broadcast** | `sdk->getBroadcastStreamingController()->startBroadcast()` | +| **Transcription** | `sdk->getLiveTranscriptionHelper()->startLiveTranscription()` | + +--- + +## Critical Timing Rules + +### 1. Helpers Control YOUR Streams Only + +```cpp +// videoHelper controls YOUR camera, not others' +sdk->getVideoHelper()->startVideo(); // Starts YOUR camera +sdk->getVideoHelper()->stopVideo(); // Stops YOUR camera + +// To SEE other users' video, subscribe via their Canvas/Pipe +IZoomVideoSDKUser* remoteUser = ...; +remoteUser->GetVideoCanvas()->subscribeWithView(hwnd, ...); +``` + +### 2. Subscribe in onUserVideoStatusChanged, NOT onUserJoin + +```cpp +// WRONG - user's video may not be ready yet +void onUserJoin(..., userList) { + user->GetVideoCanvas()->subscribeWithView(hwnd, ...); // Error 2! +} + +// CORRECT - wait for video status change +void onUserVideoStatusChanged(..., userList) { + for (auto user : userList) { + if (user->GetVideoPipe()->getVideoStatus().isOn) { + user->GetVideoCanvas()->subscribeWithView(hwnd, ...); // Works! + } + } +} +``` + +### 3. ShareAction Comes from Callback (CRITICAL!) + +⚠️ **Share subscription is DIFFERENT from video subscription!** + +```cpp +// WRONG - Don't use user->GetShareCanvas() for remote share! +user->GetShareCanvas()->subscribeWithView(hwnd, ...); // Won't work! + +// CORRECT - Use IZoomVideoSDKShareAction from the callback +void onUserShareStatusChanged(IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) { + if (!pShareAction) return; + + ZoomVideoSDKShareStatus status = pShareAction->getShareStatus(); + + if (status == ZoomVideoSDKShareStatus_Start || + status == ZoomVideoSDKShareStatus_Resume) { + // Canvas API (SDK-rendered) + IZoomVideoSDKCanvas* shareCanvas = pShareAction->getShareCanvas(); + if (shareCanvas) { + shareCanvas->subscribeWithView(shareHwnd, ZoomVideoSDKVideoAspect_Original); + } + + // OR Raw Data Pipe (self-rendered) + // IZoomVideoSDKRawDataPipe* sharePipe = pShareAction->getSharePipe(); + // sharePipe->subscribe(ZoomVideoSDKResolution_720P, myDelegate); + } + else if (status == ZoomVideoSDKShareStatus_Stop) { + // Unsubscribe when share stops + IZoomVideoSDKCanvas* shareCanvas = pShareAction->getShareCanvas(); + if (shareCanvas) { + shareCanvas->unSubscribeWithView(shareHwnd); + } + } +} +``` + +**Why?** The `IZoomVideoSDKShareAction` represents a specific share stream and is only valid within the callback context. You cannot navigate to it via user objects. + +### 4. Check nullptr Before Use + +```cpp +IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); +if (canvas) { + canvas->subscribeWithView(hwnd, aspect, resolution); +} +``` + +--- + +## Practical Rules + +### 1. Get Helpers After Initialize + +```cpp +// WRONG - SDK not initialized +IZoomVideoSDKVideoHelper* helper = sdk->getVideoHelper(); // nullptr! +sdk->initialize(params); + +// CORRECT +sdk->initialize(params); +IZoomVideoSDKVideoHelper* helper = sdk->getVideoHelper(); // Valid +``` + +### 2. Get Session/Users After Join + +```cpp +// WRONG - not in session yet +IZoomVideoSDKSession* session = sdk->getSessionInfo(); // nullptr! +sdk->joinSession(context); + +// CORRECT - wait for onSessionJoin callback +void onSessionJoin() { + IZoomVideoSDKSession* session = sdk->getSessionInfo(); // Valid + IZoomVideoSDKUser* myself = session->getMyself(); // Valid +} +``` + +### 3. One HWND Per Video Stream + +```cpp +// Each user needs their own window +HWND selfWindow = CreateWindow(...); +HWND user1Window = CreateWindow(...); +HWND user2Window = CreateWindow(...); + +myself->GetVideoCanvas()->subscribeWithView(selfWindow, ...); +user1->GetVideoCanvas()->subscribeWithView(user1Window, ...); +user2->GetVideoCanvas()->subscribeWithView(user2Window, ...); +``` + +--- + +## Deepest Paths (Maximum Depth = 5) + +| Path | Use Case | +|------|----------| +| `IZoomVideoSDK` → `getVideoHelper()` → `getCameraList()` → `IZoomVideoSDKCameraDevice` → `getDeviceId()` | Enumerate cameras | +| `IZoomVideoSDK` → `getSessionInfo()` → `getMyself()` → `GetVideoPipe()` → `subscribe(delegate)` | Raw self video | +| `IZoomVideoSDK` → `getSessionInfo()` → `getRemoteUsers()` → `user->GetVideoCanvas()` → `subscribeWithView()` | Remote video display | + +--- + +## Related Documentation + +- [API Reference](../references/windows-reference.md) - Complete method signatures +- [SKILL.md](../SKILL.md) - Main skill overview with code examples +- [Video Rendering Guide](../SKILL.md#video-rendering---two-approaches) - Canvas vs Raw Data comparison + +--- + +**TL;DR**: Start at `IZoomVideoSDK`, navigate to helpers for YOUR streams, navigate to users for THEIR streams. Subscribe to video in `onUserVideoStatusChanged`, not `onUserJoin`. diff --git a/plugins/zoom-developers/skills/video-sdk/windows/examples/cloud-recording.md b/plugins/zoom-developers/skills/video-sdk/windows/examples/cloud-recording.md new file mode 100644 index 00000000..93360268 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/examples/cloud-recording.md @@ -0,0 +1,317 @@ +# Cloud Recording + +Complete working code for controlling cloud recording in sessions. + +**Official Sample**: `VSDK_CloudRecording` in [videosdk-windows-rawdata-sample](https://github.com/zoom/videosdk-windows-rawdata-sample) + +--- + +## Overview + +Cloud recording saves session recordings to Zoom's cloud storage. Features: +- Start/stop recording programmatically +- Recording consent handling +- Recording status notifications + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ CLOUD RECORDING FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. Check canStartRecording() │ +│ 2. Start recording → startCloudRecording() │ +│ 3. Handle consent → onCloudRecordingStatus() callback │ +│ 4. Stop recording → stopCloudRecording() │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Prerequisites + +- Session must have cloud recording enabled +- User must have recording privileges (host or granted permission) +- Valid Zoom account with cloud recording quota + +--- + +## Complete Working Code + +### RecordingManager.h + +```cpp +#pragma once +#include +#include "zoom_video_sdk_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class RecordingManager { +public: + RecordingManager(IZoomVideoSDK* sdk); + + // Recording control + bool StartRecording(); + bool StopRecording(); + bool PauseRecording(); + bool ResumeRecording(); + + // Status + bool CanStartRecording(); + bool IsRecording() const { return m_isRecording; } + + // Called from delegate + void OnRecordingStatus(RecordingStatus status, + IZoomVideoSDKRecordingConsentHandler* handler); + +private: + IZoomVideoSDK* m_sdk; + IZoomVideoSDKRecordingHelper* m_recordingHelper; + bool m_isRecording; +}; +``` + +### RecordingManager.cpp + +```cpp +#include "RecordingManager.h" +#include + +RecordingManager::RecordingManager(IZoomVideoSDK* sdk) + : m_sdk(sdk) + , m_recordingHelper(nullptr) + , m_isRecording(false) { +} + +bool RecordingManager::CanStartRecording() { + m_recordingHelper = m_sdk->getRecordingHelper(); + if (!m_recordingHelper) { + std::cout << "Recording helper not available" << std::endl; + return false; + } + + ZoomVideoSDKErrors err = m_recordingHelper->canStartRecording(); + if (err == ZoomVideoSDKErrors_Success) { + return true; + } + + std::cout << "Cannot start recording: " << err << std::endl; + return false; +} + +bool RecordingManager::StartRecording() { + if (!CanStartRecording()) { + return false; + } + + ZoomVideoSDKErrors err = m_recordingHelper->startCloudRecording(); + if (err == ZoomVideoSDKErrors_Success) { + std::cout << "Cloud recording started" << std::endl; + return true; + } + + std::cout << "Start recording failed: " << err << std::endl; + return false; +} + +bool RecordingManager::StopRecording() { + if (!m_recordingHelper) { + m_recordingHelper = m_sdk->getRecordingHelper(); + } + + if (!m_recordingHelper) { + return false; + } + + ZoomVideoSDKErrors err = m_recordingHelper->stopCloudRecording(); + if (err == ZoomVideoSDKErrors_Success) { + std::cout << "Cloud recording stopped" << std::endl; + m_isRecording = false; + return true; + } + + std::cout << "Stop recording failed: " << err << std::endl; + return false; +} + +bool RecordingManager::PauseRecording() { + if (!m_recordingHelper) return false; + + ZoomVideoSDKErrors err = m_recordingHelper->pauseCloudRecording(); + if (err == ZoomVideoSDKErrors_Success) { + std::cout << "Recording paused" << std::endl; + return true; + } + return false; +} + +bool RecordingManager::ResumeRecording() { + if (!m_recordingHelper) return false; + + ZoomVideoSDKErrors err = m_recordingHelper->resumeCloudRecording(); + if (err == ZoomVideoSDKErrors_Success) { + std::cout << "Recording resumed" << std::endl; + return true; + } + return false; +} + +void RecordingManager::OnRecordingStatus(RecordingStatus status, + IZoomVideoSDKRecordingConsentHandler* handler) { + switch (status) { + case RecordingStatus_Start: + std::cout << "Recording started" << std::endl; + m_isRecording = true; + break; + + case RecordingStatus_Stop: + std::cout << "Recording stopped" << std::endl; + m_isRecording = false; + break; + + case RecordingStatus_Pause: + std::cout << "Recording paused" << std::endl; + break; + + case RecordingStatus_Connecting: + std::cout << "Recording connecting..." << std::endl; + break; + + case RecordingStatus_DiskFull: + std::cout << "Recording stopped - disk full!" << std::endl; + m_isRecording = false; + break; + + default: + std::cout << "Recording status: " << status << std::endl; + } + + // Handle consent if required + if (handler) { + // Automatically accept recording consent + // In production, you may want to prompt the user + handler->accept(); + std::cout << "Recording consent accepted" << std::endl; + } +} +``` + +### Using in Delegate + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +private: + RecordingManager* m_recordingManager; + +public: + MyDelegate(IZoomVideoSDK* sdk) { + m_recordingManager = new RecordingManager(sdk); + } + + void onSessionJoin() override { + // Start recording when session begins + if (m_recordingManager->CanStartRecording()) { + m_recordingManager->StartRecording(); + } + } + + void onSessionLeave() override { + // Stop recording before leaving + if (m_recordingManager->IsRecording()) { + m_recordingManager->StopRecording(); + } + } + + void onCloudRecordingStatus(RecordingStatus status, + IZoomVideoSDKRecordingConsentHandler* handler) override { + m_recordingManager->OnRecordingStatus(status, handler); + } + + void onUserRecordingConsent(IZoomVideoSDKUser* user) override { + std::wcout << L"User gave recording consent: " + << user->getUserName() << std::endl; + } + + // ... other callbacks +}; +``` + +--- + +## Recording Status Values + +| Status | Description | +|--------|-------------| +| `RecordingStatus_Start` | Recording has started | +| `RecordingStatus_Stop` | Recording has stopped | +| `RecordingStatus_Pause` | Recording is paused | +| `RecordingStatus_Connecting` | Connecting to recording service | +| `RecordingStatus_DiskFull` | Recording stopped due to storage full | + +--- + +## Recording Consent + +When recording starts, participants may need to consent: + +```cpp +void onCloudRecordingStatus(RecordingStatus status, + IZoomVideoSDKRecordingConsentHandler* handler) override { + if (handler) { + // Options: + handler->accept(); // Accept recording + handler->decline(); // Decline (will leave session) + } +} +``` + +--- + +## IZoomVideoSDKRecordingHelper Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `canStartRecording()` | `ZoomVideoSDKErrors` | Check if can start | +| `startCloudRecording()` | `ZoomVideoSDKErrors` | Start recording | +| `stopCloudRecording()` | `ZoomVideoSDKErrors` | Stop recording | +| `pauseCloudRecording()` | `ZoomVideoSDKErrors` | Pause recording | +| `resumeCloudRecording()` | `ZoomVideoSDKErrors` | Resume recording | +| `getCloudRecordingStatus()` | `RecordingStatus` | Get current status | + +--- + +## Common Issues + +### canStartRecording() Returns Error + +**Causes**: +- Not host or no recording permission +- Cloud recording not enabled for account +- Already recording + +**Fix**: Check permissions and account settings + +### Recording Doesn't Start + +**Cause**: Session not fully joined + +**Fix**: Wait for `onSessionJoin` before starting: +```cpp +void onSessionJoin() override { + // Safe to start recording now + recordingManager->StartRecording(); +} +``` + +### Consent Handler is NULL + +**Cause**: Consent not required for this session + +**Fix**: This is normal - not all sessions require consent + +--- + +## Related Documentation + +- [Session Join Pattern](session-join-pattern.md) - Session setup +- [Delegate Methods](../references/delegate-methods.md) - Recording callbacks +- [API Reference](../references/windows-reference.md) - Method signatures diff --git a/plugins/zoom-developers/skills/video-sdk/windows/examples/command-channel.md b/plugins/zoom-developers/skills/video-sdk/windows/examples/command-channel.md new file mode 100644 index 00000000..fb1195bb --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/examples/command-channel.md @@ -0,0 +1,330 @@ +# Command Channel + +Complete working code for custom command messaging between participants. + +**Official Sample**: `VSDK_CommandChannel` in [videosdk-windows-rawdata-sample](https://github.com/zoom/videosdk-windows-rawdata-sample) + +--- + +## Overview + +The command channel enables custom data exchange between participants. Use cases: +- Application-specific signaling +- Game state synchronization +- Custom control messages +- Real-time collaboration data + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ COMMAND CHANNEL FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ Sender: │ +│ getCmdChannel() → sendCommand() or sendCommandToAll() │ +│ │ +│ Receiver: │ +│ onCommandReceived(sender, command) callback │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Limitations + +| Limit | Value | +|-------|-------| +| Max message rate | 60 messages/second | +| Max message size | ~1KB recommended | +| Reliability | Best effort (not guaranteed) | + +**Note**: Commands are not persisted - late joiners won't receive previous commands. + +--- + +## Complete Working Code + +### CommandHandler.h + +```cpp +#pragma once +#include +#include +#include +#include "zoom_video_sdk_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class CommandHandler { +public: + CommandHandler(IZoomVideoSDK* sdk); + + // Send commands + bool SendToAll(const std::wstring& command); + bool SendToUser(IZoomVideoSDKUser* user, const std::wstring& command); + + // Connection status + bool IsConnected() const { return m_connected; } + + // Callbacks from delegate + void OnCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* command); + void OnConnectResult(bool success); + + // Set message handler + using MessageCallback = std::function; + void SetMessageHandler(MessageCallback callback) { m_callback = callback; } + +private: + IZoomVideoSDK* m_sdk; + IZoomVideoSDKCmdChannel* m_cmdChannel; + bool m_connected; + MessageCallback m_callback; +}; +``` + +### CommandHandler.cpp + +```cpp +#include "CommandHandler.h" +#include + +CommandHandler::CommandHandler(IZoomVideoSDK* sdk) + : m_sdk(sdk) + , m_cmdChannel(nullptr) + , m_connected(false) { +} + +bool CommandHandler::SendToAll(const std::wstring& command) { + if (!m_cmdChannel) { + m_cmdChannel = m_sdk->getCmdChannel(); + } + + if (!m_cmdChannel) { + std::cout << "Command channel not available" << std::endl; + return false; + } + + ZoomVideoSDKErrors err = m_cmdChannel->sendCommand(nullptr, command.c_str()); + if (err == ZoomVideoSDKErrors_Success) { + std::wcout << L"Sent to all: " << command << std::endl; + return true; + } + + std::cout << "Send failed: " << err << std::endl; + return false; +} + +bool CommandHandler::SendToUser(IZoomVideoSDKUser* user, const std::wstring& 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) { + std::wcout << L"Sent to " << user->getUserName() + << L": " << command << std::endl; + return true; + } + + std::cout << "Send failed: " << err << std::endl; + return false; +} + +void CommandHandler::OnCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* command) { + if (!sender || !command) return; + + std::wstring cmdStr(command); + std::wcout << L"Command from " << sender->getUserName() + << L": " << cmdStr << std::endl; + + // Call user handler if set + if (m_callback) { + m_callback(sender, cmdStr); + } +} + +void CommandHandler::OnConnectResult(bool success) { + m_connected = success; + std::cout << "Command channel " << (success ? "connected" : "failed") << std::endl; +} +``` + +### Using in Delegate + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +private: + CommandHandler* m_cmdHandler; + +public: + MyDelegate(IZoomVideoSDK* sdk) { + m_cmdHandler = new CommandHandler(sdk); + + // Set message handler + m_cmdHandler->SetMessageHandler([this](IZoomVideoSDKUser* sender, + const std::wstring& cmd) { + HandleCommand(sender, cmd); + }); + } + + void onSessionJoin() override { + // Send hello to all participants + m_cmdHandler->SendToAll(L"hello"); + } + + void onCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* strCmd) override { + m_cmdHandler->OnCommandReceived(sender, strCmd); + } + + void onCommandChannelConnectResult(bool isSuccess) override { + m_cmdHandler->OnConnectResult(isSuccess); + } + +private: + void HandleCommand(IZoomVideoSDKUser* sender, const std::wstring& cmd) { + // Parse and handle commands + if (cmd == L"ping") { + m_cmdHandler->SendToUser(sender, L"pong"); + } + else if (cmd.find(L"action:") == 0) { + // Handle action command + std::wstring action = cmd.substr(7); + ProcessAction(action); + } + } + + void ProcessAction(const std::wstring& action) { + std::wcout << L"Processing action: " << action << std::endl; + } +}; +``` + +--- + +## JSON Command Pattern + +For structured data, use JSON encoding: + +```cpp +#include + +// Send JSON command +void SendJsonCommand(CommandHandler* handler, const std::string& type, + const Json::Value& data) { + Json::Value root; + root["type"] = type; + root["data"] = data; + + Json::StreamWriterBuilder builder; + std::string jsonStr = Json::writeString(builder, root); + std::wstring wideStr(jsonStr.begin(), jsonStr.end()); + + handler->SendToAll(wideStr); +} + +// Receive and parse JSON +void HandleJsonCommand(const std::wstring& cmd) { + std::string narrowStr(cmd.begin(), cmd.end()); + + Json::Value root; + Json::CharReaderBuilder builder; + std::istringstream stream(narrowStr); + + if (Json::parseFromStream(builder, stream, &root, nullptr)) { + std::string type = root["type"].asString(); + Json::Value data = root["data"]; + + if (type == "position") { + int x = data["x"].asInt(); + int y = data["y"].asInt(); + // Handle position update + } + } +} + +// Usage +Json::Value posData; +posData["x"] = 100; +posData["y"] = 200; +SendJsonCommand(cmdHandler, "position", posData); +``` + +--- + +## IZoomVideoSDKCmdChannel Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `sendCommand(user, cmd)` | `ZoomVideoSDKErrors` | Send to specific user (NULL = all) | + +--- + +## Rate Limiting + +The command channel is limited to **60 messages/second**: + +```cpp +class RateLimitedSender { + std::chrono::steady_clock::time_point m_lastSend; + static const int MIN_INTERVAL_MS = 17; // ~60/sec + +public: + bool SendWithRateLimit(CommandHandler* handler, const std::wstring& cmd) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast( + now - m_lastSend).count(); + + if (elapsed < MIN_INTERVAL_MS) { + std::this_thread::sleep_for( + std::chrono::milliseconds(MIN_INTERVAL_MS - elapsed)); + } + + m_lastSend = std::chrono::steady_clock::now(); + return handler->SendToAll(cmd); + } +}; +``` + +--- + +## Common Issues + +### Commands Not Received + +**Cause**: Channel not connected + +**Fix**: Wait for `onCommandChannelConnectResult(true)`: +```cpp +void onCommandChannelConnectResult(bool isSuccess) override { + if (isSuccess) { + // Now safe to send commands + } +} +``` + +### Error 8 (Too Frequent) + +**Cause**: Exceeding 60 messages/second limit + +**Fix**: Add rate limiting (see above) + +### Unicode Issues + +**Cause**: Encoding mismatch + +**Fix**: Use `std::wstring` consistently: +```cpp +m_cmdChannel->sendCommand(user, L"message"); // Wide string literal +``` + +--- + +## Related Documentation + +- [Session Join Pattern](session-join-pattern.md) - Session setup +- [Delegate Methods](../references/delegate-methods.md) - Command callbacks +- [API Reference](../references/windows-reference.md) - Method signatures diff --git a/plugins/zoom-developers/skills/video-sdk/windows/examples/dotnet-winforms/guide.md b/plugins/zoom-developers/skills/video-sdk/windows/examples/dotnet-winforms/guide.md new file mode 100644 index 00000000..060a8a60 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/examples/dotnet-winforms/guide.md @@ -0,0 +1,1226 @@ +# UI Integration Guide for Zoom Video SDK Windows + +This guide covers three different UI approaches for integrating the Zoom Video SDK: + +1. **Win32 (Native C++)** - Direct SDK usage, no wrapper +2. **WinForms (C# .NET)** - Requires C++/CLI wrapper +3. **WPF (C# .NET)** - Requires C++/CLI wrapper + BitmapSource conversion + +## Quick Comparison + +| Aspect | Win32 | WinForms | WPF | +|--------|-------|----------|-----| +| **Language** | C++ | C# | C# | +| **Wrapper Required** | No | Yes (C++/CLI) | Yes (C++/CLI) | +| **Video Rendering** | Canvas API (SDK renders) | Raw Data Pipe (you render) | Raw Data Pipe + BitmapSource | +| **Performance** | Best | Good | Good (extra conversion) | +| **Complexity** | Medium | Medium | Higher | +| **UI Threading** | Win32 message loop | `InvokeRequired` | `Dispatcher` | + +--- + +## Option 1: Win32 (Native C++) - Direct SDK + +**No wrapper needed.** The SDK is native C++, so Win32 apps use it directly. + +### Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ +│ Win32 Dialog │────►│ Native C++ SDK │ +│ (main.cpp) │◄────│ (videosdk.dll) │ +└─────────────────┘ └─────────────────┘ + HWND Canvas API +``` + +### Key Patterns + +#### 1. SDK Manager Class (Native C++) + +```cpp +// ZoomSDKManager.h +class ZoomSDKManager { +private: + IZoomVideoSDK* m_pZoomSDK; + IZoomVideoSDKSession* m_pSession; + CustomZoomDelegate* m_pDelegate; + +public: + bool Initialize(); + bool JoinSession(const std::string& name, const std::string& token, ...); + bool StartVideo(); + bool StartVideoPreview(HWND hwnd); // Canvas API! + bool SubscribeRemoteVideo(HWND hwnd, const std::string& userId); +}; +``` + +#### 2. Delegate Implementation (All 80+ Callbacks) + +```cpp +class CustomZoomDelegate : public IZoomVideoSDKDelegate { +private: + ZoomSDKManager* m_pManager; + +public: + void onSessionJoin() override { + m_pManager->OnSessionStatusChanged(SessionStatus::InSession, "Joined"); + } + + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + // Handle video status changes + } + + // ... implement all 80+ callbacks +}; +``` + +#### 3. Video Rendering with Canvas API (SDK-Rendered) + +```cpp +// Start video preview - SDK renders directly to HWND +bool ZoomSDKManager::StartVideoPreview(HWND hwnd) { + IZoomVideoSDKVideoHelper* videoHelper = m_pZoomSDK->getVideoHelper(); + + // SDK renders directly to the window handle + ZoomVideoSDKErrors ret = videoHelper->startVideoCanvasPreview(hwnd); + return ret == ZoomVideoSDKErrors_Success; +} + +// Subscribe to remote user's video +bool ZoomSDKManager::SubscribeRemoteVideo(HWND hwnd, const std::string& userId) { + IZoomVideoSDKSession* session = m_pZoomSDK->getSessionInfo(); + IVideoSDKVector* userList = session->getRemoteUsers(); + + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); + + // SDK renders remote video directly to HWND + canvas->subscribeWithView(hwnd, ZoomVideoSDKVideoAspect_Original, ZoomVideoSDKResolution_Auto); + } + return true; +} +``` + +#### 4. Win32 Dialog with Video Panels + +```cpp +// main.cpp - Dialog procedure +INT_PTR CALLBACK MainDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { + switch (message) { + case WM_INITDIALOG: + InitializeZoomSDK(); + PopulateDeviceLists(hDlg); + return TRUE; + + case WM_COMMAND: + switch (LOWORD(wParam)) { + case IDC_START_VIDEO: + g_pSDKManager->StartVideo(); + + // Get HWND of video panel control + HWND selfVideoHwnd = GetDlgItem(hDlg, IDC_SELF_VIDEO); + g_pSDKManager->StartVideoPreview(selfVideoHwnd); + + HWND remoteVideoHwnd = GetDlgItem(hDlg, IDC_REMOTE_VIDEO); + g_pSDKManager->SubscribeRemoteVideo(remoteVideoHwnd, ""); + break; + } + } + return FALSE; +} +``` + +### Win32 Flow Summary + +``` +1. CreateZoomVideoSDKObj() +2. Initialize SDK with params +3. Create & register CustomZoomDelegate +4. Join session +5. On IDC_START_VIDEO click: + - startVideo() → transmit your camera + - startVideoCanvasPreview(selfHwnd) → see yourself + - subscribeWithView(remoteHwnd) → see others +6. SDK renders directly to HWNDs +``` + +### Sample Location +``` +C:\tempsdk\videosdk-windows-dotnet-desktop-framework-quickstart\ + └── ZoomVideoSDK.Win32\ + ├── main.cpp # Win32 dialog + event handlers + ├── ZoomSDKManager.cpp # SDK wrapper class + ├── ZoomSDKManager.h # Header with delegate + └── main.rc # Dialog resources +``` + +--- + +## Option 2: WinForms (C# + C++/CLI Wrapper) + +**Requires C++/CLI bridge** because Zoom SDK is native C++. + +### Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ C# WinForms │────►│ C++/CLI Wrapper│────►│ Native C++ SDK │ +│ (MainForm.cs) │◄────│ (ZoomSDKManager)│◄────│ (videosdk.dll) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + Events gcroot Callbacks + Bitmap^ YUV→RGB YUVRawDataI420 +``` + +### Key Patterns + +#### 1. C++/CLI Wrapper Class + +```cpp +// ZoomSDKManager.h (C++/CLI) +public ref class ZoomSDKManager { +private: + void* m_pVideoSDK; // Hide native types + void* m_pSessionHandler; // Native callback handler + +public: + // Managed events for C# consumption + event EventHandler^ SessionStatusChanged; + event EventHandler^ PreviewVideoReceived; + event EventHandler^ RemoteVideoReceived; + + bool Initialize(); + bool JoinSession(String^ name, String^ token, String^ user, String^ pw); + bool StartVideo(); +}; +``` + +#### 2. Native Callback → Managed Event (gcroot pattern) + +```cpp +// Native handler stores managed reference via gcroot +class VideoPreviewHandler : public IZoomVideoSDKRawDataPipeDelegate { +private: + gcroot m_managedHandler; // Prevents GC + +public: + VideoPreviewHandler(ZoomSDKManager^ handler) : m_managedHandler(handler) {} + + void onRawDataFrameReceived(YUVRawDataI420* data) override { + ZoomSDKManager^ handler = static_cast(m_managedHandler); + if (handler && data) { + // Convert YUV to Bitmap + Bitmap^ bitmap = handler->ConvertYUVToBitmap( + data->GetYBuffer(), data->GetUBuffer(), data->GetVBuffer(), + data->GetStreamWidth(), data->GetStreamHeight(), ...); + + // Fire managed event + handler->OnPreviewVideoReceived(bitmap); + } + } +}; +``` + +#### 3. YUV→RGB Conversion (LockBits for Performance) + +```cpp +Bitmap^ ZoomSDKManager::ConvertYUVToBitmap(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height, ...) { + Bitmap^ bitmap = gcnew Bitmap(width, height, PixelFormat::Format24bppRgb); + + // Lock for direct memory access (100x faster than SetPixel) + BitmapData^ data = bitmap->LockBits(rect, ImageLockMode::WriteOnly, ...); + unsigned char* rgbPtr = (unsigned char*)data->Scan0.ToPointer(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + // YUV420 → RGB (ITU-R BT.601) + int Y = yBuffer[y * yStride + x]; + int U = uBuffer[(y/2) * uStride + (x/2)]; + int V = vBuffer[(y/2) * vStride + (x/2)]; + + int R = (298 * (Y-16) + 409 * (V-128) + 128) >> 8; + int G = (298 * (Y-16) - 100 * (U-128) - 208 * (V-128) + 128) >> 8; + int B = (298 * (Y-16) + 516 * (U-128) + 128) >> 8; + + // Write BGR (bitmap format) + rgbPtr[y * stride + x * 3 + 0] = (byte)B; + rgbPtr[y * stride + x * 3 + 1] = (byte)G; + rgbPtr[y * stride + x * 3 + 2] = (byte)R; + } + } + + bitmap->UnlockBits(data); + return bitmap; +} +``` + +#### 4. C# Consumer (WinForms) + +```csharp +// MainForm.cs +public partial class MainForm : Form { + private ZoomSDKInterop _zoomSDK; + + public MainForm() { + InitializeComponent(); + InitializeZoomSDK(); + } + + private void InitializeZoomSDK() { + _zoomSDK = new ZoomSDKInterop(); + + // Subscribe to events + _zoomSDK.SessionJoined += OnSessionJoined; + _zoomSDK.PreviewVideoReceived += OnPreviewVideo; + _zoomSDK.RemoteVideoReceived += OnRemoteVideo; + + _zoomSDK.Initialize(); + } + + private void OnPreviewVideo(object sender, VideoFrameEventArgs e) { + // Must marshal to UI thread + if (InvokeRequired) { + BeginInvoke(new Action(() => OnPreviewVideo(sender, e))); + return; + } + + // Display bitmap in PictureBox + _selfVideoPanel.Image?.Dispose(); + _selfVideoPanel.Image = e.Frame; + } +} +``` + +### WinForms Flow Summary + +``` +C# Layer: +1. new ZoomSDKInterop() → creates C++/CLI ZoomSDKManager +2. Subscribe to events (SessionJoined, PreviewVideoReceived, etc.) +3. _zoomSDK.Initialize() → SDK init +4. _zoomSDK.JoinSession(...) → join +5. _zoomSDK.StartVideo() → start camera + preview + +C++/CLI Layer: +1. Creates native SDK via CreateZoomVideoSDKObj() +2. Creates VideoPreviewHandler with gcroot +3. Starts Raw Data Pipe subscription +4. onRawDataFrameReceived → YUV→RGB → fires PreviewVideoReceived event + +C# Layer (UI Thread): +1. OnPreviewVideo receives Bitmap +2. Checks InvokeRequired for thread safety +3. Sets PictureBox.Image = bitmap +``` + +### Sample Location +``` +C:\tempsdk\videosdk-windows-dotnet-desktop-framework-quickstart\ + ├── ZoomVideoSDK.Wrapper\ # C++/CLI Bridge + │ ├── ZoomSDKManager.h # Managed class definition + │ └── ZoomSDKManager.cpp # Native ↔ Managed bridge + │ + └── ZoomVideoSDK.WinForms\ # C# WinForms App + ├── ZoomSDKInterop.cs # High-level C# wrapper + ├── MainForm.cs # UI + event handlers + └── Program.cs # Entry point +``` + +--- + +## Option 3: WPF (C# + C++/CLI Wrapper) + +**Same C++/CLI wrapper as WinForms**, but with additional WPF-specific handling. + +### Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ C# WPF │────►│ C# Interop │────►│ C++/CLI Wrapper│────►│ Native C++ SDK │ +│ (MainWindow) │◄────│ (ZoomSDKInterop)│◄────│ (ZoomSDKManager)│◄────│ (videosdk.dll) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ + BitmapSource Bitmap→BitmapSource gcroot Callbacks + Dispatcher Conversion YUV→RGB YUVRawDataI420 +``` + +### Key Differences from WinForms + +| Aspect | WinForms | WPF | +|--------|----------|-----| +| **Video Type** | `System.Drawing.Bitmap` | `System.Windows.Media.Imaging.BitmapSource` | +| **UI Thread** | `InvokeRequired` + `BeginInvoke` | `Dispatcher.CheckAccess()` + `Dispatcher.BeginInvoke` | +| **Image Control** | `PictureBox.Image` | `Image.Source` | +| **Extra Step** | None | Bitmap → BitmapSource conversion | + +### Key Patterns + +#### 1. WPF-Specific Event Args + +```csharp +// WPF uses BitmapSource instead of Bitmap +public class VideoFrameEventArgs : EventArgs { + public BitmapSource Frame { get; set; } // WPF type + public string UserId { get; set; } +} +``` + +#### 2. Bitmap → BitmapSource Conversion + +```csharp +// ZoomSDKInterop.cs (WPF version) +private BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap) { + if (bitmap == null) return null; + + using (var memory = new MemoryStream()) { + bitmap.Save(memory, System.Drawing.Imaging.ImageFormat.Png); + memory.Position = 0; + + var bitmapImage = new BitmapImage(); + bitmapImage.BeginInit(); + bitmapImage.StreamSource = memory; + bitmapImage.CacheOption = BitmapCacheOption.OnLoad; + bitmapImage.EndInit(); + bitmapImage.Freeze(); // Make thread-safe for WPF + + return bitmapImage; + } +} + +// Event handler bridges C++/CLI Bitmap to WPF BitmapSource +_sdkManager.PreviewVideoReceived += (sender, e) => { + var wpfFrame = ConvertBitmapToBitmapSource(e.Frame); + PreviewVideoReceived?.Invoke(this, new VideoFrameEventArgs(wpfFrame, "self")); +}; +``` + +#### 3. WPF Dispatcher for UI Thread + +```csharp +// MainWindow.xaml.cs +private void OnPreviewVideoReceived(object sender, VideoFrameEventArgs e) { + // WPF uses Dispatcher instead of InvokeRequired + if (!Dispatcher.CheckAccess()) { + Dispatcher.BeginInvoke(new Action(OnPreviewVideoReceived), sender, e); + return; + } + + // Frame throttling (~30fps) + if (DateTime.Now - _lastPreviewVideoUpdate < _videoUpdateInterval) return; + _lastPreviewVideoUpdate = DateTime.Now; + + if (e.Frame != null) { + SelfVideoImage.Source = e.Frame; // WPF Image control + } +} +``` + +#### 4. Alternative: WriteableBitmap (Higher Performance) + +For better performance, you can write directly to WriteableBitmap: + +```csharp +private BitmapSource CreateErrorBitmapSource(int width, int height, string message) { + var writeableBitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgr24, null); + writeableBitmap.Lock(); + + unsafe { + byte* backBuffer = (byte*)writeableBitmap.BackBuffer; + int stride = writeableBitmap.BackBufferStride; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + byte* pixel = backBuffer + y * stride + x * 3; + pixel[0] = 0; // Blue + pixel[1] = 0; // Green + pixel[2] = 255; // Red + } + } + } + + writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height)); + writeableBitmap.Unlock(); + writeableBitmap.Freeze(); // Thread-safe + + return writeableBitmap; +} +``` + +### WPF Flow Summary + +``` +Same as WinForms, with these differences: + +C# WPF Interop Layer: +1. Receives Bitmap from C++/CLI wrapper +2. Converts Bitmap → BitmapSource (PNG stream or WriteableBitmap) +3. Calls Freeze() to make cross-thread safe +4. Fires WPF-compatible event + +MainWindow (UI Thread): +1. OnPreviewVideoReceived receives BitmapSource +2. Checks Dispatcher.CheckAccess() for thread safety +3. Sets Image.Source = bitmapSource +``` + +### Sample Location +``` +C:\tempsdk\videosdk-windows-dotnet-desktop-framework-quickstart\ + ├── ZoomVideoSDK.Wrapper\ # C++/CLI Bridge (shared with WinForms) + │ ├── ZoomSDKManager.h + │ └── ZoomSDKManager.cpp + │ + └── ZoomVideoSDK.WPF\ # C# WPF App + ├── ZoomSDKInterop.cs # WPF-specific interop (BitmapSource) + ├── MainWindow.xaml # XAML layout + ├── MainWindow.xaml.cs # Code-behind with Dispatcher + └── App.xaml # Application entry +``` + +--- + +## Decision Matrix + +| If you need... | Use | Why | +|----------------|-----|-----| +| **Best performance** | Win32 | Canvas API, SDK renders directly | +| **C++ codebase** | Win32 | No interop overhead | +| **Existing WinForms app** | WinForms + C++/CLI | Natural integration | +| **Modern .NET UI** | WPF + C++/CLI | XAML, data binding | +| **Cross-platform .NET** | Consider Avalonia | WPF-like but cross-platform | + +## C++/CLI Wrapper Patterns (For .NET Integration) + +This section teaches **general C++/CLI wrapping patterns** applicable to ANY native C++ library. + +### When to Use C++/CLI + +| Scenario | Solution | +|----------|----------| +| Native C++ library → C# app | C++/CLI wrapper (this guide) | +| C library → C# app | P/Invoke (simpler, no wrapper needed) | +| COM library → C# app | COM Interop | +| .NET library → C++ app | Reverse P/Invoke or COM | + +### Project Setup + +1. **Create C++/CLI Class Library**: + - Visual Studio → New Project → "CLR Class Library (.NET Framework)" + - Or add `/clr` to existing C++ project + +2. **Project Properties**: + ``` + Configuration Properties → General: + - Common Language Runtime Support: /clr + - .NET Target Framework: v4.8 + + C/C++ → General: + - Additional Include Directories: path\to\native\sdk\include + + Linker → General: + - Additional Library Directories: path\to\native\sdk\lib + + Linker → Input: + - Additional Dependencies: native_sdk.lib + ``` + +3. **File Structure**: + ``` + MyWrapper/ + ├── MyWrapper.h # Managed ref class definition + ├── MyWrapper.cpp # Implementation + ├── NativeCallbacks.h # Native callback classes with gcroot + └── Stdafx.h # Precompiled header + ``` + +--- + +### Pattern 1: Basic Wrapper Structure + +**Goal**: Expose native C++ class to C# + +```cpp +// MyWrapper.h (C++/CLI) +#pragma once +#include // For string conversion + +using namespace System; +using namespace System::Runtime::InteropServices; + +namespace MyLibraryWrapper { + + // Forward declare native types (hide from C#) + class NativeClass; // Don't #include native headers here! + + public ref class ManagedWrapper { + private: + NativeClass* m_pNative; // Raw pointer to native object + bool m_disposed; + + public: + ManagedWrapper(); + ~ManagedWrapper(); // Destructor (IDisposable.Dispose) + !ManagedWrapper(); // Finalizer (destructor fallback) + + // Managed methods that wrap native calls + bool Initialize(); + void DoSomething(String^ param); + String^ GetResult(); + }; +} +``` + +```cpp +// MyWrapper.cpp +#include "stdafx.h" +#include "MyWrapper.h" +#include "native_sdk.h" // Include native headers in .cpp only! + +namespace MyLibraryWrapper { + + ManagedWrapper::ManagedWrapper() : m_pNative(nullptr), m_disposed(false) { + m_pNative = new NativeClass(); + } + + ManagedWrapper::~ManagedWrapper() { + this->!ManagedWrapper(); // Call finalizer + m_disposed = true; + } + + ManagedWrapper::!ManagedWrapper() { + if (m_pNative) { + delete m_pNative; + m_pNative = nullptr; + } + } + + bool ManagedWrapper::Initialize() { + if (!m_pNative) return false; + return m_pNative->init() == 0; // Native returns 0 for success + } + + void ManagedWrapper::DoSomething(String^ param) { + if (!m_pNative) return; + + // Convert managed String^ to native std::wstring + std::wstring nativeParam = msclr::interop::marshal_as(param); + m_pNative->doSomething(nativeParam.c_str()); + } + + String^ ManagedWrapper::GetResult() { + if (!m_pNative) return nullptr; + + // Convert native wchar_t* to managed String^ + const wchar_t* result = m_pNative->getResult(); + return result ? gcnew String(result) : nullptr; + } +} +``` + +--- + +### Pattern 2: Opaque void* Pointers + +**Goal**: Hide native types from managed headers (prevents header dependency leaks) + +```cpp +// In .h file - use void* to hide native types +private: + void* m_pNativeSDK; // Actually INativeSDK* + void* m_pNativeSession; // Actually INativeSession* + +// In .cpp file - cast back to real types +bool ManagedWrapper::JoinSession() { + INativeSDK* sdk = static_cast(m_pNativeSDK); + INativeSession* session = sdk->joinSession(...); + m_pNativeSession = static_cast(session); + return session != nullptr; +} +``` + +**Why**: Native SDK headers often have complex dependencies. Using `void*` means you only need to `#include` native headers in the `.cpp` file, not the `.h` file. This prevents compile errors in consuming C# projects. + +--- + +### Pattern 3: gcroot for Native→Managed Callbacks + +**Goal**: Native code needs to call back into managed code + +```cpp +// NativeCallbacks.h +#pragma once +#include // For gcroot + +// Forward declare the managed class +namespace MyLibraryWrapper { ref class ManagedWrapper; } + +// Native class that implements SDK callback interface +class NativeEventHandler : public INativeEventListener { +private: + gcroot m_managed; // GC-safe ref + +public: + NativeEventHandler(MyLibraryWrapper::ManagedWrapper^ wrapper) + : m_managed(wrapper) {} + + // Native callback (called by SDK on background thread) + void onEvent(int eventCode, const wchar_t* message) override { + // Get managed reference (prevents GC during callback) + MyLibraryWrapper::ManagedWrapper^ wrapper = m_managed; + if (wrapper) { + wrapper->FireManagedEvent(eventCode, gcnew String(message)); + } + } + + void onDataReceived(const unsigned char* data, int length) override { + MyLibraryWrapper::ManagedWrapper^ wrapper = m_managed; + if (wrapper) { + // Copy native data to managed array + array^ managedData = gcnew array(length); + Marshal::Copy(IntPtr((void*)data), managedData, 0, length); + wrapper->FireDataEvent(managedData); + } + } +}; +``` + +```cpp +// In ManagedWrapper.h - add events +public ref class ManagedWrapper { +public: + // Managed events for C# consumption + event EventHandler^ SomethingHappened; + event EventHandler^ DataReceived; + +internal: + // Called by native callback handler + void FireManagedEvent(int code, String^ message); + void FireDataEvent(array^ data); +}; +``` + +**Critical**: `gcroot` prevents the .NET garbage collector from moving/collecting the managed object while native code holds a reference. Without it, callbacks will crash. + +--- + +### Pattern 4: Destructor + Finalizer (IDisposable) + +**Goal**: Guarantee native resource cleanup + +```cpp +public ref class ManagedWrapper { +private: + NativeClass* m_pNative; + NativeEventHandler* m_pHandler; // Must also be cleaned up + bool m_disposed; + +public: + // Destructor - called by Dispose() or 'using' statement + ~ManagedWrapper() { + if (!m_disposed) { + this->!ManagedWrapper(); // Call finalizer logic + m_disposed = true; + GC::SuppressFinalize(this); // No need for finalizer now + } + } + + // Finalizer - called by GC if Dispose wasn't called + !ManagedWrapper() { + // Clean up in reverse order of creation + if (m_pHandler) { + delete m_pHandler; + m_pHandler = nullptr; + } + if (m_pNative) { + m_pNative->shutdown(); // SDK cleanup + delete m_pNative; + m_pNative = nullptr; + } + } +}; +``` + +**C# Usage**: +```csharp +// Option 1: Explicit dispose +var wrapper = new ManagedWrapper(); +try { + wrapper.Initialize(); + // use wrapper... +} finally { + wrapper.Dispose(); // Calls ~ManagedWrapper() +} + +// Option 2: using statement (preferred) +using (var wrapper = new ManagedWrapper()) { + wrapper.Initialize(); + // use wrapper... +} // Dispose() called automatically +``` + +--- + +### Pattern 5: String Conversion + +**Goal**: Convert between managed String^ and native strings + +```cpp +#include + +// Managed String^ → Native std::wstring +void SetName(String^ name) { + std::wstring nativeName = msclr::interop::marshal_as(name); + m_pNative->setName(nativeName.c_str()); +} + +// Managed String^ → Native std::string (UTF-8) +void SetNameUtf8(String^ name) { + std::string nativeName = msclr::interop::marshal_as(name); + m_pNative->setNameUtf8(nativeName.c_str()); +} + +// Native wchar_t* → Managed String^ +String^ GetName() { + const wchar_t* name = m_pNative->getName(); + return name ? gcnew String(name) : nullptr; +} + +// Native char* (UTF-8) → Managed String^ +String^ GetNameUtf8() { + const char* name = m_pNative->getNameUtf8(); + return name ? gcnew String(name, 0, strlen(name), System::Text::Encoding::UTF8) : nullptr; +} +``` + +--- + +### Pattern 6: Array/Buffer Conversion + +**Goal**: Pass binary data between managed and native code + +```cpp +// Managed array → Native buffer +void SendData(array^ data) { + if (data == nullptr || data->Length == 0) return; + + // Pin the managed array (prevents GC from moving it) + pin_ptr pinned = &data[0]; + unsigned char* nativePtr = pinned; + + m_pNative->sendData(nativePtr, data->Length); +} +// pinned automatically unpins when out of scope + +// Native buffer → Managed array +array^ ReceiveData() { + unsigned char* buffer = nullptr; + int length = 0; + + m_pNative->receiveData(&buffer, &length); + + if (!buffer || length <= 0) return nullptr; + + array^ result = gcnew array(length); + Marshal::Copy(IntPtr(buffer), result, 0, length); + + m_pNative->freeBuffer(buffer); // SDK may require this + return result; +} +``` + +--- + +### Pattern 7: Thread Marshaling (Native Thread → UI Thread) + +**Goal**: Fire events safely when native callbacks occur on background threads + +```cpp +// In native callback handler +void NativeEventHandler::onVideoFrame(YUVData* frame) { + ManagedWrapper^ wrapper = m_managed; + if (wrapper) { + // Convert frame to managed Bitmap (still on native thread) + Bitmap^ bitmap = wrapper->ConvertYUVToBitmap(frame); + + // Fire event - consumer must marshal to UI thread + wrapper->FireVideoFrameEvent(bitmap); + } +} +``` + +**C# Consumer (WinForms)**: +```csharp +wrapper.VideoFrameReceived += (sender, e) => { + if (InvokeRequired) { + BeginInvoke(new Action(() => pictureBox.Image = e.Frame)); + } else { + pictureBox.Image = e.Frame; + } +}; +``` + +**C# Consumer (WPF)**: +```csharp +wrapper.VideoFrameReceived += (sender, e) => { + if (!Dispatcher.CheckAccess()) { + Dispatcher.BeginInvoke(new Action(() => image.Source = e.Frame)); + } else { + image.Source = e.Frame; + } +}; +``` + +--- + +### Pattern 8: LockBits for Fast Image Manipulation + +**Goal**: Convert image formats efficiently (e.g., YUV→RGB) + +```cpp +Bitmap^ ConvertYUVToBitmap(unsigned char* yuvData, int width, int height) { + Bitmap^ bitmap = gcnew Bitmap(width, height, PixelFormat::Format24bppRgb); + + // Lock bitmap memory for direct access (100x faster than SetPixel) + Rectangle rect(0, 0, width, height); + BitmapData^ data = bitmap->LockBits(rect, ImageLockMode::WriteOnly, + PixelFormat::Format24bppRgb); + + unsigned char* rgbPtr = (unsigned char*)data->Scan0.ToPointer(); + int stride = data->Stride; // May include padding! + + // YUV420 to RGB conversion + 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 = yuvData[yIndex]; + int U = yuvData[width * height + uvIndex]; + int V = yuvData[width * height + (width/2)*(height/2) + uvIndex]; + + // BT.601 conversion + int C = Y - 16, D = U - 128, 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 and write BGR (bitmap format) + unsigned char* pixel = rgbPtr + y * stride + x * 3; + pixel[0] = (B < 0) ? 0 : (B > 255) ? 255 : B; + pixel[1] = (G < 0) ? 0 : (G > 255) ? 255 : G; + pixel[2] = (R < 0) ? 0 : (R > 255) ? 255 : R; + } + } + + bitmap->UnlockBits(data); + return bitmap; +} +``` + +--- + +### Complete Wrapper Checklist + +When wrapping ANY native C++ library: + +``` +[ ] Project: C++/CLI Class Library with /clr enabled +[ ] Headers: Native includes in .cpp only, void* in .h +[ ] Lifecycle: Destructor (~) + Finalizer (!) pattern +[ ] Callbacks: gcroot in native handler classes +[ ] Strings: marshal_as or gcnew String() +[ ] Arrays: pin_ptr for managed→native, Marshal::Copy for native→managed +[ ] Threading: Document which thread callbacks fire on +[ ] Cleanup: Delete native objects in reverse order of creation +[ ] Events: C# events for callbacks, document threading requirements +``` + +--- + +### Common Wrapper Errors + +| Error | Cause | Fix | +|-------|-------|-----| +| `LNK2020: unresolved token` | Missing native .lib | Add to Linker → Input → Additional Dependencies | +| `C3767: candidate function not accessible` | Exposing native types in public API | Use void* or wrap in managed class | +| `AccessViolationException` in callback | GC moved managed object | Use gcroot in native handler | +| `ObjectDisposedException` | Using wrapper after Dispose | Check m_disposed flag | +| `BadImageFormatException` | x86/x64 mismatch | Match Platform Target to native SDK | + +--- + +## Production Quality Review + +This section provides a detailed assessment of each sample against production standards. + +### Overall Assessment Summary + +| Sample | Quality | Issues | Production Ready | +|--------|---------|--------|------------------| +| **Win32** | Good | 4 minor | Yes (with fixes) | +| **C++/CLI Wrapper** | Good | 4 minor | Yes (with fixes) | +| **WinForms** | Good | 3 minor | Yes (with fixes) | +| **WPF** | Good | 4 minor | Yes (with fixes) | + +--- + +### Win32 Sample (ZoomVideoSDK.Win32) + +#### What's Good + +| Check | Status | Notes | +|-------|--------|-------| +| All 80+ delegate callbacks | ✅ | Lines 17-358 in ZoomSDKManager.cpp | +| Null checks on SDK pointers | ✅ | Consistent throughout | +| Exception handling | ✅ | try/catch blocks on all SDK calls | +| Cleanup sequence | ✅ | leave → removeListener → cleanup → destroy | +| Canvas API usage | ✅ | `startVideoCanvasPreview`, `subscribeWithView` | +| Device enumeration | ✅ | Proper SDK interfaces | +| UTF-8 string conversion | ✅ | WideCharToMultiByte used correctly | + +#### Issues Found + +| Severity | Issue | Location | Recommendation | +|----------|-------|----------|----------------| +| ⚠️ Medium | `audioOption.connect = true` during join | Line 489 | Set to `false`, call `startAudio()` in `onSessionJoin` callback | +| ⚠️ Low | Global pointer without thread safety | Line 22 main.cpp | Use mutex or make thread-local | +| ⚠️ Low | Arbitrary `Sleep(100)` after init | Lines 366-367 | Use callback-based readiness check | +| ⚠️ Low | No WM_DESTROY handler | main.cpp | Add cleanup in WM_DESTROY | + +#### Recommended Fix: Audio Connection + +```cpp +// BEFORE (current - problematic) +sessionContext.audioOption.connect = true; // May miss audio callbacks + +// AFTER (recommended) +sessionContext.audioOption.connect = false; + +// Then in onSessionJoin callback: +void CustomZoomDelegate::onSessionJoin() { + if (m_pManager) { + // Now safe to connect audio + IZoomVideoSDKAudioHelper* audioHelper = m_pManager->GetSDK()->getAudioHelper(); + audioHelper->startAudio(); + } +} +``` + +--- + +### C++/CLI Wrapper (ZoomVideoSDK.Wrapper) + +#### What's Good + +| Check | Status | Notes | +|-------|--------|-------| +| gcroot usage | ✅ | Lines 25, 40-41, 57 - prevents GC collection | +| Finalizer + Destructor pattern | ✅ | Lines 245-254 - proper C++/CLI cleanup | +| LockBits for YUV conversion | ✅ | Lines 831-880 - 100x faster than SetPixel | +| All 80+ delegate callbacks | ✅ | SimpleNativeHandler implements all | +| Error handling | ✅ | Status messages for all operations | +| Video preview cleanup | ✅ | StopVideoPreview cleans up handler | + +#### Issues Found + +| Severity | Issue | Location | Recommendation | +|----------|-------|----------|----------------| +| ⚠️ Medium | `audioOption.connect = true` | Line 343 | Same fix as Win32 | +| ⚠️ Medium | `unSubscribe(nullptr)` | Line 987 | Pass actual delegate used for subscription | +| ⚠️ Medium | RemoteVideoHandler memory leak | Line 947 | Store handler, delete in UnsubscribeFromUserVideo | +| ⚠️ Low | Simplified stride calculation | Lines 1162, 1212 | Use `data->GetYStride()`, `GetUStride()`, `GetVStride()` | + +#### Recommended Fix: Handler Memory Leak + +```cpp +// ZoomSDKManager.h - Add member to track handlers +private: + std::map m_remoteHandlers; + +// In SubscribeToUserVideo +RemoteVideoHandler* remoteHandler = new RemoteVideoHandler(this, userId); +m_remoteHandlers[userId] = remoteHandler; // Store for later cleanup + +// In UnsubscribeFromUserVideo +if (m_remoteHandlers.ContainsKey(userId)) { + RemoteVideoHandler* handler = m_remoteHandlers[userId]; + videoPipe->unSubscribe(handler); // Pass actual handler + delete handler; + m_remoteHandlers.Remove(userId); +} +``` + +--- + +### WinForms Sample (ZoomVideoSDK.WinForms) + +#### What's Good + +| Check | Status | Notes | +|-------|--------|-------| +| Thread marshaling | ✅ | InvokeRequired + BeginInvoke throughout | +| Frame rate throttling | ✅ | 30fps limit prevents UI overload | +| Bitmap disposal | ✅ | `.Image?.Dispose()` before assignment | +| Form closing cleanup | ✅ | LeaveSession + Cleanup in OnFormClosing | +| JWT token decoder | ✅ | Proper Base64 URL-safe handling | +| Exception handling | ✅ | All operations wrapped in try/catch | + +#### Issues Found + +| Severity | Issue | Location | Recommendation | +|----------|-------|----------|----------------| +| ⚠️ Medium | Missing IDisposable pattern | ZoomSDKInterop.cs | Add `~ZoomSDKInterop()` destructor | +| ⚠️ Low | Null check after Cleanup | Line 468-469 | Add `_sdkManager != null` check before events | +| ⚠️ Info | Duplicate YUV conversion | Lines 369-443 | Remove - wrapper already handles this | + +#### Recommended Fix: IDisposable + +```csharp +// ZoomSDKInterop.cs +public class ZoomSDKInterop : IDisposable +{ + private bool _disposed = false; + + ~ZoomSDKInterop() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + Cleanup(); + } + _disposed = true; + } + } +} +``` + +--- + +### WPF Sample (ZoomVideoSDK.WPF) + +#### What's Good + +| Check | Status | Notes | +|-------|--------|-------| +| Dispatcher thread safety | ✅ | CheckAccess() + BeginInvoke | +| BitmapImage.Freeze() | ✅ | Cross-thread safety for WPF | +| Frame rate throttling | ✅ | 30fps limit | +| Window close cleanup | ✅ | LeaveSession + Cleanup in OnClosed | +| BitmapCacheOption.OnLoad | ✅ | Proper stream handling | + +#### Issues Found + +| Severity | Issue | Location | Recommendation | +|----------|-------|----------|----------------| +| ⚠️ Medium | MemoryStream PNG conversion | Lines 384-395 | Use Imaging.CreateBitmapSourceFromHBitmap for speed | +| ⚠️ Medium | Missing IDisposable | ZoomSDKInterop.cs | Same fix as WinForms | +| ⚠️ Low | Duplicate YUV conversion | Lines 406-480 | Remove - wrapper handles this | +| ⚠️ Info | WriteableBitmap error bitmap | Lines 493-527 | No text drawn - consider DrawingContext | + +#### Recommended Fix: Faster Bitmap Conversion + +```csharp +// Faster alternative to MemoryStream PNG encoding +[DllImport("gdi32.dll")] +private static extern bool DeleteObject(IntPtr hObject); + +private BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap) +{ + if (bitmap == null) return null; + + IntPtr hBitmap = bitmap.GetHbitmap(); + try + { + var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap( + hBitmap, + IntPtr.Zero, + Int32Rect.Empty, + BitmapSizeOptions.FromEmptyOptions()); + source.Freeze(); + return source; + } + finally + { + DeleteObject(hBitmap); // Prevent GDI handle leak + } +} +``` + +--- + +### Cross-Sample Issues + +These issues appear in multiple samples: + +#### 1. Audio Connection Timing + +**Affected:** Win32, C++/CLI Wrapper + +```cpp +// Current (all samples) +sessionContext.audioOption.connect = true; + +// Recommended per Zoom docs +sessionContext.audioOption.connect = false; +// Then call startAudio() in onSessionJoin callback +``` + +**Why:** Setting `connect = true` during join may cause audio to connect before the session is fully established, potentially missing early audio callbacks. + +#### 2. Video Subscription Timing + +**Pattern followed correctly:** All samples wait for `onUserJoin` or `onUserVideoStatusChanged` before subscribing to video. + +#### 3. Resource Cleanup + +**Pattern followed correctly:** All samples implement cleanup in form/window closing handlers. + +--- + +### Production Checklist + +Use this checklist before deploying: + +``` +[ ] Audio: Set audioOption.connect = false, start in onSessionJoin +[ ] Video: Subscribe only after onUserJoin/onUserVideoStatusChanged +[ ] Memory: Track and cleanup all native handlers +[ ] Threading: Marshal all UI updates to main thread +[ ] Disposal: Implement IDisposable with destructor/finalizer +[ ] Frame Rate: Throttle video at 30fps to prevent UI lock +[ ] Error Handling: Try/catch all SDK calls +[ ] Cleanup: LeaveSession before Cleanup on app close +``` + +--- + +## Related Documentation + +- [SDK Architecture Pattern](../../concepts/sdk-architecture-pattern.md) +- [Video Rendering](../video-rendering.md) +- [Screen Share Subscription](../screen-share-subscription.md) +- [Delegate Methods](../../references/delegate-methods.md) + +## Sample Locations + +All samples are in: +``` +C:\tempsdk\videosdk-windows-dotnet-desktop-framework-quickstart\ +├── ZoomVideoSDK.Win32\ # Native Win32 + Canvas API +├── ZoomVideoSDK.Wrapper\ # C++/CLI Bridge (shared) +├── ZoomVideoSDK.WinForms\ # C# WinForms + Raw Data +└── ZoomVideoSDK.WPF\ # C# WPF + BitmapSource +``` diff --git a/plugins/zoom-developers/skills/video-sdk/windows/examples/raw-audio-capture.md b/plugins/zoom-developers/skills/video-sdk/windows/examples/raw-audio-capture.md new file mode 100644 index 00000000..a72572cd --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/examples/raw-audio-capture.md @@ -0,0 +1,289 @@ +# Raw Audio Capture + +Complete working code for capturing raw PCM audio from sessions. + +**Official Sample**: `VSDK_getRawAudio` in [videosdk-windows-rawdata-sample](https://github.com/zoom/videosdk-windows-rawdata-sample) + +--- + +## Overview + +The SDK provides two audio capture modes: +- **Mixed Audio**: All participants combined into single stream +- **Per-User Audio**: Separate stream for each participant + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ AUDIO CAPTURE FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ IZoomVideoSDKDelegate │ +│ ├── onMixedAudioRawDataReceived(AudioRawData*) [Combined] │ +│ └── onOneWayAudioRawDataReceived(AudioRawData*, user) [Per-user]│ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Audio Format + +| Property | Value | +|----------|-------| +| Format | PCM (uncompressed) | +| Sample Rate | 32000 Hz | +| Bit Depth | 16-bit signed | +| Channels | Mono (1) or Stereo (2) | +| Byte Order | Little-endian | + +**Buffer size per callback**: Varies, typically 640-1280 bytes (20-40ms of audio) + +--- + +## Complete Working Code + +### AudioCapture.h + +```cpp +#pragma once +#include +#include +#include +#include "zoom_video_sdk_delegate_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class AudioCapture { +public: + AudioCapture(const std::string& outputPath); + ~AudioCapture(); + + // Called from delegate + void OnMixedAudio(AudioRawData* data); + void OnUserAudio(AudioRawData* data, IZoomVideoSDKUser* user); + + // Statistics + int GetSampleCount() const { return m_sampleCount; } + +private: + std::ofstream m_outputFile; + std::mutex m_mutex; + int m_sampleCount; + int m_sampleRate; + int m_channels; +}; +``` + +### AudioCapture.cpp + +```cpp +#include "AudioCapture.h" +#include + +AudioCapture::AudioCapture(const std::string& outputPath) + : m_sampleCount(0) + , m_sampleRate(0) + , m_channels(0) { + + m_outputFile.open(outputPath, std::ios::binary); + if (!m_outputFile.is_open()) { + std::cerr << "Failed to open: " << outputPath << std::endl; + } +} + +AudioCapture::~AudioCapture() { + std::lock_guard lock(m_mutex); + if (m_outputFile.is_open()) { + m_outputFile.close(); + } + std::cout << "Captured " << m_sampleCount << " audio samples" << std::endl; + std::cout << "Sample rate: " << m_sampleRate << " Hz" << std::endl; + std::cout << "Channels: " << m_channels << std::endl; +} + +void AudioCapture::OnMixedAudio(AudioRawData* data) { + if (!data) return; + + std::lock_guard lock(m_mutex); + + // Get audio properties + char* buffer = data->GetBuffer(); + unsigned int length = data->GetBufferLen(); + unsigned int sampleRate = data->GetSampleRate(); + unsigned int channels = data->GetChannelNum(); + + // Log format on first callback + if (m_sampleCount == 0) { + std::cout << "Audio format: " << sampleRate << " Hz, " + << channels << " channel(s)" << std::endl; + m_sampleRate = sampleRate; + m_channels = channels; + } + + // Write PCM data + if (m_outputFile.is_open() && buffer && length > 0) { + m_outputFile.write(buffer, length); + } + + m_sampleCount++; + + // Log progress + if (m_sampleCount % 500 == 0) { + std::cout << "Audio samples: " << m_sampleCount << std::endl; + } +} + +void AudioCapture::OnUserAudio(AudioRawData* data, IZoomVideoSDKUser* user) { + if (!data || !user) return; + + // Process per-user audio + // Could write to separate files per user + std::wcout << L"Audio from: " << user->getUserName() << std::endl; +} +``` + +### Using in Delegate + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +private: + AudioCapture* m_audioCapture; + +public: + MyDelegate() { + m_audioCapture = new AudioCapture("audio.pcm"); + } + + ~MyDelegate() { + delete m_audioCapture; + } + + // Mixed audio from all participants + void onMixedAudioRawDataReceived(AudioRawData* data) override { + m_audioCapture->OnMixedAudio(data); + } + + // Audio from specific user + void onOneWayAudioRawDataReceived(AudioRawData* data, + IZoomVideoSDKUser* user) override { + m_audioCapture->OnUserAudio(data, user); + } + + // Shared audio (from screen share with audio) + void onSharedAudioRawDataReceived(AudioRawData* data) override { + // Handle shared audio separately if needed + } + + // ... other callbacks +}; +``` + +--- + +## Playing Raw PCM Audio + +### FFplay + +```cmd +ffplay -f s16le -ar 32000 -ac 1 audio.pcm +``` + +### Convert to WAV + +```cmd +ffmpeg -f s16le -ar 32000 -ac 1 -i audio.pcm output.wav +``` + +### FFmpeg Flags + +| Flag | Description | +|------|-------------| +| `-f s16le` | Signed 16-bit little-endian PCM | +| `-ar 32000` | Sample rate 32000 Hz | +| `-ac 1` | Mono (use `-ac 2` for stereo) | + +--- + +## AudioRawData Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `GetBuffer()` | `char*` | PCM sample buffer | +| `GetBufferLen()` | `unsigned int` | Buffer size in bytes | +| `GetSampleRate()` | `unsigned int` | Sample rate (usually 32000) | +| `GetChannelNum()` | `unsigned int` | 1 = mono, 2 = stereo | + +--- + +## Audio Callback Types + +### onMixedAudioRawDataReceived + +Combined audio from all participants: + +```cpp +void onMixedAudioRawDataReceived(AudioRawData* data) override { + // All participants mixed together + // Best for recording entire session +} +``` + +### onOneWayAudioRawDataReceived + +Per-user audio (requires special setup): + +```cpp +void onOneWayAudioRawDataReceived(AudioRawData* data, + IZoomVideoSDKUser* user) override { + // Audio from specific user + // Best for transcription per speaker +} +``` + +### onSharedAudioRawDataReceived + +Audio from screen share: + +```cpp +void onSharedAudioRawDataReceived(AudioRawData* data) override { + // Audio from shared content + // Separate from participant audio +} +``` + +--- + +## Common Issues + +### No Audio Callbacks + +**Cause**: Audio not connected + +**Fix**: Connect audio in `onSessionJoin`: +```cpp +void onSessionJoin() override { + sdk->getAudioHelper()->startAudio(); +} +``` + +### Wrong Sample Rate in FFmpeg + +**Cause**: Assuming 44100 Hz instead of 32000 Hz + +**Fix**: Use correct rate: +```cmd +ffplay -ar 32000 ... # Not 44100! +``` + +### Audio is Silent + +**Cause**: No one is speaking or all muted + +**Fix**: Check `onUserAudioStatusChanged` for mute status + +--- + +## Related Documentation + +- [Raw Video Capture](raw-video-capture.md) - Video frame capture +- [Send Raw Audio](send-raw-audio.md) - Audio injection +- [Delegate Methods](../references/delegate-methods.md) - All callbacks +- [API Reference](../references/windows-reference.md) - AudioRawData methods diff --git a/plugins/zoom-developers/skills/video-sdk/windows/examples/raw-video-capture.md b/plugins/zoom-developers/skills/video-sdk/windows/examples/raw-video-capture.md new file mode 100644 index 00000000..d71c2dc8 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/examples/raw-video-capture.md @@ -0,0 +1,424 @@ +# Raw Video Capture + +Complete working code for capturing raw YUV420 video frames for custom processing. + +## Overview + +Use Raw Data Pipe when you need frame-level access for: +- AI/ML video processing +- Custom video effects +- Recording to custom formats +- Video compositing + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ RAW DATA FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ GetVideoPipe() → subscribe(resolution, delegate) │ +│ ↓ │ +│ onRawDataFrameReceived(YUVRawDataI420*) │ +│ ↓ │ +│ Process: GetYBuffer(), GetUBuffer(), GetVBuffer() │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## YUV420 Format Explained + +### Memory Layout (I420 Planar) + +``` +┌─────────────────────────────────────────┐ +│ Y Plane (Luminance/Brightness) │ width × height bytes +│ Full resolution │ +├─────────────────────────────────────────┤ +│ U Plane (Blue-difference Chrominance) │ (width/2) × (height/2) bytes +│ Quarter resolution │ +├─────────────────────────────────────────┤ +│ V Plane (Red-difference Chrominance) │ (width/2) × (height/2) bytes +│ Quarter resolution │ +└─────────────────────────────────────────┘ + +Total size = width × height × 1.5 bytes +Example: 1280×720 = 1,382,400 bytes (~1.3 MB per frame) +``` + +### Buffer Sizes + +| Resolution | Y Buffer | U Buffer | V Buffer | Total | +|------------|----------|----------|----------|-------| +| 720p (1280×720) | 921,600 | 230,400 | 230,400 | 1,382,400 | +| 1080p (1920×1080) | 2,073,600 | 518,400 | 518,400 | 3,110,400 | +| 360p (640×360) | 230,400 | 57,600 | 57,600 | 345,600 | + +--- + +## Complete Working Code + +### RawVideoCapture.h + +```cpp +#pragma once +#include +#include +#include +#include +#include "zoom_video_sdk_interface.h" +#include "zoom_sdk_raw_data_def.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class RawVideoCapture : public IZoomVideoSDKRawDataPipeDelegate { +public: + RawVideoCapture(const std::string& outputPath); + ~RawVideoCapture(); + + // IZoomVideoSDKRawDataPipeDelegate + void onRawDataFrameReceived(YUVRawDataI420* data) override; + void onRawDataStatusChanged(RawDataStatus status) override; + + // Statistics + int GetFrameCount() const { return m_frameCount; } + int GetWidth() const { return m_width; } + int GetHeight() const { return m_height; } + +private: + std::ofstream m_outputFile; + std::mutex m_mutex; + int m_frameCount; + int m_width; + int m_height; + std::string m_outputPath; +}; +``` + +### RawVideoCapture.cpp + +```cpp +#include "RawVideoCapture.h" +#include + +RawVideoCapture::RawVideoCapture(const std::string& outputPath) + : m_outputPath(outputPath) + , m_frameCount(0) + , m_width(0) + , m_height(0) { + + m_outputFile.open(outputPath, std::ios::binary); + if (!m_outputFile.is_open()) { + std::cerr << "Failed to open output file: " << outputPath << std::endl; + } +} + +RawVideoCapture::~RawVideoCapture() { + std::lock_guard lock(m_mutex); + if (m_outputFile.is_open()) { + m_outputFile.close(); + } + std::cout << "Captured " << m_frameCount << " frames" << std::endl; +} + +void RawVideoCapture::onRawDataFrameReceived(YUVRawDataI420* data) { + if (!data) return; + + std::lock_guard lock(m_mutex); + + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Detect resolution change + if (width != m_width || height != m_height) { + std::cout << "Resolution: " << width << "x" << height << std::endl; + m_width = width; + m_height = height; + } + + // Calculate buffer sizes + size_t ySize = width * height; + size_t uvSize = ySize / 4; // Quarter resolution + + // Write YUV data to file + if (m_outputFile.is_open()) { + m_outputFile.write(data->GetYBuffer(), ySize); + m_outputFile.write(data->GetUBuffer(), uvSize); + m_outputFile.write(data->GetVBuffer(), uvSize); + } + + m_frameCount++; + + // Log progress every 100 frames + if (m_frameCount % 100 == 0) { + std::cout << "Captured " << m_frameCount << " frames" << std::endl; + } +} + +void RawVideoCapture::onRawDataStatusChanged(RawDataStatus status) { + if (status == RawData_On) { + std::cout << "Raw data started" << std::endl; + } else { + std::cout << "Raw data stopped" << std::endl; + } +} +``` + +### Using in Delegate + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +private: + IZoomVideoSDK* m_sdk; + std::map m_captures; + std::map m_pipes; + +public: + MyDelegate(IZoomVideoSDK* sdk) : m_sdk(sdk) {} + + ~MyDelegate() { + // Cleanup all captures + for (auto& pair : m_captures) { + delete pair.second; + } + } + + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + IZoomVideoSDKUser* myself = m_sdk->getSessionInfo()->getMyself(); + + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + + ZoomVideoSDKVideoStatus status = user->GetVideoPipe()->getVideoStatus(); + + if (status.isOn) { + // Start capture if not already capturing + if (m_captures.find(user) == m_captures.end()) { + StartCapture(user); + } + } else { + // Stop capture + StopCapture(user); + } + } + } + + void StartCapture(IZoomVideoSDKUser* user) { + // Create unique filename + std::wstring name = user->getUserName(); + std::string filename = "video_" + + std::string(name.begin(), name.end()) + ".yuv"; + + // Create capture delegate + RawVideoCapture* capture = new RawVideoCapture(filename); + m_captures[user] = capture; + + // Subscribe to raw data + IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); + if (pipe) { + ZoomVideoSDKErrors err = pipe->subscribe( + ZoomVideoSDKResolution_720P, + capture + ); + + if (err == ZoomVideoSDKErrors_Success) { + m_pipes[user] = pipe; + std::wcout << L"Started capture for: " << user->getUserName() << std::endl; + } else { + std::wcout << L"Failed to subscribe: " << err << std::endl; + delete capture; + m_captures.erase(user); + } + } + } + + void StopCapture(IZoomVideoSDKUser* user) { + auto pipeIt = m_pipes.find(user); + auto captureIt = m_captures.find(user); + + if (pipeIt != m_pipes.end() && captureIt != m_captures.end()) { + pipeIt->second->unSubscribe(captureIt->second); + delete captureIt->second; + + m_pipes.erase(pipeIt); + m_captures.erase(captureIt); + + std::wcout << L"Stopped capture for: " << user->getUserName() << std::endl; + } + } + + void onUserLeave(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + StopCapture(userList->GetItem(i)); + } + } + + // ... other callbacks +}; +``` + +--- + +## Playing Raw YUV Files + +Raw YUV files have no headers - you must specify format explicitly. + +### Play with FFplay + +```cmd +ffplay -video_size 1280x720 -pixel_format yuv420p -f rawvideo video.yuv +``` + +### Convert to MP4 + +```cmd +ffmpeg -video_size 1280x720 -pixel_format yuv420p -framerate 30 -f rawvideo -i video.yuv -c:v libx264 output.mp4 +``` + +### Key FFmpeg Flags + +| Flag | Description | +|------|-------------| +| `-video_size WxH` | Frame dimensions (must match!) | +| `-pixel_format yuv420p` | I420/YUV420 planar format | +| `-f rawvideo` | Raw video input (no container) | +| `-framerate 30` | Assumed frame rate | + +--- + +## YUV to RGB Conversion + +For real-time display, convert YUV420 to RGB: + +```cpp +void ConvertYUV420ToRGB(YUVRawDataI420* data, unsigned char* rgbBuffer) { + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + char* yBuffer = data->GetYBuffer(); + char* uBuffer = data->GetUBuffer(); + char* vBuffer = data->GetVBuffer(); + + 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]; + + // ITU-R BT.601 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 = (R < 0) ? 0 : (R > 255) ? 255 : R; + G = (G < 0) ? 0 : (G > 255) ? 255 : G; + B = (B < 0) ? 0 : (B > 255) ? 255 : B; + + // Store as BGR (Windows format) + int rgbIndex = yIndex * 3; + rgbBuffer[rgbIndex + 0] = (unsigned char)B; + rgbBuffer[rgbIndex + 1] = (unsigned char)G; + rgbBuffer[rgbIndex + 2] = (unsigned char)R; + } + } +} +``` + +--- + +## YUVRawDataI420 Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `GetYBuffer()` | `char*` | Y plane (luminance) | +| `GetUBuffer()` | `char*` | U plane (chrominance) | +| `GetVBuffer()` | `char*` | V plane (chrominance) | +| `GetStreamWidth()` | `unsigned int` | Frame width | +| `GetStreamHeight()` | `unsigned int` | Frame height | +| `GetRotation()` | `unsigned int` | 0, 90, 180, 270 | +| `GetTimeStamp()` | `unsigned long long` | Frame timestamp | +| `GetBufferLen()` | `unsigned int` | Total buffer size | +| `CanAddRef()` | `bool` | Can add reference? | +| `AddRef()` | `bool` | Add reference | +| `Release()` | `int` | Release reference | + +--- + +## Performance Tips + +### 1. Pre-allocate Buffers + +```cpp +class OptimizedCapture : public IZoomVideoSDKRawDataPipeDelegate { + unsigned char* m_rgbBuffer = nullptr; + int m_bufferSize = 0; + + void onRawDataFrameReceived(YUVRawDataI420* data) override { + int size = data->GetStreamWidth() * data->GetStreamHeight() * 3; + + // Reallocate only if needed + if (size != m_bufferSize) { + delete[] m_rgbBuffer; + m_rgbBuffer = new unsigned char[size]; + m_bufferSize = size; + } + + // Process... + } +}; +``` + +### 2. Process on Separate Thread + +```cpp +void onRawDataFrameReceived(YUVRawDataI420* data) override { + // Add reference so frame survives after callback + if (data->CanAddRef()) { + data->AddRef(); + + // Queue for processing thread + m_frameQueue.push(data); + } +} + +// Processing thread +void ProcessingLoop() { + while (m_running) { + YUVRawDataI420* frame = m_frameQueue.pop(); + if (frame) { + ProcessFrame(frame); + frame->Release(); // Release when done + } + } +} +``` + +### 3. Use Lower Resolution for Processing + +```cpp +// Subscribe at lower resolution for AI processing +pipe->subscribe(ZoomVideoSDKResolution_360P, aiDelegate); + +// Subscribe at higher resolution for display (Canvas API) +user->GetVideoCanvas()->subscribeWithView(hwnd, aspect, ZoomVideoSDKResolution_720P); +``` + +--- + +## Related Documentation + +- [Canvas vs Raw Data](../concepts/canvas-vs-raw-data.md) - Choose your approach +- [Video Rendering](video-rendering.md) - Canvas API (easier) +- [API Reference](../references/windows-reference.md) - Full method signatures + +--- + +**TL;DR**: Subscribe via `GetVideoPipe()->subscribe(resolution, delegate)`, receive frames in `onRawDataFrameReceived()`, write Y/U/V buffers to file or convert to RGB. diff --git a/plugins/zoom-developers/skills/video-sdk/windows/examples/screen-share-subscription.md b/plugins/zoom-developers/skills/video-sdk/windows/examples/screen-share-subscription.md new file mode 100644 index 00000000..c86cebc5 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/examples/screen-share-subscription.md @@ -0,0 +1,571 @@ +# Screen Share Subscription - Complete Guide + +## Overview + +Screen share subscription in the Zoom Video SDK is **fundamentally different** from video subscription. This guide explains why and provides complete working code for both Canvas API and Raw Data approaches. + +## Why Screen Share is Different from Video + +| Aspect | Video | Screen Share | +|--------|-------|--------------| +| **Streams per user** | One video stream | Multiple share streams possible (multi-share) | +| **Access method** | `user->GetVideoCanvas()` | `IZoomVideoSDKShareAction*` from callback | +| **Subscription timing** | `onUserVideoStatusChanged` | `onUserShareStatusChanged` | +| **Key object** | `IZoomVideoSDKUser*` | `IZoomVideoSDKShareAction*` | + +**The critical difference**: A user can have multiple active share actions simultaneously (e.g., sharing screen + sharing a whiteboard). The `IZoomVideoSDKShareAction` object in the callback represents a specific share stream. + +## The Wrong Way (Common Mistake) + +```cpp +// WRONG - This won't work for remote screen shares! +IZoomVideoSDKUser* sharingUser = ...; +IZoomVideoSDKCanvas* shareCanvas = sharingUser->GetShareCanvas(); +shareCanvas->subscribeWithView(hwnd, aspect); // May fail or show nothing! +``` + +**Why it fails**: `GetShareCanvas()` on the user object doesn't give you access to the active share stream. You MUST use the `IZoomVideoSDKShareAction*` provided in the callback. + +## The Correct Way + +### Canvas API (Recommended) + +The Canvas API lets the SDK render the shared screen directly to your window handle. + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +private: + HWND shareWindow_; + std::map activeShares_; + +public: + MyDelegate(HWND shareWnd) : shareWindow_(shareWnd) {} + + void onUserShareStatusChanged( + IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) override + { + if (!pShareAction) return; + + ZoomVideoSDKShareStatus status = pShareAction->getShareStatus(); + ZoomVideoSDKShareType type = pShareAction->getShareType(); + + // Get user name for logging + const zchar_t* userName = pUser ? pUser->getUserName() : L"Unknown"; + + switch (status) { + case ZoomVideoSDKShareStatus_Start: + case ZoomVideoSDKShareStatus_Resume: + SubscribeToShare(pShareAction, userName); + break; + + case ZoomVideoSDKShareStatus_Pause: + // Share is paused - you may want to show a "Paused" overlay + // The subscription remains active + break; + + case ZoomVideoSDKShareStatus_Stop: + UnsubscribeFromShare(pShareAction, userName); + break; + } + } + +private: + void SubscribeToShare(IZoomVideoSDKShareAction* pShareAction, + const zchar_t* userName) + { + // Prevent duplicate subscriptions + if (activeShares_.find(pShareAction) != activeShares_.end()) { + return; + } + + // Get the share canvas from the ShareAction (NOT from the user!) + IZoomVideoSDKCanvas* shareCanvas = pShareAction->getShareCanvas(); + if (!shareCanvas) { + // Error: Share canvas not available + return; + } + + // Subscribe with Canvas API + ZoomVideoSDKErrors ret = shareCanvas->subscribeWithView( + shareWindow_, + ZoomVideoSDKVideoAspect_Original // Show full content, letterbox if needed + ); + + if (ret == ZoomVideoSDKErrors_Success) { + activeShares_[pShareAction] = true; + // Successfully subscribed to share from [userName] + } else { + // Failed to subscribe: error code [ret] + } + } + + void UnsubscribeFromShare(IZoomVideoSDKShareAction* pShareAction, + const zchar_t* userName) + { + auto it = activeShares_.find(pShareAction); + if (it == activeShares_.end()) { + return; // Not subscribed + } + + IZoomVideoSDKCanvas* shareCanvas = pShareAction->getShareCanvas(); + if (shareCanvas) { + shareCanvas->unSubscribeWithView(shareWindow_); + } + + activeShares_.erase(it); + // Unsubscribed from share + } +}; +``` + +### Raw Data Pipe (Advanced) + +Use Raw Data when you need to process the shared screen frames yourself (recording, effects, computer vision). + +```cpp +class ShareRawDataDelegate : public IZoomVideoSDKRawDataPipeDelegate { +private: + std::function frameCallback_; + +public: + ShareRawDataDelegate(std::function callback) + : frameCallback_(callback) {} + + void onRawDataFrameReceived(YUVRawDataI420* data) override { + if (!data) return; + + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + + // Share frames can be large (1080p, 4K) + // Process efficiently or queue for async processing + + if (frameCallback_) { + frameCallback_(data); + } + } + + void onRawDataStatusChanged(RawDataStatus status) override { + switch (status) { + case RawData_On: + // Share raw data stream started + break; + case RawData_Off: + // Share raw data stream stopped + break; + } + } +}; + +class MyDelegate : public IZoomVideoSDKDelegate { +private: + std::map shareDataDelegates_; + +public: + void onUserShareStatusChanged( + IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) override + { + if (!pShareAction) return; + + ZoomVideoSDKShareStatus status = pShareAction->getShareStatus(); + + if (status == ZoomVideoSDKShareStatus_Start || + status == ZoomVideoSDKShareStatus_Resume) + { + SubscribeToShareRawData(pShareAction); + } + else if (status == ZoomVideoSDKShareStatus_Stop) + { + UnsubscribeFromShareRawData(pShareAction); + } + } + +private: + void SubscribeToShareRawData(IZoomVideoSDKShareAction* pShareAction) { + if (shareDataDelegates_.find(pShareAction) != shareDataDelegates_.end()) { + return; // Already subscribed + } + + // Get the raw data pipe from ShareAction + IZoomVideoSDKRawDataPipe* sharePipe = pShareAction->getSharePipe(); + if (!sharePipe) { + return; + } + + // Create delegate to receive frames + auto* delegate = new ShareRawDataDelegate([](YUVRawDataI420* frame) { + // Process share frame here + // Example: save to file, apply effects, send to encoder + ProcessShareFrame(frame); + }); + + // Subscribe with desired resolution + ZoomVideoSDKErrors ret = sharePipe->subscribe( + ZoomVideoSDKResolution_1080P, // Request high quality for screen share + delegate + ); + + if (ret == ZoomVideoSDKErrors_Success) { + shareDataDelegates_[pShareAction] = delegate; + } else { + delete delegate; + } + } + + void UnsubscribeFromShareRawData(IZoomVideoSDKShareAction* pShareAction) { + auto it = shareDataDelegates_.find(pShareAction); + if (it == shareDataDelegates_.end()) { + return; + } + + IZoomVideoSDKRawDataPipe* sharePipe = pShareAction->getSharePipe(); + if (sharePipe) { + sharePipe->unSubscribe(it->second); + } + + delete it->second; + shareDataDelegates_.erase(it); + } + + static void ProcessShareFrame(YUVRawDataI420* frame) { + // Your frame processing logic + // Note: This runs on SDK thread - don't block! + } +}; +``` + +## Complete Working Example + +Here's a complete example showing screen share subscription with proper lifecycle management: + +```cpp +#include +#include +#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 ScreenShareManager : public IZoomVideoSDKDelegate { +private: + IZoomVideoSDK* sdk_; + HWND mainShareWindow_; + + // Track active share subscriptions + struct ShareSubscription { + IZoomVideoSDKShareAction* action; + IZoomVideoSDKUser* user; + ZoomVideoSDKShareType type; + bool isSubscribed; + }; + std::vector activeSubscriptions_; + +public: + ScreenShareManager(IZoomVideoSDK* sdk, HWND shareWindow) + : sdk_(sdk), mainShareWindow_(shareWindow) {} + + // ========================================== + // IZoomVideoSDKDelegate - Share Events + // ========================================== + + void onUserShareStatusChanged( + IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) override + { + if (!pShareAction || !pUser) return; + + ZoomVideoSDKShareStatus status = pShareAction->getShareStatus(); + ZoomVideoSDKShareType type = pShareAction->getShareType(); + const zchar_t* userName = pUser->getUserName(); + + // Check if this is our own share + IZoomVideoSDKSession* session = sdk_->getSessionInfo(); + IZoomVideoSDKUser* myself = session ? session->getMyself() : nullptr; + bool isMyShare = (pUser == myself); + + switch (status) { + case ZoomVideoSDKShareStatus_Start: + HandleShareStart(pShareAction, pUser, type, isMyShare); + break; + + case ZoomVideoSDKShareStatus_Resume: + HandleShareResume(pShareAction, pUser); + break; + + case ZoomVideoSDKShareStatus_Pause: + HandleSharePause(pShareAction, pUser); + break; + + case ZoomVideoSDKShareStatus_Stop: + HandleShareStop(pShareAction, pUser); + break; + } + } + +private: + void HandleShareStart(IZoomVideoSDKShareAction* action, + IZoomVideoSDKUser* user, + ZoomVideoSDKShareType type, + bool isMyShare) + { + // Don't subscribe to our own share (we're sending it) + if (isMyShare) { + return; + } + + // Get share canvas from the action + IZoomVideoSDKCanvas* shareCanvas = action->getShareCanvas(); + if (!shareCanvas) { + return; + } + + // Choose aspect based on share type + ZoomVideoSDKVideoAspect aspect = ZoomVideoSDKVideoAspect_Original; + if (type == ZoomVideoSDKShareType_Camera) { + // Camera share might benefit from pan-and-scan + aspect = ZoomVideoSDKVideoAspect_PanAndScan; + } + + // Subscribe to the share + ZoomVideoSDKErrors ret = shareCanvas->subscribeWithView( + mainShareWindow_, + aspect + ); + + if (ret == ZoomVideoSDKErrors_Success) { + ShareSubscription sub = {action, user, type, true}; + activeSubscriptions_.push_back(sub); + } + } + + void HandleShareResume(IZoomVideoSDKShareAction* action, + IZoomVideoSDKUser* user) + { + // Check if we need to re-subscribe + auto* sub = FindSubscription(action); + if (sub && !sub->isSubscribed) { + IZoomVideoSDKCanvas* canvas = action->getShareCanvas(); + if (canvas) { + canvas->subscribeWithView(mainShareWindow_, + ZoomVideoSDKVideoAspect_Original); + sub->isSubscribed = true; + } + } + } + + void HandleSharePause(IZoomVideoSDKShareAction* action, + IZoomVideoSDKUser* user) + { + // Optionally show a "Share Paused" UI overlay + // The canvas subscription remains active + auto* sub = FindSubscription(action); + if (sub) { + // You could trigger UI update here + } + } + + void HandleShareStop(IZoomVideoSDKShareAction* action, + IZoomVideoSDKUser* user) + { + auto* sub = FindSubscription(action); + if (!sub) return; + + // Unsubscribe from canvas + IZoomVideoSDKCanvas* canvas = action->getShareCanvas(); + if (canvas && sub->isSubscribed) { + canvas->unSubscribeWithView(mainShareWindow_); + } + + // Remove from tracking + RemoveSubscription(action); + + // Clear the share window or show placeholder + InvalidateRect(mainShareWindow_, NULL, TRUE); + } + + ShareSubscription* FindSubscription(IZoomVideoSDKShareAction* action) { + for (auto& sub : activeSubscriptions_) { + if (sub.action == action) { + return ⊂ + } + } + return nullptr; + } + + void RemoveSubscription(IZoomVideoSDKShareAction* action) { + activeSubscriptions_.erase( + std::remove_if(activeSubscriptions_.begin(), + activeSubscriptions_.end(), + [action](const ShareSubscription& s) { + return s.action == action; + }), + activeSubscriptions_.end() + ); + } + +public: + // Cleanup all subscriptions (call on session leave) + void CleanupAllShares() { + for (auto& sub : activeSubscriptions_) { + if (sub.isSubscribed && sub.action) { + IZoomVideoSDKCanvas* canvas = sub.action->getShareCanvas(); + if (canvas) { + canvas->unSubscribeWithView(mainShareWindow_); + } + } + } + activeSubscriptions_.clear(); + } + + // ========================================== + // Implement remaining IZoomVideoSDKDelegate methods as empty + // (required but not shown for brevity) + // ========================================== + void onSessionJoin() override {} + void onSessionLeave() override { CleanupAllShares(); } + void onError(ZoomVideoSDKErrors errorCode) override {} + void onUserJoin(IZoomVideoSDKUserHelper*, IVideoSDKVector*) override {} + void onUserLeave(IZoomVideoSDKUserHelper*, IVideoSDKVector*) override {} + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper*, IVideoSDKVector*) override {} + void onUserAudioStatusChanged(IZoomVideoSDKAudioHelper*, IVideoSDKVector*) override {} + // ... implement all other required callbacks as empty +}; +``` + +## Share Types + +The `IZoomVideoSDKShareAction::getShareType()` returns: + +| Type | Description | +|------|-------------| +| `ZoomVideoSDKShareType_Normal` | Desktop/window share | +| `ZoomVideoSDKShareType_Camera` | Camera share (second camera) | + +## Share Status Flow + +``` +User starts sharing + │ + ▼ +onUserShareStatusChanged (status = Start) + │ + ├──► Subscribe to share canvas + │ + ▼ +[Share is active and visible] + │ + ├──► User pauses share + │ │ + │ ▼ + │ onUserShareStatusChanged (status = Pause) + │ │ + │ ├──► Show "paused" UI (optional) + │ │ + │ ▼ + │ [Share paused] + │ │ + │ ├──► User resumes share + │ │ │ + │ │ ▼ + │ │ onUserShareStatusChanged (status = Resume) + │ │ │ + │ │ └──► Re-subscribe if needed + │ │ + ▼ ▼ +[Share continues...] + │ + ▼ +User stops sharing + │ + ▼ +onUserShareStatusChanged (status = Stop) + │ + ├──► Unsubscribe from canvas + ├──► Clear share window + └──► Remove from tracking +``` + +## Best Practices + +### 1. Always Use ShareAction from Callback + +```cpp +// CORRECT +void onUserShareStatusChanged(..., IZoomVideoSDKShareAction* pShareAction) { + pShareAction->getShareCanvas()->subscribeWithView(...); +} + +// WRONG +user->GetShareCanvas()->subscribeWithView(...); +``` + +### 2. Track Subscriptions for Cleanup + +```cpp +std::map subscribedShares_; + +// Subscribe +subscribedShares_[pShareAction] = true; + +// On session leave - cleanup all +for (auto& pair : subscribedShares_) { + // Unsubscribe each +} +``` + +### 3. Don't Subscribe to Your Own Share + +```cpp +IZoomVideoSDKUser* myself = session->getMyself(); +if (pUser == myself) { + return; // Don't subscribe to our own share +} +``` + +### 4. Handle All Share Statuses + +```cpp +switch (status) { + case ZoomVideoSDKShareStatus_Start: // New share started + case ZoomVideoSDKShareStatus_Resume: // Paused share resumed + case ZoomVideoSDKShareStatus_Pause: // Share temporarily paused + case ZoomVideoSDKShareStatus_Stop: // Share ended +} +``` + +### 5. Use Appropriate Aspect Ratio + +```cpp +// For screen share - show all content +ZoomVideoSDKVideoAspect_Original // Letterbox, no crop + +// For camera share - fill window +ZoomVideoSDKVideoAspect_PanAndScan // May crop edges +``` + +## Common Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| Share not visible | Using `user->GetShareCanvas()` | Use `pShareAction->getShareCanvas()` from callback | +| Multiple shares not handled | Not tracking by ShareAction | Use map keyed by `IZoomVideoSDKShareAction*` | +| Share doesn't update | Not handling Resume status | Subscribe on both Start and Resume | +| Crash on session leave | Not unsubscribing | Call `unSubscribeWithView` before cleanup | +| Can see own share | Not filtering self | Check `pUser == session->getMyself()` | + +## Related Documentation + +- **[Video Rendering](video-rendering.md)** - Video subscription (different pattern) +- **[SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md)** - Universal pattern +- **[Singleton Hierarchy](../concepts/singleton-hierarchy.md)** - SDK navigation +- **[Delegate Methods](../references/delegate-methods.md)** - All callback methods + +--- + +**Key Takeaway**: Always get the share canvas from `IZoomVideoSDKShareAction*` in the callback, never from the user object directly. diff --git a/plugins/zoom-developers/skills/video-sdk/windows/examples/send-raw-audio.md b/plugins/zoom-developers/skills/video-sdk/windows/examples/send-raw-audio.md new file mode 100644 index 00000000..083132af --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/examples/send-raw-audio.md @@ -0,0 +1,343 @@ +# Send Raw Audio + +Complete working code for sending custom audio as a virtual microphone. + +**Official Sample**: `VSDK_sendRawAudio` in [videosdk-windows-rawdata-sample](https://github.com/zoom/videosdk-windows-rawdata-sample) + +--- + +## Overview + +Inject custom audio into your session. Use cases: +- Virtual microphone with custom content +- Text-to-speech output +- Pre-recorded audio playback +- Audio effects/processing pipeline + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ AUDIO INJECTION FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ Your Audio Source (file, TTS, generated) │ +│ ↓ │ +│ PCM format (16-bit, 32000 Hz) │ +│ ↓ │ +│ IZoomVideoSDKVirtualAudioMic::onMicInitialize() │ +│ ↓ │ +│ IZoomVideoSDKAudioSender::send() │ +│ ↓ │ +│ Participants hear your custom audio │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Audio Format Requirements + +| Property | Value | +|----------|-------| +| Format | PCM (uncompressed) | +| Sample Rate | 32000 Hz (required) | +| Bit Depth | 16-bit signed | +| Channels | Mono (1) | +| Byte Order | Little-endian | + +**Important**: The SDK requires exactly 32000 Hz sample rate. + +--- + +## Complete Working Code + +### VirtualMic.h + +```cpp +#pragma once +#include +#include +#include +#include +#include "zoom_video_sdk_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class VirtualMic : public IZoomVideoSDKVirtualAudioMic { +public: + VirtualMic(); + ~VirtualMic(); + + // Set audio source file + void SetAudioFile(const std::string& pcmFile); + + // IZoomVideoSDKVirtualAudioMic interface + void onMicInitialize(IZoomVideoSDKAudioSender* sender) override; + void onMicStartSend() override; + void onMicStopSend() override; + void onMicUninitialized() override; + +private: + void SendLoop(); + void GenerateSineWave(char* buffer, int samples, int frequency); + + IZoomVideoSDKAudioSender* m_sender; + std::thread m_sendThread; + std::atomic m_sending; + std::string m_audioFile; + std::ifstream m_fileStream; + + static const int SAMPLE_RATE = 32000; + static const int CHANNELS = 1; + static const int BITS_PER_SAMPLE = 16; +}; +``` + +### VirtualMic.cpp + +```cpp +#include "VirtualMic.h" +#include +#include +#include + +VirtualMic::VirtualMic() + : m_sender(nullptr) + , m_sending(false) { +} + +VirtualMic::~VirtualMic() { + m_sending = false; + if (m_sendThread.joinable()) { + m_sendThread.join(); + } +} + +void VirtualMic::SetAudioFile(const std::string& pcmFile) { + m_audioFile = pcmFile; +} + +void VirtualMic::onMicInitialize(IZoomVideoSDKAudioSender* sender) { + std::cout << "VirtualMic initialized" << std::endl; + m_sender = sender; +} + +void VirtualMic::onMicStartSend() { + std::cout << "VirtualMic start sending" << std::endl; + + // Open audio file if specified + if (!m_audioFile.empty()) { + m_fileStream.open(m_audioFile, std::ios::binary); + if (!m_fileStream.is_open()) { + std::cerr << "Failed to open: " << m_audioFile << std::endl; + } + } + + m_sending = true; + m_sendThread = std::thread(&VirtualMic::SendLoop, this); +} + +void VirtualMic::onMicStopSend() { + std::cout << "VirtualMic stop sending" << std::endl; + m_sending = false; + if (m_sendThread.joinable()) { + m_sendThread.join(); + } + + if (m_fileStream.is_open()) { + m_fileStream.close(); + } +} + +void VirtualMic::onMicUninitialized() { + std::cout << "VirtualMic uninitialized" << std::endl; + m_sender = nullptr; +} + +void VirtualMic::SendLoop() { + // Send 20ms chunks (640 samples at 32000 Hz) + const int SAMPLES_PER_CHUNK = 640; + const int BYTES_PER_CHUNK = SAMPLES_PER_CHUNK * sizeof(short); + + char* buffer = new char[BYTES_PER_CHUNK]; + int sampleOffset = 0; + + auto chunkInterval = std::chrono::milliseconds(20); + + while (m_sending && m_sender) { + auto startTime = std::chrono::steady_clock::now(); + + bool hasData = false; + + // Read from file or generate tone + if (m_fileStream.is_open() && m_fileStream.good()) { + m_fileStream.read(buffer, BYTES_PER_CHUNK); + hasData = m_fileStream.gcount() > 0; + + // Loop file + if (!hasData) { + m_fileStream.clear(); + m_fileStream.seekg(0); + m_fileStream.read(buffer, BYTES_PER_CHUNK); + hasData = m_fileStream.gcount() > 0; + } + } else { + // Generate 440 Hz sine wave (A4 note) + GenerateSineWave(buffer, SAMPLES_PER_CHUNK, 440); + sampleOffset += SAMPLES_PER_CHUNK; + hasData = true; + } + + if (hasData) { + // Send audio chunk + ZoomVideoSDKErrors err = m_sender->send( + buffer, + BYTES_PER_CHUNK, + SAMPLE_RATE + ); + + if (err != ZoomVideoSDKErrors_Success) { + std::cout << "Audio send error: " << err << std::endl; + } + } + + // Maintain timing + auto elapsed = std::chrono::steady_clock::now() - startTime; + auto sleepTime = chunkInterval - elapsed; + if (sleepTime.count() > 0) { + std::this_thread::sleep_for(sleepTime); + } + } + + delete[] buffer; +} + +void VirtualMic::GenerateSineWave(char* buffer, int samples, int frequency) { + short* samples16 = reinterpret_cast(buffer); + static int phase = 0; + + const double amplitude = 16000; // ~50% volume + const double twoPi = 2.0 * 3.14159265358979323846; + + for (int i = 0; i < samples; i++) { + double t = (double)(phase + i) / SAMPLE_RATE; + double value = amplitude * sin(twoPi * frequency * t); + samples16[i] = (short)value; + } + + phase += samples; +} +``` + +### Registering the Virtual Mic + +```cpp +void StartVirtualMic() { + IZoomVideoSDKAudioHelper* audioHelper = sdk->getAudioHelper(); + if (!audioHelper) return; + + // Create virtual mic + VirtualMic* virtualMic = new VirtualMic(); + + // Optional: set audio file + // virtualMic->SetAudioFile("audio.pcm"); + + // Set as audio source + ZoomVideoSDKErrors err = audioHelper->setExternalAudioSource(virtualMic); + if (err != ZoomVideoSDKErrors_Success) { + std::cout << "setExternalAudioSource failed: " << err << std::endl; + return; + } + + // Start audio (this triggers onMicStartSend) + audioHelper->startAudio(); +} +``` + +--- + +## Audio File Preparation + +### Convert WAV to PCM + +```cmd +ffmpeg -i input.wav -f s16le -ar 32000 -ac 1 output.pcm +``` + +### Convert MP3 to PCM + +```cmd +ffmpeg -i input.mp3 -f s16le -ar 32000 -ac 1 output.pcm +``` + +### Key FFmpeg Flags + +| Flag | Description | +|------|-------------| +| `-f s16le` | 16-bit signed little-endian PCM | +| `-ar 32000` | Sample rate 32000 Hz (required!) | +| `-ac 1` | Mono channel | + +--- + +## IZoomVideoSDKAudioSender::send() + +```cpp +ZoomVideoSDKErrors send( + char* data, // PCM audio buffer + unsigned int length, // Buffer size in bytes + int sampleRate // Must be 32000 +); +``` + +**Timing**: Send 20ms chunks (640 samples = 1280 bytes at 32000 Hz mono) + +--- + +## Common Issues + +### No Audio Output + +**Cause**: Not calling `startAudio()` after setting source + +**Fix**: +```cpp +audioHelper->setExternalAudioSource(virtualMic); +audioHelper->startAudio(); // Required! +``` + +### Audio is Choppy + +**Cause**: Inconsistent send timing + +**Fix**: Use precise 20ms intervals: +```cpp +auto chunkInterval = std::chrono::milliseconds(20); +// ... send chunk ... +auto elapsed = std::chrono::steady_clock::now() - startTime; +std::this_thread::sleep_for(chunkInterval - elapsed); +``` + +### Wrong Sample Rate + +**Cause**: Using 44100 Hz instead of 32000 Hz + +**Fix**: Always use 32000 Hz: +```cmd +ffmpeg -ar 32000 ... # Not 44100! +``` + +### Audio Too Loud/Quiet + +**Cause**: PCM amplitude out of range + +**Fix**: Normalize to ±32767 range: +```cpp +const double amplitude = 16000; // 50% volume +``` + +--- + +## Related Documentation + +- [Raw Audio Capture](raw-audio-capture.md) - Capture audio +- [Send Raw Video](send-raw-video.md) - Video injection +- [Session Join Pattern](session-join-pattern.md) - Audio setup +- [API Reference](../references/windows-reference.md) - Method signatures diff --git a/plugins/zoom-developers/skills/video-sdk/windows/examples/send-raw-video.md b/plugins/zoom-developers/skills/video-sdk/windows/examples/send-raw-video.md new file mode 100644 index 00000000..bde034e2 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/examples/send-raw-video.md @@ -0,0 +1,368 @@ +# Send Raw Video + +Complete working code for sending custom video as a virtual camera. + +**Official Sample**: `VSDK_sendRawVideo` in [videosdk-windows-rawdata-sample](https://github.com/zoom/videosdk-windows-rawdata-sample) + +--- + +## Overview + +Inject custom video frames into your session. Use cases: +- Virtual camera with custom content +- Video filters/effects +- Pre-recorded video playback +- Computer-generated graphics + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ VIDEO INJECTION FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ Your Video Source (file, camera, generated) │ +│ ↓ │ +│ Convert to YUV420 (I420) │ +│ ↓ │ +│ IZoomVideoSDKVideoSource::onInitialize() │ +│ ↓ │ +│ IZoomVideoSDKVideoSender::sendVideoFrame() │ +│ ↓ │ +│ Participants see your custom video │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Video Format Requirements + +| Property | Value | +|----------|-------| +| Format | YUV420 (I420) planar | +| Color Space | YUV (not RGB) | +| Supported Resolutions | 90p, 180p, 360p, 720p, 1080p | +| Frame Rate | Up to 30 fps | + +--- + +## Complete Working Code + +### VideoSource.h + +```cpp +#pragma once +#include +#include +#include +#include "zoom_video_sdk_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class VideoSource : public IZoomVideoSDKVideoSource { +public: + VideoSource(); + ~VideoSource(); + + // IZoomVideoSDKVideoSource interface + void onInitialize(IZoomVideoSDKVideoSender* sender, + IVideoSDKVector* support_cap_list, + VideoSourceCapability& suggest_cap) override; + void onPropertyChange(IVideoSDKVector* support_cap_list, + VideoSourceCapability suggest_cap) override; + void onStartSend() override; + void onStopSend() override; + void onUninitialized() override; + +private: + void SendLoop(); + void GenerateTestFrame(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height, int frameNum); + + IZoomVideoSDKVideoSender* m_sender; + std::thread m_sendThread; + std::atomic m_sending; + int m_width; + int m_height; + int m_fps; +}; +``` + +### VideoSource.cpp + +```cpp +#include "VideoSource.h" +#include +#include + +VideoSource::VideoSource() + : m_sender(nullptr) + , m_sending(false) + , m_width(1280) + , m_height(720) + , m_fps(30) { +} + +VideoSource::~VideoSource() { + m_sending = false; + if (m_sendThread.joinable()) { + m_sendThread.join(); + } +} + +void VideoSource::onInitialize(IZoomVideoSDKVideoSender* sender, + IVideoSDKVector* support_cap_list, + VideoSourceCapability& suggest_cap) { + std::cout << "VideoSource initialized" << std::endl; + m_sender = sender; + + // Use suggested capability + m_width = suggest_cap.width; + m_height = suggest_cap.height; + m_fps = suggest_cap.frame; + + std::cout << "Resolution: " << m_width << "x" << m_height + << " @ " << m_fps << " fps" << std::endl; +} + +void VideoSource::onPropertyChange(IVideoSDKVector* support_cap_list, + VideoSourceCapability suggest_cap) { + std::cout << "Property changed: " << suggest_cap.width << "x" + << suggest_cap.height << std::endl; + m_width = suggest_cap.width; + m_height = suggest_cap.height; + m_fps = suggest_cap.frame; +} + +void VideoSource::onStartSend() { + std::cout << "Start sending video" << std::endl; + m_sending = true; + m_sendThread = std::thread(&VideoSource::SendLoop, this); +} + +void VideoSource::onStopSend() { + std::cout << "Stop sending video" << std::endl; + m_sending = false; + if (m_sendThread.joinable()) { + m_sendThread.join(); + } +} + +void VideoSource::onUninitialized() { + std::cout << "VideoSource uninitialized" << std::endl; + m_sender = nullptr; +} + +void VideoSource::SendLoop() { + int frameNum = 0; + auto frameInterval = std::chrono::milliseconds(1000 / m_fps); + + // Allocate YUV buffers + size_t ySize = m_width * m_height; + size_t uvSize = ySize / 4; + + char* yBuffer = new char[ySize]; + char* uBuffer = new char[uvSize]; + char* vBuffer = new char[uvSize]; + + while (m_sending && m_sender) { + auto startTime = std::chrono::steady_clock::now(); + + // Generate or load frame + GenerateTestFrame(yBuffer, uBuffer, vBuffer, m_width, m_height, frameNum); + + // Send frame + ZoomVideoSDKErrors err = m_sender->sendVideoFrame( + yBuffer, + uBuffer, + vBuffer, + m_width, + m_height, + ySize, + 0 // rotation + ); + + if (err != ZoomVideoSDKErrors_Success) { + std::cout << "sendVideoFrame error: " << err << std::endl; + } + + frameNum++; + + // Maintain frame rate + auto elapsed = std::chrono::steady_clock::now() - startTime; + auto sleepTime = frameInterval - elapsed; + if (sleepTime.count() > 0) { + std::this_thread::sleep_for(sleepTime); + } + } + + delete[] yBuffer; + delete[] uBuffer; + delete[] vBuffer; + + std::cout << "Sent " << frameNum << " frames" << std::endl; +} + +void VideoSource::GenerateTestFrame(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height, int frameNum) { + // Generate a simple color gradient that changes over time + int colorShift = frameNum % 256; + + // Y plane (brightness) + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + // Gradient pattern + int brightness = ((x + colorShift) % 256); + yBuffer[y * width + x] = (char)brightness; + } + } + + // U and V planes (color) + int uvWidth = width / 2; + int uvHeight = height / 2; + + for (int y = 0; y < uvHeight; y++) { + for (int x = 0; x < uvWidth; x++) { + int index = y * uvWidth + x; + uBuffer[index] = (char)(128 + (colorShift / 2)); // Blue tint + vBuffer[index] = (char)(128 - (colorShift / 2)); // Red tint + } + } +} +``` + +### Registering the Video Source + +```cpp +void StartVirtualCamera() { + IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper(); + if (!videoHelper) return; + + // Create and set video source + VideoSource* videoSource = new VideoSource(); + + ZoomVideoSDKErrors err = videoHelper->setExternalVideoSource(videoSource); + if (err != ZoomVideoSDKErrors_Success) { + std::cout << "setExternalVideoSource failed: " << err << std::endl; + return; + } + + // Start video (this triggers onStartSend) + videoHelper->startVideo(); +} + +void StopVirtualCamera() { + IZoomVideoSDKVideoHelper* videoHelper = sdk->getVideoHelper(); + if (videoHelper) { + videoHelper->stopVideo(); + } +} +``` + +--- + +## Loading Video from File + +### Read YUV File + +```cpp +bool LoadYUVFrame(const std::string& filename, int frameIndex, + char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height) { + std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) return false; + + size_t ySize = width * height; + size_t uvSize = ySize / 4; + size_t frameSize = ySize + uvSize * 2; + + // Seek to frame + file.seekg(frameIndex * frameSize); + + // Read YUV planes + file.read(yBuffer, ySize); + file.read(uBuffer, uvSize); + file.read(vBuffer, uvSize); + + return file.good(); +} +``` + +### Convert RGB to YUV + +```cpp +void RGBToYUV420(unsigned char* rgb, char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int rgbIndex = (y * width + x) * 3; + int R = rgb[rgbIndex + 2]; // BGR order + int G = rgb[rgbIndex + 1]; + int B = rgb[rgbIndex + 0]; + + // RGB to YUV conversion (BT.601) + int Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16; + + yBuffer[y * width + x] = (char)Y; + + // U and V at quarter resolution + if (y % 2 == 0 && x % 2 == 0) { + int uvIndex = (y / 2) * (width / 2) + (x / 2); + int U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128; + int V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128; + + uBuffer[uvIndex] = (char)U; + vBuffer[uvIndex] = (char)V; + } + } + } +} +``` + +--- + +## sendVideoFrame Parameters + +```cpp +ZoomVideoSDKErrors sendVideoFrame( + char* frameBuffer, // Y plane or full frame + char* uBuffer, // U plane (can be nullptr for NV12) + char* vBuffer, // V plane (can be nullptr for NV12) + int width, // Frame width + int height, // Frame height + int frameLength, // Y plane size (width * height) + int rotation // 0, 90, 180, 270 +); +``` + +--- + +## Common Issues + +### Video Not Showing + +**Cause**: Not calling `startVideo()` after setting source + +**Fix**: +```cpp +videoHelper->setExternalVideoSource(source); +videoHelper->startVideo(); // Required! +``` + +### Frame Rate Too Low + +**Cause**: Frame generation slower than frame rate + +**Fix**: Optimize frame generation or reduce resolution + +### Color Looks Wrong + +**Cause**: RGB/BGR order mismatch or wrong YUV conversion + +**Fix**: Verify color space conversion formula + +--- + +## Related Documentation + +- [Raw Video Capture](raw-video-capture.md) - Capture video +- [Send Raw Audio](send-raw-audio.md) - Audio injection +- [Canvas vs Raw Data](../concepts/canvas-vs-raw-data.md) - Rendering approaches +- [API Reference](../references/windows-reference.md) - Method signatures diff --git a/plugins/zoom-developers/skills/video-sdk/windows/examples/session-join-pattern.md b/plugins/zoom-developers/skills/video-sdk/windows/examples/session-join-pattern.md new file mode 100644 index 00000000..e9aae1ff --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/examples/session-join-pattern.md @@ -0,0 +1,416 @@ +# Session Join Pattern + +Complete working code for initializing the SDK and joining a session. + +## Overview + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ InitSDK │───►│ AddListener│───►│ JoinSession │───►│ onSessionJoin│ +│ │ │ (delegate) │ │ (JWT) │ │ callback │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ +``` + +--- + +## Complete Working Code + +### main.cpp + +```cpp +#include +#include +#include +#include +#include +#include +#include + +// SDK headers +#include "zoom_video_sdk_api.h" +#include "zoom_video_sdk_interface.h" +#include "zoom_video_sdk_delegate_interface.h" + +// JSON parsing (optional - for config file) +#include + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +// Global state +IZoomVideoSDK* g_sdk = nullptr; +bool g_inSession = false; +bool g_exit = false; + +// Configuration +std::wstring g_jwt; +std::wstring g_sessionName; +std::wstring g_sessionPassword; +std::wstring g_userName; + +//───────────────────────────────────────────────────────────────────────────── +// DELEGATE IMPLEMENTATION +//───────────────────────────────────────────────────────────────────────────── + +class MyDelegate : public IZoomVideoSDKDelegate { +public: + // === SESSION LIFECYCLE === + + void onSessionJoin() override { + std::cout << "[EVENT] Session joined successfully!" << std::endl; + g_inSession = true; + + // Connect audio + IZoomVideoSDKAudioHelper* audioHelper = g_sdk->getAudioHelper(); + if (audioHelper) { + audioHelper->startAudio(); + std::cout << "[ACTION] Audio connected" << std::endl; + } + + // Start video + IZoomVideoSDKVideoHelper* videoHelper = g_sdk->getVideoHelper(); + if (videoHelper) { + videoHelper->startVideo(); + std::cout << "[ACTION] Video started" << std::endl; + } + } + + void onSessionLeave() override { + std::cout << "[EVENT] Session left" << std::endl; + g_inSession = false; + g_exit = true; + } + + void onError(ZoomVideoSDKErrors errorCode, int detailErrorCode) override { + std::cout << "[ERROR] Code: " << errorCode + << ", Detail: " << detailErrorCode << std::endl; + } + + // === USER EVENTS === + + void onUserJoin(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + std::wcout << L"[EVENT] User joined: " << user->getUserName() << std::endl; + } + } + + void onUserLeave(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + std::wcout << L"[EVENT] User left: " << user->getUserName() << std::endl; + } + } + + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + bool isOn = user->GetVideoPipe()->getVideoStatus().isOn; + std::wcout << L"[EVENT] Video " << (isOn ? L"ON" : L"OFF") + << L": " << user->getUserName() << std::endl; + } + } + + void onUserAudioStatusChanged(IZoomVideoSDKAudioHelper* helper, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + bool isMuted = user->getAudioStatus().isMuted; + std::wcout << L"[EVENT] Audio " << (isMuted ? L"muted" : L"unmuted") + << L": " << user->getUserName() << std::endl; + } + } + + // === CHAT === + + void onChatNewMessageNotify(IZoomVideoSDKChatHelper* helper, + IZoomVideoSDKChatMessage* msg) override { + std::wcout << L"[CHAT] " << msg->getSendUser()->getUserName() + << L": " << msg->getContent() << std::endl; + } + + // === REQUIRED EMPTY IMPLEMENTATIONS === + // (All pure virtual methods must be implemented) + + void onSessionLeave(ZoomVideoSDKSessionLeaveReason reason) override {} + void onSessionNeedPassword(IZoomVideoSDKPasswordHandler* handler) override {} + void onSessionPasswordWrong(IZoomVideoSDKPasswordHandler* handler) override {} + void onUserHostChanged(IZoomVideoSDKUserHelper* helper, IZoomVideoSDKUser* user) override {} + void onUserManagerChanged(IZoomVideoSDKUser* user) override {} + void onUserNameChanged(IZoomVideoSDKUser* user) override {} + void onUserActiveAudioChanged(IZoomVideoSDKAudioHelper* helper, IVideoSDKVector* list) override {} + void onMixedAudioRawDataReceived(AudioRawData* data) override {} + void onOneWayAudioRawDataReceived(AudioRawData* data, IZoomVideoSDKUser* user) override {} + void onSharedAudioRawDataReceived(AudioRawData* data) override {} + void onUserShareStatusChanged(IZoomVideoSDKShareHelper* helper, IZoomVideoSDKUser* user, IZoomVideoSDKShareAction* action) override {} + void onShareContentChanged(IZoomVideoSDKShareHelper* helper, IZoomVideoSDKUser* user, IZoomVideoSDKShareAction* action) override {} + void onFailedToStartShare(IZoomVideoSDKShareHelper* helper, IZoomVideoSDKUser* user) override {} + void onShareContentSizeChanged(IZoomVideoSDKShareHelper* helper, IZoomVideoSDKUser* user, IZoomVideoSDKShareAction* action) override {} + void onLiveStreamStatusChanged(IZoomVideoSDKLiveStreamHelper* helper, ZoomVideoSDKLiveStreamStatus status) override {} + void onChatMsgDeleteNotification(IZoomVideoSDKChatHelper* helper, const zchar_t* msgID, ZoomVideoSDKChatMessageDeleteType type) override {} + void onChatPrivilegeChanged(IZoomVideoSDKChatHelper* helper, ZoomVideoSDKChatPrivilegeType privilege) override {} + void onLiveTranscriptionStatus(ZoomVideoSDKLiveTranscriptionStatus status) override {} + void onLiveTranscriptionMsgInfoReceived(ILiveTranscriptionMessageInfo* info) override {} + void onOriginalLanguageMsgReceived(ILiveTranscriptionMessageInfo* info) override {} + void onLiveTranscriptionMsgError(ILiveTranscriptionLanguage* spoken, ILiveTranscriptionLanguage* transcript) override {} + void onProxyDetectComplete() override {} + void onProxySettingNotification(IZoomVideoSDKProxySettingHandler* handler) override {} + void onSSLCertVerifiedFailNotification(IZoomVideoSDKSSLCertificateInfo* info) override {} + void onUserVideoNetworkStatusChanged(ZoomVideoSDKNetworkStatus status, IZoomVideoSDKUser* user) override {} + void onCallCRCDeviceStatusChanged(ZoomVideoSDKCRCCallStatus status) override {} + void onVideoCanvasSubscribeFail(ZoomVideoSDKSubscribeFailReason reason, IZoomVideoSDKUser* user, void* handle) override {} + void onShareCanvasSubscribeFail(IZoomVideoSDKUser* user, void* handle, IZoomVideoSDKShareAction* action) override {} + void onAnnotationHelperCleanUp(IZoomVideoSDKAnnotationHelper* helper) override {} + void onAnnotationPrivilegeChange(IZoomVideoSDKUser* user, IZoomVideoSDKShareAction* action) override {} + void onAnnotationHelperActived(void* handle) override {} + void onSendFileStatus(IZoomVideoSDKSendFile* file, const FileTransferStatus& status) override {} + void onReceiveFileStatus(IZoomVideoSDKReceiveFile* file, const FileTransferStatus& status) override {} + void onInviteByPhoneStatus(PhoneStatus status, PhoneFailedReason reason) override {} + void onCalloutJoinSuccess(IZoomVideoSDKUser* user, const zchar_t* phoneNumber) override {} + void onCameraControlRequestResult(IZoomVideoSDKUser* user, bool approved) override {} + void onCameraControlRequestReceived(IZoomVideoSDKUser* user, ZoomVideoSDKCameraControlRequestType type, IZoomVideoSDKCameraControlRequestHandler* handler) override {} + void onCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* cmd) override {} + void onCommandChannelConnectResult(bool success) override {} + void onCloudRecordingStatus(RecordingStatus status, IZoomVideoSDKRecordingConsentHandler* handler) override {} + void onHostAskUnmute() override {} + void onUserRecordingConsent(IZoomVideoSDKUser* user) override {} + void onMultiCameraStreamStatusChanged(ZoomVideoSDKMultiCameraStreamStatus status, IZoomVideoSDKUser* user, IZoomVideoSDKRawDataPipe* pipe) override {} + void onMicSpeakerVolumeChanged(unsigned int micVol, unsigned int speakerVol) override {} + void onAudioDeviceStatusChanged(ZoomVideoSDKAudioDeviceType type, ZoomVideoSDKAudioDeviceStatus status) override {} + void onTestMicStatusChanged(ZoomVideoSDK_TESTMIC_STATUS status) override {} + void onSelectedAudioDeviceChanged() override {} + void onCameraListChanged() override {} + void onSpotlightVideoChanged(IZoomVideoSDKVideoHelper* helper, IVideoSDKVector* list) override {} + void onVideoAlphaChannelStatusChanged(bool isOn) override {} + void onRemoteControlStatus(IZoomVideoSDKUser* user, IZoomVideoSDKShareAction* action, ZoomVideoSDKRemoteControlStatus status) override {} + void onRemoteControlRequestReceived(IZoomVideoSDKUser* user, IZoomVideoSDKShareAction* action, IZoomVideoSDKRemoteControlRequestHandler* handler) override {} + void onRemoteControlServiceInstallResult(bool success) override {} + void onBindIncomingLiveStreamResponse(bool success, const zchar_t* streamKeyID) override {} + void onUnbindIncomingLiveStreamResponse(bool success, const zchar_t* streamKeyID) override {} + void onIncomingLiveStreamStatusResponse(bool success, IVideoSDKVector* list) override {} + void onStartIncomingLiveStreamResponse(bool success, const zchar_t* streamKeyID) override {} + void onStopIncomingLiveStreamResponse(bool success, const zchar_t* streamKeyID) override {} + void onUserWhiteboardShareStatusChanged(IZoomVideoSDKUser* user, IZoomVideoSDKWhiteboardHelper* helper) override {} +}; + +//───────────────────────────────────────────────────────────────────────────── +// CONFIGURATION +//───────────────────────────────────────────────────────────────────────────── + +bool LoadConfig(const std::string& filename) { + std::ifstream file(filename); + if (!file.is_open()) { + std::cerr << "Cannot open " << filename << std::endl; + return false; + } + + Json::Value config; + file >> config; + + std::string jwt = config["jwt"].asString(); + std::string sessionName = config["session_name"].asString(); + std::string password = config.get("password", "").asString(); + std::string userName = config.get("user_name", "Bot").asString(); + + g_jwt = std::wstring(jwt.begin(), jwt.end()); + g_sessionName = std::wstring(sessionName.begin(), sessionName.end()); + g_sessionPassword = std::wstring(password.begin(), password.end()); + g_userName = std::wstring(userName.begin(), userName.end()); + + return true; +} + +//───────────────────────────────────────────────────────────────────────────── +// SDK OPERATIONS +//───────────────────────────────────────────────────────────────────────────── + +bool InitializeSDK() { + g_sdk = CreateZoomVideoSDKObj(); + if (!g_sdk) { + std::cerr << "Failed to create SDK object" << std::endl; + return false; + } + + ZoomVideoSDKInitParams params; + params.domain = L"https://zoom.us"; + params.enableLog = true; + params.logFilePrefix = L"zoom_video_sdk"; + params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; + params.shareRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; + params.audioRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; + + ZoomVideoSDKErrors err = g_sdk->initialize(params); + if (err != ZoomVideoSDKErrors_Success) { + std::cerr << "SDK initialize failed: " << err << std::endl; + return false; + } + + std::cout << "SDK initialized successfully" << std::endl; + return true; +} + +bool JoinSession() { + // Register delegate BEFORE joining + g_sdk->addListener(new MyDelegate()); + + ZoomVideoSDKSessionContext context; + context.sessionName = g_sessionName.c_str(); + context.userName = g_userName.c_str(); + context.token = g_jwt.c_str(); + context.sessionPassword = g_sessionPassword.c_str(); + + // IMPORTANT: Connect audio in onSessionJoin callback + context.audioOption.connect = false; + context.audioOption.mute = true; + context.videoOption.localVideoOn = false; + + IZoomVideoSDKSession* session = g_sdk->joinSession(context); + if (!session) { + std::cerr << "joinSession returned null" << std::endl; + return false; + } + + std::cout << "Join session initiated..." << std::endl; + return true; +} + +void Cleanup() { + if (g_sdk) { + if (g_inSession) { + g_sdk->leaveSession(false); + } + g_sdk->cleanup(); + DestroyZoomVideoSDKObj(); + g_sdk = nullptr; + } +} + +//───────────────────────────────────────────────────────────────────────────── +// MAIN +//───────────────────────────────────────────────────────────────────────────── + +int main() { + // Initialize COM (required for some SDK features) + CoInitialize(NULL); + + // Load configuration + if (!LoadConfig("config.json")) { + return 1; + } + + // Initialize SDK + if (!InitializeSDK()) { + return 1; + } + + // Join session + if (!JoinSession()) { + Cleanup(); + return 1; + } + + // ═══════════════════════════════════════════════════════════════════════ + // CRITICAL: Windows message loop + // Without this, callbacks will NEVER fire! + // ═══════════════════════════════════════════════════════════════════════ + std::cout << "Running message loop (Ctrl+C to exit)..." << std::endl; + + MSG msg; + while (!g_exit) { + // Process all pending Windows messages + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + g_exit = true; + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Small sleep to avoid busy-waiting + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + // Cleanup + Cleanup(); + CoUninitialize(); + + std::cout << "Exited cleanly" << std::endl; + return 0; +} +``` + +### config.json + +```json +{ + "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "session_name": "my-session", + "password": "", + "user_name": "Bot" +} +``` + +--- + +## Key Points + +### 1. Register Delegate BEFORE Joining + +```cpp +// CORRECT +g_sdk->addListener(new MyDelegate()); +g_sdk->joinSession(context); + +// WRONG - callbacks will be missed +g_sdk->joinSession(context); +g_sdk->addListener(new MyDelegate()); // Too late! +``` + +### 2. Set audioOption.connect = false + +```cpp +context.audioOption.connect = false; // Connect in onSessionJoin +context.audioOption.mute = true; +``` + +Then connect audio in the callback: + +```cpp +void onSessionJoin() override { + g_sdk->getAudioHelper()->startAudio(); +} +``` + +### 3. Windows Message Loop is MANDATORY + +```cpp +// This is NOT optional! +while (!g_exit) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Sleep(10); +} +``` + +### 4. Implement ALL Delegate Methods + +The `IZoomVideoSDKDelegate` interface has 80+ pure virtual methods. **All must be implemented**, even if empty. + +--- + +## Related Documentation + +- [Windows Message Loop](../troubleshooting/windows-message-loop.md) - Why message loop is critical +- [Delegate Methods](../references/delegate-methods.md) - All 80+ callback methods +- [Video Rendering](video-rendering.md) - Subscribe to video after join +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Universal pattern + +--- + +**TL;DR**: Initialize → Add delegate → Join with `audioOption.connect = false` → Run message loop → Connect audio in `onSessionJoin`. diff --git a/plugins/zoom-developers/skills/video-sdk/windows/examples/transcription.md b/plugins/zoom-developers/skills/video-sdk/windows/examples/transcription.md new file mode 100644 index 00000000..e24421b4 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/examples/transcription.md @@ -0,0 +1,405 @@ +# Live Transcription + +Complete working code for real-time speech-to-text transcription. + +**Official Sample**: `VSDK_TranscriptionAndTranslation` in [videosdk-windows-rawdata-sample](https://github.com/zoom/videosdk-windows-rawdata-sample) + +--- + +## Overview + +Live transcription provides real-time captions of speech. Features: +- Automatic speech recognition +- Multiple language support +- Speaker identification +- Translation (optional) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ TRANSCRIPTION FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. Enable transcription → startLiveTranscription() │ +│ 2. Receive captions → onLiveTranscriptionMsgInfoReceived() │ +│ 3. Display/process text │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Prerequisites + +- Live transcription must be enabled for the session +- Valid Zoom account with transcription feature + +--- + +## Complete Working Code + +### TranscriptionManager.h + +```cpp +#pragma once +#include +#include +#include +#include +#include "zoom_video_sdk_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +struct TranscriptionMessage { + std::wstring speakerName; + std::wstring text; + bool isFinal; + unsigned long long timestamp; +}; + +class TranscriptionManager { +public: + TranscriptionManager(IZoomVideoSDK* sdk); + + // Control + bool StartTranscription(); + bool StopTranscription(); + bool IsTranscribing() const { return m_isTranscribing; } + + // Language settings + bool SetSpokenLanguage(ILiveTranscriptionLanguage* language); + bool SetTranslationLanguage(ILiveTranscriptionLanguage* language); + std::vector GetAvailableLanguages(); + + // Callbacks from delegate + void OnStatusChanged(ZoomVideoSDKLiveTranscriptionStatus status); + void OnMessageReceived(ILiveTranscriptionMessageInfo* info); + void OnOriginalLanguageReceived(ILiveTranscriptionMessageInfo* info); + void OnError(ILiveTranscriptionLanguage* spoken, + ILiveTranscriptionLanguage* transcript); + + // Set message handler + using MessageCallback = std::function; + void SetMessageHandler(MessageCallback callback) { m_callback = callback; } + +private: + IZoomVideoSDK* m_sdk; + IZoomVideoSDKLiveTranscriptionHelper* m_helper; + bool m_isTranscribing; + MessageCallback m_callback; +}; +``` + +### TranscriptionManager.cpp + +```cpp +#include "TranscriptionManager.h" +#include + +TranscriptionManager::TranscriptionManager(IZoomVideoSDK* sdk) + : m_sdk(sdk) + , m_helper(nullptr) + , m_isTranscribing(false) { +} + +bool TranscriptionManager::StartTranscription() { + m_helper = m_sdk->getLiveTranscriptionHelper(); + if (!m_helper) { + std::cout << "Transcription helper not available" << std::endl; + return false; + } + + // Check if transcription is available + if (!m_helper->canStartLiveTranscription()) { + std::cout << "Cannot start transcription" << std::endl; + return false; + } + + ZoomVideoSDKErrors err = m_helper->startLiveTranscription(); + if (err == ZoomVideoSDKErrors_Success) { + std::cout << "Transcription started" << std::endl; + return true; + } + + std::cout << "Start transcription failed: " << err << std::endl; + return false; +} + +bool TranscriptionManager::StopTranscription() { + if (!m_helper) return false; + + ZoomVideoSDKErrors err = m_helper->stopLiveTranscription(); + if (err == ZoomVideoSDKErrors_Success) { + std::cout << "Transcription stopped" << std::endl; + m_isTranscribing = false; + return true; + } + + return false; +} + +std::vector TranscriptionManager::GetAvailableLanguages() { + std::vector languages; + + if (!m_helper) { + m_helper = m_sdk->getLiveTranscriptionHelper(); + } + + if (m_helper) { + IVideoSDKVector* langList = + m_helper->getAvailableSpokenLanguages(); + + if (langList) { + for (int i = 0; i < langList->GetCount(); i++) { + languages.push_back(langList->GetItem(i)); + } + } + } + + return languages; +} + +bool TranscriptionManager::SetSpokenLanguage(ILiveTranscriptionLanguage* language) { + if (!m_helper || !language) return false; + + ZoomVideoSDKErrors err = m_helper->setSpokenLanguage(language); + if (err == ZoomVideoSDKErrors_Success) { + std::wcout << L"Spoken language set to: " + << language->getLTTLanguageName() << std::endl; + return true; + } + return false; +} + +bool TranscriptionManager::SetTranslationLanguage(ILiveTranscriptionLanguage* language) { + if (!m_helper || !language) return false; + + ZoomVideoSDKErrors err = m_helper->setTranslationLanguage(language); + if (err == ZoomVideoSDKErrors_Success) { + std::wcout << L"Translation language set to: " + << language->getLTTLanguageName() << std::endl; + return true; + } + return false; +} + +void TranscriptionManager::OnStatusChanged(ZoomVideoSDKLiveTranscriptionStatus status) { + switch (status) { + case ZoomVideoSDKLiveTranscription_Status_Start: + std::cout << "Transcription started" << std::endl; + m_isTranscribing = true; + break; + + case ZoomVideoSDKLiveTranscription_Status_Stop: + std::cout << "Transcription stopped" << std::endl; + m_isTranscribing = false; + break; + + case ZoomVideoSDKLiveTranscription_Status_Connecting: + std::cout << "Transcription connecting..." << std::endl; + break; + + default: + std::cout << "Transcription status: " << status << std::endl; + } +} + +void TranscriptionManager::OnMessageReceived(ILiveTranscriptionMessageInfo* info) { + if (!info) return; + + TranscriptionMessage msg; + + // Get speaker + IZoomVideoSDKUser* speaker = info->getSpeaker(); + if (speaker) { + msg.speakerName = speaker->getUserName(); + } + + // Get text + const zchar_t* text = info->getMessageContent(); + if (text) { + msg.text = text; + } + + // Get metadata + msg.isFinal = (info->getMessageType() == + ZoomVideoSDKLiveTranscriptionOperationType_Complete); + msg.timestamp = info->getTimeStamp(); + + // Display + std::wcout << L"[" << msg.speakerName << L"] " << msg.text; + if (msg.isFinal) { + std::wcout << L" (final)"; + } + std::wcout << std::endl; + + // Call user handler + if (m_callback) { + m_callback(msg); + } +} + +void TranscriptionManager::OnOriginalLanguageReceived(ILiveTranscriptionMessageInfo* info) { + // Original language message (before translation) + if (!info) return; + + const zchar_t* text = info->getMessageContent(); + if (text) { + std::wcout << L"[Original] " << text << std::endl; + } +} + +void TranscriptionManager::OnError(ILiveTranscriptionLanguage* spoken, + ILiveTranscriptionLanguage* transcript) { + std::cout << "Transcription error" << std::endl; + if (spoken) { + std::wcout << L"Spoken language: " << spoken->getLTTLanguageName() << std::endl; + } + if (transcript) { + std::wcout << L"Transcript language: " << transcript->getLTTLanguageName() << std::endl; + } +} +``` + +### Using in Delegate + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +private: + TranscriptionManager* m_transcription; + +public: + MyDelegate(IZoomVideoSDK* sdk) { + m_transcription = new TranscriptionManager(sdk); + + // Set message handler + m_transcription->SetMessageHandler([](const TranscriptionMessage& msg) { + // Process transcription (e.g., save to file, analyze) + if (msg.isFinal) { + SaveTranscript(msg.speakerName, msg.text); + } + }); + } + + void onSessionJoin() override { + // Start transcription + m_transcription->StartTranscription(); + } + + void onLiveTranscriptionStatus(ZoomVideoSDKLiveTranscriptionStatus status) override { + m_transcription->OnStatusChanged(status); + } + + void onLiveTranscriptionMsgInfoReceived(ILiveTranscriptionMessageInfo* info) override { + m_transcription->OnMessageReceived(info); + } + + void onOriginalLanguageMsgReceived(ILiveTranscriptionMessageInfo* info) override { + m_transcription->OnOriginalLanguageReceived(info); + } + + void onLiveTranscriptionMsgError(ILiveTranscriptionLanguage* spoken, + ILiveTranscriptionLanguage* transcript) override { + m_transcription->OnError(spoken, transcript); + } + + // ... other callbacks +}; +``` + +--- + +## Message Types + +| Type | Description | +|------|-------------| +| `ZoomVideoSDKLiveTranscriptionOperationType_N` | Interim result (may change) | +| `ZoomVideoSDKLiveTranscriptionOperationType_Complete` | Final result | +| `ZoomVideoSDKLiveTranscriptionOperationType_Update` | Updated previous result | + +**Note**: Interim results allow real-time display but may be revised. + +--- + +## ILiveTranscriptionMessageInfo Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `getMessageContent()` | `const zchar_t*` | Transcribed text | +| `getSpeaker()` | `IZoomVideoSDKUser*` | Speaker user object | +| `getTimeStamp()` | `unsigned long long` | Message timestamp | +| `getMessageType()` | `ZoomVideoSDKLiveTranscriptionOperationType` | Interim/final | +| `getMessageID()` | `const zchar_t*` | Unique message ID | + +--- + +## Language Support + +### List Available Languages + +```cpp +auto languages = transcriptionManager->GetAvailableLanguages(); +for (auto lang : languages) { + std::wcout << lang->getLTTLanguageID() << L": " + << lang->getLTTLanguageName() << std::endl; +} +``` + +### Common Language Codes + +| Code | Language | +|------|----------| +| `en` | English | +| `es` | Spanish | +| `fr` | French | +| `de` | German | +| `zh` | Chinese | +| `ja` | Japanese | + +--- + +## Common Issues + +### Transcription Not Available + +**Cause**: Feature not enabled for session + +**Fix**: Check `canStartLiveTranscription()` first: +```cpp +if (helper->canStartLiveTranscription()) { + helper->startLiveTranscription(); +} +``` + +### No Messages Received + +**Cause**: No one is speaking or audio not connected + +**Fix**: Ensure audio is connected: +```cpp +void onSessionJoin() override { + sdk->getAudioHelper()->startAudio(); // Connect audio first + transcription->StartTranscription(); +} +``` + +### Wrong Language + +**Cause**: Spoken language not set correctly + +**Fix**: Set spoken language: +```cpp +auto languages = manager->GetAvailableLanguages(); +for (auto lang : languages) { + if (wcscmp(lang->getLTTLanguageID(), L"en") == 0) { + manager->SetSpokenLanguage(lang); + break; + } +} +``` + +--- + +## Related Documentation + +- [Raw Audio Capture](raw-audio-capture.md) - Alternative audio processing +- [Session Join Pattern](session-join-pattern.md) - Session setup +- [Delegate Methods](../references/delegate-methods.md) - Transcription callbacks +- [API Reference](../references/windows-reference.md) - Method signatures diff --git a/plugins/zoom-developers/skills/video-sdk/windows/examples/video-rendering.md b/plugins/zoom-developers/skills/video-sdk/windows/examples/video-rendering.md new file mode 100644 index 00000000..c849a5da --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/examples/video-rendering.md @@ -0,0 +1,447 @@ +# Video Rendering with Canvas API + +Complete working code for subscribing to and displaying video using the Canvas API. + +## Overview + +The Canvas API lets the SDK render video directly to your window. This is the **recommended approach** for standard video applications. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ VIDEO SUBSCRIPTION FLOW │ +├─────────────────────────────────────────────────────────────────┤ +│ onSessionJoin → Subscribe to self video │ +│ onUserVideoStatusChanged → Subscribe to remote video (when ON)│ +│ onUserLeave → Unsubscribe and cleanup │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Critical Rule + +### Subscribe in onUserVideoStatusChanged, NOT onUserJoin + +```cpp +// WRONG - Video may not be ready yet! +void onUserJoin(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + // This returns Error 2 (Internal_Error) - video not ready! + user->GetVideoCanvas()->subscribeWithView(hwnd, aspect, resolution); + } +} + +// CORRECT - Wait for video status change +void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + IZoomVideoSDKUser* myself = g_sdk->getSessionInfo()->getMyself(); + + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + + // Skip self (handled separately) + if (user == myself) continue; + + // Check if video is actually on + ZoomVideoSDKVideoStatus status = user->GetVideoPipe()->getVideoStatus(); + if (status.isOn) { + // NOW it's safe to subscribe + SubscribeToUser(user); + } else { + // Video turned off - unsubscribe + UnsubscribeFromUser(user); + } + } +} +``` + +--- + +## Complete Working Code + +### VideoCanvasManager.h + +```cpp +#pragma once +#include +#include +#include +#include "zoom_video_sdk_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +class VideoCanvasManager { +public: + VideoCanvasManager(HWND parentWindow, IZoomVideoSDK* sdk); + ~VideoCanvasManager(); + + // Subscribe to self video + void SubscribeToSelf(); + + // Subscribe/unsubscribe to remote user + void SubscribeToUser(IZoomVideoSDKUser* user); + void UnsubscribeFromUser(IZoomVideoSDKUser* user); + + // Cleanup + void UnsubscribeAll(); + + // Layout + void UpdateLayout(); + +private: + HWND CreateVideoWindow(); + void DestroyVideoWindow(HWND hwnd); + + IZoomVideoSDK* m_sdk; + HWND m_parentWindow; + + // Self video + HWND m_selfVideoWindow; + IZoomVideoSDKCanvas* m_selfCanvas; + + // Remote users: user -> window mapping + std::map m_userWindows; + std::map m_userCanvases; +}; +``` + +### VideoCanvasManager.cpp + +```cpp +#include "VideoCanvasManager.h" +#include + +VideoCanvasManager::VideoCanvasManager(HWND parentWindow, IZoomVideoSDK* sdk) + : m_parentWindow(parentWindow) + , m_sdk(sdk) + , m_selfVideoWindow(nullptr) + , m_selfCanvas(nullptr) { +} + +VideoCanvasManager::~VideoCanvasManager() { + UnsubscribeAll(); +} + +HWND VideoCanvasManager::CreateVideoWindow() { + // Create a child window for video rendering + HWND hwnd = CreateWindowExW( + 0, + L"STATIC", // Simple static window class + L"", + WS_CHILD | WS_VISIBLE | WS_BORDER, + 0, 0, 320, 240, // Size will be set by UpdateLayout() + m_parentWindow, + nullptr, + GetModuleHandle(nullptr), + nullptr + ); + + return hwnd; +} + +void VideoCanvasManager::DestroyVideoWindow(HWND hwnd) { + if (hwnd) { + DestroyWindow(hwnd); + } +} + +void VideoCanvasManager::SubscribeToSelf() { + IZoomVideoSDKSession* session = m_sdk->getSessionInfo(); + if (!session) return; + + IZoomVideoSDKUser* myself = session->getMyself(); + if (!myself) return; + + // Create window for self video + if (!m_selfVideoWindow) { + m_selfVideoWindow = CreateVideoWindow(); + } + + // Subscribe + m_selfCanvas = myself->GetVideoCanvas(); + if (m_selfCanvas) { + ZoomVideoSDKErrors err = m_selfCanvas->subscribeWithView( + m_selfVideoWindow, + ZoomVideoSDKVideoAspect_PanAndScan, + ZoomVideoSDKResolution_Auto + ); + + if (err == ZoomVideoSDKErrors_Success) { + std::cout << "Self video subscribed" << std::endl; + } else { + std::cout << "Self video subscribe failed: " << err << std::endl; + } + } + + UpdateLayout(); +} + +void VideoCanvasManager::SubscribeToUser(IZoomVideoSDKUser* user) { + if (!user) return; + + // Skip if already subscribed + if (m_userWindows.find(user) != m_userWindows.end()) { + return; + } + + // Check if video is on + IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); + if (!pipe || !pipe->getVideoStatus().isOn) { + return; + } + + // Create window for this user + HWND hwnd = CreateVideoWindow(); + m_userWindows[user] = hwnd; + + // Subscribe to canvas + IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); + if (canvas) { + ZoomVideoSDKErrors err = canvas->subscribeWithView( + hwnd, + ZoomVideoSDKVideoAspect_PanAndScan, + ZoomVideoSDKResolution_Auto + ); + + if (err == ZoomVideoSDKErrors_Success) { + m_userCanvases[user] = canvas; + std::wcout << L"Subscribed to: " << user->getUserName() << std::endl; + } else { + std::wcout << L"Subscribe failed for: " << user->getUserName() + << L" error: " << err << std::endl; + // Cleanup on failure + DestroyVideoWindow(hwnd); + m_userWindows.erase(user); + } + } + + UpdateLayout(); +} + +void VideoCanvasManager::UnsubscribeFromUser(IZoomVideoSDKUser* user) { + if (!user) return; + + auto it = m_userWindows.find(user); + if (it == m_userWindows.end()) { + return; + } + + HWND hwnd = it->second; + + // Unsubscribe from canvas + auto canvasIt = m_userCanvases.find(user); + if (canvasIt != m_userCanvases.end()) { + IZoomVideoSDKCanvas* canvas = canvasIt->second; + if (canvas) { + canvas->unSubscribeWithView(hwnd); + } + m_userCanvases.erase(canvasIt); + } + + // Destroy window + DestroyVideoWindow(hwnd); + m_userWindows.erase(it); + + std::wcout << L"Unsubscribed from: " << user->getUserName() << std::endl; + + UpdateLayout(); +} + +void VideoCanvasManager::UnsubscribeAll() { + // Unsubscribe self + if (m_selfCanvas && m_selfVideoWindow) { + m_selfCanvas->unSubscribeWithView(m_selfVideoWindow); + DestroyVideoWindow(m_selfVideoWindow); + m_selfVideoWindow = nullptr; + m_selfCanvas = nullptr; + } + + // Unsubscribe all remote users + for (auto& pair : m_userCanvases) { + IZoomVideoSDKCanvas* canvas = pair.second; + IZoomVideoSDKUser* user = pair.first; + + auto windowIt = m_userWindows.find(user); + if (windowIt != m_userWindows.end() && canvas) { + canvas->unSubscribeWithView(windowIt->second); + } + } + + for (auto& pair : m_userWindows) { + DestroyVideoWindow(pair.second); + } + + m_userCanvases.clear(); + m_userWindows.clear(); +} + +void VideoCanvasManager::UpdateLayout() { + RECT parentRect; + GetClientRect(m_parentWindow, &parentRect); + + int totalVideos = (m_selfVideoWindow ? 1 : 0) + m_userWindows.size(); + if (totalVideos == 0) return; + + // Calculate grid dimensions + int cols = (int)ceil(sqrt((double)totalVideos)); + int rows = (int)ceil((double)totalVideos / cols); + + int cellWidth = (parentRect.right - parentRect.left) / cols; + int cellHeight = (parentRect.bottom - parentRect.top) / rows; + + int index = 0; + + // Position self video (top-left) + if (m_selfVideoWindow) { + int row = index / cols; + int col = index % cols; + SetWindowPos(m_selfVideoWindow, NULL, + col * cellWidth, row * cellHeight, + cellWidth, cellHeight, + SWP_NOZORDER); + index++; + } + + // Position remote videos + for (auto& pair : m_userWindows) { + HWND hwnd = pair.second; + int row = index / cols; + int col = index % cols; + SetWindowPos(hwnd, NULL, + col * cellWidth, row * cellHeight, + cellWidth, cellHeight, + SWP_NOZORDER); + index++; + } +} +``` + +### Using in Delegate + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +private: + VideoCanvasManager* m_videoManager; + +public: + MyDelegate(HWND parentWindow, IZoomVideoSDK* sdk) { + m_videoManager = new VideoCanvasManager(parentWindow, sdk); + } + + ~MyDelegate() { + delete m_videoManager; + } + + void onSessionJoin() override { + // Subscribe to self video + m_videoManager->SubscribeToSelf(); + } + + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + IZoomVideoSDKUser* myself = g_sdk->getSessionInfo()->getMyself(); + + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + if (user == myself) continue; + + ZoomVideoSDKVideoStatus status = user->GetVideoPipe()->getVideoStatus(); + if (status.isOn) { + m_videoManager->SubscribeToUser(user); + } else { + m_videoManager->UnsubscribeFromUser(user); + } + } + } + + void onUserLeave(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + for (int i = 0; i < userList->GetCount(); i++) { + m_videoManager->UnsubscribeFromUser(userList->GetItem(i)); + } + } + + void onSessionLeave() override { + m_videoManager->UnsubscribeAll(); + } + + // ... other callbacks +}; +``` + +--- + +## Error Handling + +### Subscribe Fail Callback + +```cpp +void onVideoCanvasSubscribeFail(ZoomVideoSDKSubscribeFailReason reason, + IZoomVideoSDKUser* user, void* handle) override { + std::wcout << L"Subscribe failed for: " << user->getUserName() + << L" reason: " << reason << std::endl; + + switch (reason) { + case ZoomVideoSDKSubscribeFailReason_HasSubscribe1080POr720P: + std::cout << "Already have a 1080p/720p subscription" << std::endl; + break; + case ZoomVideoSDKSubscribeFailReason_HasSubscribeExceededLimit: + std::cout << "Subscription limit exceeded" << std::endl; + break; + case ZoomVideoSDKSubscribeFailReason_TooFrequentCall: + std::cout << "Calling too frequently - add Sleep(200)" << std::endl; + break; + } +} +``` + +### Subscribe Fail Reasons + +| Code | Reason | Solution | +|------|--------|----------| +| 0 | None | - | +| 1 | HasSubscribe1080POr720P | Already have HD subscription | +| 2 | HasSubscribeTwo720P | Max 2x 720p subscriptions | +| 3 | HasSubscribeExceededLimit | Too many subscriptions | +| 4 | HasSubscribeTwoShare | Max 2 share subscriptions | +| 5 | HasSubscribeVideo1080POr720PAndOneShare | Limit reached | +| 6 | TooFrequentCall | Add `Sleep(200)` between calls | + +--- + +## Aspect Ratio Options + +| Option | Behavior | Use Case | +|--------|----------|----------| +| `ZoomVideoSDKVideoAspect_Original` | Letterbox/pillarbox | Show full video | +| `ZoomVideoSDKVideoAspect_FullFilled` | Fill, may crop | Full coverage | +| `ZoomVideoSDKVideoAspect_PanAndScan` | Smart crop | Balanced (recommended) | +| `ZoomVideoSDKVideoAspect_LetterBox` | Black bars | Preserve aspect | + +--- + +## Resolution Options + +| Option | Resolution | Use Case | +|--------|------------|----------| +| `ZoomVideoSDKResolution_90P` | 160x90 | Thumbnails | +| `ZoomVideoSDKResolution_180P` | 320x180 | Small previews | +| `ZoomVideoSDKResolution_360P` | 640x360 | Standard | +| `ZoomVideoSDKResolution_720P` | 1280x720 | HD | +| `ZoomVideoSDKResolution_1080P` | 1920x1080 | Full HD | +| `ZoomVideoSDKResolution_Auto` | SDK chooses | Recommended | + +--- + +## Related Documentation + +- [Canvas vs Raw Data](../concepts/canvas-vs-raw-data.md) - Rendering approach comparison +- [Raw Video Capture](raw-video-capture.md) - For custom processing +- [Singleton Hierarchy](../concepts/singleton-hierarchy.md) - Canvas/Pipe navigation +- [Common Issues](../troubleshooting/common-issues.md) - Error codes + +--- + +**TL;DR**: Subscribe to self in `onSessionJoin`, subscribe to remote users in `onUserVideoStatusChanged` (when `status.isOn == true`), unsubscribe in `onUserLeave`. diff --git a/plugins/zoom-developers/skills/video-sdk/windows/references/delegate-methods.md b/plugins/zoom-developers/skills/video-sdk/windows/references/delegate-methods.md new file mode 100644 index 00000000..3f2df854 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/references/delegate-methods.md @@ -0,0 +1,591 @@ +# Delegate Methods Reference + +Complete list of all `IZoomVideoSDKDelegate` callback methods. **All 80+ methods must be implemented**, even if empty. + +> **Note**: The callback count has grown significantly in SDK v2.4.x with additions for subsessions (breakout rooms), broadcast streaming, whiteboard, RTMS, and enhanced annotation support. + +--- + +## Quick Template + +Copy this template and add your implementation: + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +public: + // ═══════════════════════════════════════════════════════════════════════ + // SESSION LIFECYCLE + // ═══════════════════════════════════════════════════════════════════════ + + void onSessionJoin() override { + // Called when successfully joined session + } + + void onSessionLeave() override { + // Called when left session (no reason) + } + + void onSessionLeave(ZoomVideoSDKSessionLeaveReason reason) override { + // Called when left session (with reason) + } + + void onError(ZoomVideoSDKErrors errorCode, int detailErrorCode) override { + // Called on SDK errors + } + + void onSessionNeedPassword(IZoomVideoSDKPasswordHandler* handler) override { + // Called when session requires password + } + + void onSessionPasswordWrong(IZoomVideoSDKPasswordHandler* handler) override { + // Called when password is incorrect + } + + // ═══════════════════════════════════════════════════════════════════════ + // USER EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onUserJoin(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + // Called when users join session + } + + void onUserLeave(IZoomVideoSDKUserHelper* helper, + IVideoSDKVector* userList) override { + // Called when users leave session + } + + void onUserHostChanged(IZoomVideoSDKUserHelper* helper, + IZoomVideoSDKUser* user) override { + // Called when host changes + } + + void onUserManagerChanged(IZoomVideoSDKUser* user) override { + // Called when manager status changes + } + + void onUserNameChanged(IZoomVideoSDKUser* user) override { + // Called when user name changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // VIDEO EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + // IMPORTANT: Subscribe to video here, not in onUserJoin + } + + void onSpotlightVideoChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) override { + // Called when spotlight changes + } + + void onVideoCanvasSubscribeFail(ZoomVideoSDKSubscribeFailReason reason, + IZoomVideoSDKUser* user, void* handle) override { + // Called when video subscription fails + } + + void onVideoAlphaChannelStatusChanged(bool isAlphaModeOn) override { + // Called when alpha channel mode changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // AUDIO EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onUserAudioStatusChanged(IZoomVideoSDKAudioHelper* helper, + IVideoSDKVector* userList) override { + // Called when audio status changes (mute/unmute) + } + + void onUserActiveAudioChanged(IZoomVideoSDKAudioHelper* helper, + IVideoSDKVector* userList) override { + // Called when active speaker changes + } + + void onHostAskUnmute() override { + // Called when host requests you to unmute + } + + // ═══════════════════════════════════════════════════════════════════════ + // RAW AUDIO EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onMixedAudioRawDataReceived(AudioRawData* data) override { + // Called with mixed audio from all participants + } + + void onOneWayAudioRawDataReceived(AudioRawData* data, + IZoomVideoSDKUser* user) override { + // Called with audio from specific user + } + + void onSharedAudioRawDataReceived(AudioRawData* data) override { + // Called with shared audio + } + + // ═══════════════════════════════════════════════════════════════════════ + // SHARE EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onUserShareStatusChanged(IZoomVideoSDKShareHelper* helper, + IZoomVideoSDKUser* user, + IZoomVideoSDKShareAction* shareAction) override { + // Called when share status changes - use shareAction to subscribe + } + + void onShareContentChanged(IZoomVideoSDKShareHelper* helper, + IZoomVideoSDKUser* user, + IZoomVideoSDKShareAction* shareAction) override { + // Called when share content type changes + } + + void onFailedToStartShare(IZoomVideoSDKShareHelper* helper, + IZoomVideoSDKUser* user) override { + // Called when share fails to start + } + + void onShareContentSizeChanged(IZoomVideoSDKShareHelper* helper, + IZoomVideoSDKUser* user, + IZoomVideoSDKShareAction* shareAction) override { + // Called when share size changes + } + + void onShareCanvasSubscribeFail(IZoomVideoSDKUser* user, void* handle, + IZoomVideoSDKShareAction* shareAction) override { + // Called when share subscription fails + } + + // ═══════════════════════════════════════════════════════════════════════ + // CHAT EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onChatNewMessageNotify(IZoomVideoSDKChatHelper* helper, + IZoomVideoSDKChatMessage* messageItem) override { + // Called when new chat message received + } + + void onChatMsgDeleteNotification(IZoomVideoSDKChatHelper* helper, + const zchar_t* msgID, + ZoomVideoSDKChatMessageDeleteType deleteBy) override { + // Called when chat message deleted + } + + void onChatPrivilegeChanged(IZoomVideoSDKChatHelper* helper, + ZoomVideoSDKChatPrivilegeType privilege) override { + // Called when chat privilege changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // COMMAND CHANNEL EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* strCmd) override { + // Called when command received + } + + void onCommandChannelConnectResult(bool isSuccess) override { + // Called when command channel connection result + } + + // ═══════════════════════════════════════════════════════════════════════ + // RECORDING EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onCloudRecordingStatus(RecordingStatus status, + IZoomVideoSDKRecordingConsentHandler* handler) override { + // Called when cloud recording status changes + } + + void onUserRecordingConsent(IZoomVideoSDKUser* user) override { + // Called when user gives recording consent + } + + // ═══════════════════════════════════════════════════════════════════════ + // LIVE STREAM EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onLiveStreamStatusChanged(IZoomVideoSDKLiveStreamHelper* helper, + ZoomVideoSDKLiveStreamStatus status) override { + // Called when live stream status changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // LIVE TRANSCRIPTION EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onLiveTranscriptionStatus(ZoomVideoSDKLiveTranscriptionStatus status) override { + // Called when transcription status changes + } + + void onLiveTranscriptionMsgInfoReceived(ILiveTranscriptionMessageInfo* info) override { + // Called when transcription message received + } + + void onOriginalLanguageMsgReceived(ILiveTranscriptionMessageInfo* info) override { + // Called when original language message received + } + + void onLiveTranscriptionMsgError(ILiveTranscriptionLanguage* spokenLanguage, + ILiveTranscriptionLanguage* transcriptLanguage) override { + // Called on transcription error + } + + // ═══════════════════════════════════════════════════════════════════════ + // PHONE EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onInviteByPhoneStatus(PhoneStatus status, PhoneFailedReason reason) override { + // Called when phone invite status changes + } + + void onCalloutJoinSuccess(IZoomVideoSDKUser* user, const zchar_t* phoneNumber) override { + // Called when callout user joins + } + + // ═══════════════════════════════════════════════════════════════════════ + // CAMERA CONTROL EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onCameraControlRequestResult(IZoomVideoSDKUser* user, bool isApproved) override { + // Called when camera control request result + } + + void onCameraControlRequestReceived(IZoomVideoSDKUser* user, + ZoomVideoSDKCameraControlRequestType requestType, + IZoomVideoSDKCameraControlRequestHandler* handler) override { + // Called when camera control request received + } + + // ═══════════════════════════════════════════════════════════════════════ + // REMOTE CONTROL EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onRemoteControlStatus(IZoomVideoSDKUser* user, + IZoomVideoSDKShareAction* shareAction, + ZoomVideoSDKRemoteControlStatus status) override { + // Called when remote control status changes + } + + void onRemoteControlRequestReceived(IZoomVideoSDKUser* user, + IZoomVideoSDKShareAction* shareAction, + IZoomVideoSDKRemoteControlRequestHandler* handler) override { + // Called when remote control request received + } + + void onRemoteControlServiceInstallResult(bool bSuccess) override { + // Called when remote control service install result + } + + // ═══════════════════════════════════════════════════════════════════════ + // MULTI-CAMERA EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onMultiCameraStreamStatusChanged(ZoomVideoSDKMultiCameraStreamStatus status, + IZoomVideoSDKUser* user, + IZoomVideoSDKRawDataPipe* pipe) override { + // Called when multi-camera stream status changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // DEVICE EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onMicSpeakerVolumeChanged(unsigned int micVolume, + unsigned int speakerVolume) override { + // Called when mic/speaker volume changes + } + + void onAudioDeviceStatusChanged(ZoomVideoSDKAudioDeviceType type, + ZoomVideoSDKAudioDeviceStatus status) override { + // Called when audio device status changes + } + + void onTestMicStatusChanged(ZoomVideoSDK_TESTMIC_STATUS status) override { + // Called when test mic status changes + } + + void onSelectedAudioDeviceChanged() override { + // Called when selected audio device changes + } + + void onCameraListChanged() override { + // Called when camera list changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // NETWORK EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onUserVideoNetworkStatusChanged(ZoomVideoSDKNetworkStatus status, + IZoomVideoSDKUser* user) override { + // Called when video network status changes + } + + void onProxyDetectComplete() override { + // Called when proxy detection completes + } + + void onProxySettingNotification(IZoomVideoSDKProxySettingHandler* handler) override { + // Called when proxy settings notification + } + + void onSSLCertVerifiedFailNotification(IZoomVideoSDKSSLCertificateInfo* info) override { + // Called when SSL cert verification fails + } + + // ═══════════════════════════════════════════════════════════════════════ + // CRC EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onCallCRCDeviceStatusChanged(ZoomVideoSDKCRCCallStatus status) override { + // Called when CRC device status changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // ANNOTATION EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onAnnotationHelperCleanUp(IZoomVideoSDKAnnotationHelper* helper) override { + // Called when annotation helper cleanup + } + + void onAnnotationPrivilegeChange(IZoomVideoSDKUser* user, + IZoomVideoSDKShareAction* shareAction) override { + // Called when annotation privilege changes + } + + void onAnnotationHelperActived(void* handle) override { + // Called when annotation helper activated + } + + // ═══════════════════════════════════════════════════════════════════════ + // FILE TRANSFER EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onSendFileStatus(IZoomVideoSDKSendFile* file, + const FileTransferStatus& status) override { + // Called when send file status changes + } + + void onReceiveFileStatus(IZoomVideoSDKReceiveFile* file, + const FileTransferStatus& status) override { + // Called when receive file status changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // INCOMING LIVE STREAM EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onBindIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) override { + // Called when bind incoming live stream response + } + + void onUnbindIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) override { + // Called when unbind incoming live stream response + } + + void onIncomingLiveStreamStatusResponse(bool bSuccess, + IVideoSDKVector* list) override { + // Called when incoming live stream status response + } + + void onStartIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) override { + // Called when start incoming live stream response + } + + void onStopIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) override { + // Called when stop incoming live stream response + } + + // ═══════════════════════════════════════════════════════════════════════ + // SHARE SETTING & CONTENT EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onShareSettingChanged(ZoomVideoSDKShareSetting setting) override { + // Called when share settings change + } + + void onUnsharingWindowsChanged(IVideoSDKVector* windowsList, + IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) override { + // Called when list of unsharing windows changes (macOS only) + } + + void onSharingActiveMonitorChanged(IVideoSDKVector* monitorIDs, + IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) override { + // Called when active monitors displaying share changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // AUDIO LEVEL & NETWORK EVENTS (NEW) + // ═══════════════════════════════════════════════════════════════════════ + + void onAudioLevelChanged(unsigned int level, bool bAudioSharing, + IZoomVideoSDKUser* pUser) override { + // Called when audio level changes (range 0-9) + } + + void onUserNetworkStatusChanged(ZoomVideoSDKDataType type, + ZoomVideoSDKNetworkStatus level, + IZoomVideoSDKUser* pUser) override { + // Called when network status changes for specific data type + } + + void onUserOverallNetworkStatusChanged(ZoomVideoSDKNetworkStatus level, + IZoomVideoSDKUser* pUser) override { + // Called when overall network status changes + } + + void onShareNetworkStatusChanged(ZoomVideoSDKNetworkStatus shareNetworkStatus, + bool isSendingShare) override { + // Called when share network status changes (deprecated) + } + + // ═══════════════════════════════════════════════════════════════════════ + // LIVE TRANSCRIPTION EVENTS (ADDITIONAL) + // ═══════════════════════════════════════════════════════════════════════ + + void onSpokenLanguageChanged(ILiveTranscriptionLanguage* spokenLanguage) override { + // Called when spoken language changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // ANNOTATION EVENTS (ADDITIONAL) + // ═══════════════════════════════════════════════════════════════════════ + + void onAnnotationToolTypeChanged(IZoomVideoSDKAnnotationHelper* helper, + void* handle, + ZoomVideoSDKAnnotationToolType toolType) override { + // Called when annotation tool type changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // WHITEBOARD EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onUserWhiteboardShareStatusChanged(IZoomVideoSDKUser* user, + IZoomVideoSDKWhiteboardHelper* helper) override { + // Called when whiteboard share status changes + } + + void onWhiteboardExported(ZoomVideoSDKExportFormat format, + unsigned char* data, long length) override { + // Called when whiteboard export completes + } + + // ═══════════════════════════════════════════════════════════════════════ + // SUBSESSION (BREAKOUT ROOM) EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onSubSessionStatusChanged(ZoomVideoSDKSubSessionStatus status, + IVideoSDKVector* pSubSessionKitList) override { + // Called when subsession status changes + } + + void onSubSessionManagerHandle(IZoomVideoSDKSubSessionManager* pManager) override { + // Called when user gains subsession manager privilege + } + + void onSubSessionParticipantHandle(IZoomVideoSDKSubSessionParticipant* pParticipant) override { + // Called when user gains/loses subsession participant privileges + } + + void onSubSessionUsersUpdate(ISubSessionKit* pSubSessionKit) override { + // Called when subsession users are updated + } + + void onBroadcastMessageFromMainSession(const zchar_t* sMessage, + const zchar_t* sUserName) override { + // Called when receiving broadcast message from main session + } + + void onSubSessionUserHelpRequest(ISubSessionUserHelpRequestHandler* pHandler) override { + // Called when receiving help request from subsession + } + + void onSubSessionUserHelpRequestResult(ZoomVideoSDKUserHelpRequestResult eResult) override { + // Called with help request result + } + + // ═══════════════════════════════════════════════════════════════════════ + // BROADCAST STREAMING EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onStartBroadcastResponse(bool bSuccess, const zchar_t* channelID) override { + // Called when start broadcast response received + } + + void onStopBroadcastResponse(bool bSuccess) override { + // Called when stop broadcast response received + } + + void onGetBroadcastControlStatus(bool bSuccess, + ZoomVideoSDKBroadcastControlStatus status) override { + // Called when get broadcast status response received + } + + void onStreamingJoinStatusChanged(ZoomVideoSDKStreamingJoinStatus status) override { + // Called when viewer's join status changes + } + + // ═══════════════════════════════════════════════════════════════════════ + // RTMS (REAL-TIME MEDIA STREAMS) EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onRealTimeMediaStreamsStatus(RealTimeMediaStreamsStatus status) override { + // Called when RTMS status changes + } + + void onRealTimeMediaStreamsFail(RealTimeMediaStreamsFailReason failReason) override { + // Called when RTMS fails + } + + // ═══════════════════════════════════════════════════════════════════════ + // CANVAS SNAPSHOT EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + void onCanvasSnapshotTaken(IZoomVideoSDKUser* pUser, bool isShare) override { + // Called when canvas snapshot is taken successfully + } + + void onCanvasSnapshotIncompatible(IZoomVideoSDKUser* pUser) override { + // Called when snapshot cannot be taken due to compatibility + } +}; +``` + +--- + +## Most Important Callbacks + +| Callback | When to Use | +|----------|-------------| +| `onSessionJoin` | Start audio/video, subscribe to self | +| `onSessionLeave` | Cleanup resources | +| `onError` | Handle errors | +| `onUserJoin` | Track new users (but don't subscribe video here!) | +| `onUserLeave` | Cleanup user resources | +| `onUserVideoStatusChanged` | **Subscribe to video here** | +| `onUserAudioStatusChanged` | Track mute/unmute | +| `onChatNewMessageNotify` | Handle chat messages | +| `onUserShareStatusChanged` | Subscribe to screen share | +| `onVideoCanvasSubscribeFail` | Handle subscription failures | + +--- + +## Related Documentation + +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Event-driven design +- [Video Rendering](../examples/video-rendering.md) - Using video callbacks +- [Build Errors](../troubleshooting/build-errors.md) - Abstract class errors +- [API Reference](windows-reference.md) - Full method signatures + +--- + +**TL;DR**: Copy the template above and implement all methods. Focus on session, user, video, and audio events for basic functionality. diff --git a/plugins/zoom-developers/skills/video-sdk/windows/references/samples.md b/plugins/zoom-developers/skills/video-sdk/windows/references/samples.md new file mode 100644 index 00000000..236b2aaa --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/references/samples.md @@ -0,0 +1,282 @@ +# Official Sample Applications + +Reference guide for Zoom Video SDK Windows sample applications. + +**Official Repository**: https://github.com/zoom/videosdk-windows-rawdata-sample + +--- + +## Sample Overview + +| Sample | Description | Key Features | +|--------|-------------|--------------| +| **VSDK_SkeletonDemo** | Minimal session join | Simplest starting point | +| **VSDK_getRawVideo** | Capture raw video | YUV420 frame extraction | +| **VSDK_getRawAudio** | Capture raw audio | PCM audio extraction | +| **VSDK_getRawShare** | Capture screen share | Share content capture | +| **VSDK_sendRawVideo** | Send custom video | Virtual camera injection | +| **VSDK_sendRawAudio** | Send custom audio | Virtual microphone injection | +| **VSDK_sendRawShare** | Send custom share | Custom screen share source | +| **VSDK_CloudRecording** | Cloud recording | Start/stop cloud recording | +| **VSDK_CommandChannel** | Custom messaging | Send/receive custom commands | +| **VSDK_CallIn** | PSTN dial-in | Phone dial-in support | +| **VSDK_Callout** | PSTN dial-out | Phone dial-out support | +| **VSDK_ServiceQuality** | Network statistics | Quality monitoring | +| **VSDK_TranscriptionAndTranslation** | Live transcription | Real-time captions | +| **VSDK_MultiStreamVideo** | Multiple video streams | Multi-camera support | +| **VSDK_PreviewCameraAndMicrophone** | Device preview | Pre-join device testing | +| **VSDK_Share2ndCameraAsMultiCam** | Secondary camera | Multi-camera sharing | +| **VSDK_Share2ndCameraAsShareScreenDemo** | Camera as share | Camera content sharing | +| **VSDK_ShareScreenPreprocessorDemo** | Share preprocessing | Custom share processing | +| **VSDK_RTMSDemo** | Real-time messaging | RTMS integration | +| **VSDK_DuilibDemo2** | Full UI demo | Complete GUI application | + +--- + +## Recommended Learning Path + +### 1. Start Here: VSDK_SkeletonDemo + +Minimal code to join a session. Demonstrates: +- SDK initialization +- JWT authentication +- Session join/leave +- Windows message loop +- Basic delegate implementation + +**Key patterns to learn:** +```cpp +// 1. Create SDK +IZoomVideoSDK* sdk = CreateZoomVideoSDKObj(); + +// 2. Initialize +ZoomVideoSDKInitParams params; +params.domain = L"https://zoom.us"; +sdk->initialize(params); + +// 3. Add delegate +sdk->addListener(myDelegate); + +// 4. Join session +ZoomVideoSDKSessionContext ctx; +ctx.sessionName = L"session"; +ctx.token = L"jwt"; +ctx.audioOption.connect = false; +sdk->joinSession(ctx); + +// 5. Message loop (CRITICAL) +while (running) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Sleep(10); +} +``` + +### 2. Video Capture: VSDK_getRawVideo + +Capture raw YUV420 video frames. Demonstrates: +- `IZoomVideoSDKRawDataPipeDelegate` implementation +- `onRawDataFrameReceived()` callback +- YUV buffer extraction (Y, U, V planes) +- Resolution and rotation handling + +**Key patterns:** +```cpp +class VideoCapture : public IZoomVideoSDKRawDataPipeDelegate { + void onRawDataFrameReceived(YUVRawDataI420* data) override { + int width = data->GetStreamWidth(); + int height = data->GetStreamHeight(); + char* yBuffer = data->GetYBuffer(); + char* uBuffer = data->GetUBuffer(); + char* vBuffer = data->GetVBuffer(); + // Process frames... + } +}; + +// Subscribe +IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); +pipe->subscribe(ZoomVideoSDKResolution_720P, videoCapture); +``` + +### 3. Audio Capture: VSDK_getRawAudio + +Capture raw PCM audio. Demonstrates: +- Mixed audio (all participants) +- Per-user audio separation +- Audio format (sample rate, channels) + +**Key patterns:** +```cpp +void onMixedAudioRawDataReceived(AudioRawData* data) override { + char* buffer = data->GetBuffer(); + int length = data->GetBufferLen(); + int sampleRate = data->GetSampleRate(); // 32000 Hz + int channels = data->GetChannelNum(); // 1 or 2 + // Process PCM audio... +} +``` + +### 4. Video Injection: VSDK_sendRawVideo + +Send custom video as virtual camera. Demonstrates: +- `IZoomVideoSDKVideoSource` implementation +- Frame sending with `sendVideoFrame()` +- YUV frame creation +- Frame rate control + +### 5. Audio Injection: VSDK_sendRawAudio + +Send custom audio as virtual microphone. Demonstrates: +- `IZoomVideoSDKVirtualAudioMic` implementation +- PCM audio sending +- Sample rate matching + +--- + +## Sample Categories + +### Raw Data Capture + +| Sample | Input | Output | +|--------|-------|--------| +| VSDK_getRawVideo | Remote video | YUV420 frames | +| VSDK_getRawAudio | Session audio | PCM samples | +| VSDK_getRawShare | Screen share | YUV420 frames | + +### Raw Data Injection + +| Sample | Input | Output | +|--------|-------|--------| +| VSDK_sendRawVideo | YUV420 frames | Virtual camera | +| VSDK_sendRawAudio | PCM samples | Virtual mic | +| VSDK_sendRawShare | YUV420 frames | Screen share | + +### Communication + +| Sample | Feature | +|--------|---------| +| VSDK_CommandChannel | Custom command messaging (60 msgs/sec) | +| VSDK_CallIn | PSTN phone dial-in | +| VSDK_Callout | PSTN phone dial-out | + +### Recording & Streaming + +| Sample | Feature | +|--------|---------| +| VSDK_CloudRecording | Zoom cloud recording | +| VSDK_RTMSDemo | Real-time messaging service | + +### Advanced Features + +| Sample | Feature | +|--------|---------| +| VSDK_ServiceQuality | Network quality statistics | +| VSDK_TranscriptionAndTranslation | Live captions | +| VSDK_MultiStreamVideo | Multiple video streams | +| VSDK_PreviewCameraAndMicrophone | Device preview before join | +| VSDK_ShareScreenPreprocessorDemo | Custom share processing | + +--- + +## Common Patterns Across Samples + +### 1. Initialization Pattern + +All samples follow this pattern: +```cpp +// Initialize SDK +IZoomVideoSDK* sdk = CreateZoomVideoSDKObj(); +ZoomVideoSDKInitParams params; +params.domain = L"https://zoom.us"; +params.videoRawDataMemoryMode = ZoomVideoSDKRawDataMemoryModeHeap; +sdk->initialize(params); +``` + +### 2. Delegate Registration + +Always register before joining: +```cpp +sdk->addListener(new MyDelegate()); +sdk->joinSession(context); +``` + +### 3. Audio Connection + +Connect audio in callback, not during join: +```cpp +context.audioOption.connect = false; // Join config + +void onSessionJoin() override { + sdk->getAudioHelper()->startAudio(); // Connect here +} +``` + +### 4. Message Loop + +All samples include message loop: +```cpp +while (!g_exit) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Sleep(10); +} +``` + +--- + +## Building Samples + +### Prerequisites + +1. Visual Studio 2019 or 2022 +2. Windows SDK 10.0.19041.0+ +3. Zoom Video SDK (download from Marketplace) + +### Build Steps + +1. Open solution file (`.sln`) +2. Set platform to x64 +3. Set configuration to Release +4. Build solution (Ctrl+Shift+B) +5. Copy SDK DLLs to output directory + +### Configuration + +Each sample uses `config.json`: +```json +{ + "jwt": "your-jwt-token", + "session_name": "test-session", + "password": "", + "user_name": "Bot" +} +``` + +--- + +## Related Documentation + +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Universal patterns +- [Session Join Pattern](../examples/session-join-pattern.md) - Complete join code +- [Raw Video Capture](../examples/raw-video-capture.md) - YUV capture details +- [API Reference](windows-reference.md) - Method signatures +- [Delegate Methods](delegate-methods.md) - All callbacks + +--- + +## External Resources + +- **GitHub Repository**: https://github.com/zoom/videosdk-windows-rawdata-sample +- **Official Documentation**: https://developers.zoom.us/docs/video-sdk/windows/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/custom/windows/ +- **Developer Forum**: https://devforum.zoom.us/ + +--- + +**Recommendation**: Start with `VSDK_SkeletonDemo` to understand the basic flow, then move to `VSDK_getRawVideo` or `VSDK_getRawAudio` based on your needs. diff --git a/plugins/zoom-developers/skills/video-sdk/windows/references/windows-reference.md b/plugins/zoom-developers/skills/video-sdk/windows/references/windows-reference.md new file mode 100644 index 00000000..910c95a1 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/references/windows-reference.md @@ -0,0 +1,1221 @@ +# Zoom Video SDK Windows - API Reference + +**Source**: https://marketplacefront.zoom.us/sdk/custom/windows/ + +--- + +## API Hierarchy (5 Levels Deep) + +Understanding the SDK requires navigating from the singleton entry point through 5 levels of objects. Start from `IZoomVideoSDK` and follow return types. + +### Level 1: Entry Point (Singleton) + +``` +CreateZoomVideoSDKObj() → IZoomVideoSDK* +``` + +| Method | Returns | Purpose | +|--------|---------|---------| +| `initialize(params)` | `ZoomVideoSDKErrors` | Initialize SDK (call once) | +| `joinSession(context)` | `IZoomVideoSDKSession*` | Join and get session object | +| `leaveSession(end)` | `ZoomVideoSDKErrors` | Leave or end session | +| `addListener(delegate)` | `void` | Register event callbacks | +| `getSessionInfo()` | `IZoomVideoSDKSession*` | Get current session | +| `getVideoHelper()` | `IZoomVideoSDKVideoHelper*` | Camera/video control | +| `getAudioHelper()` | `IZoomVideoSDKAudioHelper*` | Mic/speaker control | +| `getShareHelper()` | `IZoomVideoSDKShareHelper*` | Screen sharing | +| `getChatHelper()` | `IZoomVideoSDKChatHelper*` | Chat messaging | +| `getUserHelper()` | `IZoomVideoSDKUserHelper*` | User management | +| `getRecordingHelper()` | `IZoomVideoSDKRecordingHelper*` | Cloud recording | +| `getCmdChannel()` | `IZoomVideoSDKCmdChannel*` | Custom signaling | + +### Level 2: Core Helpers & Session + +#### IZoomVideoSDKSession +```cpp +IZoomVideoSDKSession* session = sdk->getSessionInfo(); +``` + +| Method | Returns | Purpose | +|--------|---------|---------| +| `getMyself()` | `IZoomVideoSDKUser*` | Current user object | +| `getRemoteUsers()` | `IVideoSDKVector*` | All remote users | +| `getSessionName()` | `const zchar_t*` | Session name | +| `getSessionID()` | `const zchar_t*` | Unique session ID | +| `getSessionPassword()` | `const zchar_t*` | Session password | +| `getSessionHost()` | `IZoomVideoSDKUser*` | Host user | + +#### IZoomVideoSDKVideoHelper +Controls YOUR camera. Does NOT control remote users' video. + +| Method | Returns | Purpose | +|--------|---------|---------| +| `startVideo()` | `ZoomVideoSDKErrors` | Turn on your camera | +| `stopVideo()` | `ZoomVideoSDKErrors` | Turn off your camera | +| `rotateMyVideo(rotation)` | `bool` | Rotate camera output | +| `switchCamera(deviceId)` | `bool` | Change camera device | +| `getCameraList()` | `IVideoSDKVector*` | Available cameras | +| `getNumberOfCameras()` | `uint32_t` | Camera count | +| `startVideoCanvasPreview(hwnd, aspect, resolution)` | `ZoomVideoSDKErrors` | Preview your video | +| `stopVideoCanvasPreview(hwnd)` | `ZoomVideoSDKErrors` | Stop preview | + +#### IZoomVideoSDKAudioHelper +| Method | Returns | Purpose | +|--------|---------|---------| +| `startAudio()` | `ZoomVideoSDKErrors` | Connect to audio | +| `stopAudio()` | `ZoomVideoSDKErrors` | Disconnect audio | +| `muteAudio(user)` | `ZoomVideoSDKErrors` | Mute a user | +| `unmuteAudio(user)` | `ZoomVideoSDKErrors` | Unmute a user | +| `getMicList()` | `IVideoSDKVector*` | Available mics | +| `getSpeakerList()` | `IVideoSDKVector*` | Available speakers | +| `selectMic(deviceId, name)` | `ZoomVideoSDKErrors` | Select mic | +| `selectSpeaker(deviceId, name)` | `ZoomVideoSDKErrors` | Select speaker | + +#### IZoomVideoSDKShareHelper +| Method | Returns | Purpose | +|--------|---------|---------| +| `startShareScreen(monitorId)` | `ZoomVideoSDKErrors` | Share a monitor | +| `startShareWindow(hwnd)` | `ZoomVideoSDKErrors` | Share a window | +| `stopShare()` | `ZoomVideoSDKErrors` | Stop sharing | +| `isShareLocked()` | `bool` | Check if locked | +| `lockShare(lock)` | `ZoomVideoSDKErrors` | Lock sharing | +| `isOtherSharing()` | `bool` | Someone else sharing? | + +### Level 3: User & Rendering Objects + +#### IZoomVideoSDKUser +Represents a participant. Get from `session->getMyself()` or callbacks. + +| Method | Returns | Purpose | +|--------|---------|---------| +| `getUserID()` | `const zchar_t*` | Unique user ID | +| `getUserName()` | `const zchar_t*` | Display name | +| `isHost()` | `bool` | Is session host? | +| `isManager()` | `bool` | Is manager? | +| `GetVideoCanvas()` | `IZoomVideoSDKCanvas*` | **SDK-rendered video** | +| `GetVideoPipe()` | `IZoomVideoSDKRawDataPipe*` | **Raw YUV frames** | +| `GetShareCanvas()` | `IZoomVideoSDKCanvas*` | SDK-rendered share | +| `GetSharePipe()` | `IZoomVideoSDKRawDataPipe*` | Raw share frames | +| `getVideoStatus()` | `ZoomVideoSDKVideoStatus` | Video on/off state | +| `getAudioStatus()` | `ZoomVideoSDKAudioStatus` | Audio mute state | + +#### IZoomVideoSDKCanvas (SDK Rendering) +Let the SDK render video directly to your HWND. **Recommended for most apps.** + +| Method | Returns | Purpose | +|--------|---------|---------| +| `subscribeWithView(hwnd, aspect, resolution)` | `ZoomVideoSDKErrors` | Start rendering to HWND | +| `unSubscribeWithView(hwnd)` | `ZoomVideoSDKErrors` | Stop rendering | +| `setAspectMode(aspect)` | `ZoomVideoSDKErrors` | Change aspect ratio | +| `setResolution(resolution)` | `ZoomVideoSDKErrors` | Change resolution | + +#### IZoomVideoSDKRawDataPipe (Raw YUV Access) +Get raw YUV420 frames for custom processing. + +| Method | Returns | Purpose | +|--------|---------|---------| +| `subscribe(resolution, delegate)` | `ZoomVideoSDKErrors` | Start receiving frames | +| `unSubscribe(delegate)` | `ZoomVideoSDKErrors` | Stop receiving | +| `getVideoStatus()` | `ZoomVideoSDKVideoStatus` | Check if video is on | + +#### IZoomVideoSDKShareAction +Received in `onUserShareStatusChanged` callback. Controls remote share subscription. + +| Method | Returns | Purpose | +|--------|---------|---------| +| `subscribe()` | `ZoomVideoSDKErrors` | Subscribe to share | +| `unSubscribe()` | `ZoomVideoSDKErrors` | Unsubscribe | +| `subscribeWithView(hwnd, aspect)` | `ZoomVideoSDKErrors` | Render share to HWND | +| `unSubscribeWithView(hwnd)` | `ZoomVideoSDKErrors` | Stop rendering | +| `getShareCanvas()` | `IZoomVideoSDKCanvas*` | Get share canvas | +| `getSharePipe()` | `IZoomVideoSDKRawDataPipe*` | Get raw share pipe | +| `getShareType()` | `ZoomVideoSDKShareType` | Screen/window/etc | + +### Level 4: Devices, Chat & Callbacks + +#### IZoomVideoSDKCameraDevice +| Method | Returns | +|--------|---------| +| `getDeviceId()` | `const zchar_t*` | +| `getDeviceName()` | `const zchar_t*` | +| `isSelectedDevice()` | `bool` | + +#### IZoomVideoSDKMicDevice / IZoomVideoSDKSpeakerDevice +| Method | Returns | +|--------|---------| +| `getDeviceId()` | `const zchar_t*` | +| `getDeviceName()` | `const zchar_t*` | +| `isSelectedDevice()` | `bool` | + +#### IZoomVideoSDKChatHelper +| Method | Returns | Purpose | +|--------|---------|---------| +| `sendChatToAll(message)` | `ZoomVideoSDKErrors` | Broadcast message | +| `sendChatToUser(user, message)` | `ZoomVideoSDKErrors` | Private message | +| `canChatMessageBeDeleted(msgId)` | `bool` | Check delete permission | +| `deleteChatMessage(msgId)` | `ZoomVideoSDKErrors` | Delete a message | + +#### IZoomVideoSDKChatMessage +Received in `onChatNewMessageNotify` callback. + +| Method | Returns | +|--------|---------| +| `getMessageID()` | `const zchar_t*` | +| `getSendUser()` | `IZoomVideoSDKUser*` | +| `getReceiverUser()` | `IZoomVideoSDKUser*` | +| `getContent()` | `const zchar_t*` | +| `getTimeStamp()` | `time_t` | +| `isChatToAll()` | `bool` | +| `isSelfSend()` | `bool` | + +#### IZoomVideoSDKRawDataPipeDelegate +Implement this to receive raw YUV frames. + +```cpp +class MyVideoRenderer : public IZoomVideoSDKRawDataPipeDelegate { + void onRawDataFrameReceived(YUVRawDataI420* data) override { + // Process frame + } + void onRawDataStatusChanged(RawDataStatus status) override { + // Handle on/off + } +}; +``` + +### Level 5: Raw Data & Utilities + +#### YUVRawDataI420 +Video frame in YUV420 format (I420). + +| Method | Returns | Purpose | +|--------|---------|---------| +| `GetYBuffer()` | `char*` | Y plane (luminance) | +| `GetUBuffer()` | `char*` | U plane (chrominance) | +| `GetVBuffer()` | `char*` | V plane (chrominance) | +| `GetStreamWidth()` | `unsigned int` | Frame width | +| `GetStreamHeight()` | `unsigned int` | Frame height | +| `GetRotation()` | `unsigned int` | 0, 90, 180, 270 | +| `GetTimeStamp()` | `unsigned long long` | Frame timestamp | +| `CanAddRef()` / `AddRef()` / `Release()` | - | Reference counting | + +#### AudioRawData +PCM audio samples (16-bit signed). + +| Method | Returns | Purpose | +|--------|---------|---------| +| `GetBuffer()` | `char*` | PCM sample buffer | +| `GetBufferLen()` | `unsigned int` | Buffer size (bytes) | +| `GetSampleRate()` | `unsigned int` | Sample rate (Hz) | +| `GetChannelNum()` | `unsigned int` | 1=mono, 2=stereo | + +#### IZoomVideoSDKUserHelper +Admin actions on users. + +| Method | Returns | Purpose | +|--------|---------|---------| +| `removeUser(user)` | `bool` | Kick user from session | +| `makeHost(user)` | `bool` | Transfer host role | +| `makeManager(user)` | `bool` | Promote to manager | +| `revokeManager(user)` | `bool` | Demote manager | +| `changeName(user, name)` | `bool` | Rename user | + +#### IZoomVideoSDKCmdChannel +Custom signaling (max 60 messages/second). + +| Method | Returns | Purpose | +|--------|---------|---------| +| `sendCommand(user, command)` | `ZoomVideoSDKErrors` | Send to one user | +| `sendCommandToAll(command)` | `ZoomVideoSDKErrors` | Broadcast to all | + +#### IVirtualBackgroundItem +| Method | Returns | +|--------|---------| +| `getImageFilePath()` | `const zchar_t*` | +| `getImageName()` | `const zchar_t*` | +| `getType()` | `ZoomVideoSDKVirtualBackgroundDataType` | +| `isSelected()` | `bool` | + +--- + +## Critical Timing Rules + +### ⚠️ CRITICAL: Subscribe in onUserVideoStatusChanged, NOT onUserJoin + +**WRONG** (causes Error 2 - Internal_Error): +```cpp +void onUserJoin(IZoomVideoSDKUserHelper* helper, IVideoSDKVector* userList) { + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + // ERROR: Video may not be ready yet! + user->GetVideoCanvas()->subscribeWithView(hwnd, aspect, resolution); + } +} +``` + +**CORRECT**: +```cpp +void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* helper, + IVideoSDKVector* userList) { + IZoomVideoSDKUser* myself = sdk->getSessionInfo()->getMyself(); + + for (int i = 0; i < userList->GetCount(); i++) { + IZoomVideoSDKUser* user = userList->GetItem(i); + if (user == myself) continue; // Skip self + + // Check if video is actually on + IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); + if (pipe) { + ZoomVideoSDKVideoStatus status = pipe->getVideoStatus(); + if (status.isOn) { + // NOW it's safe to subscribe + IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); + canvas->subscribeWithView(hwnd, aspect, resolution); + } + } + } +} +``` + +### Video Status Structure + +```cpp +struct ZoomVideoSDKVideoStatus { + bool isOn; // true = video is transmitting + bool hasSource; // true = camera is available +}; +``` + +- `isOn == true` → Safe to subscribe +- `isOn == false` → Unsubscribe or skip +- `hasSource == false` → User has no camera + +### Subscribe Fail Reasons (onVideoCanvasSubscribeFail) + +```cpp +enum ZoomVideoSDKSubscribeFailReason { + ZoomVideoSDKSubscribeFailReason_None = 0, + ZoomVideoSDKSubscribeFailReason_HasSubscribe1080POr720P = 1, + ZoomVideoSDKSubscribeFailReason_HasSubscribeTwo720P = 2, + ZoomVideoSDKSubscribeFailReason_HasSubscribeExceededLimit = 3, + ZoomVideoSDKSubscribeFailReason_HasSubscribeTwoShare = 4, + ZoomVideoSDKSubscribeFailReason_HasSubscribeVideo1080POr720PAndOneShare = 5, + ZoomVideoSDKSubscribeFailReason_TooFrequentCall = 6 +}; +``` + +**Handling TooFrequentCall**: Add `Sleep(200)` between subscribe calls. + +### SDK Error Codes Quick Reference + +| Code | Name | Common Cause | +|------|------|--------------| +| 0 | Success | - | +| 1 | Wrong_Usage | Calling method in wrong state | +| 2 | Internal_Error | Video not ready, subscribe too early | +| 7 | Invalid_Parameter | NULL pointer, bad HWND | +| 8 | Call_Too_Frequently | Need Sleep() between calls | + +--- + +## Two Rendering Approaches + +| Approach | Interface | When to Use | +|----------|-----------|-------------| +| **Canvas API** | `IZoomVideoSDKCanvas::subscribeWithView(HWND)` | Standard apps, best quality | +| **Raw Data Pipe** | `IZoomVideoSDKRawDataPipe::subscribe(delegate)` | Custom processing, effects, recording | + +### Canvas API (Recommended) +```cpp +// SDK renders directly to your window - no YUV conversion needed +IZoomVideoSDKCanvas* canvas = user->GetVideoCanvas(); +canvas->subscribeWithView(hwnd, ZoomVideoSDKVideoAspect_PanAndScan, ZoomVideoSDKResolution_Auto); +``` + +### Raw Data Pipe (Advanced) +```cpp +// You receive YUV420 frames and must render them yourself +class MyRenderer : public IZoomVideoSDKRawDataPipeDelegate { + void onRawDataFrameReceived(YUVRawDataI420* data) override { + // Convert YUV to RGB, then render with GDI/DirectX/OpenGL + } +}; + +IZoomVideoSDKRawDataPipe* pipe = user->GetVideoPipe(); +pipe->subscribe(ZoomVideoSDKResolution_720P, myRenderer); +``` + +--- + +## Complete Class List + +### Core SDK + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDK` | Main singleton object - session creation, callbacks, features | +| `IZoomVideoSDKSession` | Session information interface | +| `IZoomVideoSDKDelegate` | Event callbacks for session events | +| `IZoomVideoSDKUser` | User object interface | +| `IZoomVideoSDKUserHelper` | User management helper | + +### Raw Data Interfaces + +| Class | Description | +|-------|-------------| +| `AudioRawData` | Audio raw data handler (PCM 16-bit) | +| `YUVRawDataI420` | YUV raw data handler (I420 format) | +| `YUVProcessDataI420` | YUV processing data | +| `IZoomVideoSDKRawDataPipe` | Video/share raw data pipe | +| `IZoomVideoSDKRawDataPipeDelegate` | Video/share raw data sink | +| `IYUVRawDataI420Converter` | I420 YUV converter | + +### Virtual Devices + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKVirtualAudioMic` | Virtual audio microphone for injection | +| `IZoomVideoSDKVirtualAudioSpeaker` | Virtual audio speaker | +| `IZoomVideoSDKVideoSource` | Video source for injection | +| `IZoomVideoSDKVideoSourcePreProcessor` | Video preprocessing | +| `IZoomVideoSDKShareSource` | Share source for injection | +| `IZoomVideoSDKSharePreprocessor` | Share preprocessing | + +### Senders + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKAudioSender` | Audio raw data sender | +| `IZoomVideoSDKVideoSender` | Video raw data sender | +| `IZoomVideoSDKShareSender` | Share raw data sender | +| `IZoomVideoSDKShareAudioSender` | Share audio sender | +| `IZoomVideoSDKShareAudioSource` | Share audio source | + +### Helpers + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKAudioHelper` | Audio controls | +| `IZoomVideoSDKVideoHelper` | Video/camera controls | +| `IZoomVideoSDKShareHelper` | Screen sharing | +| `IZoomVideoSDKChatHelper` | Chat messaging | +| `IZoomVideoSDKRecordingHelper` | Cloud recording | +| `IZoomVideoSDKLiveStreamHelper` | RTMP live streaming | +| `IZoomVideoSDKLiveTranscriptionHelper` | Live transcription | +| `IZoomVideoSDKPhoneHelper` | Phone dial-out | +| `IZoomVideoSDKCmdChannel` | Command channel | +| `IZoomVideoSDKCRCHelper` | CRC helper | +| `IZoomVideoSDKWhiteboardHelper` | Whiteboard | +| `IZoomVideoSDKAnnotationHelper` | Annotations | +| `IZoomVideoSDKNetworkConnectionHelper` | Network connection | +| `IZoomVideoSDKSubSessionHelper` | Subsession helper | +| `IZoomVideoSDKSubSessionManager` | Subsession manager | +| `IZoomVideoSDKRTMSHelper` | Real-time media streams | +| `IZoomVideoSDKIncomingLiveStreamHelper` | Incoming live stream | + +### Settings Helpers + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKAudioSettingHelper` | Audio settings | +| `IZoomVideoSDKVideoSettingHelper` | Video settings | +| `IZoomVideoSDKShareSettingHelper` | Share settings | +| `IZoomVideoSDKTestAudioDeviceHelper` | Audio device testing | + +### Devices + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKCameraDevice` | Camera device | +| `IZoomVideoSDKMicDevice` | Microphone device | +| `IZoomVideoSDKSpeakerDevice` | Speaker device | +| `IVirtualBackgroundItem` | Virtual background item | + +### Streaming + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKBroadcastStreamingController` | Broadcast controller | +| `IZoomVideoSDKBroadcastStreamingViewer` | Broadcast viewer | +| `IZoomVideoSDKBroadcastStreamingAudioCallback` | Broadcast audio callback | +| `IZoomVideoSDKBroadcastStreamingVideoCallback` | Broadcast video callback | + +### Session & Messages + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKChatMessage` | Chat message | +| `ILiveTranscriptionLanguage` | Transcription language | +| `ILiveTranscriptionMessageInfo` | Transcription message | +| `IZoomVideoSDKSessionDialInNumberInfo` | Dial-in info | +| `IZoomVideoSDKPhoneSupportCountryInfo` | Phone country info | + +### Subsessions + +| Class | Description | +|-------|-------------| +| `ISubSessionKit` | Subsession kit | +| `ISubSessionUser` | Subsession user | +| `ISubSessionUserHelpRequestHandler` | Help request handler | +| `IZoomVideoSDKSubSessionParticipant` | Subsession participant | + +### File Transfer + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKFileTransferBaseInfo` | File transfer base info | +| `IZoomVideoSDKSendFile` | Send file interface | +| `IZoomVideoSDKReceiveFile` | Receive file interface | + +### Handlers + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKPasswordHandler` | Password handler | +| `IZoomVideoSDKRecordingConsentHandler` | Recording consent | +| `IZoomVideoSDKCameraControlRequestHandler` | Camera control requests | +| `IZoomVideoSDKRemoteCameraControlHelper` | Remote camera control | +| `IZoomVideoSDKProxySettingHandler` | Proxy settings | +| `IZoomVideoSDKSSLCertificateInfo` | SSL certificate info | + +### Canvas & Actions + +| Class | Description | +|-------|-------------| +| `IZoomVideoSDKCanvas` | Video/share canvas | +| `IZoomVideoSDKShareAction` | Share action | +| `IMonitorListBuilder` | Monitor list builder | + +### Utilities + +| Class | Description | +|-------|-------------| +| `IVideoSDKVector` | SDK vector collection | + +--- + +## Structures + +### Initialization + +```cpp +struct ZoomVideoSDKInitParams { + const zchar_t* domain; // Required: L"https://zoom.us" + bool enableLog; // Enable logging + const zchar_t* logFilePrefix; // Log file prefix + ZoomVideoSDKRawDataMemoryMode videoRawDataMemoryMode; + ZoomVideoSDKRawDataMemoryMode shareRawDataMemoryMode; + ZoomVideoSDKRawDataMemoryMode audioRawDataMemoryMode; + bool enableIndirectRawdata; // Indirect raw data access + ZoomVideoSDKExtendParams* extendParams; // Extended parameters +}; + +struct ZoomVideoSDKExtendParams { + const zchar_t* speakerTestFilePath; + // Additional extended parameters +}; +``` + +### Session Context + +```cpp +struct ZoomVideoSDKSessionContext { + const zchar_t* sessionName; // Required + const zchar_t* sessionPassword; // Optional + const zchar_t* userName; // Required + const zchar_t* token; // Required: JWT token + unsigned int sessionIdleTimeoutMins; // 0 = never timeout, default 40 + bool autoLoadMutliStream; // Auto-load multiple streams + + ZoomVideoSDKVideoOption videoOption; + ZoomVideoSDKAudioOption audioOption; + + IZoomVideoSDKVideoSource* externalVideoSource; + IZoomVideoSDKVirtualAudioMic* virtualAudioMic; + IZoomVideoSDKVirtualAudioSpeaker* virtualAudioSpeaker; + IZoomVideoSDKVideoSourcePreProcessor* preProcessor; +}; + +struct ZoomVideoSDKVideoOption { + bool localVideoOn; // Start with video on +}; + +struct ZoomVideoSDKAudioOption { + bool connect; // Connect to audio + bool mute; // Start muted +}; +``` + +### Statistics + +```cpp +struct ZoomVideoSDKSessionAudioStatisticInfo { + int frequency; + int latency; + int Jitter; + float packetLossAvg; + float packetLossMax; +}; + +struct ZoomVideoSDKSessionASVStatisticInfo { + int frame_width; + int frame_height; + int fps; + int latency; + int Jitter; + float packetLossAvg; + float packetLossMax; +}; + +// Aliases +typedef ZoomVideoSDKSessionASVStatisticInfo _SessionASVStatisticInfo; +typedef ZoomVideoSDKSessionAudioStatisticInfo _SessionAudioStatisticInfo; + +struct ZoomVideoSDKVideoStatisticInfo { + int width; + int height; + int fps; + int bps; +}; + +struct ZoomVideoSDKShareStatisticInfo { + int width; + int height; + int fps; + int bps; +}; +``` + +### Video/Audio Status + +```cpp +struct ZoomVideoSDKVideoStatus { + bool isOn; + bool hasSource; +}; + +struct ZoomVideoSDKAudioStatus { + bool isMuted; + bool isAudioConnected; + ZoomVideoSDKAudioType audioType; +}; + +struct VideoSourceCapability { + unsigned int width; + unsigned int height; + unsigned int frame; // FPS +}; +``` + +### Live Streaming + +```cpp +struct ZoomVideoSDKLiveStreamParams { + const zchar_t* streamUrl; // RTMP URL + const zchar_t* streamKey; // Stream key + const zchar_t* broadcastUrl; // Broadcast URL +}; + +struct ZoomVideoSDKLiveStreamSetting { + // Live stream settings +}; + +struct IncomingLiveStreamStatus { + // Incoming stream status +}; +``` + +### Share + +```cpp +struct ZoomVideoSDKShareOption { + bool isWithDeviceAudio; + bool isOptimizeForSharedVideo; +}; + +struct ZoomVideoSDKShareCursorData { + int x; + int y; + // Cursor information +}; + +struct ZoomVideoSDKSharePreprocessParam { + // Preprocessing parameters +}; +``` + +### File Transfer + +```cpp +struct FileTransferProgress { + unsigned long long transferredSize; + unsigned long long totalSize; + float percentage; +}; + +struct ZoomVideoSDKFileStatus { + // File transfer status +}; +``` + +### Misc + +```cpp +struct ZoomVideoSDKViewSize { + int width; + int height; +}; + +struct tagVideoPreferenceSetting { + // Video preference settings +}; + +struct tagProxySettings { + // Proxy configuration +}; + +struct InvitePhoneUserInfo { + const zchar_t* countryCode; + const zchar_t* phoneNumber; + const zchar_t* displayName; +}; + +struct ZoomVideoSDKSteamingJoinContext { + // Streaming join context +}; +``` + +--- + +## IZoomVideoSDK (Main Interface) + +```cpp +class IZoomVideoSDK { +public: + // Lifecycle + virtual ZoomVideoSDKErrors initialize(ZoomVideoSDKInitParams& params) = 0; + virtual ZoomVideoSDKErrors cleanup() = 0; + + // Session management + virtual IZoomVideoSDKSession* joinSession(ZoomVideoSDKSessionContext& params) = 0; + virtual ZoomVideoSDKErrors leaveSession(bool end) = 0; + virtual IZoomVideoSDKSession* getSessionInfo() = 0; + virtual bool isInSession() = 0; + + // Listeners + virtual void addListener(IZoomVideoSDKDelegate* listener) = 0; + virtual void removeListener(IZoomVideoSDKDelegate* listener) = 0; + + // Helpers + virtual IZoomVideoSDKAudioHelper* getAudioHelper() = 0; + virtual IZoomVideoSDKVideoHelper* getVideoHelper() = 0; + virtual IZoomVideoSDKUserHelper* getUserHelper() = 0; + virtual IZoomVideoSDKShareHelper* getShareHelper() = 0; + virtual IZoomVideoSDKRecordingHelper* getRecordingHelper() = 0; + virtual IZoomVideoSDKLiveStreamHelper* getLiveStreamHelper() = 0; + virtual IZoomVideoSDKChatHelper* getChatHelper() = 0; + virtual IZoomVideoSDKCmdChannel* getCmdChannel() = 0; + virtual IZoomVideoSDKPhoneHelper* getPhoneHelper() = 0; + virtual IZoomVideoSDKLiveTranscriptionHelper* getLiveTranscriptionHelper() = 0; + virtual IZoomVideoSDKCRCHelper* getCRCHelper() = 0; + virtual IZoomVideoSDKWhiteboardHelper* getWhiteboardHelper() = 0; + virtual IZoomVideoSDKSubSessionHelper* getSubSessionHelper() = 0; + virtual IZoomVideoSDKRTMSHelper* getRealTimeMediaStreamsHelper() = 0; + virtual IZoomVideoSDKIncomingLiveStreamHelper* getIncomingLiveStreamHelper() = 0; + + // Settings + virtual IZoomVideoSDKAudioSettingHelper* getAudioSettingHelper() = 0; + virtual IZoomVideoSDKVideoSettingHelper* getVideoSettingHelper() = 0; + virtual IZoomVideoSDKShareSettingHelper* getShareSettingHelper() = 0; + virtual IZoomVideoSDKTestAudioDeviceHelper* GetAudioDeviceTestHelper() = 0; + virtual IZoomVideoSDKNetworkConnectionHelper* getNetworkConnectionHelper() = 0; + + // Utilities + virtual const zchar_t* getSDKVersion() = 0; + virtual const zchar_t* exportLog() = 0; + virtual ZoomVideoSDKErrors cleanAllExportedLogs() = 0; +}; +``` + +### Factory Functions + +```cpp +// Create SDK object +IZoomVideoSDK* CreateZoomVideoSDKObj(); + +// Destroy SDK object +void DestroyZoomVideoSDKObj(); +``` + +--- + +## IZoomVideoSDKDelegate (Callbacks) + +```cpp +class IZoomVideoSDKDelegate { +public: + // Session callbacks + virtual void onSessionJoin() = 0; + virtual void onSessionLeave() = 0; + virtual void onSessionLeave(ZoomVideoSDKSessionLeaveReason eReason) = 0; + virtual void onError(ZoomVideoSDKErrors errorCode, int detailErrorCode) = 0; + virtual void onSessionNeedPassword(IZoomVideoSDKPasswordHandler* handler) = 0; + virtual void onSessionPasswordWrong(IZoomVideoSDKPasswordHandler* handler) = 0; + + // User callbacks + virtual void onUserJoin(IZoomVideoSDKUserHelper* pUserHelper, + IVideoSDKVector* userList) = 0; + virtual void onUserLeave(IZoomVideoSDKUserHelper* pUserHelper, + IVideoSDKVector* userList) = 0; + virtual void onUserHostChanged(IZoomVideoSDKUserHelper* pUserHelper, + IZoomVideoSDKUser* pUser) = 0; + virtual void onUserManagerChanged(IZoomVideoSDKUser* pUser) = 0; + virtual void onUserNameChanged(IZoomVideoSDKUser* pUser) = 0; + + // Video callbacks + virtual void onUserVideoStatusChanged(IZoomVideoSDKVideoHelper* pVideoHelper, + IVideoSDKVector* userList) = 0; + virtual void onSpotlightVideoChanged(IZoomVideoSDKVideoHelper* pVideoHelper, + IVideoSDKVector* userList) = 0; + + // Audio callbacks + virtual void onUserAudioStatusChanged(IZoomVideoSDKAudioHelper* pAudioHelper, + IVideoSDKVector* userList) = 0; + virtual void onUserActiveAudioChanged(IZoomVideoSDKAudioHelper* pAudioHelper, + IVideoSDKVector* list) = 0; + + // Raw audio callbacks + virtual void onMixedAudioRawDataReceived(AudioRawData* data_) = 0; + virtual void onOneWayAudioRawDataReceived(AudioRawData* data_, + IZoomVideoSDKUser* pUser) = 0; + virtual void onSharedAudioRawDataReceived(AudioRawData* data_) = 0; + + // Share callbacks + virtual void onUserShareStatusChanged(IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) = 0; + virtual void onShareContentChanged(IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) = 0; + virtual void onFailedToStartShare(IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser) = 0; + virtual void onShareContentSizeChanged(IZoomVideoSDKShareHelper* pShareHelper, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) = 0; + + // Chat callbacks + virtual void onChatNewMessageNotify(IZoomVideoSDKChatHelper* pChatHelper, + IZoomVideoSDKChatMessage* messageItem) = 0; + virtual void onChatMsgDeleteNotification(IZoomVideoSDKChatHelper* pChatHelper, + const zchar_t* msgID, + ZoomVideoSDKChatMessageDeleteType deleteBy) = 0; + virtual void onChatPrivilegeChanged(IZoomVideoSDKChatHelper* pChatHelper, + ZoomVideoSDKChatPrivilegeType privilege) = 0; + + // Command channel callbacks + virtual void onCommandReceived(IZoomVideoSDKUser* sender, const zchar_t* strCmd) = 0; + virtual void onCommandChannelConnectResult(bool isSuccess) = 0; + + // Recording callbacks + virtual void onCloudRecordingStatus(RecordingStatus status, + IZoomVideoSDKRecordingConsentHandler* pHandler) = 0; + virtual void onUserRecordingConsent(IZoomVideoSDKUser* pUser) = 0; + virtual void onHostAskUnmute() = 0; + + // Live stream callbacks + virtual void onLiveStreamStatusChanged(IZoomVideoSDKLiveStreamHelper* pLiveStreamHelper, + ZoomVideoSDKLiveStreamStatus status) = 0; + + // Live transcription callbacks + virtual void onLiveTranscriptionStatus(ZoomVideoSDKLiveTranscriptionStatus status) = 0; + virtual void onOriginalLanguageMsgReceived(ILiveTranscriptionMessageInfo* messageInfo) = 0; + virtual void onLiveTranscriptionMsgInfoReceived(ILiveTranscriptionMessageInfo* messageInfo) = 0; + virtual void onLiveTranscriptionMsgError(ILiveTranscriptionLanguage* spokenLanguage, + ILiveTranscriptionLanguage* transcriptLanguage) = 0; + + // Phone callbacks + virtual void onInviteByPhoneStatus(PhoneStatus status, PhoneFailedReason reason) = 0; + virtual void onCalloutJoinSuccess(IZoomVideoSDKUser* pUser, const zchar_t* phoneNumber) = 0; + + // Camera control callbacks + virtual void onCameraControlRequestResult(IZoomVideoSDKUser* pUser, bool isApproved) = 0; + virtual void onCameraControlRequestReceived(IZoomVideoSDKUser* pUser, + ZoomVideoSDKCameraControlRequestType requestType, + IZoomVideoSDKCameraControlRequestHandler* handler) = 0; + + // Remote control callbacks + virtual void onRemoteControlStatus(IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction, + ZoomVideoSDKRemoteControlStatus status) = 0; + virtual void onRemoteControlRequestReceived(IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction, + IZoomVideoSDKRemoteControlRequestHandler* handler) = 0; + virtual void onRemoteControlServiceInstallResult(bool bSuccess) = 0; + + // Multi-camera callbacks + virtual void onMultiCameraStreamStatusChanged(ZoomVideoSDKMultiCameraStreamStatus status, + IZoomVideoSDKUser* pUser, + IZoomVideoSDKRawDataPipe* pVideoPipe) = 0; + + // Device callbacks + virtual void onMicSpeakerVolumeChanged(unsigned int micVolume, + unsigned int speakerVolume) = 0; + virtual void onAudioDeviceStatusChanged(ZoomVideoSDKAudioDeviceType type, + ZoomVideoSDKAudioDeviceStatus status) = 0; + virtual void onTestMicStatusChanged(ZoomVideoSDK_TESTMIC_STATUS status) = 0; + virtual void onSelectedAudioDeviceChanged() = 0; + virtual void onCameraListChanged() = 0; + + // Network callbacks + virtual void onUserVideoNetworkStatusChanged(ZoomVideoSDKNetworkStatus status, + IZoomVideoSDKUser* pUser) = 0; + virtual void onProxyDetectComplete() = 0; + virtual void onProxySettingNotification(IZoomVideoSDKProxySettingHandler* handler) = 0; + virtual void onSSLCertVerifiedFailNotification(IZoomVideoSDKSSLCertificateInfo* info) = 0; + + // CRC callbacks + virtual void onCallCRCDeviceStatusChanged(ZoomVideoSDKCRCCallStatus status) = 0; + + // Canvas callbacks + virtual void onVideoCanvasSubscribeFail(ZoomVideoSDKSubscribeFailReason fail_reason, + IZoomVideoSDKUser* pUser, void* handle) = 0; + virtual void onShareCanvasSubscribeFail(IZoomVideoSDKUser* pUser, void* handle, + IZoomVideoSDKShareAction* pShareAction) = 0; + + // Annotation callbacks + virtual void onAnnotationHelperCleanUp(IZoomVideoSDKAnnotationHelper* helper) = 0; + virtual void onAnnotationPrivilegeChange(IZoomVideoSDKUser* pUser, + IZoomVideoSDKShareAction* pShareAction) = 0; + virtual void onAnnotationHelperActived(void* handle) = 0; + + // File transfer callbacks + virtual void onSendFileStatus(IZoomVideoSDKSendFile* file, + const FileTransferStatus& status) = 0; + virtual void onReceiveFileStatus(IZoomVideoSDKReceiveFile* file, + const FileTransferStatus& status) = 0; + + // Misc callbacks + virtual void onVideoAlphaChannelStatusChanged(bool isAlphaModeOn) = 0; + + // Incoming live stream callbacks + virtual void onBindIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) = 0; + virtual void onUnbindIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) = 0; + virtual void onIncomingLiveStreamStatusResponse(bool bSuccess, + IVideoSDKVector* list) = 0; + virtual void onStartIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) = 0; + virtual void onStopIncomingLiveStreamResponse(bool bSuccess, const zchar_t* strStreamKeyID) = 0; +}; +``` + +--- + +## Raw Data Interfaces + +### AudioRawData + +```cpp +class AudioRawData { +public: + virtual char* GetBuffer() = 0; // PCM 16-bit buffer + virtual unsigned int GetBufferLen() = 0; // Buffer length in bytes + virtual unsigned int GetSampleRate() = 0;// Sample rate (Hz) + virtual unsigned int GetChannelNum() = 0;// Channels (1=mono, 2=stereo) + virtual unsigned long long GetTimeStamp() = 0; +}; +``` + +### YUVRawDataI420 + +```cpp +class YUVRawDataI420 { +public: + virtual char* GetYBuffer() = 0; + virtual char* GetUBuffer() = 0; + virtual char* GetVBuffer() = 0; + virtual char* GetAlphaBuffer() = 0; // Optional alpha channel + virtual char* GetBuffer() = 0; // Full YUV buffer + virtual unsigned int GetBufferLen() = 0; + virtual unsigned int GetStreamWidth() = 0; + virtual unsigned int GetStreamHeight() = 0; + virtual unsigned int GetRotation() = 0; // 0, 90, 180, 270 + virtual unsigned long long GetSourceID() = 0; + virtual unsigned long long GetTimeStamp() = 0; + virtual bool IsLimitedI420() = 0; + + // Reference counting + virtual bool CanAddRef() = 0; + virtual bool AddRef() = 0; + virtual int Release() = 0; +}; +``` + +### IZoomVideoSDKRawDataPipeDelegate + +```cpp +class IZoomVideoSDKRawDataPipeDelegate { +public: + virtual void onRawDataFrameReceived(YUVRawDataI420* data) = 0; + virtual void onRawDataStatusChanged(RawDataStatus status) = 0; +}; + +enum RawDataStatus { + RawData_On, + RawData_Off +}; +``` + +--- + +## Virtual Device Interfaces + +### IZoomVideoSDKVirtualAudioMic + +```cpp +class IZoomVideoSDKVirtualAudioMic { +public: + virtual void onMicInitialize(IZoomVideoSDKAudioSender* sender) = 0; + virtual void onMicStartSend() = 0; + virtual void onMicStopSend() = 0; + virtual void onMicUninitialized() = 0; +}; + +class IZoomVideoSDKAudioSender { +public: + virtual ZoomVideoSDKErrors Send(char* data, + unsigned int dataLength, + int sampleRate) = 0; +}; +``` + +### IZoomVideoSDKVirtualAudioSpeaker + +```cpp +class IZoomVideoSDKVirtualAudioSpeaker { +public: + virtual void onVirtualSpeakerMixedAudioReceived(AudioRawData* data_) = 0; + virtual void onVirtualSpeakerOneWayAudioReceived(AudioRawData* data_, + IZoomVideoSDKUser* pUser) = 0; + virtual void onVirtualSpeakerSharedAudioReceived(AudioRawData* data_) = 0; +}; +``` + +### IZoomVideoSDKVideoSource + +```cpp +class IZoomVideoSDKVideoSource { +public: + virtual void onInitialize(IZoomVideoSDKVideoSender* sender, + IVideoSDKVector* supportCapList, + VideoSourceCapability& suggestCap) = 0; + virtual void onPropertyChange(IVideoSDKVector* supportCapList, + VideoSourceCapability suggestCap) = 0; + virtual void onStartSend() = 0; + virtual void onStopSend() = 0; + virtual void onUninitialized() = 0; +}; + +class IZoomVideoSDKVideoSender { +public: + virtual ZoomVideoSDKErrors sendVideoFrame(char* frameBuffer, + int width, int height, + int frameLength, + int rotation) = 0; + // Alternative with Y, U, V planes + virtual ZoomVideoSDKErrors sendVideoFrame(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height, + int frameLength, + int rotation) = 0; +}; +``` + +### IZoomVideoSDKShareSource + +```cpp +class IZoomVideoSDKShareSource { +public: + virtual void onShareSendStarted(IZoomVideoSDKShareSender* pSender) = 0; + virtual void onShareSendStopped() = 0; +}; + +class IZoomVideoSDKShareSender { +public: + virtual ZoomVideoSDKErrors sendShareFrame(char* frameBuffer, + int width, int height, + int frameLength) = 0; + virtual ZoomVideoSDKErrors sendShareFrame(char* yBuffer, char* uBuffer, char* vBuffer, + int width, int height, + int frameLength, + int rotation) = 0; +}; +``` + +--- + +## Enumerations + +### ZoomVideoSDKErrors + +```cpp +enum ZoomVideoSDKErrors { + ZoomVideoSDKErrors_Success = 0, + ZoomVideoSDKErrors_Wrong_Usage = 1, + ZoomVideoSDKErrors_Internal_Error = 2, + ZoomVideoSDKErrors_Uninitialize = 3, + ZoomVideoSDKErrors_Memory_Error = 4, + ZoomVideoSDKErrors_Load_Module_Error = 5, + ZoomVideoSDKErrors_UnLoad_Module_Error = 6, + ZoomVideoSDKErrors_Invalid_Parameter = 7, + ZoomVideoSDKErrors_Call_Too_Frequently = 8, + ZoomVideoSDKErrors_No_Impl = 9, + ZoomVideoSDKErrors_Dont_Support_Feature = 10, + ZoomVideoSDKErrors_Unknown = 100, + + // Auth errors (1000+) + ZoomVideoSDKErrors_Auth_Error = 1001, + ZoomVideoSDKErrors_Auth_Empty_Key_or_Secret = 1002, + ZoomVideoSDKErrors_Auth_Wrong_Key_or_Secret = 1003, + ZoomVideoSDKErrors_Auth_DoesNot_Support_SDK = 1004, + ZoomVideoSDKErrors_Auth_Disable_SDK = 1005, + + // Session errors (3000+) + ZoomVideoSDKErrors_Session_Join_Failed = 3001, + ZoomVideoSDKErrors_Session_No_Rights = 3002, + ZoomVideoSDKErrors_Session_Already_In_Progress = 3003, + ZoomVideoSDKErrors_Session_Dont_Support_SessionType = 3004, + ZoomVideoSDKErrors_Session_Reconnecting = 3005, + ZoomVideoSDKErrors_Session_Disconnecting = 3006, + ZoomVideoSDKErrors_Session_Not_Started = 3007, + ZoomVideoSDKErrors_Session_Need_Password = 3008, + ZoomVideoSDKErrors_Session_Password_Wrong = 3009, + ZoomVideoSDKErrors_Session_Remote_DB_Error = 3010, + ZoomVideoSDKErrors_Session_Invalid_Param = 3011, + + // Audio/Video errors + ZoomVideoSDKErrors_Session_Audio_Error = 4001, + ZoomVideoSDKErrors_Session_Audio_No_Microphone = 4002, + ZoomVideoSDKErrors_Session_Video_Error = 5001, + ZoomVideoSDKErrors_Session_Video_Device_Error = 5002, + + // Share errors + ZoomVideoSDKErrors_Session_Share_Error = 6001, + ZoomVideoSDKErrors_Session_Share_Module_Not_Ready = 6002, + ZoomVideoSDKErrors_Session_Share_You_Are_Not_Sharing = 6003, + ZoomVideoSDKErrors_Session_Share_Type_Is_Not_Support = 6004, + ZoomVideoSDKErrors_Session_Share_Internal_Error = 6005, + + ZoomVideoSDKErrors_Dont_Support_Multi_Stream_Video_User = 7001, +}; +``` + +### Resolution Options + +```cpp +enum ZoomVideoSDKResolution { + ZoomVideoSDKResolution_90P, + ZoomVideoSDKResolution_180P, + ZoomVideoSDKResolution_360P, + ZoomVideoSDKResolution_720P, + ZoomVideoSDKResolution_1080P +}; +``` + +### Recording Status + +```cpp +enum RecordingStatus { + Recording_Start, + Recording_Stop, + Recording_Pause, + Recording_Connecting, + Recording_DiskFull +}; +``` + +### Memory Mode + +```cpp +enum ZoomVideoSDKRawDataMemoryMode { + ZoomVideoSDKRawDataMemoryModeStack, + ZoomVideoSDKRawDataMemoryModeHeap +}; +``` + +### Session Leave Reason + +```cpp +enum ZoomVideoSDKSessionLeaveReason { + ZoomVideoSDKSessionLeaveReason_EndByHost, + ZoomVideoSDKSessionLeaveReason_HostEndForAll, + ZoomVideoSDKSessionLeaveReason_KickedByHost, + ZoomVideoSDKSessionLeaveReason_Timeout, + ZoomVideoSDKSessionLeaveReason_SessionIdleTimeout, + ZoomVideoSDKSessionLeaveReason_Default +}; +``` + +### Live Transcription Status + +```cpp +enum ZoomVideoSDKLiveTranscriptionStatus { + ZoomVideoSDKLiveTranscription_Status_Stop, + ZoomVideoSDKLiveTranscription_Status_Start +}; +``` + +### Network Status + +```cpp +enum ZoomVideoSDKNetworkStatus { + ZoomVideoSDKNetworkStatus_Good, + ZoomVideoSDKNetworkStatus_Normal, + ZoomVideoSDKNetworkStatus_Poor, + ZoomVideoSDKNetworkStatus_Bad, + ZoomVideoSDKNetworkStatus_Connecting +}; +``` + +--- + +## Essential Headers + +```cpp +// Core headers +#include "zoom_video_sdk_api.h" // CreateZoomVideoSDKObj, DestroyZoomVideoSDKObj +#include "zoom_video_sdk_interface.h" // IZoomVideoSDK +#include "zoom_video_sdk_delegate_interface.h" // IZoomVideoSDKDelegate +#include "zoom_video_sdk_def.h" // Structures, enums +#include "zoom_video_sdk_platform.h" // Platform definitions +#include "zoom_sdk_raw_data_def.h" // Raw data types + +// Helper headers +#include "helpers/zoom_video_sdk_user_helper_interface.h" +#include "helpers/zoom_video_sdk_audio_helper_interface.h" +#include "helpers/zoom_video_sdk_video_helper_interface.h" +#include "helpers/zoom_video_sdk_share_helper_interface.h" +#include "helpers/zoom_video_sdk_chat_helper_interface.h" +#include "helpers/zoom_video_sdk_recording_helper_interface.h" +#include "helpers/zoom_video_sdk_livestream_helper_interface.h" +#include "helpers/zoom_video_sdk_livetranscription_helper_interface.h" +#include "helpers/zoom_video_sdk_cmd_channel_interface.h" +#include "helpers/zoom_video_sdk_phone_helper_interface.h" +#include "helpers/zoom_video_sdk_audio_send_rawdata_interface.h" +#include "helpers/zoom_video_sdk_video_source_helper_interface.h" + +// Message interfaces +#include "zoom_video_sdk_chat_message_interface.h" +#include "zoom_video_sdk_session_info_interface.h" + +// Namespace +using namespace ZOOM_VIDEO_SDK_NAMESPACE; +// Or use macro +USING_ZOOM_VIDEO_SDK_NAMESPACE +``` + +--- + +## Additional Resources + +- **Official Docs**: https://developers.zoom.us/docs/video-sdk/windows/ +- **API Reference**: https://marketplacefront.zoom.us/sdk/custom/windows/ +- **Sample Code**: https://github.com/zoom/videosdk-windows-rawdata-sample +- **Dev Forum**: https://devforum.zoom.us +- **C# .NET Sample**: https://github.com/zoom/videosdk-windows-dotnet-quickstart diff --git a/plugins/zoom-developers/skills/video-sdk/windows/troubleshooting/build-errors.md b/plugins/zoom-developers/skills/video-sdk/windows/troubleshooting/build-errors.md new file mode 100644 index 00000000..3ed96007 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/troubleshooting/build-errors.md @@ -0,0 +1,321 @@ +# Build Errors Guide + +Common build errors when working with the Zoom Video SDK for Windows and how to fix them. + +--- + +## Include Order (CRITICAL) + +SDK headers have dependency issues. **Include in this exact order**: + +```cpp +#include // MUST be first +#include // MUST be second (for uint32_t) + +// Standard library +#include +#include +#include +#include +#include +#include + +// SDK headers +#include "zoom_video_sdk_api.h" +#include "zoom_video_sdk_interface.h" +#include "zoom_video_sdk_delegate_interface.h" +#include "zoom_sdk_raw_data_def.h" // For YUVRawDataI420 +``` + +--- + +## Error: 'uint32_t' is not a member of 'std' + +### Symptom + +``` +error C2039: 'uint32_t': is not a member of 'std' +``` + +### Cause + +SDK headers use `uint32_t` without including ``. + +### Fix + +Add `#include ` **before** SDK headers: + +```cpp +#include +#include // Add this! +#include "zoom_video_sdk_api.h" +``` + +--- + +## Error: 'YUVRawDataI420' undeclared identifier + +### Symptom + +``` +error C2065: 'YUVRawDataI420': undeclared identifier +``` + +### Cause + +The class is only forward-declared in some headers. + +### Fix + +Include the raw data definition header: + +```cpp +#include "zoom_sdk_raw_data_def.h" +``` + +--- + +## Error: Cannot open include file 'json/json.h' + +### Symptom + +``` +fatal error C1083: Cannot open include file: 'json/json.h': No such file or directory +``` + +### Cause + +jsoncpp library not installed or not in include path. + +### Fix + +Install via vcpkg and configure include paths: + +```powershell +# Install vcpkg +git clone https://github.com/Microsoft/vcpkg.git C:\vcpkg +cd C:\vcpkg +.\bootstrap-vcpkg.bat +.\vcpkg integrate install + +# Install jsoncpp +.\vcpkg install jsoncpp:x64-windows +``` + +Then add to **Additional Include Directories**: +``` +C:\vcpkg\packages\jsoncpp_x64-windows\include +``` + +--- + +## Error: unresolved external symbol 'CreateZoomVideoSDKObj' + +### Symptom + +``` +error LNK2019: unresolved external symbol "CreateZoomVideoSDKObj" +``` + +### Cause + +SDK library not linked. + +### Fix + +1. Add to **Additional Library Directories**: + ``` + $(SolutionDir)SDK\lib + ``` + +2. Add to **Additional Dependencies**: + ``` + sdk.lib + ``` + +--- + +## Error: sdk.dll not found + +### Symptom + +``` +The code execution cannot proceed because sdk.dll was not found. +``` + +### Cause + +SDK DLLs not in output directory. + +### Fix + +Add Post-Build Event: + +```cmd +xcopy /Y /D "$(SolutionDir)SDK\bin\*.*" "$(OutDir)" +``` + +Or manually copy all DLLs from `SDK\bin\` to your output directory. + +--- + +## Error: Abstract class cannot be instantiated + +### Symptom + +``` +error C2259: 'MyDelegate': cannot instantiate abstract class +``` + +### Cause + +Not all pure virtual methods implemented in your delegate class. + +### Fix + +Implement ALL 80+ methods in `IZoomVideoSDKDelegate`. Even unused methods need empty implementations: + +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { +public: + // Implement all methods, even if empty + void onSessionJoin() override { /* your code */ } + void onSessionLeave() override {} + void onError(ZoomVideoSDKErrors, int) override {} + void onUserJoin(IZoomVideoSDKUserHelper*, IVideoSDKVector*) override {} + // ... all 80+ methods +}; +``` + +See [Delegate Methods](../references/delegate-methods.md) for complete list. + +--- + +## Error: 'IZoomVideoSDK' is undefined + +### Symptom + +``` +error C2027: use of undefined type 'ZOOMVIDEOSDK_NAMESPACE::IZoomVideoSDK' +``` + +### Cause + +Missing include or namespace. + +### Fix + +```cpp +#include "zoom_video_sdk_interface.h" + +// Use namespace +using namespace ZOOMVIDEOSDK_NAMESPACE; +// Or +USING_ZOOM_VIDEO_SDK_NAMESPACE +``` + +--- + +## Visual Studio Project Configuration + +### Include Directories + +**C/C++ → General → Additional Include Directories:** + +``` +$(SolutionDir)SDK\h +$(SolutionDir)SDK\h\helpers +C:\vcpkg\packages\jsoncpp_x64-windows\include +``` + +### Library Directories + +**Linker → General → Additional Library Directories:** + +``` +$(SolutionDir)SDK\lib +C:\vcpkg\packages\jsoncpp_x64-windows\lib +``` + +### Additional Dependencies + +**Linker → Input → Additional Dependencies:** + +``` +sdk.lib +jsoncpp.lib +``` + +### Post-Build Event + +**Build Events → Post-Build Event → Command Line:** + +```cmd +xcopy /Y /D "$(SolutionDir)SDK\bin\*.*" "$(OutDir)" +xcopy /Y /D "$(ProjectDir)config.json" "$(OutDir)" +``` + +### Runtime Library + +**C/C++ → Code Generation → Runtime Library:** + +- Debug: Multi-threaded Debug DLL (/MDd) +- Release: Multi-threaded DLL (/MD) + +--- + +## MSBuild from Git Bash + +When building from Git Bash, use this pattern: + +```bash +MSYS_NO_PATHCONV=1 "/c/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin/MSBuild.exe" \ + YourProject.vcxproj \ + /p:Configuration=Release \ + /p:Platform=x64 \ + /t:Build \ + /verbosity:minimal +``` + +**Key points:** +- `MSYS_NO_PATHCONV=1` prevents path conversion issues +- Use full path to MSBuild.exe in quotes +- Escape or quote paths with spaces + +--- + +## SDK Directory Structure + +Expected structure: + +``` +YourProject/ +├── SDK/ +│ ├── bin/ # DLLs (copy to output) +│ │ ├── sdk.dll +│ │ ├── *.dll # Many other DLLs +│ ├── h/ # Headers +│ │ ├── zoom_video_sdk_api.h +│ │ ├── zoom_video_sdk_interface.h +│ │ ├── zoom_video_sdk_delegate_interface.h +│ │ ├── zoom_sdk_raw_data_def.h +│ │ └── helpers/ +│ │ └── *.h +│ └── lib/ # Libraries +│ └── sdk.lib +├── YourProject.cpp +├── YourProject.vcxproj +└── config.json +``` + +--- + +## Related Documentation + +- [Windows Reference](../references/windows-reference.md) - Full project setup +- [Delegate Methods](../references/delegate-methods.md) - All required callbacks +- [Common Issues](common-issues.md) - Runtime troubleshooting + +--- + +**TL;DR**: Include `` first, then ``, then SDK headers. Link `sdk.lib`. Copy DLLs to output. Implement all 80+ delegate methods. diff --git a/plugins/zoom-developers/skills/video-sdk/windows/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/video-sdk/windows/troubleshooting/common-issues.md new file mode 100644 index 00000000..7ec336fa --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/troubleshooting/common-issues.md @@ -0,0 +1,289 @@ +# Common Issues + +Quick diagnostic guide for Zoom Video SDK Windows issues. + +--- + +## Quick Diagnostic Checklist + +| Symptom | Likely Cause | Solution | +|---------|--------------|----------| +| Callbacks don't fire | Missing message loop | [Windows Message Loop](windows-message-loop.md) | +| Build errors | Include order / missing headers | [Build Errors](build-errors.md) | +| Abstract class error | Missing delegate methods | [Delegate Methods](../references/delegate-methods.md) | +| Video subscribe fails | Subscribing too early | Subscribe in `onUserVideoStatusChanged` | +| Error code 2 | Video not ready | Wait for `status.isOn == true` | +| Error code 8 | Too frequent calls | Add `Sleep(200)` between calls | +| DLL not found | DLLs not copied | Copy SDK `bin\` to output | + +--- + +## Error Codes (ZoomVideoSDKErrors) + +| Code | Name | Description | Solution | +|------|------|-------------|----------| +| 0 | Success | Operation succeeded | - | +| 1 | Wrong_Usage | API called incorrectly | Check API documentation | +| 2 | Internal_Error | SDK internal error | Often: video not ready yet | +| 3 | Uninitialize | SDK not initialized | Call `initialize()` first | +| 4 | Memory_Error | Memory allocation failed | Check system resources | +| 5 | Load_Module_Error | Failed to load DLL | Check DLLs are present | +| 6 | UnLoad_Module_Error | Failed to unload DLL | - | +| 7 | Invalid_Parameter | Bad parameter | Check NULL pointers, HWNDs | +| 8 | Call_Too_Frequently | API called too often | Add `Sleep(200)` | +| 9 | No_Impl | Feature not implemented | - | +| 10 | Dont_Support_Feature | Feature not supported | Check SDK version | +| 100 | Unknown | Unknown error | Check logs | + +### Auth Errors (1000+) + +| Code | Name | Solution | +|------|------|----------| +| 1001 | Auth_Error | Check JWT token | +| 1002 | Auth_Empty_Key_or_Secret | Provide credentials | +| 1003 | Auth_Wrong_Key_or_Secret | Verify credentials | +| 1004 | Auth_DoesNot_Support_SDK | Check SDK version | +| 1005 | Auth_Disable_SDK | Contact Zoom support | + +### Session Errors (3000+) + +| Code | Name | Solution | +|------|------|----------| +| 3001 | Session_Join_Failed | Check session name/token | +| 3002 | Session_No_Rights | Check permissions | +| 3003 | Session_Already_In_Progress | Leave first | +| 3005 | Session_Reconnecting | Wait for reconnect | +| 3008 | Session_Need_Password | Provide password | +| 3009 | Session_Password_Wrong | Check password | + +--- + +## Subscribe Fail Reasons + +When `onVideoCanvasSubscribeFail` fires: + +| Code | Reason | Solution | +|------|--------|----------| +| 0 | None | - | +| 1 | HasSubscribe1080POr720P | Already have HD subscription | +| 2 | HasSubscribeTwo720P | Max 2x 720p | +| 3 | HasSubscribeExceededLimit | Too many subscriptions | +| 4 | HasSubscribeTwoShare | Max 2 share subscriptions | +| 5 | HasSubscribeVideo1080POr720PAndOneShare | Combined limit | +| 6 | TooFrequentCall | Add `Sleep(200)` | + +--- + +## Common Issues by Category + +### Session Issues + +#### "joinSession returns success but onSessionJoin never fires" + +**Cause**: Missing Windows message loop + +**Fix**: +```cpp +while (!done) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Sleep(10); +} +``` + +#### "Session join fails with error 3001" + +**Cause**: Invalid JWT token or session name + +**Fix**: +1. Verify JWT token is valid and not expired +2. Check session name matches token +3. Ensure token has correct role (host/attendee) + +--- + +### Video Issues + +#### "subscribeWithView returns error 2" + +**Cause**: Video not ready when subscribing + +**Fix**: Subscribe in `onUserVideoStatusChanged`, not `onUserJoin`: + +```cpp +void onUserVideoStatusChanged(..., userList) override { + for (auto user : userList) { + if (user->GetVideoPipe()->getVideoStatus().isOn) { + user->GetVideoCanvas()->subscribeWithView(hwnd, ...); + } + } +} +``` + +#### "Video shows black screen" + +**Causes**: +1. Camera not started +2. Wrong HWND +3. Subscription failed silently + +**Fixes**: +```cpp +// 1. Start your camera +sdk->getVideoHelper()->startVideo(); + +// 2. Verify HWND is valid and visible +HWND hwnd = CreateWindow(...); +ShowWindow(hwnd, SW_SHOW); + +// 3. Check subscribe return value +ZoomVideoSDKErrors err = canvas->subscribeWithView(hwnd, ...); +if (err != ZoomVideoSDKErrors_Success) { + std::cout << "Subscribe failed: " << err << std::endl; +} +``` + +#### "onVideoCanvasSubscribeFail fires with reason 6" + +**Cause**: Calling subscribe too frequently + +**Fix**: Add delay between subscribe calls: +```cpp +canvas->subscribeWithView(hwnd1, ...); +Sleep(200); // Add delay +canvas->subscribeWithView(hwnd2, ...); +``` + +--- + +### Audio Issues + +#### "No audio after joining" + +**Cause**: Audio not connected + +**Fix**: Connect audio in `onSessionJoin`: +```cpp +void onSessionJoin() override { + sdk->getAudioHelper()->startAudio(); +} +``` + +Also ensure `audioOption.connect = false` during join: +```cpp +context.audioOption.connect = false; // Connect in callback +``` + +#### "Cannot mute/unmute" + +**Cause**: Trying to control audio before connected + +**Fix**: Wait for audio to be connected: +```cpp +void onUserAudioStatusChanged(...) override { + if (user->getAudioStatus().isAudioConnected) { + // Now safe to mute/unmute + } +} +``` + +--- + +### Build Issues + +#### "Cannot instantiate abstract class" + +**Cause**: Not all delegate methods implemented + +**Fix**: Implement ALL 80+ methods: +```cpp +class MyDelegate : public IZoomVideoSDKDelegate { + void onSessionJoin() override { } + void onSessionLeave() override { } + void onError(ZoomVideoSDKErrors, int) override { } + // ... all 80+ methods +}; +``` + +See [Delegate Methods](../references/delegate-methods.md). + +#### "Unresolved external symbol" + +**Cause**: sdk.lib not linked + +**Fix**: Add to linker settings: +- Additional Library Directories: `$(SolutionDir)SDK\lib` +- Additional Dependencies: `sdk.lib` + +--- + +### Runtime Issues + +#### "DLL not found" + +**Cause**: SDK DLLs not in executable directory + +**Fix**: Copy all DLLs from `SDK\bin\` to output directory. + +Post-Build Event: +```cmd +xcopy /Y /D "$(SolutionDir)SDK\bin\*.*" "$(OutDir)" +``` + +#### "Application crashes on exit" + +**Cause**: Cleanup order incorrect + +**Fix**: Proper cleanup sequence: +```cpp +void Cleanup() { + // 1. Leave session first + if (sdk->isInSession()) { + sdk->leaveSession(false); + } + + // 2. Wait for onSessionLeave callback + // (process messages while waiting) + + // 3. Cleanup SDK + sdk->cleanup(); + + // 4. Destroy SDK object + DestroyZoomVideoSDKObj(); +} +``` + +--- + +## Logging + +Enable SDK logging for debugging: + +```cpp +ZoomVideoSDKInitParams params; +params.enableLog = true; +params.logFilePrefix = L"zoom_video_sdk"; +``` + +Logs are written to: `%APPDATA%\ZoomVideoSDK\logs\` + +--- + +## Related Documentation + +- [Windows Message Loop](windows-message-loop.md) - Callback issues +- [Build Errors](build-errors.md) - Compile/link errors +- [Delegate Methods](../references/delegate-methods.md) - Required callbacks +- [Video Rendering](../examples/video-rendering.md) - Video subscription +- [API Reference](../references/windows-reference.md) - Error codes + +--- + +**Quick fixes:** +1. Callbacks don't fire → Add message loop +2. Error 2 → Subscribe in `onUserVideoStatusChanged` +3. Error 8 → Add `Sleep(200)` between calls +4. Abstract class → Implement all 80+ delegate methods diff --git a/plugins/zoom-developers/skills/video-sdk/windows/troubleshooting/windows-message-loop.md b/plugins/zoom-developers/skills/video-sdk/windows/troubleshooting/windows-message-loop.md new file mode 100644 index 00000000..7110b0ca --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/troubleshooting/windows-message-loop.md @@ -0,0 +1,254 @@ +# Windows Message Loop + +## The #1 Cause of "Callbacks Don't Fire" + +If your SDK callbacks aren't firing (onSessionJoin, onUserJoin, etc.), you're almost certainly missing the Windows message loop. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ SYMPTOM │ CAUSE │ +├─────────────────────────────────────────────────────────────────┤ +│ joinSession() returns success │ │ +│ but onSessionJoin() never │ Missing Windows message loop │ +│ fires │ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Why It's Required + +The Zoom Video SDK uses **Windows messaging** to dispatch callbacks. When an event occurs (user joins, video starts, etc.), the SDK posts a message to your thread's message queue. If you never process those messages, the callbacks never fire. + +``` +SDK Event Occurs + ↓ +SDK posts message to your thread's queue + ↓ +Your message loop calls PeekMessage/GetMessage + ↓ +Message is dispatched + ↓ +Your callback fires +``` + +**Without the message loop, step 3 never happens.** + +--- + +## The Fix + +### Console Application (No GUI) + +```cpp +int main() { + // Initialize SDK + IZoomVideoSDK* sdk = CreateZoomVideoSDKObj(); + sdk->initialize(params); + sdk->addListener(new MyDelegate()); + sdk->joinSession(context); + + // ═══════════════════════════════════════════════════════════════ + // CRITICAL: Windows message loop + // ═══════════════════════════════════════════════════════════════ + bool running = true; + while (running) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + running = false; + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Small sleep to avoid 100% CPU + Sleep(10); + } + + // Cleanup + sdk->leaveSession(false); + sdk->cleanup(); + + return 0; +} +``` + +### GUI Application (WinMain) + +GUI applications using standard WinMain already have a message loop, but make sure it's running: + +```cpp +int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, + LPSTR lpCmdLine, int nCmdShow) { + // Create window, initialize SDK, etc. + + // Standard message loop + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + return (int)msg.wParam; +} +``` + +--- + +## Common Mistakes + +### Mistake 1: No Message Loop At All + +```cpp +// WRONG - callbacks will never fire +int main() { + sdk->joinSession(context); + + // Waiting forever, but callbacks never fire + while (!g_joined) { + Sleep(100); // No message processing! + } +} +``` + +### Mistake 2: Message Loop in Wrong Thread + +```cpp +// WRONG - SDK callbacks are tied to the thread that called joinSession +void WorkerThread() { + sdk->joinSession(context); // Callbacks tied to this thread +} + +int main() { + std::thread worker(WorkerThread); + + // Message loop on main thread won't help worker thread's callbacks + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } +} +``` + +**Fix**: Run message loop on the same thread that calls SDK methods. + +### Mistake 3: Blocking the Message Loop + +```cpp +// WRONG - blocking call prevents message processing +void onSessionJoin() override { + // This blocks the message loop! + std::this_thread::sleep_for(std::chrono::seconds(10)); + + // Or this: + while (waiting) { } // Infinite loop blocks everything +} +``` + +**Fix**: Keep callbacks fast. Use async/threading for long operations. + +--- + +## Diagnostic Checklist + +If callbacks aren't firing: + +1. **Is there a message loop?** + - Look for `PeekMessage` or `GetMessage` in your code + - Must be on the same thread that calls `joinSession()` + +2. **Is the message loop running?** + - Add logging: `std::cout << "Processing messages..." << std::endl;` + - Should print continuously + +3. **Is the delegate registered?** + - `sdk->addListener(delegate)` must be called BEFORE `joinSession()` + +4. **Are all delegate methods implemented?** + - Missing pure virtual methods = compile error + - Wrong signature = callback not called + +5. **Is the SDK initialized?** + - Check return value of `initialize()` + +--- + +## Minimal Working Example + +```cpp +#include +#include +#include "zoom_video_sdk_api.h" +#include "zoom_video_sdk_interface.h" +#include "zoom_video_sdk_delegate_interface.h" + +USING_ZOOM_VIDEO_SDK_NAMESPACE + +bool g_joined = false; + +class TestDelegate : public IZoomVideoSDKDelegate { +public: + void onSessionJoin() override { + std::cout << "*** onSessionJoin fired! ***" << std::endl; + g_joined = true; + } + + void onError(ZoomVideoSDKErrors err, int detail) override { + std::cout << "*** onError: " << err << " ***" << std::endl; + } + + // ... implement all other methods as empty +}; + +int main() { + IZoomVideoSDK* sdk = CreateZoomVideoSDKObj(); + + ZoomVideoSDKInitParams params; + params.domain = L"https://zoom.us"; + sdk->initialize(params); + + sdk->addListener(new TestDelegate()); + + ZoomVideoSDKSessionContext ctx; + ctx.sessionName = L"test"; + ctx.userName = L"Bot"; + ctx.token = L"your-jwt"; + ctx.audioOption.connect = false; + + sdk->joinSession(ctx); + + std::cout << "Starting message loop..." << std::endl; + + // THE CRITICAL PART + while (!g_joined) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Sleep(10); + } + + std::cout << "Joined! Exiting..." << std::endl; + + sdk->leaveSession(false); + sdk->cleanup(); + + return 0; +} +``` + +--- + +## Related Documentation + +- [Session Join Pattern](../examples/session-join-pattern.md) - Complete working code +- [Common Issues](common-issues.md) - Other troubleshooting +- [SDK Architecture Pattern](../concepts/sdk-architecture-pattern.md) - Event-driven design + +--- + +**TL;DR**: Add `PeekMessage`/`TranslateMessage`/`DispatchMessage` loop on the same thread that calls `joinSession()`. This is NOT optional. diff --git a/plugins/zoom-developers/skills/video-sdk/windows/windows.md b/plugins/zoom-developers/skills/video-sdk/windows/windows.md new file mode 100644 index 00000000..b4605122 --- /dev/null +++ b/plugins/zoom-developers/skills/video-sdk/windows/windows.md @@ -0,0 +1,46 @@ +--- +name: video-sdk/windows +description: "Zoom Video SDK for Windows - C++ integration for video sessions, raw audio/video capture, screen sharing, recording, and real-time communication" +--- + +# Zoom Video SDK (Windows) + +Windows platform support for Zoom Video SDK. Build custom video applications with C++ or C#/.NET integration. + +For complete documentation, see **[SKILL.md](SKILL.md)** + +## Quick Links + +- **[Windows SDK Guide](SKILL.md)** - Complete Windows development guide +- **[API Reference](references/windows-reference.md)** - Complete API documentation +- **Official Docs**: https://developers.zoom.us/docs/video-sdk/windows/ + +## Features + +- Full video session control +- Raw audio/video capture (PCM, YUV I420) +- Raw media injection (virtual audio/video) +- Cloud recording & live streaming +- Multi-platform support (x64, x86, ARM64) + +## UI Framework Integration + +| Framework | Approach | Guide | +|-----------|----------|-------| +| **Win32** | Direct SDK + Canvas API | [Win32 Guide](examples/dotnet-winforms/guide.md#option-1-win32-native-c---direct-sdk) | +| **WinForms** | C++/CLI wrapper + Raw Data | [WinForms Guide](examples/dotnet-winforms/guide.md#option-2-winforms-c--ccli-wrapper) | +| **WPF** | C++/CLI wrapper + BitmapSource | [WPF Guide](examples/dotnet-winforms/guide.md#option-3-wpf-c--ccli-wrapper) | + +## C++/CLI Wrapper Patterns (Any Native Library → .NET) + +Complete 8-pattern guide for wrapping native C++ libraries: +- [Full Guide](examples/dotnet-winforms/guide.md#ccli-wrapper-patterns-for-net-integration) +- Patterns: void*, gcroot, Finalizer, Strings, Arrays, Threading, LockBits + +## Sample Repositories + +- **GitHub**: https://github.com/zoom/videosdk-windows-rawdata-sample +- **Local Samples**: + - `C:\tempsdk\Zoom_VideoSDK_Windows_RawDataDemos\` - C++ demos + - `C:\tempsdk\videosdk-windows-dotnet-desktop-framework-quickstart\` - C# demos + - `C:\tempsdk\sdksamples\zoom-video-sdk-windows-2.4.12\` - Official SDK samples diff --git a/plugins/zoom-developers/skills/virtual-agent/RUNBOOK.md b/plugins/zoom-developers/skills/virtual-agent/RUNBOOK.md new file mode 100644 index 00000000..434b8576 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/RUNBOOK.md @@ -0,0 +1,31 @@ +# Virtual Agent 5-Minute Runbook + +## 1. Credentials and Product Access + +- Confirm Virtual Agent license is active. +- Confirm campaign or entry ID exists and is published. +- Confirm API key and environment (`us01` or `eu01`) are correct. + +## 2. Browser or WebView Readiness + +- Verify CSP allows Zoom SDK script, websocket, media, and wasm execution. +- Verify no blocker/proxy is stripping `zcc-sdk.js`. +- For WebView, verify JavaScript is enabled. + +## 3. Lifecycle Order + +- Load SDK script. +- Wait for `zoomCampaignSdk:ready` or `waitForReady()`. +- Register event handlers. +- Call `open()` / `show()` only after readiness. + +## 4. Native Bridge (Android/iOS) + +- Inject `window.zoomCampaignSdk.native` on readiness. +- Wire `exitHandler`, `commonHandler`, and `support_handoff` callbacks. +- Verify URL policy (`target="_blank"`, `window.open`) is implemented. + +## 5. Drift Check + +- Validate docs naming (`Virtual Agent`) vs sample naming (`Virtual Assistant` / `LiveSDK`). +- Treat `openURL` command path as legacy/deprecated and prefer DOM links or `window.open`. diff --git a/plugins/zoom-developers/skills/virtual-agent/SKILL.md b/plugins/zoom-developers/skills/virtual-agent/SKILL.md new file mode 100644 index 00000000..0a968b03 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/SKILL.md @@ -0,0 +1,25 @@ +--- +name: build-zoom-virtual-agent +description: Use when using Virtual Agent. +--- + +# Build Zoom Virtual Agent + +Use this skill when the workflow embeds or wraps Zoom Virtual Agent, including web campaigns and mobile WebView integrations. + +## Workflow + +1. Identify the client: web, Android WebView, iOS WKWebView, campaign entry, support handoff, or knowledge-base sync pipeline. +2. Route to the platform skill before coding because event handling and native bridge behavior differ by client. +3. Confirm campaign, entry ID, allowed origins, lifecycle events, and support handoff behavior. +4. Keep user context updates, native URL handling, and handoff payloads explicit. +5. Debug by checking SDK readiness, campaign configuration, bridge injection, CSP, WebView lifecycle, and version drift. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- Web: [web/SKILL.md](web/SKILL.md) +- Android: [android/SKILL.md](android/SKILL.md) +- iOS: [ios/SKILL.md](ios/SKILL.md) +- Architecture and lifecycle: [concepts/architecture-and-lifecycle.md](concepts/architecture-and-lifecycle.md) +- Common drift and breaks: [troubleshooting/common-drift-and-breaks.md](troubleshooting/common-drift-and-breaks.md) diff --git a/plugins/zoom-developers/skills/virtual-agent/android/SKILL.md b/plugins/zoom-developers/skills/virtual-agent/android/SKILL.md new file mode 100644 index 00000000..26651b8b --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/android/SKILL.md @@ -0,0 +1,34 @@ +--- +name: zoom-virtual-agent-android +description: "Zoom Virtual Agent Android integration via WebView. Use for Java/Kotlin bridge callbacks, native URL handling, support_handoff relay, and lifecycle-safe embedding." +--- + +# Zoom Virtual Agent - Android + +Official docs: +- https://developers.zoom.us/docs/virtual-agent/android/ + +## Quick Links + +1. [concepts/webview-lifecycle.md](concepts/webview-lifecycle.md) +2. [examples/js-bridge-patterns.md](examples/js-bridge-patterns.md) +3. [references/android-reference-map.md](references/android-reference-map.md) +4. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Integration Model + +- Host campaign URL in Android WebView. +- Inject runtime context (`window.zoomCampaignSdkConfig`). +- Register JavaScript bridge for `exitHandler`, `commonHandler`, `support_handoff`. +- Apply URL policy via `shouldOverrideUrlLoading` and optional multi-window callbacks. + +## Hard Guardrails + +- Initialize handlers before expecting JS callbacks. +- Treat legacy `openURL` command handling as compatibility path only. +- Prefer DOM links or `window.open` handling plus explicit native routing. + +## Chaining + +- Product-level patterns: [../SKILL.md](../SKILL.md) +- Contact Center mobile scope: [../../contact-center/android/SKILL.md](../../contact-center/android/SKILL.md) diff --git a/plugins/zoom-developers/skills/virtual-agent/android/concepts/webview-lifecycle.md b/plugins/zoom-developers/skills/virtual-agent/android/concepts/webview-lifecycle.md new file mode 100644 index 00000000..a74677d8 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/android/concepts/webview-lifecycle.md @@ -0,0 +1,9 @@ +# Android WebView Lifecycle + +1. Build intent with URL and policy flags. +2. Configure WebView (`JavaScriptEnabled`, optional multi-window support). +3. Inject user context config before page interaction. +4. Inject bridge script on `zoomCampaignSdk:ready`. +5. Handle callbacks via `@JavascriptInterface`. +6. Route URL actions and handoff payloads. +7. Close view and clean references on exit. diff --git a/plugins/zoom-developers/skills/virtual-agent/android/examples/js-bridge-patterns.md b/plugins/zoom-developers/skills/virtual-agent/android/examples/js-bridge-patterns.md new file mode 100644 index 00000000..b449b33e --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/android/examples/js-bridge-patterns.md @@ -0,0 +1,37 @@ +# Android JS Bridge Patterns + +## Inject Native Bridge + +```kotlin +private fun injectJavaScriptFunction() { + val js = """ + javascript: window.addEventListener('zoomCampaignSdk:ready', () => { + if (window.zoomCampaignSdk) { + window.zoomCampaignSdk.native = { + exitHandler: { handle: function() { AndroidExit.handleExit(); } }, + commonHandler: { handle: function(e) { AndroidCommon.handleCommon(JSON.stringify(e)); } } + }; + } + }); + """.trimIndent() + webView.loadUrl(js) +} +``` + +## Handoff Relay + +```kotlin +private fun injectHandoffFunction() { + val js = """ + javascript: window.addEventListener('support_handoff', (e) => { + AndroidHandoff.handleHandoff(JSON.stringify(e.detail)); + }); + """.trimIndent() + webView.loadUrl(js) +} +``` + +## URL Governance + +- Use `shouldOverrideUrlLoading` for in-app vs system-browser policy. +- Use multi-window callbacks for `target="_blank"` handling. diff --git a/plugins/zoom-developers/skills/virtual-agent/android/references/android-reference-map.md b/plugins/zoom-developers/skills/virtual-agent/android/references/android-reference-map.md new file mode 100644 index 00000000..e0b5ae7d --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/android/references/android-reference-map.md @@ -0,0 +1,14 @@ +# Android Reference Map + +## Core Docs + +- Get started: https://developers.zoom.us/docs/virtual-agent/android/get-started/ +- Integration scenarios: https://developers.zoom.us/docs/virtual-agent/android/integration-scenarios/ +- JavaScript events: https://developers.zoom.us/docs/virtual-agent/android/javascript-events/ +- Resources: https://developers.zoom.us/docs/virtual-agent/android/resources/ + +## Observed Sample Patterns + +- Java and Kotlin implementations follow the same bridge contract. +- Bridge command routing centers around `commonHandler` JSON payloads. +- `support_handoff` events are emitted from JS and consumed in native layer. diff --git a/plugins/zoom-developers/skills/virtual-agent/android/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/virtual-agent/android/troubleshooting/common-issues.md new file mode 100644 index 00000000..8e0096a3 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/android/troubleshooting/common-issues.md @@ -0,0 +1,21 @@ +# Android Common Issues + +## Bridge Callback Never Fires + +- Ensure JS bridge injection runs after page load and SDK readiness. +- Ensure `addJavascriptInterface` names match injected handler names. + +## Link Opens in Wrong Context + +- Implement both `shouldOverrideUrlLoading` and multi-window behavior. +- Distinguish `_self` and `_blank` paths explicitly. + +## Deprecated `openURL` Path + +- Avoid relying on `{"cmd":"openURL"...}` as primary flow. +- Prefer anchor or `window.open` plus native interception policy. + +## Campaign Works on Web but Not Mobile + +- Verify campaign targeting includes mobile. +- Verify same API key/env pair used in WebView build. diff --git a/plugins/zoom-developers/skills/virtual-agent/concepts/architecture-and-lifecycle.md b/plugins/zoom-developers/skills/virtual-agent/concepts/architecture-and-lifecycle.md new file mode 100644 index 00000000..79021072 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/concepts/architecture-and-lifecycle.md @@ -0,0 +1,27 @@ +# Architecture and Lifecycle + +## Architecture + +```text +Web or Mobile Host App + -> Zoom Campaign SDK (zcc-sdk.js) + -> Campaign or Entry routing + -> Bot conversation state + -> Optional native bridge (Android/iOS) + -> Optional backend automation (OAuth + REST KB APIs) +``` + +## Lifecycle + +1. Provision bot flow and campaign/entry configuration. +2. Embed SDK snippet and provide runtime config (`apikey`, `env`, user context). +3. Wait for readiness, then register event callbacks. +4. Start or show engagement (`open`, `show`). +5. React to events (`open`, `close`, `engagement_started`, `engagement_ended`). +6. For mobile wrappers, forward bridge events (`support_handoff`, exit, URL commands). +7. End session (`endChat`) and detach handlers. + +## Version Drift Notes + +- Documentation now uses "Virtual Agent" naming. +- Official sample repositories still contain older naming (`virtual-assistant`, `liveSDK`) that can mislead search and code mapping. diff --git a/plugins/zoom-developers/skills/virtual-agent/ios/SKILL.md b/plugins/zoom-developers/skills/virtual-agent/ios/SKILL.md new file mode 100644 index 00000000..d7c31b3b --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/ios/SKILL.md @@ -0,0 +1,34 @@ +--- +name: zoom-virtual-agent-ios +description: "Zoom Virtual Agent iOS integration via WKWebView. Use for Swift/Objective-C script injection, message handlers, support_handoff relay, and URL routing policies." +--- + +# Zoom Virtual Agent - iOS + +Official docs: +- https://developers.zoom.us/docs/virtual-agent/ios/ + +## Quick Links + +1. [concepts/webview-lifecycle.md](concepts/webview-lifecycle.md) +2. [examples/js-bridge-patterns.md](examples/js-bridge-patterns.md) +3. [references/ios-reference-map.md](references/ios-reference-map.md) +4. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Integration Model + +- Load campaign URL in `WKWebView`. +- Inject `window.zoomCampaignSdkConfig` using `WKUserScript`. +- Register message handlers for exit/common/handoff flows. +- Handle URL behavior in navigation delegates (`in-app`, `SFSafariViewController`, or system browser). + +## Hard Guardrails + +- Register scripts and handlers before web interaction. +- Handle iOS 14.5+ download behavior where needed. +- Keep deprecated `openURL` command support as fallback only. + +## Chaining + +- Product-level patterns: [../SKILL.md](../SKILL.md) +- Contact Center mobile scope: [../../contact-center/ios/SKILL.md](../../contact-center/ios/SKILL.md) diff --git a/plugins/zoom-developers/skills/virtual-agent/ios/concepts/webview-lifecycle.md b/plugins/zoom-developers/skills/virtual-agent/ios/concepts/webview-lifecycle.md new file mode 100644 index 00000000..ebb23c3b --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/ios/concepts/webview-lifecycle.md @@ -0,0 +1,9 @@ +# iOS WKWebView Lifecycle + +1. Create `WKWebViewConfiguration` and `WKUserContentController`. +2. Add user scripts for context injection and bridge handlers. +3. Register message handlers before navigation. +4. Push/present webview controller with campaign URL. +5. Process callbacks in `userContentController:didReceiveScriptMessage:`. +6. Route navigation and external links in WKNavigation delegate callbacks. +7. Remove message handlers during teardown. diff --git a/plugins/zoom-developers/skills/virtual-agent/ios/examples/js-bridge-patterns.md b/plugins/zoom-developers/skills/virtual-agent/ios/examples/js-bridge-patterns.md new file mode 100644 index 00000000..f214e264 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/ios/examples/js-bridge-patterns.md @@ -0,0 +1,32 @@ +# iOS JS Bridge Patterns + +## Inject Exit and Common Handlers + +```swift +let exitHandlerScript = """ +window.addEventListener('zoomCampaignSdk:ready', () => { + if (window.zoomCampaignSdk) { + window.zoomCampaignSdk.native = { + exitHandler: { handle: function() { window.webkit.messageHandlers.zoomLiveSDKMessageHandler.postMessage('close_web_vc'); } }, + commonHandler: { handle: function(e) { window.webkit.messageHandlers.commonMessageHandler.postMessage(JSON.stringify(e)); } } + }; + } +}); +""" +``` + +## Inject Support Handoff + +```swift +let handoffScript = """ +window.addEventListener('support_handoff', (e) => { + window.webkit.messageHandlers.support_handoff.postMessage(JSON.stringify(e.detail)); +}); +""" +``` + +## URL Handling Policy + +- `WKNavigationActionPolicyAllow` for trusted in-app routes. +- `UIApplication.openURL` for system-browser policy. +- Optional in-app browser route (for example `SFSafariViewController`). diff --git a/plugins/zoom-developers/skills/virtual-agent/ios/references/ios-reference-map.md b/plugins/zoom-developers/skills/virtual-agent/ios/references/ios-reference-map.md new file mode 100644 index 00000000..f94a4c83 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/ios/references/ios-reference-map.md @@ -0,0 +1,14 @@ +# iOS Reference Map + +## Core Docs + +- Get started: https://developers.zoom.us/docs/virtual-agent/ios/get-started/ +- Integration scenarios: https://developers.zoom.us/docs/virtual-agent/ios/integration-scenarios/ +- JavaScript events: https://developers.zoom.us/docs/virtual-agent/ios/javascript-events/ +- Resources: https://developers.zoom.us/docs/virtual-agent/ios/resources/ + +## Observed Sample Patterns + +- Objective-C and Swift variants expose equivalent bridge behavior. +- Message handler constants in sample code still use legacy naming. +- URL routing policy is split between delegate interception and message-handler command processing. diff --git a/plugins/zoom-developers/skills/virtual-agent/ios/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/virtual-agent/ios/troubleshooting/common-issues.md new file mode 100644 index 00000000..fc9f2d92 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/ios/troubleshooting/common-issues.md @@ -0,0 +1,20 @@ +# iOS Common Issues + +## Message Handlers Not Triggering + +- Ensure `WKUserScript` and handlers are registered before page load. +- Verify handler names exactly match injected JS references. + +## URL Opens Unexpectedly + +- Explicitly branch in `decidePolicyForNavigationAction`. +- Handle `_blank`/`window.open` paths as separate cases. + +## Deprecated `openURL` Command Drift + +- Treat command-based open URL as fallback. +- Prefer DOM links and `window.open` with delegate-driven routing. + +## File Download Inconsistency + +- Download behavior via WKWebView requires iOS 14.5+ support paths. diff --git a/plugins/zoom-developers/skills/virtual-agent/references/environment-variables.md b/plugins/zoom-developers/skills/virtual-agent/references/environment-variables.md new file mode 100644 index 00000000..1efef71f --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/references/environment-variables.md @@ -0,0 +1,22 @@ +# Environment Variables + +## Web Embed Runtime + +- `ZVA_API_KEY`: Virtual Agent API key for campaign SDK script. +- `ZVA_ENV`: Deployment region (`us01` or `eu01`). +- `ZVA_CAMPAIGN_ID`: Optional campaign identifier when switching campaigns programmatically. +- `ZVA_ENTRY_ID`: Optional entry ID path when campaign mode is not used. + +## Knowledge Base API Automation + +- `ZOOM_ACCOUNT_ID`: Account ID for Server-to-Server OAuth app. +- `ZOOM_CLIENT_ID`: S2S OAuth client ID. +- `ZOOM_CLIENT_SECRET`: S2S OAuth client secret. +- `ZOOM_ACCESS_TOKEN`: Runtime bearer token (short-lived). +- `ZVA_KB_ID`: Knowledge base ID for custom API sync. + +## Where to Find Keys + +1. Zoom Marketplace app credentials: OAuth app in Marketplace. +2. Virtual Agent campaign/entry settings: Zoom admin portal AI Management. +3. KB ID: Knowledge Base settings in AI Management. diff --git a/plugins/zoom-developers/skills/virtual-agent/references/full-guide.md b/plugins/zoom-developers/skills/virtual-agent/references/full-guide.md new file mode 100644 index 00000000..c5d14e7d --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/references/full-guide.md @@ -0,0 +1,63 @@ +# /build-zoom-virtual-agent + +Background reference for Zoom Virtual Agent across: +- Web campaign/chat embeds. +- Android WebView wrappers. +- iOS WKWebView wrappers. +- Knowledge-base sync and custom API ingestion. + +Official docs: +- https://developers.zoom.us/docs/virtual-agent/ +- https://developers.zoom.us/docs/virtual-agent/web/ +- https://developers.zoom.us/docs/virtual-agent/android/ +- https://developers.zoom.us/docs/virtual-agent/ios/ + +## Routing Guardrail + +- If the user is implementing Contact Center app surfaces inside Zoom client, chain with [../contact-center/SKILL.md](../../contact-center/SKILL.md). +- If the user needs backend knowledge-base CRUD or automation scripts, chain with [../rest-api/SKILL.md](../../rest-api/SKILL.md) and [../oauth/SKILL.md](../../oauth/SKILL.md). +- If the user asks only for website bot embed and campaign controls, stay on [web/SKILL.md](../web/SKILL.md). +- If the user asks for mobile native wrappers around web chat, route to [android/SKILL.md](../android/SKILL.md) or [ios/SKILL.md](../ios/SKILL.md). + +## Quick Links + +1. [concepts/architecture-and-lifecycle.md](../concepts/architecture-and-lifecycle.md) +2. [scenarios/high-level-scenarios.md](../scenarios/high-level-scenarios.md) +3. [references/versioning-and-drift.md](../references/versioning-and-drift.md) +4. [references/samples-validation.md](../references/samples-validation.md) +5. [references/environment-variables.md](../references/environment-variables.md) +6. [troubleshooting/common-drift-and-breaks.md](../troubleshooting/common-drift-and-breaks.md) +7. [RUNBOOK.md](../RUNBOOK.md) + +Platform skills: +- [web/SKILL.md](../web/SKILL.md) +- [android/SKILL.md](../android/SKILL.md) +- [ios/SKILL.md](../ios/SKILL.md) + +## Common Lifecycle Pattern + +1. Configure campaign or entry ID in Virtual Agent admin. +2. Initialize SDK in web or WebView container. +3. Wait for readiness (`zoomCampaignSdk:ready` or `waitForReady()`) before calling APIs. +4. Register bridge handlers (`exitHandler`, `commonHandler`, `support_handoff`) when native orchestration is needed. +5. Handle conversation lifecycle (`engagement_started`, `engagement_ended`) and UI state. +6. End chat (`endChat`) and clean up listeners. + +## High-Level Scenarios + +- Website campaign launcher with contextual customer attributes. +- Mobile app WebView chat with native close/handoff bridge. +- External URL handling via system browser vs in-app browser policy. +- Knowledge-base sync from external systems using custom API connector. +- Cross-team support flow that escalates from bot to live support with handoff payload. + +## Chaining + +- Contact Center app/web/mobile patterns: [../contact-center/SKILL.md](../../contact-center/SKILL.md) +- OAuth app setup and tokens: [../oauth/SKILL.md](../../oauth/SKILL.md) +- API workflows for KB automation: [../rest-api/SKILL.md](../../rest-api/SKILL.md) +- Event-driven backend follow-up: [../webhooks/SKILL.md](../../webhooks/SKILL.md) + +## Operations + +- [RUNBOOK.md](../RUNBOOK.md) - 5-minute preflight and debugging checklist. diff --git a/plugins/zoom-developers/skills/virtual-agent/references/samples-validation.md b/plugins/zoom-developers/skills/virtual-agent/references/samples-validation.md new file mode 100644 index 00000000..e1899838 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/references/samples-validation.md @@ -0,0 +1,22 @@ +# Samples Validation + +Validated repositories: +- https://github.com/zoom/virtual-assistant-android-sample +- https://github.com/zoom/virtual-assistant-iOS-sample + +Latest commit observed during validation: +- Android sample: `faab2b6` (2024-10-16), commit message references `OpenUrl` deprecation. +- iOS sample: `dd31e95` (2024-10-16), commit message references URL opening method update. + +## Confirmed Relevant Patterns + +- `zoomCampaignSdk:ready` event gating before native bridge registration. +- `window.zoomCampaignSdk.native.exitHandler/commonHandler` bridge contract. +- `support_handoff` event forwarding from JavaScript to native. +- WebView URL policy split between in-app browsing and system browser. + +## Contradictions and Caveats + +- Samples still document legacy command contract (`{"cmd":"openURL","value":"..."}`) while marking it deprecated. +- Naming in sample classes uses "LiveSDK" and "Virtual Assistant" while docs use "Virtual Agent". +- Treat sample repos as implementation patterns, not canonical naming source. diff --git a/plugins/zoom-developers/skills/virtual-agent/references/versioning-and-drift.md b/plugins/zoom-developers/skills/virtual-agent/references/versioning-and-drift.md new file mode 100644 index 00000000..a70de270 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/references/versioning-and-drift.md @@ -0,0 +1,21 @@ +# Versioning and Drift + +## Naming Drift + +- Current docs and product naming: **Virtual Agent**. +- Sample repo naming still includes legacy terms: **virtual-assistant**, **liveSDK**, `ZMLiveSDKWebviewController`. +- Integration code should follow current docs semantics while mapping legacy symbol names from samples. + +## Deprecated or Legacy Patterns + +- `openURL` command JSON payload is marked deprecated in 2024 sample code comments. +- Preferred URL launch patterns: + - DOM anchor links with `target="_blank"`. + - `window.open()` in JavaScript context. + - Native URL interception in WebView delegates. + +## Stability Strategy + +- Wrap SDK calls behind readiness gates. +- Centralize bridge constants so command/event renames are isolated. +- Keep fallback path for legacy keys only where backward compatibility is required. diff --git a/plugins/zoom-developers/skills/virtual-agent/scenarios/high-level-scenarios.md b/plugins/zoom-developers/skills/virtual-agent/scenarios/high-level-scenarios.md new file mode 100644 index 00000000..a2d7e729 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/scenarios/high-level-scenarios.md @@ -0,0 +1,30 @@ +# High-Level Scenarios + +## 1. Website Campaign Entry + +- Use campaign embed snippet to control initial bot routing without code redeploys. +- Use `show()/hide()/open()/close()` for page-specific behavior. + +## 2. Native Mobile Wrapper (Android/iOS) + +- Host campaign URL in WebView/WKWebView. +- Inject customer context (`language`, name, profile fields). +- Route exit and handoff messages into native app state. + +## 3. Bot-to-Agent Escalation + +- Listen for `support_handoff` payload. +- Persist payload to backend for CRM/ticket enrichment. +- Route customer into live support workflow. + +## 4. URL Navigation Governance + +- Open trusted links inside app. +- Send external links to system browser when policy requires. +- Handle `target="_blank"` and `window.open` explicitly. + +## 5. Knowledge-Base Sync Pipeline + +- Use web sync for crawlable documentation. +- Use custom API connector for external CMS pull/push synchronization. +- Re-run sync on content release cadence. diff --git a/plugins/zoom-developers/skills/virtual-agent/troubleshooting/common-drift-and-breaks.md b/plugins/zoom-developers/skills/virtual-agent/troubleshooting/common-drift-and-breaks.md new file mode 100644 index 00000000..96a48a51 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/troubleshooting/common-drift-and-breaks.md @@ -0,0 +1,40 @@ +# Common Drift and Breaks + +## SDK Not Ready + +Symptoms: +- `window.zoomCampaignSdk` is undefined. + +Fix: +- Register logic only after `zoomCampaignSdk:ready`. +- Prefer `waitForReady()` when available. + +## Campaign Configured but Not Showing + +Checks: +- Confirm campaign targeting includes mobile when using Android/iOS WebView. +- Validate style/config API network responses. + +## Subdomain/Login Failures + +Symptoms: +- Login fails due to subdomain connection issue. + +Fix: +- Verify subdomain allowlist and environment settings in Virtual Agent preferences. + +## Script Tag Loads Inconsistently + +Cause: +- `defer` can break execution order in certain third-party-link flows. + +Fix: +- Remove `defer` or use `async` based on page lifecycle. + +## Deprecated URL Command Usage + +Symptoms: +- Legacy `openURL` command path behaves unpredictably across versions. + +Fix: +- Use DOM links (`target="_blank"`) or `window.open` and explicit native navigation handlers. diff --git a/plugins/zoom-developers/skills/virtual-agent/web/SKILL.md b/plugins/zoom-developers/skills/virtual-agent/web/SKILL.md new file mode 100644 index 00000000..d1fffa8d --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/web/SKILL.md @@ -0,0 +1,30 @@ +--- +name: zoom-virtual-agent-web +description: "Zoom Virtual Agent SDK for web embeds. Use for campaign or entry ID chat launch, event-driven controls, user context updates, and CSP-safe deployment." +--- + +# Zoom Virtual Agent SDK - Web + +Official docs: +- https://developers.zoom.us/docs/virtual-agent/web/ +- https://developers.zoom.us/docs/virtual-agent/web/reference/ + +## Quick Links + +1. [concepts/lifecycle-and-events.md](concepts/lifecycle-and-events.md) +2. [examples/campaign-and-entry-patterns.md](examples/campaign-and-entry-patterns.md) +3. [references/web-reference-map.md](references/web-reference-map.md) +4. [troubleshooting/common-issues.md](troubleshooting/common-issues.md) + +## Hard Guardrails + +- Gate calls behind readiness (`zoomCampaignSdk:ready` or `waitForReady()`). +- Do not call `show/hide/open/close` before SDK initialization. +- Keep CSP and script host policy validated before debugging business logic. +- Prefer campaign embed over entry ID when minimizing user friction is a priority. + +## Chaining + +- Product-level architecture and drift checks: [../SKILL.md](../SKILL.md) +- Contact Center web context: [../../contact-center/web/SKILL.md](../../contact-center/web/SKILL.md) +- OAuth or REST for backend workflows: [../../oauth/SKILL.md](../../oauth/SKILL.md), [../../rest-api/SKILL.md](../../rest-api/SKILL.md) diff --git a/plugins/zoom-developers/skills/virtual-agent/web/concepts/lifecycle-and-events.md b/plugins/zoom-developers/skills/virtual-agent/web/concepts/lifecycle-and-events.md new file mode 100644 index 00000000..6040b204 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/web/concepts/lifecycle-and-events.md @@ -0,0 +1,30 @@ +# Lifecycle and Events (Web) + +## Lifecycle + +1. Inject script with API key and env. +2. Wait for `zoomCampaignSdk:ready` or `waitForReady()`. +3. Register event listeners. +4. Execute control calls (`open`, `close`, `show`, `hide`, `endChat`). +5. Optionally refresh user variables via `updateUserContext()`. +6. Remove listeners during page teardown. + +## Event Surface + +- `open` +- `close` +- `show` +- `hide` +- `engagement_started` +- `engagement_ended` + +## Method Surface + +- `close()` +- `endChat()` +- `hide()` +- `show()` +- `ChangeCampaign(id, channel?)` +- `updateUserContext()` +- `waitForInit()` +- `waitForReady()` diff --git a/plugins/zoom-developers/skills/virtual-agent/web/examples/campaign-and-entry-patterns.md b/plugins/zoom-developers/skills/virtual-agent/web/examples/campaign-and-entry-patterns.md new file mode 100644 index 00000000..e5c39abe --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/web/examples/campaign-and-entry-patterns.md @@ -0,0 +1,37 @@ +# Campaign and Entry Patterns + +## Campaign-First Pattern (Recommended) + +```html + + +``` + +## Runtime User Context Refresh + +```javascript +window.zoomCampaignSdkConfig = { + env: 'us01', + apikey: 'YOUR_API_KEY', + firstName: 'Ada', + email: 'ada@example.com' +}; + +window.addEventListener('zoomCampaignSdk:ready', async () => { + if (window.zoomCampaignSdk.waitForReady) { + await window.zoomCampaignSdk.waitForReady(); + } + window.zoomCampaignSdk.updateUserContext(); +}); +``` + +## Entry ID Fallback Pattern + +Use entry ID only when your flow requires pre-chat data collection that cannot be handled in campaign configuration. diff --git a/plugins/zoom-developers/skills/virtual-agent/web/references/web-reference-map.md b/plugins/zoom-developers/skills/virtual-agent/web/references/web-reference-map.md new file mode 100644 index 00000000..93c9d0b7 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/web/references/web-reference-map.md @@ -0,0 +1,14 @@ +# Web Reference Map + +## Core Docs + +- Get started: https://developers.zoom.us/docs/virtual-agent/web/get-started/ +- Chat embed: https://developers.zoom.us/docs/virtual-agent/web/chat/ +- Campaign controls: https://developers.zoom.us/docs/virtual-agent/web/campaigns/ +- SDK reference: https://developers.zoom.us/docs/virtual-agent/web/reference/ + +## Operational Notes + +- Campaign mode supports central admin routing and lower app-code churn. +- Entry ID mode can increase friction and should be selective. +- Keep script host, CSP, and environment alignment (`us01`/`eu01`) in preflight checks. diff --git a/plugins/zoom-developers/skills/virtual-agent/web/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/virtual-agent/web/troubleshooting/common-issues.md new file mode 100644 index 00000000..57209516 --- /dev/null +++ b/plugins/zoom-developers/skills/virtual-agent/web/troubleshooting/common-issues.md @@ -0,0 +1,21 @@ +# Web Common Issues + +## `window.zoomCampaignSdk` Is Undefined + +- Confirm script URL is reachable and not blocked. +- Confirm initialization completed before method calls. + +## CSP Blocks SDK + +- Add CSP directives for script/connect/media/font/image paths required by Zoom SDK. +- Re-test with browser console open for wasm or websocket policy errors. + +## Campaign Not Triggering + +- Validate campaign targeting rules and page conditions. +- Inspect network calls and config/style responses. + +## Third-Party Link Behavior + +- Avoid fragile `defer` script behavior when startup triggers external links. +- Prefer explicit handling for `target="_blank"` and `window.open` flows. diff --git a/plugins/zoom-developers/skills/webhooks/RUNBOOK.md b/plugins/zoom-developers/skills/webhooks/RUNBOOK.md new file mode 100644 index 00000000..5cbf0f00 --- /dev/null +++ b/plugins/zoom-developers/skills/webhooks/RUNBOOK.md @@ -0,0 +1,83 @@ +# Webhooks 5-Minute Preflight Runbook + +Use this before deep debugging. It catches common webhook failures quickly. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm Endpoint Reachability + +- Public HTTPS endpoint is reachable from Zoom. +- Reverse proxy routes to the correct service path. + +## 2) Confirm Signature Verification + +- Verify `x-zm-signature` with raw request body. +- Use `x-zm-request-timestamp` and reject stale timestamps. +- Do not re-serialize parsed JSON for signature material. + +### Signature Formula Reminder + +```text +payload = "v0:" + x-zm-request-timestamp + ":" + raw_body +expected = "v0=" + HMAC_SHA256(webhook_secret, payload) +``` + +If `raw_body` differs from original bytes (pretty print/re-stringify), verification fails. + +## 3) Confirm URL Validation Handling + +- Handle `endpoint.url_validation` challenge correctly. +- Return expected `plainToken` and computed `encryptedToken` when required. + +### URL Validation Reminder + +On `event = endpoint.url_validation`, hash `payload.plainToken` with your webhook secret and return both values exactly. + +## 4) Confirm Event Subscription Setup + +- Feature/Event subscriptions enabled in app config. +- Required event types selected and saved. + +## 5) Confirm Processing Pattern + +- Respond HTTP 200 quickly. +- Process business logic asynchronously. +- Make handlers idempotent for retries. + +## 6) Quick Probes + +- Local test payload verifies signature path. +- Zoom test event reaches endpoint and is logged. +- No repeated non-200 responses in logs. + +### Copy/Paste Validation Commands + +```bash +# 1) Reachability check (replace with your webhook route) +curl -sS -i "https://your-domain.example/webhook" + +# 2) Check service logs quickly while sending test events +# (replace command with your runtime: pm2/docker/systemd) +pm2 logs your-service --lines 100 + +# 3) Basic endpoint health check if available +curl -sS -i "https://your-domain.example/health" +``` + +Expected: endpoint is reachable over HTTPS, events appear once, and responses are consistently 2xx. + +## 7) Fast Decision Tree + +- **No events received** -> endpoint unreachable or wrong subscription. +- **401 invalid signature** -> raw body mismatch/secret mismatch. +- **Duplicate events** -> no idempotency or delayed responses. + +## 8) Retry and Idempotency Guardrail + +- Treat webhook delivery as at-least-once. +- Deduplicate by event ID/timestamp/resource key before side effects. +- Keep handlers safe to re-run. diff --git a/plugins/zoom-developers/skills/webhooks/SKILL.md b/plugins/zoom-developers/skills/webhooks/SKILL.md new file mode 100644 index 00000000..359243cc --- /dev/null +++ b/plugins/zoom-developers/skills/webhooks/SKILL.md @@ -0,0 +1,25 @@ +--- +name: setup-zoom-webhooks +description: Use when building Zoom webhooks. +--- + +# Setup Zoom Webhooks + +Use this skill when the integration receives Zoom events over HTTP. If the user needs persistent low-latency event streams, compare against `setup-zoom-websockets`. + +## Workflow + +1. Identify the event types and resource scope before creating subscriptions. +2. Implement endpoint verification and signature verification before processing business logic. +3. Store raw event IDs, timestamps, and delivery metadata for replay protection and debugging. +4. Make handlers idempotent because Zoom can retry delivery. +5. Separate webhook ingestion from downstream API calls with a queue when reliability matters. +6. Debug by checking endpoint reachability, TLS, validation token handling, signature base string, app event subscription, and account-level settings. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- Signature verification: [references/signature-verification.md](references/signature-verification.md) +- Subscriptions: [references/subscriptions.md](references/subscriptions.md) +- Events: [references/events.md](references/events.md) +- Common issues: [troubleshooting/common-issues.md](troubleshooting/common-issues.md) diff --git a/plugins/zoom-developers/skills/webhooks/references/environment-variables.md b/plugins/zoom-developers/skills/webhooks/references/environment-variables.md new file mode 100644 index 00000000..a3abf0f2 --- /dev/null +++ b/plugins/zoom-developers/skills/webhooks/references/environment-variables.md @@ -0,0 +1,14 @@ +# Zoom Webhooks Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_WEBHOOK_SECRET` | Yes | HMAC signature verification for webhook payloads | Zoom Marketplace -> Event Subscriptions -> Secret Token | +| `WEBHOOK_SECRET_TOKEN` | Alias | Same secret token under alternate naming | Same as above | +| `ZOOM_VERIFICATION_TOKEN` | Legacy only | Legacy endpoint verification | Marketplace legacy field (older app configs) | + +## Notes + +- Prefer `ZOOM_WEBHOOK_SECRET` / Secret Token for current implementations. +- Keep webhook secret in server-side secret storage only. diff --git a/plugins/zoom-developers/skills/webhooks/references/events.md b/plugins/zoom-developers/skills/webhooks/references/events.md new file mode 100644 index 00000000..ee1aa7fe --- /dev/null +++ b/plugins/zoom-developers/skills/webhooks/references/events.md @@ -0,0 +1,75 @@ +# Webhooks - Events + +Complete reference of Zoom webhook events. + +## Overview + +Zoom sends webhook events for various actions across meetings, users, recordings, and more. + +## Meeting Events + +| Event | Description | +|-------|-------------| +| `meeting.created` | Meeting created | +| `meeting.updated` | Meeting updated | +| `meeting.deleted` | Meeting deleted | +| `meeting.started` | Meeting started | +| `meeting.ended` | Meeting ended | +| `meeting.participant_joined` | Participant joined | +| `meeting.participant_left` | Participant left | +| `meeting.sharing_started` | Screen share started | +| `meeting.sharing_ended` | Screen share ended | + +## Recording Events + +| Event | Description | +|-------|-------------| +| `recording.started` | Recording started | +| `recording.stopped` | Recording stopped | +| `recording.paused` | Recording paused | +| `recording.resumed` | Recording resumed | +| `recording.completed` | Recording ready for download | +| `recording.trashed` | Recording moved to trash | +| `recording.deleted` | Recording permanently deleted | + +## User Events + +| Event | Description | +|-------|-------------| +| `user.created` | User created | +| `user.updated` | User updated | +| `user.deleted` | User deleted | +| `user.activated` | User activated | +| `user.deactivated` | User deactivated | + +## Webinar Events + +| Event | Description | +|-------|-------------| +| `webinar.created` | Webinar created | +| `webinar.updated` | Webinar updated | +| `webinar.deleted` | Webinar deleted | +| `webinar.started` | Webinar started | +| `webinar.ended` | Webinar ended | + +## Event Payload Structure + +```json +{ + "event": "meeting.started", + "event_ts": 1234567890, + "payload": { + "account_id": "account_id", + "object": { + "id": "meeting_id", + "topic": "Meeting Topic", + "host_id": "host_user_id", + "start_time": "2024-01-15T10:00:00Z" + } + } +} +``` + +## Resources + +- **Events reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/events/ diff --git a/plugins/zoom-developers/skills/webhooks/references/full-guide.md b/plugins/zoom-developers/skills/webhooks/references/full-guide.md new file mode 100644 index 00000000..e796dfd0 --- /dev/null +++ b/plugins/zoom-developers/skills/webhooks/references/full-guide.md @@ -0,0 +1,106 @@ +# /setup-zoom-webhooks + +Background reference for Zoom event delivery over HTTP. Prefer workflow skills first, then use this file for verification, subscription, and delivery details. + +## Prerequisites + +- Zoom app with Event Subscriptions enabled +- HTTPS endpoint to receive webhooks +- Webhook secret token for verification + +> **Need help with authentication?** See the **[zoom-oauth](../../oauth/SKILL.md)** skill for OAuth setup. + +## Quick Start + +```javascript +// Express.js webhook handler +const crypto = require('crypto'); + +// Capture raw body for signature verification (avoid re-serializing JSON). +app.use(require('express').json({ + verify: (req, _res, buf) => { req.rawBody = buf; } +})); + +app.post('/webhook', (req, res) => { + // Verify webhook signature + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + const body = req.rawBody ? req.rawBody.toString('utf8') : JSON.stringify(req.body); + const payload = `v0:${timestamp}:${body}`; + const hash = crypto.createHmac('sha256', WEBHOOK_SECRET) + .update(payload).digest('hex'); + + if (signature !== `v0=${hash}`) { + return res.status(401).send('Invalid signature'); + } + + // Handle event + const { event, payload } = req.body; + console.log(`Received: ${event}`); + + res.status(200).send(); +}); +``` + +## Common Events + +| Event | Description | +|-------|-------------| +| `meeting.started` | Meeting has started | +| `meeting.ended` | Meeting has ended | +| `meeting.participant_joined` | Participant joined meeting | +| `recording.completed` | Cloud recording ready | +| `user.created` | New user added | + +## Detailed References + +- **[references/events.md](../references/events.md)** - Complete event types reference +- **[references/verification.md](../references/verification.md)** - Webhook URL validation +- **[references/subscriptions.md](../references/subscriptions.md)** - Event subscriptions API + +## Troubleshooting + +- **[RUNBOOK.md](../RUNBOOK.md)** - 5-minute preflight checks before deep debugging +- **[troubleshooting/common-issues.md](../troubleshooting/common-issues.md)** - Signature verification, retries, URL validation + +## Sample Repositories + +### Official (by Zoom) + +| Type | Repository | Stars | +|------|------------|-------| +| Node.js | [webhook-sample](https://github.com/zoom/webhook-sample) | 34 | +| PostgreSQL | [webhook-to-postgres](https://github.com/zoom/webhook-to-postgres) | 5 | +| Go/Fiber | [Go-Webhooks](https://github.com/zoom/Go-Webhooks) | - | +| Header Auth | [zoom-webhook-verification-headers](https://github.com/zoom/zoom-webhook-verification-headers) | - | + +### Community + +| Language | Repository | Description | +|----------|------------|-------------| +| Laravel | [binary-cats/laravel-webhooks](https://github.com/binary-cats/laravel-webhooks) | Laravel webhook handler | +| AWS Lambda | [splunk/zoom-webhook-to-hec](https://github.com/splunk/zoom-webhook-to-hec) | Serverless to Splunk HEC | +| Node.js | [Will4950/zoom-webhook-listener](https://github.com/Will4950/zoom-webhook-listener) | Webhook forwarder | +| Express+Redis | [ojusave/eventSubscriptionPlayground](https://github.com/ojusave/eventSubscriptionPlayground) | Socket.io + Redis | + +### Multi-Language Samples (by tanchunsiong) + +| Language | Repository | +|----------|------------| +| Node.js | [Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-NodeJS](https://github.com/tanchunsiong/Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-NodeJS) | +| C# | [Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-ASP.NET-Core-C-](https://github.com/tanchunsiong/Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-ASP.NET-Core-C-) | +| Java | [Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-Java-Spring-Boot](https://github.com/tanchunsiong/Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-Java-Spring-Boot) | +| Python | [Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-Python](https://github.com/tanchunsiong/Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-Python) | +| PHP | [Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-PHP](https://github.com/tanchunsiong/Zoom-Webhook-Signature-OAuth-and-REST-API-Development-Sample-In-PHP) | + +**Full list**: See [general/references/community-repos.md](../../general/references/community-repos.md) + +## Resources + +- **Webhook docs**: https://developers.zoom.us/docs/api/webhooks/ +- **Event reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/events/ +- **Developer forum**: https://devforum.zoom.us/ + +## Environment Variables + +- See [references/environment-variables.md](../references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/plugins/zoom-developers/skills/webhooks/references/signature-verification.md b/plugins/zoom-developers/skills/webhooks/references/signature-verification.md new file mode 100644 index 00000000..5958a199 --- /dev/null +++ b/plugins/zoom-developers/skills/webhooks/references/signature-verification.md @@ -0,0 +1,6 @@ +# Signature Verification + +This content lives in `verification.md` (URL validation + request signature verification). + +- See: [verification.md](verification.md) + diff --git a/plugins/zoom-developers/skills/webhooks/references/subscriptions.md b/plugins/zoom-developers/skills/webhooks/references/subscriptions.md new file mode 100644 index 00000000..79baaeb6 --- /dev/null +++ b/plugins/zoom-developers/skills/webhooks/references/subscriptions.md @@ -0,0 +1,272 @@ +# Webhooks - Subscriptions + +Configure webhook event subscriptions for real-time notifications. + +## Overview + +Subscribe to Zoom events to receive real-time notifications at your endpoint. This enables: +- Real-time meeting tracking (started, ended, participant changes) +- Automated recording processing pipelines +- User lifecycle management +- Skill chaining: Combine REST API calls with event-driven workflows + +## Configuring Subscriptions + +### Method 1: Via Marketplace Portal (Recommended for Setup) + +1. Go to your app in [Marketplace](https://marketplace.zoom.us/) +2. Navigate to **Feature** → **Event Subscriptions** +3. Add subscription name and endpoint URL +4. Select events to subscribe to +5. Save and activate + +### Method 2: Via API (Programmatic Management) + +Use the Webhook Subscriptions API for programmatic subscription management. + +#### Create Subscription + +```bash +POST /webhooks/options +``` + +```json +{ + "notification_endpoint_url": "https://your-server.com/zoom/webhook", + "events": [ + "meeting.started", + "meeting.ended", + "meeting.participant_joined", + "recording.completed" + ] +} +``` + +#### Get Subscription + +```bash +GET /webhooks/options +``` + +**Response:** +```json +{ + "notification_endpoint_url": "https://your-server.com/zoom/webhook", + "events": [ + "meeting.started", + "meeting.ended", + "meeting.participant_joined", + "recording.completed" + ] +} +``` + +#### Update Subscription + +```bash +PATCH /webhooks/options +``` + +```json +{ + "events": [ + "meeting.started", + "meeting.ended", + "recording.completed", + "user.created" + ] +} +``` + +### Subscription Settings + +| Setting | Description | Required | +|---------|-------------|----------| +| **notification_endpoint_url** | Your HTTPS endpoint (must be publicly accessible) | Yes | +| **events** | Array of event types to subscribe to | Yes | + +## Event Categories + +### Meeting Events +| Event | Trigger | +|-------|---------| +| `meeting.created` | Meeting scheduled | +| `meeting.updated` | Meeting settings changed | +| `meeting.deleted` | Meeting deleted | +| `meeting.started` | Meeting begins | +| `meeting.ended` | Meeting ends | +| `meeting.participant_joined` | Participant joins | +| `meeting.participant_left` | Participant leaves | +| `meeting.sharing_started` | Screen share begins | +| `meeting.sharing_ended` | Screen share ends | + +### Recording Events +| Event | Trigger | +|-------|---------| +| `recording.started` | Cloud recording begins | +| `recording.stopped` | Cloud recording paused/stopped | +| `recording.paused` | Cloud recording paused | +| `recording.resumed` | Cloud recording resumed | +| `recording.completed` | Recording processed and available | +| `recording.trashed` | Recording moved to trash | +| `recording.deleted` | Recording permanently deleted | +| `recording.recovered` | Recording restored from trash | + +### User Events +| Event | Trigger | +|-------|---------| +| `user.created` | New user added | +| `user.updated` | User details changed | +| `user.deleted` | User removed | +| `user.deactivated` | User deactivated | +| `user.activated` | User activated | + +### Webinar Events +| Event | Trigger | +|-------|---------| +| `webinar.created` | Webinar scheduled | +| `webinar.started` | Webinar begins | +| `webinar.ended` | Webinar ends | +| `webinar.registration_created` | New registration | + +## Subscription Object Schema + +```json +{ + "notification_endpoint_url": "string (HTTPS URL)", + "events": ["array of event type strings"], + "secret_token": "string (for signature verification)" +} +``` + +## Code Examples + +### JavaScript - Express Webhook Handler with Subscription Check + +```javascript +const express = require('express'); +const crypto = require('crypto'); +const axios = require('axios'); + +const app = express(); +app.use(express.json()); + +// Verify webhook signature +function verifyWebhookSignature(req, secret) { + const message = `v0:${req.headers['x-zm-request-timestamp']}:${JSON.stringify(req.body)}`; + const hashForVerify = crypto + .createHmac('sha256', secret) + .update(message) + .digest('hex'); + + const signature = `v0=${hashForVerify}`; + return signature === req.headers['x-zm-signature']; +} + +// Handle webhook events +app.post('/zoom/webhook', (req, res) => { + const WEBHOOK_SECRET = process.env.ZOOM_WEBHOOK_SECRET; + + if (!verifyWebhookSignature(req, WEBHOOK_SECRET)) { + return res.status(401).send('Unauthorized'); + } + + const { event, payload } = req.body; + + switch (event) { + case 'meeting.started': + console.log(`Meeting started: ${payload.object.topic}`); + break; + case 'meeting.ended': + console.log(`Meeting ended: ${payload.object.uuid}`); + break; + case 'recording.completed': + processRecording(payload.object); + break; + } + + res.status(200).send('OK'); +}); +``` + +### JavaScript - Manage Subscriptions via API + +```javascript +async function updateWebhookSubscription(accessToken, events) { + const response = await axios.patch( + 'https://api.zoom.us/v2/webhooks/options', + { events }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ); + return response.data; +} + +// Add new events to subscription +await updateWebhookSubscription(token, [ + 'meeting.started', + 'meeting.ended', + 'recording.completed', + 'user.created', // New event + 'user.deleted' // New event +]); +``` + +## Multiple Subscriptions + +You can create multiple subscriptions to: +- Send different events to different endpoints +- Separate production and development endpoints +- Organize by event type +- Route events to different microservices + +## Skill Chaining + +Webhooks are commonly combined with REST API in skill chains: + +| Chain | Pattern | Use Case | +|-------|---------|----------| +| REST API → Webhooks | Create meeting, subscribe to events | Track meeting lifecycle | +| Webhooks → REST API | Receive event, fetch details | Recording download on completion | +| Users API → Webhooks | Create user, subscribe to user events | User lifecycle tracking | + +**Example: Meeting creation with event tracking** +```javascript +// Step 1: Subscribe to meeting events (one-time setup) +await updateWebhookSubscription(token, ['meeting.started', 'meeting.ended']); + +// Step 2: Create meeting via REST API +const meeting = await createMeeting(token, { topic: 'Team Standup', type: 2 }); + +// Step 3: When meeting.started webhook arrives, track it +// Step 4: When meeting.ended webhook arrives, process attendance +``` + +See [meeting-details-with-events.md](../../general/use-cases/meeting-details-with-events.md) for complete skill chaining patterns. + +## Testing Webhooks + +1. **Local development**: Use [ngrok](https://ngrok.com/) to expose local endpoint + ```bash + ngrok http 3000 + ``` +2. **Webhook logs**: Check Marketplace portal → App → Webhooks → Logs +3. **Test endpoint**: Validate signature handling before going live +4. **Retry behavior**: Zoom retries failed webhooks (5xx responses) up to 3 times + +## Required Scopes + +| Scope | Operations | +|-------|------------| +| `webhook:read:admin` | View webhook settings | +| `webhook:write:admin` | Modify webhook settings | + +## Resources + +- **Webhooks overview**: https://developers.zoom.us/docs/api/rest/webhook-reference/ +- **Event types**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/events/ +- **Signature verification**: See [signature-verification.md](signature-verification.md) diff --git a/plugins/zoom-developers/skills/webhooks/references/verification.md b/plugins/zoom-developers/skills/webhooks/references/verification.md new file mode 100644 index 00000000..a0169cb6 --- /dev/null +++ b/plugins/zoom-developers/skills/webhooks/references/verification.md @@ -0,0 +1,94 @@ +# Webhooks - Verification + +Verify webhook authenticity and handle URL validation. + +## Overview + +Zoom provides two verification mechanisms: +1. **URL Validation** - Verify your endpoint during setup +2. **Request Signature** - Verify each webhook request + +## URL Validation + +When you configure a webhook endpoint, Zoom sends a validation request: + +```json +{ + "event": "endpoint.url_validation", + "payload": { + "plainToken": "random_token_string" + } +} +``` + +### Response Required + +Hash the token with your webhook secret and respond: + +```javascript +const crypto = require('crypto'); + +app.post('/webhook', (req, res) => { + const { event, payload } = req.body; + + if (event === 'endpoint.url_validation') { + const hashForValidation = crypto + .createHmac('sha256', WEBHOOK_SECRET_TOKEN) + .update(payload.plainToken) + .digest('hex'); + + return res.json({ + plainToken: payload.plainToken, + encryptedToken: hashForValidation + }); + } + + // Handle other events... + res.status(200).send(); +}); +``` + +## Request Signature Verification + +Verify each webhook request is from Zoom: + +### Headers + +| Header | Description | +|--------|-------------| +| `x-zm-signature` | Request signature | +| `x-zm-request-timestamp` | Request timestamp | + +### Verification Code + +```javascript +const crypto = require('crypto'); + +function verifyWebhook(req) { + const signature = req.headers['x-zm-signature']; + const timestamp = req.headers['x-zm-request-timestamp']; + // Prefer raw body bytes captured by your framework to avoid JSON re-serialization mismatches. + const body = req.rawBody ? req.rawBody.toString('utf8') : JSON.stringify(req.body); + + const message = `v0:${timestamp}:${body}`; + const hash = crypto + .createHmac('sha256', WEBHOOK_SECRET_TOKEN) + .update(message) + .digest('hex'); + + const expectedSignature = `v0=${hash}`; + + return signature === expectedSignature; +} +``` + +## Security Best Practices + +1. Always verify signatures +2. Check timestamp to prevent replay attacks +3. Use HTTPS endpoints only +4. Keep webhook secret secure + +## Resources + +- **Webhook verification**: https://developers.zoom.us/docs/api/rest/webhook-reference/#verify-webhook-events diff --git a/plugins/zoom-developers/skills/webhooks/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/webhooks/troubleshooting/common-issues.md new file mode 100644 index 00000000..155da961 --- /dev/null +++ b/plugins/zoom-developers/skills/webhooks/troubleshooting/common-issues.md @@ -0,0 +1,34 @@ +# Common Issues + +Quick diagnostics for Zoom Webhooks integrations. + +## Signature Verification Fails (401 / "Invalid signature") + +**Common causes**: +- You are computing the HMAC over a **re-serialized** body (different whitespace/key order). +- You are using the wrong secret (webhook secret vs OAuth secret). +- You are not including the `v0:{timestamp}:{body}` prefix exactly. + +**Fix**: +- Verify signatures using the exact raw request body bytes (capture raw body before JSON parsing). +- Validate both `x-zm-signature` and `x-zm-request-timestamp` and reject stale timestamps (replay protection). + +See: [verification.md](../references/verification.md) + +## Timeouts / Retries / Duplicate Events + +**Symptom**: Zoom retries delivery, you process the same event multiple times. + +**Fix**: +- Respond fast (acknowledge ASAP, then enqueue work). +- Make handlers idempotent (dedupe by event ID/timestamp + payload identifiers). + +## URL Validation Fails + +**Symptom**: You can’t enable the webhook endpoint in Marketplace; validation fails. + +**Fix**: +- Implement `endpoint.url_validation` response correctly (plainToken + encryptedToken). + +See: [verification.md](../references/verification.md) + diff --git a/plugins/zoom-developers/skills/websockets/RUNBOOK.md b/plugins/zoom-developers/skills/websockets/RUNBOOK.md new file mode 100644 index 00000000..50b3961c --- /dev/null +++ b/plugins/zoom-developers/skills/websockets/RUNBOOK.md @@ -0,0 +1,85 @@ +# WebSockets 5-Minute Preflight Runbook + +Use this before deep debugging. It catches common Zoom WebSockets failures quickly. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm OAuth Token Generation + +- Use S2S credentials and account ID. +- Token endpoint: `https://zoom.us/oauth/token`. +- Refresh token before expiry. + +### Token Sanity Checks + +- Verify token response is JSON and contains `access_token`. +- Record token expiry and refresh proactively. +- If auth intermittently fails, check for clock skew and stale cached tokens. + +## 2) Confirm Subscription Configuration + +- Event subscription created with WebSockets delivery type. +- Required event types selected and saved. + +## 3) Confirm Connection URL and Auth + +- Use exact WebSocket URL from Zoom subscription config. +- Attach access token as required by protocol/headers. + +## 4) Confirm Runtime Reliability + +- Implement reconnect with backoff. +- Handle heartbeat/ping-pong and connection lifecycle events. +- Prevent duplicate consumers if multiple workers run. + +### Minimal Reliability Policy + +- Backoff: exponential with jitter. +- Cap retries and alert after sustained failures. +- Ensure only one active consumer per subscription stream in each environment. + +## 5) Confirm Event Processing Semantics + +- Handle ordering assumptions carefully. +- Make event handlers idempotent. +- Log event IDs and delivery timestamps. + +## 6) Quick Probes + +- Access token request succeeds and returns JSON. +- WebSocket connects and receives at least one subscribed event. +- Reconnect path works after forced disconnect. + +### Copy/Paste Validation Commands + +```bash +# 1) Validate S2S token request +curl -X POST "https://zoom.us/oauth/token" \ + -H "Authorization: Basic $(printf '%s:%s' "$ZOOM_CLIENT_ID" "$ZOOM_CLIENT_SECRET" | base64)" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=account_credentials&account_id=$ZOOM_ACCOUNT_ID" + +# 2) Basic Zoom API probe with token +curl -X GET "https://api.zoom.us/v2/users/me" \ + -H "Authorization: Bearer $ZOOM_ACCESS_TOKEN" + +# 3) Tail app logs while forcing reconnect tests +pm2 logs your-websocket-service --lines 120 +``` + +Expected: token/API probes return JSON; websocket service logs show connect -> receive -> reconnect sequence. + +## 7) Fast Decision Tree + +- **Connection refused/closed** -> token invalid, wrong URL, or subscription config issue. +- **Connected but no events** -> wrong event selection or no triggering activity. +- **Event storms/duplicates** -> missing dedupe/idempotency logic. + +## 8) WebSockets vs Webhooks Guardrail + +- If your use case does not need persistent low-latency delivery, webhook delivery may be simpler to operate. +- Choose WebSockets when you can own connection lifecycle monitoring and reconnect behavior. diff --git a/plugins/zoom-developers/skills/websockets/SKILL.md b/plugins/zoom-developers/skills/websockets/SKILL.md new file mode 100644 index 00000000..dab034d4 --- /dev/null +++ b/plugins/zoom-developers/skills/websockets/SKILL.md @@ -0,0 +1,24 @@ +--- +name: setup-zoom-websockets +description: Use when building Zoom WebSockets. +--- + +# Setup Zoom WebSockets + +Use this skill when the integration needs persistent Zoom event delivery instead of HTTP webhook callbacks. If normal webhook retries and delivery are enough, prefer `setup-zoom-webhooks`. + +## Workflow + +1. Confirm WebSockets are justified by latency, firewall, connection model, or deployment constraints. +2. Configure the app and event subscriptions for the required event stream. +3. Implement connection setup, authentication, heartbeat, reconnect, backoff, and shutdown handling. +4. Normalize events into the same internal contract used by webhook handlers when both are supported. +5. Add observability for connection state, reconnect count, event lag, and dropped messages. +6. Debug by isolating token/auth problems, app subscription settings, network proxies, TLS interception, and reconnect loops. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- Connection: [references/connection.md](references/connection.md) +- Events: [references/events.md](references/events.md) +- Common issues: [troubleshooting/common-issues.md](troubleshooting/common-issues.md) diff --git a/plugins/zoom-developers/skills/websockets/references/connection.md b/plugins/zoom-developers/skills/websockets/references/connection.md new file mode 100644 index 00000000..ccfa27d4 --- /dev/null +++ b/plugins/zoom-developers/skills/websockets/references/connection.md @@ -0,0 +1,435 @@ +# WebSockets - Connection Management + +Detailed guide for managing WebSocket connections to Zoom. + +## Connection Lifecycle + +``` +1. Generate access token (S2S OAuth) + ↓ +2. Open WebSocket connection with token + ↓ +3. Receive events in real-time + ↓ +4. Handle disconnects and reconnect + ↓ +5. Close connection when done +``` + +## Authentication + +WebSocket connections require a valid Server-to-Server OAuth access token. + +### Generate Access Token + +```javascript +const axios = require('axios'); + +async function getAccessToken(accountId, clientId, clientSecret) { + const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); + + const response = await axios.post( + 'https://zoom.us/oauth/token', + new URLSearchParams({ + grant_type: 'account_credentials', + account_id: accountId + }), + { + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + return { + accessToken: response.data.access_token, + expiresIn: response.data.expires_in // Usually 3600 seconds (1 hour) + }; +} +``` + +### Token Refresh + +Access tokens expire after 1 hour. Implement token refresh before expiration: + +```javascript +class ZoomWebSocketClient { + constructor(accountId, clientId, clientSecret, subscriptionId) { + this.accountId = accountId; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.subscriptionId = subscriptionId; + this.ws = null; + this.tokenExpiry = null; + } + + async refreshTokenIfNeeded() { + const now = Date.now(); + const bufferTime = 5 * 60 * 1000; // 5 minutes before expiry + + if (!this.tokenExpiry || now >= this.tokenExpiry - bufferTime) { + const { accessToken, expiresIn } = await getAccessToken( + this.accountId, this.clientId, this.clientSecret + ); + this.accessToken = accessToken; + this.tokenExpiry = now + (expiresIn * 1000); + + // Reconnect with new token + if (this.ws) { + this.ws.close(); + await this.connect(); + } + } + } + + async connect() { + await this.refreshTokenIfNeeded(); + + const wsUrl = `wss://ws.zoom.us/ws?subscriptionId=${this.subscriptionId}&access_token=${this.accessToken}`; + this.ws = new WebSocket(wsUrl); + + // Set up event handlers... + } +} +``` + +## Connection URL + +``` +wss://ws.zoom.us/ws?subscriptionId={SUBSCRIPTION_ID}&access_token={ACCESS_TOKEN} +``` + +| Parameter | Description | +|-----------|-------------| +| `subscriptionId` | Your WebSocket subscription ID from Marketplace | +| `access_token` | Valid S2S OAuth access token | + +## Connection Limits + +| Limit | Value | +|-------|-------| +| **Connections per subscription** | 1 (opening new connection closes existing) | +| **Connection timeout** | Varies (implement keep-alive) | +| **Message size** | Check Zoom docs for current limits | + +## Keep-Alive / Heartbeat + +Maintain connection with periodic pings: + +```javascript +class WebSocketManager { + constructor() { + this.ws = null; + this.pingInterval = null; + } + + startHeartbeat() { + // Ping every 30 seconds + this.pingInterval = setInterval(() => { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.ping(); + console.log('Ping sent'); + } + }, 30000); + } + + stopHeartbeat() { + if (this.pingInterval) { + clearInterval(this.pingInterval); + this.pingInterval = null; + } + } + + connect(url) { + this.ws = new WebSocket(url); + + this.ws.on('open', () => { + console.log('Connected'); + this.startHeartbeat(); + }); + + this.ws.on('pong', () => { + console.log('Pong received - connection alive'); + }); + + this.ws.on('close', () => { + this.stopHeartbeat(); + }); + } +} +``` + +## Reconnection Strategy + +Implement exponential backoff for reconnection: + +```javascript +class ReconnectingWebSocket { + constructor(config) { + this.config = config; + this.ws = null; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 10; + this.baseDelay = 1000; // 1 second + this.maxDelay = 30000; // 30 seconds + } + + async connect() { + try { + const token = await getAccessToken( + this.config.accountId, + this.config.clientId, + this.config.clientSecret + ); + + const url = `wss://ws.zoom.us/ws?subscriptionId=${this.config.subscriptionId}&access_token=${token.accessToken}`; + + this.ws = new WebSocket(url); + + this.ws.on('open', () => { + console.log('Connected successfully'); + this.reconnectAttempts = 0; // Reset on successful connection + }); + + this.ws.on('close', (code, reason) => { + console.log(`Disconnected: ${code} - ${reason}`); + this.scheduleReconnect(); + }); + + this.ws.on('error', (error) => { + console.error('WebSocket error:', error.message); + }); + + this.ws.on('message', (data) => { + this.handleMessage(JSON.parse(data)); + }); + + } catch (error) { + console.error('Connection failed:', error.message); + this.scheduleReconnect(); + } + } + + scheduleReconnect() { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error('Max reconnection attempts reached'); + return; + } + + // Exponential backoff with jitter + const delay = Math.min( + this.baseDelay * Math.pow(2, this.reconnectAttempts) + Math.random() * 1000, + this.maxDelay + ); + + console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`); + + setTimeout(() => { + this.reconnectAttempts++; + this.connect(); + }, delay); + } + + handleMessage(event) { + // Override this method to handle events + console.log('Event:', event.event, event.payload); + } + + close() { + if (this.ws) { + this.ws.close(); + this.ws = null; + } + } +} +``` + +## Error Handling + +### Common Error Codes + +| Code | Meaning | Action | +|------|---------|--------| +| 1000 | Normal closure | Clean shutdown | +| 1001 | Going away | Server shutting down, reconnect | +| 1006 | Abnormal closure | Network issue, reconnect | +| 1008 | Policy violation | Check token validity | +| 1011 | Internal error | Server error, retry later | + +### Error Handling Example + +```javascript +ws.on('close', (code, reason) => { + switch (code) { + case 1000: + console.log('Connection closed normally'); + break; + case 1001: + case 1006: + console.log('Connection lost, reconnecting...'); + scheduleReconnect(); + break; + case 1008: + console.log('Auth error - refreshing token'); + refreshTokenAndReconnect(); + break; + default: + console.log(`Unexpected close: ${code} - ${reason}`); + scheduleReconnect(); + } +}); + +ws.on('error', (error) => { + console.error('WebSocket error:', error); + // The 'close' event will follow, handle reconnection there +}); +``` + +## Complete Example + +```javascript +const WebSocket = require('ws'); +const axios = require('axios'); + +class ZoomWebSocketClient { + constructor(config) { + this.config = config; + this.ws = null; + this.accessToken = null; + this.tokenExpiry = null; + this.pingInterval = null; + this.reconnectAttempts = 0; + this.handlers = new Map(); + } + + on(event, handler) { + this.handlers.set(event, handler); + } + + async getAccessToken() { + const credentials = Buffer.from( + `${this.config.clientId}:${this.config.clientSecret}` + ).toString('base64'); + + const response = await axios.post( + 'https://zoom.us/oauth/token', + new URLSearchParams({ + grant_type: 'account_credentials', + account_id: this.config.accountId + }), + { + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + this.accessToken = response.data.access_token; + this.tokenExpiry = Date.now() + (response.data.expires_in * 1000); + + return this.accessToken; + } + + async connect() { + await this.getAccessToken(); + + const url = `wss://ws.zoom.us/ws?subscriptionId=${this.config.subscriptionId}&access_token=${this.accessToken}`; + + this.ws = new WebSocket(url); + + this.ws.on('open', () => { + console.log('WebSocket connected'); + this.reconnectAttempts = 0; + this.startPing(); + this.scheduleTokenRefresh(); + }); + + this.ws.on('message', (data) => { + const event = JSON.parse(data); + const handler = this.handlers.get(event.event); + if (handler) { + handler(event.payload); + } + }); + + this.ws.on('close', (code, reason) => { + console.log(`Disconnected: ${code}`); + this.stopPing(); + if (code !== 1000) { + this.reconnect(); + } + }); + + this.ws.on('error', (error) => { + console.error('Error:', error.message); + }); + } + + startPing() { + this.pingInterval = setInterval(() => { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.ping(); + } + }, 30000); + } + + stopPing() { + if (this.pingInterval) { + clearInterval(this.pingInterval); + } + } + + scheduleTokenRefresh() { + const refreshIn = this.tokenExpiry - Date.now() - 300000; // 5 min before expiry + setTimeout(() => this.refreshToken(), refreshIn); + } + + async refreshToken() { + await this.getAccessToken(); + // Close and reconnect with new token + this.ws?.close(1000); + await this.connect(); + } + + reconnect() { + const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); + this.reconnectAttempts++; + console.log(`Reconnecting in ${delay}ms...`); + setTimeout(() => this.connect(), delay); + } + + disconnect() { + this.stopPing(); + this.ws?.close(1000); + } +} + +// Usage +const client = new ZoomWebSocketClient({ + accountId: process.env.ZOOM_ACCOUNT_ID, + clientId: process.env.ZOOM_CLIENT_ID, + clientSecret: process.env.ZOOM_CLIENT_SECRET, + subscriptionId: process.env.ZOOM_SUBSCRIPTION_ID +}); + +client.on('meeting.started', (payload) => { + console.log(`Meeting started: ${payload.object.topic}`); +}); + +client.on('meeting.ended', (payload) => { + console.log(`Meeting ended: ${payload.object.uuid}`); +}); + +client.on('meeting.participant_joined', (payload) => { + console.log(`Participant joined: ${payload.object.participant.user_name}`); +}); + +client.connect(); +``` + +## Resources + +- **WebSockets docs**: https://developers.zoom.us/docs/api/websockets/ +- **S2S OAuth guide**: https://developers.zoom.us/docs/internal-apps/s2s-oauth/ diff --git a/plugins/zoom-developers/skills/websockets/references/environment-variables.md b/plugins/zoom-developers/skills/websockets/references/environment-variables.md new file mode 100644 index 00000000..1336ccba --- /dev/null +++ b/plugins/zoom-developers/skills/websockets/references/environment-variables.md @@ -0,0 +1,18 @@ +# Zoom WebSockets Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_CLIENT_ID` | Yes | OAuth app identity for WebSocket subscriptions | Zoom Marketplace -> OAuth app -> App Credentials | +| `ZOOM_CLIENT_SECRET` | Yes | OAuth app secret | Zoom Marketplace -> OAuth app -> App Credentials | +| `ZOOM_ACCOUNT_ID` | S2S OAuth mode | Account-level token grant for service apps | Zoom Marketplace -> Server-to-Server OAuth app credentials | +| `ZOOM_SUBSCRIPTION_ID` | After setup | Persisted subscription identifier for reconnect/resume | Returned by subscription create API response | + +## Runtime-only values + +- `ZOOM_ACCESS_TOKEN` + +## Notes + +- `ZOOM_SUBSCRIPTION_ID` is not from Marketplace UI; your app stores it after calling the subscription API. diff --git a/plugins/zoom-developers/skills/websockets/references/events.md b/plugins/zoom-developers/skills/websockets/references/events.md new file mode 100644 index 00000000..8d4b0132 --- /dev/null +++ b/plugins/zoom-developers/skills/websockets/references/events.md @@ -0,0 +1,522 @@ +# WebSockets - Event Types + +Complete reference for events available via Zoom WebSockets. + +## Event Structure + +All WebSocket events follow this structure: + +```json +{ + "event": "event.type", + "event_ts": 1706123456789, + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + // Event-specific data + } + } +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `event` | string | Event type identifier | +| `event_ts` | number | Unix timestamp (milliseconds) | +| `payload.account_id` | string | Zoom account ID | +| `payload.object` | object | Event-specific payload | + +## Meeting Events + +### meeting.created + +Triggered when a meeting is scheduled. + +```json +{ + "event": "meeting.created", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789", + "topic": "Weekly Team Sync", + "type": 2, + "start_time": "2024-01-25T10:00:00Z", + "duration": 60, + "timezone": "America/Los_Angeles" + } + } +} +``` + +### meeting.updated + +Triggered when meeting settings are changed. + +```json +{ + "event": "meeting.updated", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "topic": "Updated: Weekly Team Sync" + }, + "old_object": { + "topic": "Weekly Team Sync" + } + } +} +``` + +### meeting.deleted + +Triggered when a meeting is deleted. + +```json +{ + "event": "meeting.deleted", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789" + } + } +} +``` + +### meeting.started + +Triggered when a meeting begins. + +```json +{ + "event": "meeting.started", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789", + "topic": "Weekly Team Sync", + "type": 2, + "start_time": "2024-01-25T10:00:00Z", + "timezone": "America/Los_Angeles" + } + } +} +``` + +### meeting.ended + +Triggered when a meeting ends. + +```json +{ + "event": "meeting.ended", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789", + "topic": "Weekly Team Sync", + "start_time": "2024-01-25T10:00:00Z", + "end_time": "2024-01-25T11:05:00Z", + "duration": 65 + } + } +} +``` + +### meeting.participant_joined + +Triggered when a participant joins the meeting. + +```json +{ + "event": "meeting.participant_joined", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789", + "participant": { + "id": "participant123", + "user_id": "user456", + "user_name": "John Doe", + "email": "john@example.com", + "join_time": "2024-01-25T10:02:00Z" + } + } + } +} +``` + +### meeting.participant_left + +Triggered when a participant leaves the meeting. + +```json +{ + "event": "meeting.participant_left", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "participant": { + "id": "participant123", + "user_name": "John Doe", + "leave_time": "2024-01-25T10:45:00Z", + "leave_reason": "left the meeting" + } + } + } +} +``` + +### meeting.sharing_started + +Triggered when screen sharing begins. + +```json +{ + "event": "meeting.sharing_started", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "participant": { + "id": "participant123", + "user_name": "John Doe" + }, + "sharing_details": { + "content": "screen", + "date_time": "2024-01-25T10:15:00Z" + } + } + } +} +``` + +### meeting.sharing_ended + +Triggered when screen sharing ends. + +```json +{ + "event": "meeting.sharing_ended", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "participant": { + "id": "participant123", + "user_name": "John Doe" + } + } + } +} +``` + +## Recording Events + +### recording.started + +Triggered when cloud recording starts. + +```json +{ + "event": "recording.started", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789", + "topic": "Weekly Team Sync", + "start_time": "2024-01-25T10:00:00Z", + "recording_start": "2024-01-25T10:01:00Z" + } + } +} +``` + +### recording.stopped + +Triggered when cloud recording stops (paused or ended). + +```json +{ + "event": "recording.stopped", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "recording_start": "2024-01-25T10:01:00Z", + "recording_end": "2024-01-25T11:00:00Z" + } + } +} +``` + +### recording.completed + +Triggered when cloud recording is processed and ready for download. + +```json +{ + "event": "recording.completed", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789", + "topic": "Weekly Team Sync", + "start_time": "2024-01-25T10:00:00Z", + "duration": 60, + "total_size": 157286400, + "recording_count": 2, + "recording_files": [ + { + "id": "file123", + "meeting_id": "abcdefgh-1234-5678-abcd-1234567890ab", + "recording_start": "2024-01-25T10:01:00Z", + "recording_end": "2024-01-25T11:00:00Z", + "file_type": "MP4", + "file_size": 104857600, + "download_url": "https://zoom.us/rec/download/...", + "status": "completed" + }, + { + "id": "file124", + "file_type": "TRANSCRIPT", + "file_size": 52428800, + "download_url": "https://zoom.us/rec/download/..." + } + ] + } + } +} +``` + +### recording.trashed + +Triggered when recording is moved to trash. + +### recording.deleted + +Triggered when recording is permanently deleted. + +### recording.recovered + +Triggered when recording is restored from trash. + +## User Events + +### user.created + +Triggered when a new user is added to the account. + +```json +{ + "event": "user.created", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": "user789", + "first_name": "Jane", + "last_name": "Smith", + "email": "jane.smith@example.com", + "type": 2, + "created_at": "2024-01-25T09:00:00Z" + } + } +} +``` + +### user.updated + +Triggered when user details are changed. + +```json +{ + "event": "user.updated", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": "user789", + "first_name": "Jane", + "last_name": "Smith-Jones" + }, + "old_object": { + "last_name": "Smith" + } + } +} +``` + +### user.deleted + +Triggered when a user is removed from the account. + +```json +{ + "event": "user.deleted", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": "user789", + "email": "jane.smith@example.com" + } + } +} +``` + +### user.deactivated + +Triggered when a user is deactivated. + +### user.activated + +Triggered when a user is activated. + +## Webinar Events + +### webinar.created + +### webinar.updated + +### webinar.deleted + +### webinar.started + +### webinar.ended + +### webinar.registration_created + +Triggered when someone registers for a webinar. + +```json +{ + "event": "webinar.registration_created", + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 9876543210, + "uuid": "webinar-uuid-here", + "registrant": { + "id": "registrant123", + "email": "attendee@example.com", + "first_name": "Attendee", + "last_name": "User", + "join_url": "https://zoom.us/w/..." + } + } + } +} +``` + +## Event Handling Example + +```javascript +const eventHandlers = { + // Meeting events + 'meeting.created': (payload) => { + console.log(`New meeting: ${payload.object.topic}`); + notifyCalendarService(payload.object); + }, + + 'meeting.started': (payload) => { + console.log(`Meeting started: ${payload.object.topic}`); + updateMeetingStatus(payload.object.id, 'in_progress'); + }, + + 'meeting.ended': (payload) => { + console.log(`Meeting ended: ${payload.object.uuid}`); + updateMeetingStatus(payload.object.id, 'completed'); + calculateAttendance(payload.object); + }, + + 'meeting.participant_joined': (payload) => { + const { participant } = payload.object; + console.log(`${participant.user_name} joined`); + trackAttendance(payload.object.id, participant); + }, + + // Recording events + 'recording.completed': (payload) => { + console.log(`Recording ready: ${payload.object.topic}`); + downloadRecordings(payload.object.recording_files); + }, + + // User events + 'user.created': (payload) => { + console.log(`New user: ${payload.object.email}`); + sendWelcomeEmail(payload.object); + } +}; + +ws.on('message', (data) => { + const event = JSON.parse(data); + const handler = eventHandlers[event.event]; + + if (handler) { + handler(event.payload); + } else { + console.log(`Unhandled event: ${event.event}`); + } +}); +``` + +## Subscribing to Events + +Configure which events to receive in your Zoom Marketplace app: + +1. Go to **Feature** → **Event Subscriptions** +2. Select **WebSockets** as method type +3. Check the events you want to receive +4. Save the subscription + +**Note:** You can modify subscriptions at any time. Changes take effect immediately. + +## Event Filtering + +If you're receiving too many events, consider: + +1. **Subscribe selectively** - Only subscribe to events you need +2. **Filter in handler** - Drop events that don't match your criteria +3. **Use multiple subscriptions** - Route different events to different handlers + +```javascript +// Filter example: Only process events for specific hosts +ws.on('message', (data) => { + const event = JSON.parse(data); + + // Only process meetings from specific hosts + const allowedHosts = ['host1@example.com', 'host2@example.com']; + + if (event.event.startsWith('meeting.') && + event.payload.object.host_id && + !allowedHosts.includes(getHostEmail(event.payload.object.host_id))) { + return; // Skip this event + } + + processEvent(event); +}); +``` + +## Resources + +- **Event reference**: https://developers.zoom.us/docs/api/rest/reference/zoom-api/events/ +- **WebSockets docs**: https://developers.zoom.us/docs/api/websockets/ diff --git a/plugins/zoom-developers/skills/websockets/references/full-guide.md b/plugins/zoom-developers/skills/websockets/references/full-guide.md new file mode 100644 index 00000000..a6ebc467 --- /dev/null +++ b/plugins/zoom-developers/skills/websockets/references/full-guide.md @@ -0,0 +1,238 @@ +# /setup-zoom-websockets + +Background reference for persistent Zoom event streams. Prefer workflow routing first, then use this file when WebSockets are plausibly better than webhooks. + +## WebSockets vs Webhooks + +| Aspect | WebSockets | Webhooks | +|--------|------------|----------| +| **Connection** | Persistent, bidirectional | One-time HTTP POST | +| **Latency** | Lower (no HTTP overhead) | Higher (new connection per event) | +| **Security** | Direct connection, no exposed endpoint | Requires endpoint validation, IP whitelisting | +| **Model** | Pull (you connect to Zoom) | Push (Zoom connects to you) | +| **State** | Stateful (maintains connection) | Stateless (each event independent) | +| **Setup** | More complex (access token, connection) | Simpler (just endpoint URL) | + +**Choose WebSockets when:** +- Real-time, low-latency updates are critical +- Security is paramount (banking, healthcare, finance) +- You don't want to expose a public endpoint +- You need bidirectional communication + +**Choose Webhooks when:** +- Simpler setup is preferred +- Small number of event notifications +- Existing HTTP infrastructure + +## Prerequisites + +- Server-to-Server OAuth app in [Zoom Marketplace](https://marketplace.zoom.us/) +- Account ID, Client ID, and Client Secret +- WebSocket subscription with events enabled + +> **Need help with S2S OAuth?** See the **[zoom-oauth](../../oauth/SKILL.md)** skill for complete authentication flows. + +> **Start troubleshooting fast:** Use the **[5-Minute Runbook](../RUNBOOK.md)** before deep debugging. + +## Quick Start + +### 1. Create Server-to-Server OAuth App + +1. Go to [Zoom Marketplace](https://marketplace.zoom.us/develop/create) +2. Create a **Server-to-Server OAuth** app +3. Copy Account ID, Client ID, Client Secret + +### 2. Enable WebSocket Subscription + +1. In your app, go to **Feature** → **Event Subscriptions** +2. Add an Event Subscription +3. Select **WebSockets** as the method type +4. Select events to subscribe to (e.g., `meeting.created`, `meeting.started`) +5. Save - an endpoint URL will be generated + +### 3. Connect via WebSocket + +```javascript +const WebSocket = require('ws'); +const axios = require('axios'); + +// Step 1: Get access token +async function getAccessToken() { + const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64'); + + const response = await axios.post( + 'https://zoom.us/oauth/token', + new URLSearchParams({ + grant_type: 'account_credentials', + account_id: ACCOUNT_ID + }), + { + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + return response.data.access_token; +} + +// Step 2: Connect to WebSocket +async function connectWebSocket() { + const accessToken = await getAccessToken(); + + // WebSocket URL from your subscription settings + const wsUrl = `wss://ws.zoom.us/ws?subscriptionId=${SUBSCRIPTION_ID}&access_token=${accessToken}`; + + const ws = new WebSocket(wsUrl); + + ws.on('open', () => { + console.log('WebSocket connection established'); + }); + + ws.on('message', (data) => { + const event = JSON.parse(data); + console.log('Event received:', event.event); + + // Handle different event types + switch (event.event) { + case 'meeting.started': + console.log(`Meeting started: ${event.payload.object.topic}`); + break; + case 'meeting.ended': + console.log(`Meeting ended: ${event.payload.object.uuid}`); + break; + case 'meeting.participant_joined': + console.log(`Participant joined: ${event.payload.object.participant.user_name}`); + break; + } + }); + + ws.on('close', (code, reason) => { + console.log(`Connection closed: ${code} - ${reason}`); + // Implement reconnection logic + }); + + ws.on('error', (error) => { + console.error('WebSocket error:', error); + }); + + return ws; +} + +connectWebSocket(); +``` + +## Event Format + +Events received via WebSocket have the same format as webhook events: + +```json +{ + "event": "meeting.started", + "event_ts": 1706123456789, + "payload": { + "account_id": "abcD3ojkdbjfg", + "object": { + "id": 1234567890, + "uuid": "abcdefgh-1234-5678-abcd-1234567890ab", + "host_id": "xyz789", + "topic": "Team Standup", + "type": 2, + "start_time": "2024-01-25T10:00:00Z", + "timezone": "America/Los_Angeles" + } + } +} +``` + +## Common Events + +| Event | Description | +|-------|-------------| +| `meeting.created` | Meeting scheduled | +| `meeting.updated` | Meeting settings changed | +| `meeting.deleted` | Meeting deleted | +| `meeting.started` | Meeting begins | +| `meeting.ended` | Meeting ends | +| `meeting.participant_joined` | Participant joins meeting | +| `meeting.participant_left` | Participant leaves meeting | +| `recording.completed` | Cloud recording ready | +| `user.created` | New user added | +| `user.updated` | User details changed | + +## Connection Management + +### Keep-Alive + +WebSocket connections require periodic heartbeats. Zoom will close idle connections. + +```javascript +// Send ping every 30 seconds +setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.ping(); + } +}, 30000); +``` + +### Reconnection + +Implement automatic reconnection for reliability: + +```javascript +function connectWithReconnect() { + const ws = connectWebSocket(); + + ws.on('close', () => { + console.log('Connection lost. Reconnecting in 5 seconds...'); + setTimeout(connectWithReconnect, 5000); + }); + + return ws; +} +``` + +### Single Connection Limit + +**Important:** Only ONE WebSocket connection can be open per subscription at a time. Opening a new connection will close the existing one. + +## Detailed References + +- **[references/connection.md](../references/connection.md)** - Connection lifecycle, authentication, error handling +- **[references/events.md](../references/events.md)** - Complete event types reference + +## Troubleshooting + +- **[troubleshooting/common-issues.md](../troubleshooting/common-issues.md)** - Subscription URL confusion, disconnects, no-events debugging + +## Sample Repositories + +### Official / Community + +| Type | Repository | Description | +|------|------------|-------------| +| Node.js | [just-zoomit/zoom-websockets](https://github.com/just-zoomit/zoom-websockets) | WebSocket sample with S2S OAuth | + +## WebSockets vs RTMS + +Don't confuse WebSockets with RTMS (Realtime Media Streams): + +| Feature | WebSockets | RTMS | +|---------|------------|------| +| **Purpose** | Event notifications | Media streams | +| **Data** | Meeting events, user events | Audio, video, transcripts | +| **Use case** | React to Zoom events | AI/ML, live transcription | +| **Skill** | This skill | **rtms** | + +For real-time audio/video/transcript data, use the **rtms** skill instead. + +## Resources + +- **WebSockets docs**: https://developers.zoom.us/docs/api/websockets/ +- **Webhooks comparison**: https://www.zoom.com/en/blog/a-guide-to-webhooks-and-websockets/ +- **Developer forum**: https://devforum.zoom.us/ + +## Environment Variables + +- See [references/environment-variables.md](../references/environment-variables.md) for standardized `.env` keys and where to find each value. diff --git a/plugins/zoom-developers/skills/websockets/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/websockets/troubleshooting/common-issues.md new file mode 100644 index 00000000..b7ec7d6d --- /dev/null +++ b/plugins/zoom-developers/skills/websockets/troubleshooting/common-issues.md @@ -0,0 +1,34 @@ +# Common Issues + +Quick diagnostics for Zoom WebSockets event subscriptions. + +## "Where Is the WebSocket URL?" + +**Symptom**: You can’t find a generic `wss://...` endpoint that works for everyone. + +**Reality**: Your connection is parameterized by your subscription (`subscriptionId`) and an access token. + +See: [connection.md](../references/connection.md) + +## Disconnects / Reconnect Loops + +**Common causes**: +- Access token expired (typically ~1 hour). +- Single-connection limit per subscription (a new connection may close the previous one). +- No heartbeat/keep-alive handling in your client. + +**Fix**: +- Refresh token proactively and reconnect with the new token. +- Implement exponential backoff (with jitter). +- Ensure only one active connection per subscription. + +## No Events Received + +**Common causes**: +- Subscribed event topics don’t match what you’re testing. +- App/subscription not enabled or not deployed as required by your account settings. + +**Fix**: +- Confirm topics in Marketplace and generate an event you actually subscribed to. +- Log raw incoming messages and validate parsing. + diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/RUNBOOK.md b/plugins/zoom-developers/skills/zoom-apps-sdk/RUNBOOK.md new file mode 100644 index 00000000..d7247f3d --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/RUNBOOK.md @@ -0,0 +1,106 @@ +# Zoom Apps SDK 5-Minute Preflight Runbook + +Use this before deep debugging. It catches common Zoom Apps integration failures quickly. + +## Skill Doc Standard Note + +- Agent-skill standard entrypoint is `SKILL.md`. +- This runbook is an operational convention (recommended), not a required skill file. +- `SKILL.md` is also a navigation convention for larger skill docs. + +## 1) Confirm App Type and Context + +- App type must be Zoom App in Marketplace. +- Confirm expected running context (`inMeeting`, `inMainClient`, `inWebinar`, etc.). + +Context mismatch often looks like missing APIs. + +## 2) Confirm Domain Allowlist + +- Whitelist the exact dev/prod domains used by your app. +- If app panel is blank or refuses to load, domain allowlist is first check. + +### Blank Panel Triage (60s) + +- Confirm app URL is HTTPS and reachable directly in a browser. +- Confirm the exact host is allowlisted in Marketplace (including subdomain differences). +- Confirm no redirect loop (watch network tab for repeated 30x responses). +- Confirm CSP/X-Frame-Options do not block Zoom embedded browser usage. +- Confirm local tunnel URL in app config matches current active tunnel. + +## 3) Confirm In-Client OAuth Setup + +- Use correct redirect/callback handling for Zoom Apps flow. +- Validate state/PKCE handling if implemented. +- Confirm scopes and re-authorize after scope changes. + +## 4) Confirm SDK Capability Usage + +- Call APIs only when supported in current context/capability set. +- Inspect initialization and capability negotiation results. + +### Capability Probe Snippet + +Use this early in app startup to avoid calling unavailable APIs: + +```javascript +import zoomSdk from '@zoom/appssdk'; + +async function probeSdk() { + const config = await zoomSdk.config({ + capabilities: [ + 'getSupportedJsApis', + 'getRunningContext', + 'authorize', + 'openUrl', + 'shareApp', + ], + }); + + console.log('runningContext:', config.runningContext); + console.log('supportedApis:', config.supportedApis || []); + + const supported = new Set(config.supportedApis || []); + if (!supported.has('authorize')) { + console.warn('authorize API unavailable in this context/capability set'); + } +} +``` + +## 5) Confirm Local Development Tunnel + +- Use stable HTTPS tunnel (ngrok or equivalent). +- Update Marketplace config when tunnel URL changes. + +## 6) Quick Probes + +- App loads inside Zoom client without blank panel. +- SDK init succeeds and returns expected capabilities. +- OAuth flow completes and API calls work with granted scopes. + +### Copy/Paste Validation Commands + +```bash +# 1) Verify app URL is reachable and returns HTML +curl -sS -i "$ZOOM_APP_URL" + +# 2) Verify OAuth callback URL is reachable +curl -sS -i "$ZOOM_APP_CALLBACK_URL" + +# 3) Verify backend token/config endpoint returns JSON +curl -sS -i "$ZOOM_APP_BASE_URL/api/config" +``` + +Expected: HTTP 200/3xx and valid HTML/JSON (not generic 404/502 pages). + +## 7) Fast Decision Tree + +- **Blank panel** -> domain allowlist, HTTPS, CSP headers. +- **API unavailable** -> wrong running context or capability not granted. +- **OAuth loop/failure** -> redirect/state/scope mismatch. + +## 8) SDK Selection Guardrail + +- Use **Zoom Apps SDK** when app runs inside Zoom client contexts. +- Use **Meeting SDK** when embedding Zoom meeting UI into your own website/app. +- If you are debugging "missing Zoom Apps APIs" in a standalone browser page, you are likely in the wrong SDK/runtime. diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/SKILL.md b/plugins/zoom-developers/skills/zoom-apps-sdk/SKILL.md new file mode 100644 index 00000000..4e48ca3d --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/SKILL.md @@ -0,0 +1,27 @@ +--- +name: zoom-apps-sdk +description: Use when using Apps SDK. +--- + +# Zoom Apps SDK + +Use this skill when the app runs inside the Zoom client. If the user only needs to embed a meeting in an external app, route to Meeting SDK instead. + +## Workflow + +1. Confirm the running context: meeting, webinar, main client, phone, collaborate mode, immersive mode, camera mode, or Layers API. +2. Configure Marketplace app settings, allowed domains, redirect URIs, and in-client OAuth before implementing SDK calls. +3. Initialize `zoomSdk` and gate features by capability and running context. +4. Design client communication and data flow: frontend SDK calls, backend REST calls, in-client OAuth tokens, and webhook handoff. +5. Implement advanced client features only after the base app loads reliably: Layers API, breakout rooms, guest mode, collaborate mode, or ZMail. +6. Debug blank panels, domain allowlist issues, CSP, missing capabilities, running-context mismatch, and OAuth redirect problems independently. + +## References + +- Full preserved guide: [references/full-guide.md](references/full-guide.md) +- Architecture: [concepts/architecture.md](concepts/architecture.md) +- Running contexts: [concepts/running-contexts.md](concepts/running-contexts.md) +- Meeting SDK versus Zoom Apps: [concepts/meeting-sdk-vs-zoom-apps.md](concepts/meeting-sdk-vs-zoom-apps.md) +- Layers API: [references/layers-api.md](references/layers-api.md) +- In-client OAuth: [examples/in-client-oauth.md](examples/in-client-oauth.md) +- Common issues: [troubleshooting/common-issues.md](troubleshooting/common-issues.md) diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/concepts/architecture.md b/plugins/zoom-developers/skills/zoom-apps-sdk/concepts/architecture.md new file mode 100644 index 00000000..269f52a0 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/concepts/architecture.md @@ -0,0 +1,222 @@ +# Zoom Apps Architecture + +## Overview + +A Zoom App is a web application that runs inside the Zoom client's embedded browser. It consists of two parts: + +- **Frontend**: Your web app loaded inside Zoom (HTML/CSS/JS + `@zoom/appssdk`) +- **Backend**: Your server handling OAuth, REST API calls, and business logic + +The SDK (`@zoom/appssdk`) is the bridge between your frontend and the Zoom client. + +## Architecture Diagram + +``` +┌─────────────────────────────────────────────────────┐ +│ ZOOM CLIENT │ +│ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ Embedded Browser (WebView) │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────┐ │ │ +│ │ │ YOUR FRONTEND WEB APP │ │ │ +│ │ │ │ │ │ +│ │ │ import zoomSdk from '@zoom/appssdk' │ │ │ +│ │ │ zoomSdk.config({...}) │ │ │ +│ │ │ zoomSdk.getMeetingContext() │ │ │ +│ │ │ │ │ │ +│ │ │ fetch('/api/data') ──────────────────────── YOUR BACKEND +│ │ └─────────────────────────────────────────┘ │ │ (Express/Node.js) +│ │ │ │ │ - OAuth token exchange +│ │ │ SDK Bridge │ │ - REST API calls +│ │ ▼ │ │ - Business logic +│ │ Zoom Client APIs │ │ - Token storage +│ │ (meeting, user, UI) │ │ +│ └──────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +## Embedded Browser Details + +Zoom uses different browser engines per platform: + +| Platform | Browser Engine | Notes | +|----------|---------------|-------| +| Windows | WebView2 (Chromium) | Modern, good DevTools | +| macOS | WKWebView (WebKit) | Safari-like behavior | +| iOS | WKWebView | Mobile viewport | +| Android | WebView | Mobile viewport | +| Some surfaces | CEF (Chromium Embedded) | Camera mode uses this | + +**Limitations:** +- No browser extensions +- Limited `window.open` support (use `zoomSdk.openUrl()` instead) +- No access to browser-level storage across different apps +- CSP must allow `frame-ancestors zoom.us *.zoom.us` +- Cookies require `SameSite=None; Secure` + +## App Lifecycle + +### Initial Install (Web OAuth) + +``` +User clicks "Add" in Marketplace + │ + ▼ +Browser opens Zoom OAuth page +(https://zoom.us/oauth/authorize?client_id=...&code_challenge=...) + │ + ▼ +User clicks "Allow" + │ + ▼ +Zoom redirects to your redirect URI with ?code=... + │ + ▼ +Your backend exchanges code + code_verifier for access_token + │ + ▼ +Backend calls GET /v2/zoomapp/deeplink with access_token + │ + ▼ +Backend redirects user to deeplink URL + │ + ▼ +Zoom client opens, loads your frontend URL in embedded browser + │ + ▼ +Frontend calls zoomSdk.config({...}) + │ + ▼ +App is ready +``` + +### Subsequent Opens (In-Client OAuth) + +``` +User opens your app in Zoom client + │ + ▼ +Zoom loads your frontend URL in embedded browser + │ + ▼ +Frontend calls zoomSdk.config({...}) + │ + ▼ +Frontend calls zoomSdk.authorize({ codeChallenge, state }) + │ + ▼ +User approves in Zoom popup (no browser redirect) + │ + ▼ +onAuthorized event fires with authorization code + │ + ▼ +Frontend sends code to backend + │ + ▼ +Backend exchanges code + code_verifier for tokens + │ + ▼ +App is authorized +``` + +## Deep Linking + +After web-based OAuth, your backend must get a deeplink to open the app in Zoom: + +```javascript +// After token exchange, get deeplink +const response = await fetch('https://api.zoom.us/v2/zoomapp/deeplink', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ action: '' }) +}); + +const { deeplink } = await response.json(); +// deeplink = 'zoommtg://zoom.us/...' or similar + +// Redirect user to open in Zoom client +res.redirect(deeplink); +``` + +## X-Zoom-App-Context Header + +When Zoom loads your frontend, it sends an `X-Zoom-App-Context` HTTP header. This encrypted header contains user and meeting context, allowing your backend to identify the user without OAuth. + +### Decryption (Node.js) + +```javascript +const crypto = require('crypto'); + +function decryptContext(header, clientSecret) { + const buf = Buffer.from(header, 'base64'); + const iv = buf.slice(0, 12); // First 12 bytes = IV + const encryptedData = buf.slice(12, buf.length - 16); // Middle = ciphertext + const tag = buf.slice(buf.length - 16); // Last 16 bytes = auth tag + + const key = crypto.createHash('sha256') + .update(clientSecret) + .digest(); + + const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); + decipher.setAuthTag(tag); + + const decrypted = Buffer.concat([ + decipher.update(encryptedData), + decipher.final() + ]); + + return JSON.parse(decrypted.toString()); +} + +// Usage in Express middleware +app.use((req, res, next) => { + const contextHeader = req.headers['x-zoom-app-context']; + if (contextHeader) { + req.zoomContext = decryptContext(contextHeader, process.env.ZOOM_APP_CLIENT_SECRET); + // { uid: '...', aud: '...', iss: 'marketplace.zoom.us', ts: ..., ... } + } + next(); +}); +``` + +The decrypted context contains: +- `uid` - Zoom user ID +- `mid` - Meeting ID (if in meeting) +- `aud` - Your app's client ID +- `iss` - Issuer (`marketplace.zoom.us`) +- `ts` - Timestamp + +## Data Access Layers + +Zoom Apps can access data through three layers: + +| Layer | Method | Data Available | Auth Required | +|-------|--------|----------------|---------------| +| **Contextual** | SDK APIs (`getMeetingContext`, etc.) | Meeting/user/participant info | config() only | +| **Server-side** | REST API (via backend) | Full Zoom API (users, meetings, recordings) | OAuth tokens | +| **Header** | X-Zoom-App-Context header | User identity, meeting context | Client secret | + +## Domain Allowlist + +The Zoom client will **only** load URLs from domains in your app's allowlist. + +**Required domains:** +- Your app domain (e.g., `yourdomain.com`) +- `appssdk.zoom.us` (if using CDN) +- Any CDN domains (fonts, CSS, images) +- Any API domains your frontend calls directly + +**Configure in:** Marketplace -> Your App -> Feature -> Zoom App -> Add Allow List + +Without this, the embedded browser shows a blank panel with no error. + +## Resources + +- **Architecture docs**: https://developers.zoom.us/docs/zoom-apps/architecture/ +- **Data access**: https://developers.zoom.us/docs/zoom-apps/data-access/ +- **X-Zoom-App-Context**: https://developers.zoom.us/docs/zoom-apps/zoom-app-context/ diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/concepts/meeting-sdk-vs-zoom-apps.md b/plugins/zoom-developers/skills/zoom-apps-sdk/concepts/meeting-sdk-vs-zoom-apps.md new file mode 100644 index 00000000..990109f4 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/concepts/meeting-sdk-vs-zoom-apps.md @@ -0,0 +1,34 @@ +--- +title: "Zoom Apps vs Meeting SDK (Common Confusion)" +--- + +# Zoom Apps vs Meeting SDK (Common Confusion) + +Forum pattern: developers try to use Meeting SDK concepts inside Zoom Apps, or vice versa. + +## Zoom Apps (Apps SDK) + +Use when you want a **web app that runs inside the Zoom client**: + +- in-meeting panel +- main client +- webinar contexts +- Layers API (immersive / camera mode) +- collaborate mode (shared state) + +The app runs in Zoom’s embedded browser and interacts with the meeting via `@zoom/appssdk` capabilities. + +## Meeting SDK + +Use when you want to **embed the Zoom meeting experience in your own app** (outside the Zoom client): + +- your website or desktop app hosts the meeting UI +- you handle signature generation server-side + +## The Practical Decision + +- “I want a sidebar app inside Zoom” -> Zoom Apps SDK +- “I want to embed Zoom meeting UI inside my product” -> Meeting SDK + +If the question is “show contents in Zoom App via Meeting SDK”, first clarify which experience they actually want (inside Zoom vs embedded in their product). + diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/concepts/running-contexts.md b/plugins/zoom-developers/skills/zoom-apps-sdk/concepts/running-contexts.md new file mode 100644 index 00000000..60f4534c --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/concepts/running-contexts.md @@ -0,0 +1,190 @@ +# Running Contexts + +## Overview + +A Zoom App can run in multiple surfaces within the Zoom client. The `runningContext` property returned by `config()` tells you where your app is currently loaded. + +```javascript +const configResponse = await zoomSdk.config({ + capabilities: ['getMeetingContext', 'getUserContext', ...], + version: '0.16' +}); + +console.log(configResponse.runningContext); +// 'inMeeting' | 'inMainClient' | 'inWebinar' | 'inImmersive' | ... +``` + +## All Running Contexts + +| Context | Surface | Meeting APIs | User APIs | Layers APIs | Notes | +|---------|---------|-------------|-----------|-------------|-------| +| `inMeeting` | Meeting sidebar | Yes | Yes | Yes | Most common context | +| `inMainClient` | Main client panel | No | Yes | No | Home tab, no meeting running | +| `inWebinar` | Webinar sidebar | Yes | Yes | Yes | Host/panelist initially | +| `inImmersive` | Layers full-screen | Limited | Yes | Yes | After runRenderingContext | +| `inCamera` | Camera mode | Limited | Yes | Camera only | Virtual camera overlay | +| `inCollaborate` | Collaborate mode | Yes | Yes | No | Shared state context | +| `inPhone` | Zoom Phone | No | Yes | No | Phone call app surface | +| `inChat` | Team Chat | No | Yes | No | Chat sidebar | + +## Context-Specific Behavior + +### inMeeting (Most Common) + +The primary context. Your app appears as a sidebar panel during a meeting. + +```javascript +if (configResponse.runningContext === 'inMeeting') { + const meeting = await zoomSdk.getMeetingContext(); + console.log('Meeting ID:', meeting.meetingID); + console.log('Topic:', meeting.meetingTopic); + + const user = await zoomSdk.getUserContext(); + console.log('Name:', user.screenName); + console.log('Role:', user.role); // 'host' | 'coHost' | 'attendee' +} +``` + +Available: All meeting APIs, sharing, invitations, breakout rooms, recording. + +### inMainClient + +Your app runs in the main Zoom window (not during a meeting). Used for dashboards, settings, pre-meeting setup. + +```javascript +if (configResponse.runningContext === 'inMainClient') { + // NO meeting APIs available - getMeetingContext() will fail + const user = await zoomSdk.getUserContext(); + console.log('Name:', user.screenName); + + // Can still use connect() to sync with meeting instance later +} +``` + +**Key limitation:** No meeting context, no participants, no sharing. + +### inWebinar + +Similar to inMeeting but for webinars. Initially only available to host and panelists. + +```javascript +if (configResponse.runningContext === 'inWebinar') { + const user = await zoomSdk.getUserContext(); + // role: 'host' | 'panelist' | 'attendee' + if (user.role === 'attendee') { + // Limited functionality for attendees + } +} +``` + +### inImmersive / inCamera + +Layers API contexts. Your app has taken over the video rendering. + +- `inImmersive`: Full-screen custom layout (replaces gallery view) +- `inCamera`: Overlay on user's camera feed + +See [Layers API Reference](../references/layers-api.md) for details. + +## Multiple Instances + +A Zoom App can have **two instances running simultaneously**: + +1. **Main client instance** (`inMainClient`) - Always available +2. **Meeting instance** (`inMeeting`) - Created when user opens app in meeting + +### Instance Communication + +Use `connect()` and `postMessage()` to sync between instances: + +```javascript +// Both instances call connect() +await zoomSdk.connect(); + +// Listen for connection +zoomSdk.addEventListener('onConnect', (event) => { + console.log('Connected to other instance'); +}); + +// Send data to other instance +await zoomSdk.postMessage({ type: 'settings', data: mySettings }); + +// Receive data from other instance +zoomSdk.addEventListener('onMessage', (event) => { + const { type, data } = JSON.parse(event.payload); + if (type === 'settings') { + applySettings(data); + } +}); +``` + +**Pattern: Pre-Meeting Setup** + +``` +Main Client Instance Meeting Instance +───────────────────── ───────────────── +User configures settings --> connect() + listen +Store in state --> onConnect fires + postMessage('getSettings') +onMessage('getSettings') --> +postMessage(settings) --> onMessage(settings) + Apply settings to meeting +``` + +## Detecting Context at Runtime + +```javascript +import zoomSdk from '@zoom/appssdk'; + +async function init() { + const config = await zoomSdk.config({ + capabilities: [ + 'getMeetingContext', 'getUserContext', 'getRunningContext', + 'connect', 'postMessage', 'onConnect', 'onMessage', + 'shareApp', 'runRenderingContext' + ], + version: '0.16' + }); + + switch (config.runningContext) { + case 'inMeeting': + case 'inWebinar': + initMeetingUI(); + break; + case 'inMainClient': + initDashboardUI(); + break; + case 'inImmersive': + case 'inCamera': + initLayersUI(); + break; + default: + initFallbackUI(); + } +} +``` + +## Checking API Availability + +Not all APIs are available in all contexts. Use `getSupportedJsApis()` to check: + +```javascript +const { supportedApis } = await zoomSdk.getSupportedJsApis(); + +if (supportedApis.includes('authorize')) { + // In-Client OAuth is available + showAuthButton(); +} + +if (supportedApis.includes('runRenderingContext')) { + // Layers API is available + showLayersButton(); +} +``` + +Also check `configResponse.unsupportedApis` after `config()` for capabilities that were requested but not available in the current client version. + +## Resources + +- **Running contexts docs**: https://developers.zoom.us/docs/zoom-apps/guides/in-client-experience/ +- **Instance communication**: https://developers.zoom.us/docs/zoom-apps/guides/collaborate-mode/ diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/concepts/security.md b/plugins/zoom-developers/skills/zoom-apps-sdk/concepts/security.md new file mode 100644 index 00000000..451a32a4 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/concepts/security.md @@ -0,0 +1,157 @@ +# Security + +## Overview + +Zoom Apps must meet security requirements for Marketplace approval. This covers required headers, CSP configuration, cookie security, and token storage. + +## Required OWASP Headers + +Zoom's security review requires these HTTP headers on all responses: + +| Header | Required Value | Purpose | +|--------|---------------|---------| +| `Strict-Transport-Security` | `max-age=31536000` | Force HTTPS for 1 year | +| `X-Content-Type-Options` | `nosniff` | Prevent MIME type sniffing | +| `Content-Security-Policy` | `frame-ancestors 'self' zoom.us *.zoom.us` | Allow Zoom to embed your app | +| `Referrer-Policy` | `same-origin` | Limit referrer info | +| `X-Frame-Options` | `ALLOW-FROM zoom.us` | Legacy frame control | + +### Express Implementation + +```javascript +app.use((req, res, next) => { + res.setHeader('Strict-Transport-Security', 'max-age=31536000'); + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('Content-Security-Policy', + "frame-ancestors 'self' zoom.us *.zoom.us" + ); + res.setHeader('Referrer-Policy', 'same-origin'); + next(); +}); +``` + +**Critical:** The `frame-ancestors` CSP directive is what allows Zoom's embedded browser to load your app. Without it, the browser blocks the frame. + +## TLS Requirements + +- HTTPS required for all endpoints (minimum TLS 1.2) +- HTTP redirects to HTTPS +- Valid SSL certificate (self-signed will fail) +- ngrok provides HTTPS automatically for local development + +## Cookie Security + +Zoom's embedded browser requires specific cookie settings: + +```javascript +// Express cookie-session example +app.use(require('cookie-session')({ + name: 'session', + keys: [process.env.SESSION_SECRET], + maxAge: 24 * 60 * 60 * 1000, // 24 hours + sameSite: 'none', // REQUIRED - Zoom embeds your app cross-origin + secure: true // REQUIRED - SameSite=None requires Secure +})); +``` + +**Why `SameSite=None`?** Your app runs inside Zoom's embedded browser, which is a different origin. Without `SameSite=None`, the browser won't send cookies to your server, and sessions break silently. + +## PKCE OAuth Security + +All Zoom Apps OAuth flows should use PKCE (Proof Key for Code Exchange): + +```javascript +const crypto = require('crypto'); + +// Generate PKCE pair +const verifier = crypto.randomBytes(32).toString('hex'); +const challenge = crypto.createHash('sha256') + .update(verifier) + .digest('base64url'); + +// Store verifier in session (server-side) +req.session.codeVerifier = verifier; + +// Send challenge to frontend (or include in OAuth redirect URL) +res.json({ codeChallenge: challenge, state: req.session.state }); +``` + +PKCE prevents authorization code interception attacks. The `code_verifier` never leaves your server. + +## Token Storage + +**Never store tokens in the frontend (localStorage, sessionStorage, cookies).** + +| Storage | When to Use | Security | +|---------|-------------|----------| +| **Redis** | Multi-instance servers, production | Fast, TTL support, scalable | +| **Encrypted session** | Single server, simple apps | Tied to server process | +| **Firestore/DynamoDB** | Serverless (Firebase/Lambda) | Persistent, managed | +| **Database (encrypted)** | Complex apps with user accounts | Full control, encrypted at rest | + +```javascript +// Redis token storage example +const Redis = require('ioredis'); +const redis = new Redis(process.env.REDIS_URL); + +async function storeTokens(zoomUserId, tokens) { + await redis.set( + `zoom:tokens:${zoomUserId}`, + JSON.stringify(tokens), + 'EX', tokens.expires_in // Auto-expire with token + ); +} + +async function getTokens(zoomUserId) { + const data = await redis.get(`zoom:tokens:${zoomUserId}`); + return data ? JSON.parse(data) : null; +} +``` + +## State Parameter (CSRF Protection) + +Always validate the OAuth `state` parameter: + +```javascript +const crypto = require('crypto'); + +// Generate state before OAuth redirect +const state = crypto.randomBytes(16).toString('hex'); +req.session.oauthState = state; + +// Validate state on callback +app.get('/auth', (req, res) => { + if (req.query.state !== req.session.oauthState) { + return res.status(403).send('Invalid state - possible CSRF attack'); + } + // Proceed with token exchange +}); +``` + +## Data Access Layers + +| Layer | What You Access | Authorization | Risk Level | +|-------|----------------|---------------|------------| +| **SDK (contextual)** | Meeting context, user info, UI controls | config() capabilities | Low - scoped to current context | +| **REST API (server-side)** | Full Zoom API (users, meetings, recordings) | OAuth access tokens | Medium - broader data access | +| **X-Zoom-App-Context** | User identity, meeting info | Client secret decryption | Low - read-only identity | + +**Principle of least privilege:** Only request the OAuth scopes and SDK capabilities you actually need. + +## Marketplace Security Review Checklist + +- [ ] All OWASP headers set on every response +- [ ] HTTPS enforced (no HTTP endpoints) +- [ ] PKCE used for OAuth flows +- [ ] State parameter validated on OAuth callbacks +- [ ] Tokens stored server-side (never in frontend) +- [ ] Token refresh implemented (tokens expire in 1 hour) +- [ ] `SameSite=None; Secure` on cookies +- [ ] CSP allows `frame-ancestors zoom.us *.zoom.us` +- [ ] No sensitive data logged to console in production +- [ ] Environment variables for all secrets (no hardcoded credentials) + +## Resources + +- **Security docs**: https://developers.zoom.us/docs/zoom-apps/security/ +- **OWASP headers**: https://developers.zoom.us/docs/zoom-apps/security/owasp/ diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/examples/app-communication.md b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/app-communication.md new file mode 100644 index 00000000..3fd2c0dd --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/app-communication.md @@ -0,0 +1,152 @@ +# App Instance Communication + +Sync data between main client and meeting instances using connect() + postMessage(). + +## Overview + +A Zoom App can run two instances simultaneously: +- **Main client instance** (`inMainClient`) - for settings, dashboards +- **Meeting instance** (`inMeeting`) - for in-meeting features + +Use `connect()` and `postMessage()` to pass data between them. + +## Basic Setup + +```javascript +import zoomSdk from '@zoom/appssdk'; + +await zoomSdk.config({ + capabilities: ['connect', 'postMessage', 'onConnect', 'onMessage'], + version: '0.16' +}); + +// Both instances must call connect() +await zoomSdk.connect(); + +// Know when the other instance connects +zoomSdk.addEventListener('onConnect', (event) => { + console.log('Other instance connected'); +}); + +// Send a message +await zoomSdk.postMessage({ + payload: JSON.stringify({ type: 'greeting', data: 'Hello from this instance!' }) +}); + +// Receive messages +zoomSdk.addEventListener('onMessage', (event) => { + const message = JSON.parse(event.payload); + console.log('Received:', message.type, message.data); +}); +``` + +## Pattern: Pre-Meeting Settings + +Configure settings in the main client, use them during the meeting. + +### Main Client Instance + +```javascript +// Running in inMainClient context +const config = await zoomSdk.config({ + capabilities: ['connect', 'postMessage', 'onConnect', 'onMessage', 'getRunningContext'], + version: '0.16' +}); + +if (config.runningContext === 'inMainClient') { + await zoomSdk.connect(); + + // User configures settings + const settings = { + theme: 'dark', + language: 'en', + notifications: true + }; + + // When meeting instance asks for settings, send them + zoomSdk.addEventListener('onMessage', async (event) => { + const msg = JSON.parse(event.payload); + if (msg.type === 'request-settings') { + await zoomSdk.postMessage({ + payload: JSON.stringify({ type: 'settings', data: settings }) + }); + } + }); +} +``` + +### Meeting Instance + +```javascript +// Running in inMeeting context +if (config.runningContext === 'inMeeting') { + await zoomSdk.connect(); + + let settings = null; + + zoomSdk.addEventListener('onMessage', (event) => { + const msg = JSON.parse(event.payload); + if (msg.type === 'settings') { + settings = msg.data; + applySettings(settings); + } + }); + + // Request settings from main client instance + zoomSdk.addEventListener('onConnect', async () => { + await zoomSdk.postMessage({ + payload: JSON.stringify({ type: 'request-settings' }) + }); + }); +} +``` + +## Message Protocol + +Define a consistent message format: + +```javascript +// Message types +const MessageType = { + REQUEST_SETTINGS: 'request-settings', + SETTINGS: 'settings', + STATE_UPDATE: 'state-update', + ACTION: 'action' +}; + +// Send helper +async function send(type, data = {}) { + await zoomSdk.postMessage({ + payload: JSON.stringify({ type, data, timestamp: Date.now() }) + }); +} + +// Receive handler +zoomSdk.addEventListener('onMessage', (event) => { + const { type, data } = JSON.parse(event.payload); + + switch (type) { + case MessageType.REQUEST_SETTINGS: + send(MessageType.SETTINGS, currentSettings); + break; + case MessageType.STATE_UPDATE: + applyState(data); + break; + case MessageType.ACTION: + handleAction(data); + break; + } +}); +``` + +## Important Notes + +- Messages are JSON strings (must stringify/parse) +- Both instances must call `connect()` independently +- `onConnect` fires when the other instance connects +- Messages are one-to-one (not broadcast to all participants) +- If one instance isn't running, messages are lost (no queue) + +## Resources + +- **In-client experience**: https://developers.zoom.us/docs/zoom-apps/guides/in-client-experience/ diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/examples/breakout-rooms.md b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/breakout-rooms.md new file mode 100644 index 00000000..4657d93c --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/breakout-rooms.md @@ -0,0 +1,113 @@ +# Breakout Room Integration + +Detect breakout rooms, track room changes, and sync state across rooms. + +## Overview + +When a meeting uses breakout rooms, each room gets its own meeting UUID. Your app needs to handle room transitions and optionally sync state between rooms. + +## Key Concepts + +| Property | Description | +|----------|-------------| +| `meetingUUID` | Unique per room (changes when moving to breakout) | +| `parentUUID` | Original meeting UUID (available in breakout rooms) | +| `onBreakoutRoomChange` | Event fired when user moves between rooms | + +## Detecting Room Changes + +```javascript +import zoomSdk from '@zoom/appssdk'; + +await zoomSdk.config({ + capabilities: [ + 'getMeetingUUID', 'getMeetingContext', + 'onBreakoutRoomChange' + ], + version: '0.16' +}); + +// Get current room info +const { meetingUUID } = await zoomSdk.getMeetingUUID(); +console.log('Current room UUID:', meetingUUID); + +// Listen for room changes +zoomSdk.addEventListener('onBreakoutRoomChange', async (event) => { + console.log('Room changed:', event); + + // Get new room UUID + const { meetingUUID: newUUID } = await zoomSdk.getMeetingUUID(); + console.log('Now in room:', newUUID); + + // Re-initialize room-specific state + await loadRoomState(newUUID); +}); +``` + +## Cross-Room State Sync + +Use your backend to sync state across rooms. The `parentUUID` links all breakout rooms to the main meeting. + +```javascript +// Frontend: Register with backend on room entry +async function registerRoom() { + const { meetingUUID } = await zoomSdk.getMeetingUUID(); + const context = await zoomSdk.getMeetingContext(); + + await fetch('/api/room/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + roomUUID: meetingUUID, + parentUUID: context.parentUUID || meetingUUID, + // parentUUID is null in main room, set in breakout rooms + }) + }); +} + +// Backend: Aggregate state across all rooms +app.get('/api/meeting/:parentUUID/state', async (req, res) => { + const rooms = await db.find({ parentUUID: req.params.parentUUID }); + const aggregated = rooms.reduce((acc, room) => { + // Merge state from all rooms + return { ...acc, ...room.state }; + }, {}); + res.json(aggregated); +}); +``` + +## Managing Breakout Rooms via REST API + +Create and manage breakout rooms from your backend using the Zoom REST API: + +```javascript +// Create breakout rooms (requires meeting:write scope) +const response = await axios.post( + `https://api.zoom.us/v2/meetings/${meetingId}/batch_registrants`, + { + rooms: [ + { name: 'Room 1', participants: ['user1@example.com'] }, + { name: 'Room 2', participants: ['user2@example.com'] } + ] + }, + { headers: { 'Authorization': `Bearer ${accessToken}` } } +); +``` + +## Pattern: Room-Specific vs Global State + +```javascript +const { meetingUUID } = await zoomSdk.getMeetingUUID(); +const context = await zoomSdk.getMeetingContext(); +const parentUUID = context.parentUUID || meetingUUID; + +// Room-specific state (e.g., room discussion notes) +const roomState = await loadState(`room:${meetingUUID}`); + +// Global state (e.g., meeting agenda visible in all rooms) +const globalState = await loadState(`meeting:${parentUUID}`); +``` + +## Resources + +- **Breakout rooms docs**: https://developers.zoom.us/docs/zoom-apps/guides/breakout-rooms/ diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/examples/collaborate-mode.md b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/collaborate-mode.md new file mode 100644 index 00000000..4f9874e1 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/collaborate-mode.md @@ -0,0 +1,171 @@ +# Collaborate Mode + +Real-time shared app state across all meeting participants. + +## Overview + +Collaborate mode lets participants share an app experience simultaneously - like Google Docs for Zoom Apps. When one user makes a change, all participants see it in real time. + +## Starting Collaborate Mode + +```javascript +import zoomSdk from '@zoom/appssdk'; + +await zoomSdk.config({ + capabilities: [ + 'startCollaborate', 'onCollaborateChange', + 'connect', 'postMessage', 'onConnect', 'onMessage', + 'getMeetingUUID' + ], + version: '0.16' +}); + +// Host starts collaborate mode +await zoomSdk.startCollaborate({ + shareScreen: true // Also share the app screen +}); + +// Listen for collaborate state changes +zoomSdk.addEventListener('onCollaborateChange', (event) => { + console.log('Collaborate state changed:', event); +}); +``` + +## State Synchronization Patterns + +### Pattern 1: Server Relay (Socket.io) + +Best for apps with a backend. Server is the source of truth. + +```javascript +// Frontend +const socket = io('https://your-server.com'); +const meetingUUID = await zoomSdk.getMeetingUUID(); + +socket.emit('join-room', { roomId: meetingUUID.meetingUUID }); + +// Send state changes +function updateState(key, value) { + socket.emit('state-update', { key, value }); +} + +// Receive state changes +socket.on('state-update', ({ key, value }) => { + applyStateChange(key, value); +}); +``` + +### Pattern 2: SDK Message Passing + +No server needed. Direct peer messaging via `connect()` + `postMessage()`. + +```javascript +// All participants connect +await zoomSdk.connect(); + +zoomSdk.addEventListener('onConnect', () => { + console.log('Connected to other instances'); +}); + +// Send state to all connected instances +async function broadcastState(state) { + await zoomSdk.postMessage({ + payload: JSON.stringify({ type: 'state-sync', data: state }) + }); +} + +// Receive state from other instances +zoomSdk.addEventListener('onMessage', (event) => { + const message = JSON.parse(event.payload); + if (message.type === 'state-sync') { + applyState(message.data); + } +}); +``` + +### Pattern 3: CRDT (Y.js) + +Best for collaborative editing (text, whiteboards). Conflict-free resolution. + +```javascript +import * as Y from 'yjs'; +import { WebrtcProvider } from 'y-webrtc'; + +// Use meeting UUID as room name +const meetingUUID = await zoomSdk.getMeetingUUID(); + +const ydoc = new Y.Doc(); +const provider = new WebrtcProvider(meetingUUID.meetingUUID, ydoc, { + signaling: ['wss://your-signaling-server.com'] +}); + +// Shared state automatically syncs via CRDT +const sharedMap = ydoc.getMap('app-state'); + +// Update state (syncs to all peers automatically) +sharedMap.set('counter', (sharedMap.get('counter') || 0) + 1); + +// Observe changes +sharedMap.observe((event) => { + event.keysChanged.forEach((key) => { + console.log(`${key} changed to:`, sharedMap.get(key)); + }); +}); +``` + +**Note:** The texteditor sample app uses public Y.js signaling servers. For production, host your own signaling server. + +## Example: Shared Counter + +```javascript +import zoomSdk from '@zoom/appssdk'; + +let count = 0; + +async function init() { + await zoomSdk.config({ + capabilities: ['connect', 'postMessage', 'onConnect', 'onMessage'], + version: '0.16' + }); + + await zoomSdk.connect(); + + zoomSdk.addEventListener('onMessage', (event) => { + const msg = JSON.parse(event.payload); + if (msg.type === 'count-update') { + count = msg.count; + render(); + } + }); + + render(); +} + +async function increment() { + count++; + render(); + await zoomSdk.postMessage({ + payload: JSON.stringify({ type: 'count-update', count }) + }); +} + +function render() { + document.getElementById('counter').textContent = count; +} +``` + +## Meeting UUID as Room Identifier + +Use `getMeetingUUID()` to get a unique room ID for state synchronization: + +```javascript +const { meetingUUID } = await zoomSdk.getMeetingUUID(); +// Use as Socket.io room, Y.js document name, Redis key, etc. +``` + +This UUID is unique per meeting instance and consistent across all participants. + +## Resources + +- **Collaborate docs**: https://developers.zoom.us/docs/zoom-apps/guides/collaborate-mode/ +- **Sample app**: https://github.com/zoom/zoomapps-texteditor-vuejs diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/examples/guest-mode.md b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/guest-mode.md new file mode 100644 index 00000000..1f19a13e --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/guest-mode.md @@ -0,0 +1,121 @@ +# Guest Mode + +Handle unauthenticated meeting participants who haven't authorized your app. + +## Overview + +When a meeting has external guests (non-Zoom users or users who haven't installed your app), they enter your app in an unauthenticated state. Guest mode lets you progressively request authorization. + +## Three Authorization States + +``` +Unauthenticated ──> Authenticated ──> Authorized + │ │ │ + │ │ │ + No Zoom identity Has Zoom identity Has OAuth tokens + External guest Zoom user, no app Full app access + Limited UI Can promptAuthorize All APIs available +``` + +| State | Who | getUserContext Returns | Can Use APIs | +|-------|-----|----------------------|-------------| +| **Unauthenticated** | External guests, no Zoom account | Minimal (no user ID) | Very limited | +| **Authenticated** | Zoom users who haven't authorized your app | Name, role (no personal data) | Read-only context | +| **Authorized** | Users who authorized your app | Full user data | All APIs | + +## Detecting State + +```javascript +import zoomSdk from '@zoom/appssdk'; + +await zoomSdk.config({ + capabilities: ['getUserContext', 'authorize', 'promptAuthorize', 'onAuthorized'], + version: '0.16' +}); + +const user = await zoomSdk.getUserContext(); + +if (user.status === 'authorized') { + showFullApp(); +} else if (user.status === 'authenticated') { + showLimitedApp(); + showAuthorizeButton(); +} else { + showGuestView(); + showAuthorizeButton(); +} +``` + +## Prompting Authorization + +```javascript +// Show a button that triggers authorization +document.getElementById('authorize-btn').addEventListener('click', async () => { + try { + await zoomSdk.promptAuthorize(); + } catch (e) { + console.error('Authorization prompt failed:', e); + } +}); + +// Listen for successful authorization +zoomSdk.addEventListener('onAuthorized', async (event) => { + const { code, state } = event; + + // Exchange code for tokens on your backend + await fetch('/api/auth/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code, state }) + }); + + // Upgrade UI to full access + showFullApp(); +}); +``` + +## UI Pattern + +```javascript +function showGuestView() { + document.getElementById('app').innerHTML = ` +

Welcome, Guest!

+

You're viewing this app as a guest. Authorize to unlock all features.

+
+ +
+ + `; +} + +function showLimitedApp() { + document.getElementById('app').innerHTML = ` +

Welcome!

+

Authorize to unlock all features.

+
+ +
+ + `; +} + +function showFullApp() { + document.getElementById('app').innerHTML = ` +

Full App Access

+
+ +
+ `; +} +``` + +## Host-Required Authorization + +Meeting hosts can require all participants to authorize before using the app. This is configured in the Marketplace app settings under the "Guest Mode" feature. + +When enabled, unauthenticated/authenticated users see the authorization prompt immediately. + +## Resources + +- **Guest mode docs**: https://developers.zoom.us/docs/zoom-apps/guides/guest-mode/ +- **Sample app**: https://github.com/zoom/zoomapps-advancedsample-react (includes guest mode) diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/examples/in-client-oauth.md b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/in-client-oauth.md new file mode 100644 index 00000000..af135c28 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/in-client-oauth.md @@ -0,0 +1,222 @@ +# In-Client OAuth with PKCE + +Authorize users without leaving the Zoom client. Best UX for returning users. + +## Why In-Client OAuth? + +| Approach | UX | When to Use | +|----------|----|-------------| +| **Web redirect** | Opens browser, redirects back | Initial install from Marketplace | +| **In-Client** | Popup inside Zoom, no redirect | Subsequent authorizations, best UX | + +## Flow + +``` +Frontend Backend Zoom +──────── ──────── ──── +GET /api/auth/challenge --> + <-- { codeChallenge, state } + (stores code_verifier in session) + +zoomSdk.authorize({ --> --> Shows "Authorize" popup + codeChallenge, state to user +}) + +onAuthorized fires <-- <-- User clicks "Allow" +{ code, state } + +POST /api/auth/token --> +{ code, state } Validates state + Exchanges code + verifier + for access_token + <-- { success: true } +``` + +## Frontend Implementation + +```javascript +import zoomSdk from '@zoom/appssdk'; + +async function init() { + await zoomSdk.config({ + capabilities: ['authorize', 'onAuthorized', 'getUserContext'], + version: '0.16' + }); + + // Check if already authorized + try { + const response = await fetch('/api/auth/status'); + const { authorized } = await response.json(); + if (authorized) { + showApp(); + return; + } + } catch (e) { + // Not authorized yet + } + + // Set up authorization listener BEFORE calling authorize + zoomSdk.addEventListener('onAuthorized', async (event) => { + const { code, state } = event; + + const response = await fetch('/api/auth/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code, state }) + }); + + if (response.ok) { + showApp(); + } else { + showError('Authorization failed'); + } + }); + + // Get challenge and start authorization + const challengeResponse = await fetch('/api/auth/challenge'); + const { codeChallenge, state } = await challengeResponse.json(); + + await zoomSdk.authorize({ codeChallenge, state }); +} +``` + +## Backend Implementation + +```javascript +const crypto = require('crypto'); +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); + +// Generate PKCE challenge +router.get('/api/auth/challenge', (req, res) => { + const verifier = crypto.randomBytes(32).toString('hex'); + const challenge = crypto.createHash('sha256') + .update(verifier) + .digest('base64url'); + const state = crypto.randomBytes(16).toString('hex'); + + // Store in session (server-side only) + req.session.codeVerifier = verifier; + req.session.state = state; + + res.json({ codeChallenge: challenge, state }); +}); + +// Exchange authorization code for tokens +router.post('/api/auth/token', async (req, res) => { + const { code, state } = req.body; + + // Validate state (CSRF protection) + if (state !== req.session.state) { + return res.status(403).json({ error: 'Invalid state' }); + } + + try { + const tokenResponse = await axios.post('https://zoom.us/oauth/token', null, { + params: { + grant_type: 'authorization_code', + code, + redirect_uri: process.env.ZOOM_APP_REDIRECT_URI, + code_verifier: req.session.codeVerifier + }, + headers: { + 'Authorization': 'Basic ' + Buffer.from( + `${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}` + ).toString('base64') + } + }); + + // Store tokens securely (session, Redis, or database) + req.session.tokens = { + access_token: tokenResponse.data.access_token, + refresh_token: tokenResponse.data.refresh_token, + expires_at: Date.now() + (tokenResponse.data.expires_in * 1000) + }; + + // Clean up PKCE data + delete req.session.codeVerifier; + delete req.session.state; + + res.json({ success: true }); + } catch (error) { + console.error('Token exchange failed:', error.response?.data || error.message); + res.status(500).json({ error: 'Token exchange failed' }); + } +}); + +// Check authorization status +router.get('/api/auth/status', (req, res) => { + const tokens = req.session.tokens; + const authorized = tokens && tokens.expires_at > Date.now(); + res.json({ authorized }); +}); + +// Token refresh helper +async function refreshTokens(req) { + const { refresh_token } = req.session.tokens; + + const response = await axios.post('https://zoom.us/oauth/token', null, { + params: { + grant_type: 'refresh_token', + refresh_token + }, + headers: { + 'Authorization': 'Basic ' + Buffer.from( + `${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}` + ).toString('base64') + } + }); + + req.session.tokens = { + access_token: response.data.access_token, + refresh_token: response.data.refresh_token, + expires_at: Date.now() + (response.data.expires_in * 1000) + }; + + return req.session.tokens; +} + +module.exports = router; +``` + +## Re-Authorization with promptAuthorize + +For guest mode users who need to upgrade their authorization: + +```javascript +// Use when user needs to grant additional permissions +await zoomSdk.promptAuthorize(); + +// Same onAuthorized listener fires +zoomSdk.addEventListener('onAuthorized', async (event) => { + // Handle same as initial authorization +}); +``` + +## Token Refresh Pattern + +```javascript +// Middleware to auto-refresh expired tokens +async function ensureAuthorized(req, res, next) { + if (!req.session.tokens) { + return res.status(401).json({ error: 'Not authorized' }); + } + + // Refresh if expiring within 5 minutes + if (req.session.tokens.expires_at < Date.now() + 300000) { + try { + await refreshTokens(req); + } catch (error) { + return res.status(401).json({ error: 'Token refresh failed' }); + } + } + + next(); +} +``` + +## Resources + +- **Auth guide**: https://developers.zoom.us/docs/zoom-apps/authentication/ +- **OAuth reference**: [../references/oauth.md](../references/oauth.md) diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/examples/layers-camera.md b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/layers-camera.md new file mode 100644 index 00000000..7614ed85 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/layers-camera.md @@ -0,0 +1,218 @@ +# Layers API - Camera Mode + +Overlay graphics on the user's own camera feed. Create virtual camera effects, branded frames, and interactive overlays. + +## Overview + +Camera mode overlays your content on the user's camera video. Unlike immersive mode (which controls the entire meeting view), camera mode only affects the individual user's camera feed - other participants see the overlay on that user's video. + +## Quick Start + +```javascript +import zoomSdk from '@zoom/appssdk'; + +const config = await zoomSdk.config({ + capabilities: [ + 'getRunningContext', + 'runRenderingContext', 'closeRenderingContext', + 'drawParticipant', 'clearParticipant', + 'drawImage', 'clearImage', + 'drawWebView', 'clearWebView', + 'postMessage', 'onMessage', + 'onRenderedAppOpened' + ], + version: '0.16' +}); + +// renderTarget = virtual camera frame size (default: 1280x720) +const rtWidth = config.media?.renderTarget?.width || 1280; +const rtHeight = config.media?.renderTarget?.height || 720; + +// Start camera mode +await zoomSdk.runRenderingContext({ view: 'camera' }); + +// Wait for CEF to initialize +zoomSdk.addEventListener('onRenderedAppOpened', async () => { + // Draw self video as background + await zoomSdk.drawParticipant({ + participantUUID: myUUID, + x: 0, y: 0, + width: rtWidth, height: rtHeight, + zIndex: 1, + cameraModeMirroring: true // v5.13.5+ — mirror for self-view + }); + + // Add a branded frame overlay + const frame = await createBrandedFrame(rtWidth, rtHeight); + const imageData = frame.getContext('2d').getImageData(0, 0, rtWidth, rtHeight); + await zoomSdk.drawImage({ + imageData, + x: 0, y: 0, + zIndex: 2 + }); + + // Or draw webview overlay (your app's home URL rendered off-screen) + await zoomSdk.drawWebView({ + x: 0, y: 0, + width: rtWidth, height: rtHeight, + zIndex: 3 + }); +}); +``` + +## CEF Race Condition (Critical) + +Camera mode uses CEF (Chromium Embedded Framework) which takes time to initialize. Drawing too early will fail silently. + +**Solution: Retry with backoff** + +```javascript +async function drawWithRetry(drawFn, maxRetries = 5) { + for (let i = 0; i < maxRetries; i++) { + try { + await drawFn(); + return; // Success + } catch (error) { + if (i === maxRetries - 1) throw error; + // Exponential backoff: 200ms, 400ms, 800ms, 1600ms, 3200ms + await new Promise(r => setTimeout(r, 200 * Math.pow(2, i))); + } + } +} + +// Usage +await zoomSdk.runRenderingContext({ view: 'camera' }); + +await drawWithRetry(async () => { + await zoomSdk.drawImage({ + imageData: frame.toDataURL(), + x: 0, y: 0, width: 1280, height: 720, zIndex: 1 + }); +}); +``` + +## Example: Branded Camera Frame + +```javascript +async function createBrandedFrame() { + const canvas = document.createElement('canvas'); + canvas.width = 1280; + canvas.height = 720; + const ctx = canvas.getContext('2d'); + + // Transparent center (camera shows through) + ctx.clearRect(0, 0, 1280, 720); + + // Bottom bar with company branding + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 660, 1280, 60); + + // Company name + ctx.fillStyle = 'white'; + ctx.font = 'bold 20px sans-serif'; + ctx.fillText('Acme Corp', 20, 695); + + // Border frame + ctx.strokeStyle = '#2d8cff'; + ctx.lineWidth = 4; + ctx.strokeRect(2, 2, 1276, 716); + + return canvas; +} +``` + +## Example: Name Tag Overlay + +```javascript +async function drawNameTag(name, title) { + const canvas = document.createElement('canvas'); + canvas.width = 300; + canvas.height = 80; + const ctx = canvas.getContext('2d'); + + // Background + ctx.fillStyle = 'rgba(45, 140, 255, 0.85)'; + ctx.beginPath(); + ctx.roundRect(0, 0, 300, 80, 12); + ctx.fill(); + + // Name + ctx.fillStyle = 'white'; + ctx.font = 'bold 22px sans-serif'; + ctx.fillText(name, 16, 32); + + // Title + ctx.font = '16px sans-serif'; + ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.fillText(title, 16, 58); + + await zoomSdk.drawImage({ + imageData: canvas.toDataURL(), + x: 20, y: 620, + width: 300, height: 80, + zIndex: 2 + }); +} +``` + +## Exiting Camera Mode + +```javascript +await zoomSdk.closeRenderingContext(); +``` + +## drawWebView in Camera Mode + +The webview renders your app's home URL off-screen. Use it for interactive overlays controlled from your sidebar app: + +```javascript +// Draw webview filling entire camera frame +await zoomSdk.drawWebView({ + x: 0, y: 0, + width: rtWidth, height: rtHeight, + zIndex: 2 +}); + +// Or partial overlay (bottom third) +await zoomSdk.drawWebView({ + x: 0, y: rtHeight * 0.67, + width: rtWidth, height: rtHeight * 0.33, + zIndex: 2 +}); + +// Hide webview (app keeps running) +await zoomSdk.clearWebView(); +``` + +**Communication between sidebar ↔ camera mode app:** + +```javascript +// Sidebar sends command to camera mode instance +zoomSdk.postMessage({ command: 'show-nametag', name: 'John' }); + +// Camera mode instance listens (no connect() required) +zoomSdk.addEventListener('onMessage', (event) => { + if (event.command === 'show-nametag') { + document.getElementById('name').textContent = event.name; + } +}); +``` + +## Differences from Immersive Mode + +| Aspect | Immersive | Camera | +|--------|-----------|--------| +| Scope | Entire meeting view | User's camera only | +| drawParticipant | Any participant | Self only | +| drawWebView | Yes | Yes | +| Who sees it | All participants | All see it on this user's feed | +| Use case | Custom layouts | Personal overlays, branding | +| Browser | Standard WebView | CEF (has init delay) | +| Coordinate space | CSS pixels | Raw pixels (renderTarget) | +| `cameraModeMirroring` | N/A | Yes (v5.13.5+) | + +## Resources + +- **Camera mode docs**: https://developers.zoom.us/docs/zoom-apps/guides/camera-mode/ +- **Layers API reference**: [../references/layers-api.md](../references/layers-api.md) +- **Sample app**: https://github.com/zoom/zoomapps-customlayout-js diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/examples/layers-immersive.md b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/layers-immersive.md new file mode 100644 index 00000000..93e141a6 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/layers-immersive.md @@ -0,0 +1,285 @@ +# Layers API - Immersive Mode + +Custom video layouts that replace the standard gallery view. Position participant video feeds, backgrounds, and web content anywhere on screen. + +## Overview + +Immersive mode takes over the entire meeting video area. You control where each participant's video appears, add background images, and overlay web content. + +**Use cases:** Podcast layout, talk show, classroom, game show, branded meetings. + +## Quick Start + +```javascript +import zoomSdk from '@zoom/appssdk'; + +// 1. Config with Layers capabilities +await zoomSdk.config({ + capabilities: [ + 'getRunningContext', + 'runRenderingContext', 'closeRenderingContext', + 'drawParticipant', 'clearParticipant', + 'drawImage', 'clearImage', + 'drawWebView', 'clearWebView', + 'getMeetingParticipants', 'onParticipantChange', + 'postMessage', 'onMessage', + 'sendAppInvitationToAllParticipants', + 'onRenderedAppOpened' + ], + version: '0.16' +}); + +// 2. Start immersive mode (Team = person cutout, Presentation = rectangle) +await zoomSdk.runRenderingContext({ + view: 'immersive', + defaultCutout: 'person' // Removes backgrounds via AI segmentation +}); + +// 3. Draw a background (imageData = JS ImageData object, NOT base64) +const canvas = document.createElement('canvas'); +canvas.width = 1280; +canvas.height = 720; +const ctx = canvas.getContext('2d'); +ctx.fillStyle = '#1a1a2e'; +ctx.fillRect(0, 0, 1280, 720); +const imageData = ctx.getImageData(0, 0, 1280, 720); + +await zoomSdk.drawImage({ + imageData, + x: 0, y: 0, + zIndex: 0 +}); + +// 4. Position participants +const { participants } = await zoomSdk.getMeetingParticipants(); + +await zoomSdk.drawParticipant({ + participantUUID: participants[0].participantUUID, + x: 50, y: 100, + width: 500, height: 400, + zIndex: 1, + cutout: 'person' // Override default if needed +}); + +await zoomSdk.drawParticipant({ + participantUUID: participants[1].participantUUID, + x: 730, y: 100, + width: 500, height: 400, + zIndex: 1, + cutout: 'person' +}); +``` + +## Drawing Methods + +### drawParticipant + +Position a participant's video feed. In immersive mode, you can draw any participant. + +```javascript +await zoomSdk.drawParticipant({ + participantUUID: 'uuid-string', // From getMeetingParticipants() + x: 0, // PixelValue: "Npx", "N%", or number + y: 0, // PixelValue + width: 640, // PixelValue (aspect ratio maintained) + height: 480, // PixelValue (aspect ratio maintained) + zIndex: 1, // Stacking order (higher = on top) + cutout: 'person' // Optional: "person"|"standard"|"rectangle"|"circle"|"square"|"verticalRectangle" +}); +``` + +**Cutout shapes** (all have 30px rounded corners except `"standard"`): +- `"person"` — AI background removal (v5.9.3+) +- `"standard"` — Full uncropped video, squared corners (v5.11.3+) +- `"rectangle"` — Rounded rectangle (v5.11.0+) +- `"circle"` — Circle (v5.11.3+) +- `"square"` — Square with rounded corners (v5.11.3+) +- `"verticalRectangle"` — Vertical rectangle with rounded corners (v5.11.3+) + +> **Deprecated:** `participantId` — use `participantUUID` instead. + +### drawImage + +Add images (backgrounds, overlays, borders). Uses standard JavaScript `ImageData` (NOT base64): + +```javascript +const canvas = document.createElement('canvas'); +canvas.width = 1280; +canvas.height = 720; +const ctx = canvas.getContext('2d'); +// ... draw on canvas ... +const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + +const { imageId } = await zoomSdk.drawImage({ + imageData, // ImageData object from canvas.getImageData() + x: 0, y: 0, + zIndex: 0 // Behind participants +}); +// Save imageId for clearImage() later +``` + +### drawWebView + +Embed your app's webview as an interactive overlay. Only one webview per rendering context. + +```javascript +await zoomSdk.drawWebView({ + x: 400, y: 600, + width: 480, height: 100, + zIndex: 2 // On top of everything +}); +``` + +> See [../references/layers-api.md](../references/layers-api.md#drawwebview) for full drawWebView details, webview communication, and the `webviewId` documentation inconsistency. + +### Clearing + +```javascript +await zoomSdk.clearParticipant({ participantUUID: 'uuid' }); +await zoomSdk.clearImage({ imageId: 'id-from-drawImage-response' }); +await zoomSdk.clearWebView(); // No params per TypeDoc v0.16.36 +``` + +### Exit Immersive Mode + +```javascript +await zoomSdk.closeRenderingContext(); +``` + +## Complete Example: Podcast Layout + +Two hosts side-by-side with custom background: + +```javascript +import zoomSdk from '@zoom/appssdk'; + +class PodcastLayout { + constructor() { + this.active = false; + } + + async start() { + await zoomSdk.runRenderingContext({ view: 'immersive', defaultCutout: 'person' }); + this.active = true; + + // Draw background + await this.drawBackground(); + + // Position hosts + const { participants } = await zoomSdk.getMeetingParticipants(); + await this.layoutParticipants(participants); + + // React to participant changes + zoomSdk.addEventListener('onParticipantChange', async () => { + const { participants } = await zoomSdk.getMeetingParticipants(); + await this.layoutParticipants(participants); + }); + } + + async drawBackground() { + const canvas = document.createElement('canvas'); + canvas.width = 1280; + canvas.height = 720; + const ctx = canvas.getContext('2d'); + + // Gradient background + const gradient = ctx.createLinearGradient(0, 0, 1280, 720); + gradient.addColorStop(0, '#1a1a2e'); + gradient.addColorStop(1, '#16213e'); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, 1280, 720); + + // Title + ctx.fillStyle = 'white'; + ctx.font = 'bold 32px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('The Zoom Podcast', 640, 60); + + const imageData = ctx.getImageData(0, 0, 1280, 720); + await zoomSdk.drawImage({ + imageData, + x: 0, y: 0, zIndex: 0 + }); + } + + async layoutParticipants(participants) { + if (participants.length === 1) { + // Single host - centered + await zoomSdk.drawParticipant({ + participantUUID: participants[0].participantUUID, + x: 340, y: 100, width: 600, height: 500, zIndex: 1 + }); + } else if (participants.length >= 2) { + // Two hosts - side by side + await zoomSdk.drawParticipant({ + participantUUID: participants[0].participantUUID, + x: 40, y: 100, width: 580, height: 500, zIndex: 1 + }); + await zoomSdk.drawParticipant({ + participantUUID: participants[1].participantUUID, + x: 660, y: 100, width: 580, height: 500, zIndex: 1 + }); + } + } + + async stop() { + await zoomSdk.closeRenderingContext(); + this.active = false; + } +} +``` + +## HiDPI Support + +For Retina/HiDPI displays, multiply coordinates by `window.devicePixelRatio`: + +```javascript +const dpr = window.devicePixelRatio || 1; + +await zoomSdk.drawParticipant({ + participantUUID: uuid, + x: 100 * dpr, + y: 100 * dpr, + width: 640 * dpr, + height: 480 * dpr, + zIndex: 1 +}); +``` + +## Multi-Participant Sync + +The host controls the layout. Use Socket.io to broadcast layout changes: + +```javascript +// Host sends layout to all participants via your backend +socket.emit('layout-change', { + participants: [ + { uuid: 'a', x: 40, y: 100, w: 580, h: 500 }, + { uuid: 'b', x: 660, y: 100, w: 580, h: 500 } + ] +}); + +// All participants apply the layout +socket.on('layout-change', async (layout) => { + for (const p of layout.participants) { + await zoomSdk.drawParticipant({ + participantUUID: p.uuid, + x: p.x, y: p.y, width: p.w, height: p.h, zIndex: 1 + }); + } +}); +``` + +## Performance Tips + +- Use `requestAnimationFrame` for animations +- Minimize `drawImage` calls (batch updates) +- Pre-render complex backgrounds to canvas +- Keep zIndex values low (0-10 range) +- Clear unused elements to free resources + +## Resources + +- **Layers docs**: https://developers.zoom.us/docs/zoom-apps/guides/layers-api/ +- **Layers API reference**: [../references/layers-api.md](../references/layers-api.md) +- **Sample app**: https://github.com/zoom/zoomapps-customlayout-js diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/examples/quick-start.md b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/quick-start.md new file mode 100644 index 00000000..92c49bf4 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/examples/quick-start.md @@ -0,0 +1,258 @@ +# Quick Start - Hello World Zoom App + +Complete working Zoom App with Express backend and SDK frontend. + +## Prerequisites + +- Node.js 18+ +- ngrok account (free tier works) +- Zoom Marketplace app (type: "Zoom App") + +## Project Setup + +### package.json + +```json +{ + "name": "zoom-app-hello-world", + "version": "1.0.0", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "dependencies": { + "express": "^4.18.0", + "cookie-session": "^2.0.0", + "axios": "^1.6.0", + "dotenv": "^16.0.0" + } +} +``` + +### .env + +```ini +ZOOM_APP_CLIENT_ID=your_client_id +ZOOM_APP_CLIENT_SECRET=your_client_secret +ZOOM_APP_REDIRECT_URI=https://xxxxx.ngrok.io/auth +SESSION_SECRET=generate_a_random_string_here +``` + +### server.js + +```javascript +require('dotenv').config(); +const express = require('express'); +const crypto = require('crypto'); +const cookieSession = require('cookie-session'); +const axios = require('axios'); + +const app = express(); +app.use(express.json()); +app.use(express.static('public')); + +// Cookie session - SameSite=None required for Zoom embedded browser +app.use(cookieSession({ + name: 'session', + keys: [process.env.SESSION_SECRET], + maxAge: 24 * 60 * 60 * 1000, + sameSite: 'none', + secure: true +})); + +// OWASP security headers (required for Marketplace approval) +app.use((req, res, next) => { + res.setHeader('Strict-Transport-Security', 'max-age=31536000'); + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('Content-Security-Policy', "frame-ancestors 'self' zoom.us *.zoom.us"); + res.setHeader('Referrer-Policy', 'same-origin'); + next(); +}); + +// Step 1: Install - redirect to Zoom OAuth +app.get('/install', (req, res) => { + const verifier = crypto.randomBytes(32).toString('hex'); + const challenge = crypto.createHash('sha256').update(verifier).digest('base64url'); + const state = crypto.randomBytes(16).toString('hex'); + + req.session.codeVerifier = verifier; + req.session.state = state; + + const url = `https://zoom.us/oauth/authorize?` + + `client_id=${process.env.ZOOM_APP_CLIENT_ID}` + + `&response_type=code` + + `&redirect_uri=${encodeURIComponent(process.env.ZOOM_APP_REDIRECT_URI)}` + + `&code_challenge=${challenge}` + + `&code_challenge_method=S256` + + `&state=${state}`; + + res.redirect(url); +}); + +// Step 2: OAuth callback - exchange code for tokens +app.get('/auth', async (req, res) => { + const { code, state } = req.query; + + if (state !== req.session.state) { + return res.status(403).send('Invalid state'); + } + + try { + // Exchange authorization code for tokens + const tokenResponse = await axios.post('https://zoom.us/oauth/token', null, { + params: { + grant_type: 'authorization_code', + code, + redirect_uri: process.env.ZOOM_APP_REDIRECT_URI, + code_verifier: req.session.codeVerifier + }, + headers: { + 'Authorization': 'Basic ' + Buffer.from( + `${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}` + ).toString('base64') + } + }); + + req.session.tokens = tokenResponse.data; + + // Get deeplink to open app in Zoom client + const deeplink = await axios.post('https://api.zoom.us/v2/zoomapp/deeplink', + { action: '' }, + { headers: { 'Authorization': `Bearer ${tokenResponse.data.access_token}` } } + ); + + res.redirect(deeplink.data.deeplink); + } catch (error) { + console.error('OAuth error:', error.response?.data || error.message); + res.status(500).send('Authorization failed'); + } +}); + +// API endpoint for frontend +app.get('/api/user', (req, res) => { + if (!req.session.tokens) { + return res.status(401).json({ error: 'Not authenticated' }); + } + res.json({ authenticated: true }); +}); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); +``` + +### public/index.html + +```html + + + + My Zoom App + + + + +

Hello Zoom App!

+
Loading...
+ + + + +``` + +## Running Locally + +```bash +# 1. Install dependencies +npm install + +# 2. Start ngrok tunnel +ngrok http 3000 + +# 3. Copy the https URL (e.g., https://abc123.ngrok.io) + +# 4. Update .env with ngrok URL +# ZOOM_APP_REDIRECT_URI=https://abc123.ngrok.io/auth + +# 5. Start server +npm run dev +``` + +## Marketplace Configuration + +In [Zoom Marketplace](https://marketplace.zoom.us/) -> Your App: + +1. **App Credentials**: Copy Client ID and Secret to `.env` +2. **Feature tab** -> Zoom App: + - **Home URL**: `https://abc123.ngrok.io` + - **Redirect URL**: `https://abc123.ngrok.io/auth` + - **Domain Allow List**: `abc123.ngrok.io` +3. **Scopes tab**: Add `zoomapp:inmeeting` +4. **Local Test**: Click your app name in Zoom client sidebar to open + +## Next Steps + +- Add [In-Client OAuth](in-client-oauth.md) for seamless re-authorization +- Add [Layers API](layers-immersive.md) for immersive experiences +- Review [Security](../concepts/security.md) headers for Marketplace approval diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/references/apis.md b/plugins/zoom-developers/skills/zoom-apps-sdk/references/apis.md new file mode 100644 index 00000000..14437ccb --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/references/apis.md @@ -0,0 +1,274 @@ +# Zoom Apps SDK - API Reference + +Complete reference for all @zoom/appssdk methods organized by category. + +## Initialization + +### config(options) + +**Must be called first.** Initializes the SDK and declares capabilities. + +```javascript +const configResponse = await zoomSdk.config({ + capabilities: ['getMeetingContext', 'shareApp', ...], + version: '0.16' +}); +// Returns: { runningContext, clientVersion, unsupportedApis } +``` + +### getSupportedJsApis() + +Check which APIs are available in the current client. + +```javascript +const { supportedApis } = await zoomSdk.getSupportedJsApis(); +// Returns: { supportedApis: ['getMeetingContext', 'shareApp', ...] } +``` + +## Context APIs + +### getMeetingContext() + +```javascript +const context = await zoomSdk.getMeetingContext(); +// { meetingID, meetingTopic, meetingStatus } +``` + +### getUserContext() + +```javascript +const user = await zoomSdk.getUserContext(); +// { screenName, role, participantId, status } +// role: 'host' | 'coHost' | 'attendee' | 'panelist' +// status: 'authorized' | 'authenticated' | 'unauthenticated' +``` + +### getRunningContext() + +```javascript +const { context } = await zoomSdk.getRunningContext(); +// 'inMeeting' | 'inMainClient' | 'inWebinar' | 'inImmersive' | ... +``` + +### getMeetingUUID() + +```javascript +const { meetingUUID } = await zoomSdk.getMeetingUUID(); +``` + +### getMeetingJoinUrl() + +```javascript +const { joinUrl } = await zoomSdk.getMeetingJoinUrl(); +``` + +### getMeetingParticipants() + +```javascript +const { participants } = await zoomSdk.getMeetingParticipants(); +// [{ participantId, participantUUID, screenName, role }] +``` + +### getRecordingContext() + +```javascript +const recording = await zoomSdk.getRecordingContext(); +// { cloudRecordingStatus, localRecordingStatus } +``` + +## Authorization APIs + +### authorize(options) + +Trigger In-Client OAuth (no browser redirect). + +```javascript +await zoomSdk.authorize({ codeChallenge: '...', state: '...' }); +// onAuthorized event fires with { code, state } +``` + +### promptAuthorize() + +Prompt guest/authenticated users to authorize. + +```javascript +await zoomSdk.promptAuthorize(); +``` + +## Meeting Action APIs + +### shareApp() + +```javascript +await zoomSdk.shareApp(); +``` + +### sendAppInvitation(options) + +```javascript +await zoomSdk.sendAppInvitation({ participantUUIDs: ['uuid1'], action: 'open' }); +``` + +### sendAppInvitationToAllParticipants() + +```javascript +await zoomSdk.sendAppInvitationToAllParticipants(); +``` + +### sendAppInvitationToMeetingOwner() + +```javascript +await zoomSdk.sendAppInvitationToMeetingOwner(); +``` + +### showAppInvitationDialog() + +```javascript +await zoomSdk.showAppInvitationDialog(); +``` + +## UI APIs + +### expandApp(options) + +```javascript +await zoomSdk.expandApp({ action: 'expand' }); // or 'collapse' +``` + +### openUrl(options) + +```javascript +await zoomSdk.openUrl({ url: 'https://example.com' }); +``` + +### showNotification(options) + +```javascript +await zoomSdk.showNotification({ title: 'Alert', message: 'Something happened!', type: 'info' }); +``` + +## Media APIs + +### listCameras() + +```javascript +const { cameras } = await zoomSdk.listCameras(); +``` + +### setVirtualBackground(options) + +```javascript +await zoomSdk.setVirtualBackground({ imageData: 'base64-data' }); +``` + +### removeVirtualBackground() + +```javascript +await zoomSdk.removeVirtualBackground(); +``` + +### setVideoMirrorEffect(options) + +```javascript +await zoomSdk.setVideoMirrorEffect({ mirrorMyVideo: true }); +``` + +### allowParticipantToRecord(options) + +```javascript +await zoomSdk.allowParticipantToRecord({ participantUUID: 'uuid', action: 'grant' }); +``` + +### cloudRecording(options) + +```javascript +await zoomSdk.cloudRecording({ action: 'start' }); // 'stop', 'pause', 'resume' +``` + +## Communication APIs + +### connect() + +Connect to other app instances (main client <-> meeting). + +```javascript +await zoomSdk.connect(); +``` + +### postMessage(options) + +```javascript +await zoomSdk.postMessage({ payload: JSON.stringify({ type: 'update', data: {...} }) }); +``` + +## Layers APIs + +### runRenderingContext(options) + +```javascript +await zoomSdk.runRenderingContext({ view: 'immersive' }); // or 'camera' +``` + +### closeRenderingContext() + +```javascript +await zoomSdk.closeRenderingContext(); +``` + +### drawParticipant(options) + +```javascript +await zoomSdk.drawParticipant({ participantUUID: 'uuid', x: 0, y: 0, width: 640, height: 480, zIndex: 1 }); +``` + +### clearParticipant(options) + +```javascript +await zoomSdk.clearParticipant({ participantUUID: 'uuid' }); +``` + +### drawImage(options) + +```javascript +await zoomSdk.drawImage({ imageData: canvas.toDataURL(), x: 0, y: 0, width: 1280, height: 720, zIndex: 0 }); +``` + +### clearImage(options) + +```javascript +await zoomSdk.clearImage({ imageId: 'id' }); +``` + +### drawWebView(options) + +```javascript +await zoomSdk.drawWebView({ webviewId: 'my-view', x: 0, y: 0, width: 400, height: 300, zIndex: 2 }); +``` + +### clearWebView(options) + +```javascript +await zoomSdk.clearWebView({ webviewId: 'my-view' }); +``` + +## Collaborate APIs + +### startCollaborate(options) + +```javascript +await zoomSdk.startCollaborate({ shareScreen: true }); +``` + +## Event Listeners + +```javascript +zoomSdk.addEventListener('onMeeting', (event) => { ... }); +zoomSdk.removeEventListener('onMeeting', handler); +``` + +See **[events.md](events.md)** for complete event reference. + +## Resources + +- **SDK Reference**: https://appssdk.zoom.us/ +- **Capabilities list**: https://developers.zoom.us/docs/zoom-apps/ diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/references/environment-variables.md b/plugins/zoom-developers/skills/zoom-apps-sdk/references/environment-variables.md new file mode 100644 index 00000000..4d655e04 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/references/environment-variables.md @@ -0,0 +1,24 @@ +# Zoom Apps SDK Environment Variables + +## Standard `.env` keys + +| Variable | Required | Used for | Where to find | +| --- | --- | --- | --- | +| `ZOOM_APP_CLIENT_ID` | Yes | OAuth and app identity | Zoom Marketplace -> your Zoom App -> App Credentials | +| `ZOOM_APP_CLIENT_SECRET` | Yes | OAuth token exchange | Zoom Marketplace -> your Zoom App -> App Credentials | +| `ZOOM_APP_REDIRECT_URI` | Yes | OAuth callback URL | Zoom Marketplace -> your Zoom App -> OAuth allow list / redirect settings | +| `ZOOM_APP_URL` | Usually | URL loaded by Zoom client | Zoom Marketplace -> your Zoom App -> Basic Information (Development URL / Home URL) | +| `ZOOM_APP_BASE_URL` | Optional | Internal base URL alias | Set to your deployed app origin | +| `SESSION_SECRET` | Recommended | Session signing/encryption | Generate and manage in your own secret manager | + +## Runtime-only values + +- `ZOOM_ACCESS_TOKEN` +- `ZOOM_REFRESH_TOKEN` + +Generate these during OAuth flow; do not hardcode them in repo files. + +## Notes + +- Keep `ZOOM_APP_CLIENT_SECRET` server-side only. +- Use separate development and production app credentials. diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/references/events.md b/plugins/zoom-developers/skills/zoom-apps-sdk/references/events.md new file mode 100644 index 00000000..5f8a4a30 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/references/events.md @@ -0,0 +1,205 @@ +# Zoom Apps SDK - Events Reference + +Subscribe to events to respond to meeting, user, and app state changes. + +**IMPORTANT**: Register event listeners AFTER successful `zoomSdk.config()` call. Events must be listed in the `capabilities` array. + +## Subscribing to Events + +```javascript +import zoomSdk from '@zoom/appssdk'; + +await zoomSdk.config({ + capabilities: ['onMeeting', 'onParticipantChange', 'onAuthorized', ...], + version: '0.16' +}); + +zoomSdk.addEventListener('onMeeting', (event) => { + console.log('Meeting event:', event); +}); +``` + +## Meeting Events + +### onMeeting + +Meeting lifecycle changes (start, end, etc.). + +```javascript +zoomSdk.addEventListener('onMeeting', (event) => { + // event: { action: 'started' | 'ended' | ... } +}); +``` + +### onParticipantChange + +Participants join or leave. + +```javascript +zoomSdk.addEventListener('onParticipantChange', (event) => { + const { participants, action } = event; + // action: 'join' | 'leave' + // participants: [{ participantId, screenName, role }] +}); +``` + +### onActiveSpeakerChange + +Active speaker changes. + +```javascript +zoomSdk.addEventListener('onActiveSpeakerChange', (event) => { + const { participantId, screenName } = event; +}); +``` + +### onBreakoutRoomChange + +User moves between breakout rooms. + +```javascript +zoomSdk.addEventListener('onBreakoutRoomChange', (event) => { + // Re-fetch meeting UUID and state for new room +}); +``` + +## User Events + +### onMyUserContextChange + +Current user's context changes (role, name, mute status, etc.). + +```javascript +zoomSdk.addEventListener('onMyUserContextChange', (event) => { + const { role, screenName } = event; +}); +``` + +### onMyMediaChange + +Current user's audio/video status changes. + +```javascript +zoomSdk.addEventListener('onMyMediaChange', (event) => { + const { audio, video } = event; + // audio: { muted: true/false } + // video: { started: true/false } +}); +``` + +## App Events + +### onShareApp + +App sharing status changes. + +```javascript +zoomSdk.addEventListener('onShareApp', (event) => { + const { isShared } = event; +}); +``` + +### onAppPopout + +App popped out to separate window or back. + +```javascript +zoomSdk.addEventListener('onAppPopout', (event) => { + const { isPopout } = event; +}); +``` + +### onRunningContextChange + +Running context changes (e.g., meeting starts while in main client). + +```javascript +zoomSdk.addEventListener('onRunningContextChange', (event) => { + const { runningContext } = event; +}); +``` + +## Authorization Events + +### onAuthorized + +In-Client OAuth authorization completed. + +```javascript +zoomSdk.addEventListener('onAuthorized', (event) => { + const { code, state } = event; + // Send code to backend for token exchange +}); +``` + +## Communication Events + +### onConnect + +Another app instance connected. + +```javascript +zoomSdk.addEventListener('onConnect', (event) => { + console.log('Other instance connected'); +}); +``` + +### onMessage + +Message received from connected instance. + +```javascript +zoomSdk.addEventListener('onMessage', (event) => { + const data = JSON.parse(event.payload); +}); +``` + +## Collaborate Events + +### onCollaborateChange + +Collaborate mode state changes. + +```javascript +zoomSdk.addEventListener('onCollaborateChange', (event) => { + console.log('Collaborate state:', event); +}); +``` + +## Layers Events + +### onRenderedAppOpened + +Immersive/camera rendering context opened. + +```javascript +zoomSdk.addEventListener('onRenderedAppOpened', (event) => { + console.log('Rendering context ready'); +}); +``` + +## Reaction Events + +### onReaction + +Participant sends a reaction. + +```javascript +zoomSdk.addEventListener('onReaction', (event) => { + const { participantId, reaction } = event; +}); +``` + +## Removing Listeners + +```javascript +const handler = (event) => { ... }; +zoomSdk.addEventListener('onMeeting', handler); + +// Later, remove it +zoomSdk.removeEventListener('onMeeting', handler); +``` + +## Resources + +- **Events reference**: https://appssdk.zoom.us/ diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/references/full-guide.md b/plugins/zoom-developers/skills/zoom-apps-sdk/references/full-guide.md new file mode 100644 index 00000000..ac07735b --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/references/full-guide.md @@ -0,0 +1,636 @@ +# Zoom Apps SDK + +Background reference for web apps that run inside the Zoom client. Prefer `choose-zoom-approach` first, then route here for Layers API, Collaborate Mode, in-client OAuth, and runtime constraints. + +# Zoom Apps SDK + +Build web apps that run inside the Zoom client - meetings, webinars, main client, and Zoom Phone. + +**Official Documentation**: https://developers.zoom.us/docs/zoom-apps/ +**SDK Reference**: https://appssdk.zoom.us/ +**NPM Package**: https://www.npmjs.com/package/@zoom/appssdk + +## Quick Links + +**New to Zoom Apps? Follow this path:** + +1. **[Architecture](../concepts/architecture.md)** - Frontend/backend pattern, embedded browser, deep linking +2. **[Quick Start](../examples/quick-start.md)** - Complete working Express + SDK app +3. **[Running Contexts](../concepts/running-contexts.md)** - Where your app runs (inMeeting, inMainClient, etc.) +4. **[Zoom Apps vs Meeting SDK](../concepts/meeting-sdk-vs-zoom-apps.md)** - Stop mixing app types +4. **[In-Client OAuth](../examples/in-client-oauth.md)** - Seamless authorization with PKCE +5. **[API Reference](../references/apis.md)** - 100+ SDK methods +6. **Integrated Index** - see the section below in this file +7. **[5-Minute Runbook](../RUNBOOK.md)** - Preflight checks before deep debugging + +**Reference:** +- **[API Reference](../references/apis.md)** - All SDK methods by category +- **[Events Reference](../references/events.md)** - All SDK event listeners +- **[Layers API](../references/layers-api.md)** - Immersive and camera mode rendering +- **[OAuth Reference](../references/oauth.md)** - OAuth flows for Zoom Apps +- **[Zoom Mail](../references/zmail-sdk.md)** - Mail plugin integration + +**Having issues?** +- App won't load in Zoom → Check [Domain Allowlist](#url-whitelisting-required) below +- SDK errors → [Common Issues](../troubleshooting/common-issues.md) +- Local dev setup → [Debugging Guide](../troubleshooting/debugging.md) +- Version upgrade → [Migration Guide](../troubleshooting/migration.md) +- Forum-derived FAQs → [Forum Top Questions](../troubleshooting/forum-top-questions.md) + +**Building immersive experiences?** +- [Layers Immersive Mode](../examples/layers-immersive.md) - Custom video layouts +- [Camera Mode](../examples/layers-camera.md) - Virtual camera overlays + +> **Need help with OAuth?** See the **[zoom-oauth](../../oauth/SKILL.md)** skill for authentication flows. + +## SDK Overview + +The Zoom Apps SDK (`@zoom/appssdk`) provides JavaScript APIs for web apps running in Zoom's embedded browser: + +- **Context APIs** - Get meeting, user, and participant info +- **Meeting Actions** - Share app, invite participants, open URLs +- **Authorization** - In-Client OAuth with PKCE (no browser redirect) +- **Layers API** - Immersive video layouts and camera mode overlays +- **Collaborate Mode** - Shared app state across participants +- **App Communication** - Message passing between app instances (main client <-> meeting) +- **Media Controls** - Virtual backgrounds, camera listing, recording control +- **UI Controls** - Expand app, notifications, popout +- **Events** - React to meeting state, participants, sharing, and more + +## Prerequisites + +- Zoom app configured as **"Zoom App"** type in [Marketplace](https://marketplace.zoom.us/) +- OAuth credentials (Client ID + Secret) with Zoom Apps scopes +- Web application (Node.js + Express recommended) +- **Your domain whitelisted** in Marketplace domain allowlist +- ngrok or HTTPS tunnel for local development +- Node.js 18+ (for the backend server) + +## Quick Start + +### Option A: NPM (Recommended for frameworks) + +```bash +npm install @zoom/appssdk +``` + +```javascript +import zoomSdk from '@zoom/appssdk'; + +async function init() { + try { + const configResponse = await zoomSdk.config({ + capabilities: [ + 'shareApp', + 'getMeetingContext', + 'getUserContext', + 'openUrl' + ], + version: '0.16' + }); + + console.log('Running context:', configResponse.runningContext); + // 'inMeeting' | 'inMainClient' | 'inWebinar' | 'inImmersive' | ... + + const context = await zoomSdk.getMeetingContext(); + console.log('Meeting ID:', context.meetingID); + } catch (error) { + console.error('Not running inside Zoom:', error.message); + showDemoMode(); + } +} +``` + +### Option B: CDN (Vanilla JS) + +```html + + + +``` + +## Critical: Global Variable Conflict + +The CDN script defines `window.zoomSdk` globally. **Do NOT redeclare it:** + +```javascript +// WRONG - causes SyntaxError in Zoom's embedded browser +let zoomSdk = null; +zoomSdk = window.zoomSdk; + +// CORRECT - use different variable name +let sdk = window.zoomSdk; + +// ALSO CORRECT - NPM import (no conflict) +import zoomSdk from '@zoom/appssdk'; +``` + +This only applies to the CDN approach. The NPM import creates a module-scoped variable, no conflict. + +## Browser Preview / Demo Mode + +The SDK only functions inside the Zoom client. When accessed in a regular browser: +- `window.zoomSdk` exists but `sdk.config()` throws an error +- Always implement try/catch with fallback UI +- Add timeout (3 seconds) in case SDK hangs + +## URL Whitelisting (Required) + +**Your app will NOT load in Zoom unless the domain is whitelisted.** + +1. Go to [Zoom Marketplace](https://marketplace.zoom.us/) +2. Open your app -> **Feature** tab +3. Under **Zoom App**, find **Add Allow List** +4. Add your domain (e.g., `yourdomain.com` for production, `xxxxx.ngrok.io` for dev) + +Without this, the Zoom client shows a blank panel with no error message. + +## OAuth Scopes (Required) + +Capabilities require matching OAuth scopes enabled in Marketplace: + +| Capability | Required Scope | +|------------|----------------| +| `getMeetingContext` | `zoomapp:inmeeting` | +| `getUserContext` | `zoomapp:inmeeting` | +| `shareApp` | `zoomapp:inmeeting` | +| `openUrl` | `zoomapp:inmeeting` | +| `sendAppInvitation` | `zoomapp:inmeeting` | +| `runRenderingContext` | `zoomapp:inmeeting` | +| `authorize` | `zoomapp:inmeeting` | +| `getMeetingParticipants` | `zoomapp:inmeeting` | + +**To add scopes:** Marketplace -> Your App -> **Scopes** tab -> Add required scopes. + +Missing scopes = capability fails silently or throws error. Users must re-authorize if you add new scopes. + +## Running Contexts + +Your app runs in different surfaces within Zoom. The `configResponse.runningContext` tells you where: + +| Context | Surface | Description | +|---------|---------|-------------| +| `inMeeting` | Meeting sidebar | Most common. Full meeting APIs available | +| `inMainClient` | Main client panel | Home tab. No meeting context APIs | +| `inWebinar` | Webinar sidebar | Host/panelist. Meeting + webinar APIs | +| `inImmersive` | Layers API | Full-screen custom rendering | +| `inCamera` | Camera mode | Virtual camera overlay | +| `inCollaborate` | Collaborate mode | Shared state context | +| `inPhone` | Zoom Phone | Phone call app | +| `inChat` | Team Chat | Chat sidebar | + +See **[Running Contexts](../concepts/running-contexts.md)** for context-specific behavior and APIs. + +## SDK Initialization Pattern + +Every Zoom App starts with `config()`: + +```javascript +import zoomSdk from '@zoom/appssdk'; + +const configResponse = await zoomSdk.config({ + capabilities: [ + // List ALL APIs you will use + 'getMeetingContext', + 'getUserContext', + 'shareApp', + 'openUrl', + 'authorize', + 'onAuthorized' + ], + version: '0.16' +}); + +// configResponse contains: +// { +// runningContext: 'inMeeting', +// clientVersion: '5.x.x', +// unsupportedApis: [] // APIs not supported in this client version +// } +``` + +**Rules:** +1. `config()` MUST be called before any other SDK method +2. Only capabilities listed in `config()` are available +3. Capabilities must match OAuth scopes in Marketplace +4. Check `unsupportedApis` for graceful degradation + +## In-Client OAuth (Summary) + +Best UX for authorization - no browser redirect: + +```javascript +// 1. Get code challenge from your backend +const { codeChallenge, state } = await fetch('/api/auth/challenge').then(r => r.json()); + +// 2. Trigger in-client authorization +await zoomSdk.authorize({ codeChallenge, state }); + +// 3. Listen for authorization result +zoomSdk.addEventListener('onAuthorized', async (event) => { + const { code, state } = event; + // 4. Send code to backend for token exchange + await fetch('/api/auth/token', { + method: 'POST', + body: JSON.stringify({ code, state }) + }); +}); +``` + +See **[In-Client OAuth Guide](../examples/in-client-oauth.md)** for complete implementation. + +## Layers API (Summary) + +Build immersive video layouts and camera overlays: + +```javascript +// Start immersive mode - replaces gallery view +await zoomSdk.runRenderingContext({ view: 'immersive' }); + +// Position participant video feeds +await zoomSdk.drawParticipant({ + participantUUID: 'user-uuid', + x: 0, y: 0, width: 640, height: 480, zIndex: 1 +}); + +// Add overlay images +await zoomSdk.drawImage({ + imageData: canvas.toDataURL(), + x: 0, y: 0, width: 1280, height: 720, zIndex: 0 +}); + +// Exit immersive mode +await zoomSdk.closeRenderingContext(); +``` + +See **[Layers Immersive](../examples/layers-immersive.md)** and **[Camera Mode](../examples/layers-camera.md)**. + +## Environment Variables + +| Variable | Description | Where to Find | +|----------|-------------|---------------| +| `ZOOM_APP_CLIENT_ID` | App client ID | Marketplace -> App -> App Credentials | +| `ZOOM_APP_CLIENT_SECRET` | App client secret | Marketplace -> App -> App Credentials | +| `ZOOM_APP_REDIRECT_URI` | OAuth redirect URL | Your server URL + `/auth` | +| `SESSION_SECRET` | Cookie signing secret | Generate random string | +| `ZOOM_HOST` | Zoom host URL | `https://zoom.us` (or `https://zoomgov.com`) | + +## Common APIs + +| API | Description | +|-----|-------------| +| `config()` | Initialize SDK, request capabilities | +| `getMeetingContext()` | Get meeting ID, topic, status | +| `getUserContext()` | Get user name, role, participant ID | +| `getRunningContext()` | Get current running context | +| `getMeetingParticipants()` | List participants | +| `shareApp()` | Share app screen with participants | +| `openUrl({ url })` | Open URL in external browser | +| `sendAppInvitation()` | Invite users to open your app | +| `authorize()` | Trigger In-Client OAuth | +| `connect()` | Connect to other app instances | +| `postMessage()` | Send message to connected instances | +| `runRenderingContext()` | Start Layers API (immersive/camera) | +| `expandApp({ action })` | Expand/collapse app panel | +| `showNotification()` | Show notification in Zoom | + +## Complete Documentation Library + +### Core Concepts +- **[Architecture](../concepts/architecture.md)** - Frontend/backend pattern, embedded browser, deep linking, X-Zoom-App-Context +- **[Running Contexts](../concepts/running-contexts.md)** - All contexts, context-specific APIs, multi-instance communication +- **[Security](../concepts/security.md)** - OWASP headers, CSP, cookie security, PKCE, token storage + +### Complete Examples +- **[Quick Start](../examples/quick-start.md)** - Hello World Express + SDK app +- **[In-Client OAuth](../examples/in-client-oauth.md)** - PKCE authorization flow +- **[Layers Immersive](../examples/layers-immersive.md)** - Custom video layouts +- **[Camera Mode](../examples/layers-camera.md)** - Virtual camera overlays +- **[Collaborate Mode](../examples/collaborate-mode.md)** - Shared state across participants +- **[Guest Mode](../examples/guest-mode.md)** - Unauthenticated/authenticated/authorized states +- **[Breakout Rooms](../examples/breakout-rooms.md)** - Room detection and cross-room state +- **[App Communication](../examples/app-communication.md)** - connect + postMessage between instances + +### Troubleshooting +- **[Common Issues](../troubleshooting/common-issues.md)** - Quick diagnostics and error codes +- **[Debugging](../troubleshooting/debugging.md)** - Local dev, ngrok, browser preview +- **[Migration](../troubleshooting/migration.md)** - SDK version upgrade notes + +### References +- **[API Reference](../references/apis.md)** - All 100+ SDK methods +- **[Events Reference](../references/events.md)** - All SDK event listeners +- **[Layers API Reference](../references/layers-api.md)** - Drawing and rendering methods +- **[OAuth Reference](../references/oauth.md)** - OAuth flows for Zoom Apps +- **[Zoom Mail](../references/zmail-sdk.md)** - Mail plugin integration + +## Sample Repositories + +### Official (by Zoom) + +| Repository | Type | Last Updated | Status | SDK Version | +|-----------|------|-------------|--------|-------------| +| [zoomapps-sample-js](https://github.com/zoom/zoomapps-sample-js) | Hello World (Vanilla JS) | Dec 2025 | Active | ^0.16.26 | +| [zoomapps-advancedsample-react](https://github.com/zoom/zoomapps-advancedsample-react) | Advanced (React + Redis) | Oct 2025 | Active | 0.16.0 | +| [zoomapps-customlayout-js](https://github.com/zoom/zoomapps-customlayout-js) | Layers API | Nov 2023 | Stale | ^0.16.8 | +| [zoomapps-texteditor-vuejs](https://github.com/zoom/zoomapps-texteditor-vuejs) | Collaborate (Vue + Y.js) | Oct 2023 | Stale | ^0.16.7 | +| [zoomapps-serverless-vuejs](https://github.com/zoom/zoomapps-serverless-vuejs) | Serverless (Firebase) | Aug 2024 | Stale | ^0.16.21 | +| [zoomapps-cameramode-vuejs](https://github.com/zoom/zoomapps-cameramode-vuejs) | Camera Mode | - | - | - | +| [zoomapps-workshop-sample](https://github.com/zoom/zoomapps-workshop-sample) | Workshop | - | - | - | + +**Recommended for new projects:** Use `@zoom/appssdk` version `^0.16.26`. + +### Community + +| Type | Repository | Description | +|------|------------|-------------| +| Library | [harvard-edtech/zaccl](https://github.com/harvard-edtech/zaccl) | Zoom App Complete Connection Library | + +**Full list**: See [general/references/community-repos.md](../../general/references/community-repos.md) + +### Learning Path + +1. **Start**: `zoomapps-sample-js` - Simplest, most up-to-date +2. **Advanced**: `zoomapps-advancedsample-react` - Comprehensive (In-Client OAuth, Guest Mode, Collaborate) +3. **Specialized**: Pick based on feature (Layers, Serverless, Camera Mode) + +## Critical Gotchas (From Real Development) + +### 1. Global Variable Conflict +The CDN script defines `window.zoomSdk`. Declaring `let zoomSdk` in your code causes `SyntaxError: redeclaration of non-configurable global property`. Use `let sdk = window.zoomSdk` or the NPM import. + +### 2. Domain Allowlist +Your app URL **must** be in the Marketplace domain allowlist. Without it, Zoom shows a blank panel with no error. Also add `appssdk.zoom.us` and any CDN domains you use. + +### 3. Capabilities Must Be Listed +Only APIs listed in `config({ capabilities: [...] })` are available. Calling an unlisted API throws an error. This is also true for event listeners. + +### 4. SDK Only Works Inside Zoom +`zoomSdk.config()` throws outside the Zoom client. Always wrap in try/catch with browser fallback: +```javascript +try { await zoomSdk.config({...}); } catch { showBrowserPreview(); } +``` + +### 5. ngrok URL Changes +Free ngrok URLs change on restart. You must update 4 places in Marketplace: Home URL, Redirect URL, OAuth Allow List, Domain Allow List. Consider ngrok paid plan for stable subdomain. + +### 6. In-Client OAuth vs Web OAuth +Use `zoomSdk.authorize()` (In-Client) for best UX - no browser redirect. Only fall back to web redirect for initial install from Marketplace. + +### 7. Camera Mode CEF Race Condition +Camera mode uses CEF which takes time to initialize. `drawImage`/`drawWebView` may fail if called too early. Implement retry with exponential backoff. + +### 8. Cookie Configuration +Zoom's embedded browser requires cookies with `SameSite=None` and `Secure=true`. Without this, sessions break silently. + +### 9. State Validation +Always validate the OAuth `state` parameter to prevent CSRF attacks. Generate cryptographically random state, store it, and verify on callback. + +## Resources + +- **Official docs**: https://developers.zoom.us/docs/zoom-apps/ +- **SDK reference**: https://appssdk.zoom.us/ +- **NPM package**: https://www.npmjs.com/package/@zoom/appssdk +- **Developer forum**: https://devforum.zoom.us/ +- **GitHub SDK source**: https://github.com/zoom/appssdk + +--- + +**Need help?** Start with Integrated Index section below for complete navigation. + +--- + +## Integrated Index + +_This section was migrated from `SKILL.md`._ + +## Quick Start Path + +**If you're new to Zoom Apps, follow this order:** + +1. **Run preflight checks first** -> [RUNBOOK.md](../RUNBOOK.md) + +2. **Read the architecture** -> [concepts/architecture.md](../concepts/architecture.md) + - Frontend/backend pattern, embedded browser, deep linking + - Understand how Zoom loads and communicates with your app + +3. **Build your first app** -> [examples/quick-start.md](../examples/quick-start.md) + - Complete Express + SDK Hello World + - ngrok setup for local development + +4. **Understand running contexts** -> [concepts/running-contexts.md](../concepts/running-contexts.md) + - Where your app runs (inMeeting, inMainClient, inWebinar, etc.) + - Context-specific APIs and limitations + +5. **Implement OAuth** -> [examples/in-client-oauth.md](../examples/in-client-oauth.md) + - In-Client OAuth with PKCE (best UX) + - Token exchange and storage + +6. **Add features** -> [references/apis.md](../references/apis.md) + - 100+ SDK methods organized by category + - Code examples for each + +7. **Troubleshoot** -> [troubleshooting/common-issues.md](../troubleshooting/common-issues.md) + - Quick diagnostics for common problems + +--- + +## Documentation Structure + +``` +zoom-apps-sdk/ +├── SKILL.md # Main skill overview +├── SKILL.md # This file - navigation guide +│ +├── concepts/ # Core architectural patterns +│ ├── architecture.md # Frontend/backend, embedded browser, OAuth flow +│ ├── running-contexts.md # Where your app runs + context-specific APIs +│ └── security.md # OWASP headers, CSP, data access layers +│ +├── examples/ # Complete working code +│ ├── quick-start.md # Hello World - minimal Express + SDK app +│ ├── in-client-oauth.md # In-Client OAuth with PKCE +│ ├── layers-immersive.md # Layers API - immersive mode (custom layouts) +│ ├── layers-camera.md # Layers API - camera mode (virtual camera) +│ ├── collaborate-mode.md # Collaborate mode (shared state) +│ ├── guest-mode.md # Guest mode (unauthenticated -> authorized) +│ ├── breakout-rooms.md # Breakout room integration +│ └── app-communication.md # connect + postMessage between instances +│ +├── troubleshooting/ # Problem solving guides +│ ├── common-issues.md # Quick diagnostics, error codes +│ ├── debugging.md # Local dev setup, ngrok, browser preview +│ └── migration.md # SDK version migration notes +│ +└── references/ # Reference documentation + ├── apis.md # Complete API reference (100+ methods) + ├── events.md # All SDK events + ├── layers-api.md # Layers API detailed reference + ├── oauth.md # OAuth flows for Zoom Apps + └── zmail-sdk.md # Zoom Mail integration +``` + +--- + +## By Use Case + +### I want to build a basic Zoom App +1. [Architecture](../concepts/architecture.md) - Understand the pattern +2. [Quick Start](../examples/quick-start.md) - Build Hello World +3. [In-Client OAuth](../examples/in-client-oauth.md) - Add authorization +4. [Security](../concepts/security.md) - Required headers + +### I want immersive video layouts (Layers API) +1. [Layers Immersive](../examples/layers-immersive.md) - Custom video positions +2. [Layers API Reference](../references/layers-api.md) - All drawing methods +3. [App Communication](../examples/app-communication.md) - Sync layout across participants + +### I want a virtual camera overlay +1. [Camera Mode](../examples/layers-camera.md) - Camera mode rendering +2. [Layers API Reference](../references/layers-api.md) - Drawing methods + +### I want real-time collaboration +1. [Collaborate Mode](../examples/collaborate-mode.md) - Shared state APIs +2. [App Communication](../examples/app-communication.md) - Instance messaging + +### I want guest/anonymous access +1. [Guest Mode](../examples/guest-mode.md) - Three authorization states +2. [In-Client OAuth](../examples/in-client-oauth.md) - promptAuthorize flow + +### I want breakout room support +1. [Breakout Rooms](../examples/breakout-rooms.md) - Room detection and state sync + +### I want to sync between main client and meeting +1. [App Communication](../examples/app-communication.md) - connect + postMessage +2. [Running Contexts](../concepts/running-contexts.md) - Multi-instance behavior + +### I want serverless deployment +1. [Quick Start](../examples/quick-start.md) - Understand the base pattern first +2. Sample: [zoomapps-serverless-vuejs](https://github.com/zoom/zoomapps-serverless-vuejs) - Firebase pattern + +### I want to add Zoom Mail integration +1. [Zoom Mail Reference](../references/zmail-sdk.md) - REST API + mail plugins + +### I'm getting errors +1. [Common Issues](../troubleshooting/common-issues.md) - Quick diagnostic table +2. [Debugging](../troubleshooting/debugging.md) - Local dev setup, DevTools +3. [Migration](../troubleshooting/migration.md) - Version compatibility + +--- + +## Most Critical Documents + +### 1. Architecture (FOUNDATION) +**[concepts/architecture.md](../concepts/architecture.md)** + +Understand how Zoom Apps work: Frontend in embedded browser, backend for OAuth/API, SDK as the bridge. Without this, nothing else makes sense. + +### 2. Quick Start (FIRST APP) +**[examples/quick-start.md](../examples/quick-start.md)** + +Complete working code. Get something running before diving into advanced features. + +### 3. Common Issues (MOST COMMON PROBLEMS) +**[troubleshooting/common-issues.md](../troubleshooting/common-issues.md)** + +90% of Zoom Apps issues are: domain allowlist, global variable conflict, or missing capabilities. + +--- + +## Key Learnings + +### Critical Discoveries: + +1. **Global Variable Conflict is the #1 Gotcha** + - CDN script defines `window.zoomSdk` globally + - `let zoomSdk = ...` causes SyntaxError in Zoom's browser + - Use `let sdk = window.zoomSdk` or NPM import + +2. **Domain Allowlist is Non-Negotiable** + - App shows blank panel with zero error if domain not whitelisted + - Must include your domain AND `appssdk.zoom.us` AND any CDN domains + - ngrok URLs change on restart - must update Marketplace each time + +3. **config() Gates Everything** + - Must be called first, must list all capabilities + - Unlisted capabilities throw errors + - Check `unsupportedApis` for client version compatibility + +4. **In-Client OAuth > Web OAuth for UX** + - `authorize()` keeps user in Zoom (no browser redirect) + - Web redirect only needed for initial Marketplace install + - Always implement PKCE (code_verifier + code_challenge) + +5. **Two App Instances Can Run Simultaneously** + - Main client instance + meeting instance + - Use `connect()` + `postMessage()` to sync between them + - Pre-meeting setup in main client, use in meeting + +6. **Camera Mode Has CEF Quirks** + - CEF initialization takes time + - Draw calls may fail if too early + - Use retry with exponential backoff + +7. **Cookie Settings Matter** + - `SameSite=None` + `Secure=true` required + - Without this, sessions silently fail in embedded browser + +--- + +## Quick Reference + +### "App shows blank panel" +-> [Domain Allowlist](../troubleshooting/common-issues.md) - add domain to Marketplace + +### "SyntaxError: redeclaration" +-> [Global Variable](../troubleshooting/common-issues.md) - use `let sdk = window.zoomSdk` + +### "config() throws error" +-> [Browser Preview](../troubleshooting/debugging.md) - SDK only works inside Zoom + +### "API call fails silently" +-> [OAuth Scopes](../troubleshooting/common-issues.md) - add required scopes in Marketplace + +### "How do I implement [feature]?" +-> [API Reference](../references/apis.md) - find the method, check capabilities needed + +### "How do I test locally?" +-> [Debugging Guide](../troubleshooting/debugging.md) - ngrok + Marketplace config + +--- + +## Document Version + +Based on **@zoom/appssdk v0.16.x** (latest: 0.16.26+) + +--- + +**Happy coding!** + +Start with [Architecture](../concepts/architecture.md) to understand the pattern, then [Quick Start](../examples/quick-start.md) to build your first app. diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/references/layers-api.md b/plugins/zoom-developers/skills/zoom-apps-sdk/references/layers-api.md new file mode 100644 index 00000000..2dc90baa --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/references/layers-api.md @@ -0,0 +1,501 @@ +# Zoom Apps SDK - Layers API Reference + +Build immersive video layouts and camera overlays using the Layers API. + +## Overview + +The Layers API (v1.5) provides rendering modes for custom visual experiences. Requires Zoom Client v5.10.6+. + +| Mode | Description | Use Case | +|------|-------------|----------| +| **Team** (`immersive` + `person` cutout) | Canvas with background-removed participant cutouts | Podcast, talk show, classroom | +| **Presentation** (`immersive` + `rectangle` cutout) | Canvas with full-width participant video tiles | Presentations, branded meetings | +| **Camera** | Overlay on user's own camera feed (OSR) | Branding, name tags, effects | +| **Controller** | Sidebar app that coordinates Layers modes | Required for all modes above | + +> **Note:** When using the Layers API, your app is categorized as an "Immersive App" on the Marketplace. + +## Required Capabilities + +```javascript +await zoomSdk.config({ + capabilities: [ + 'getRunningContext', + 'runRenderingContext', 'closeRenderingContext', + 'drawParticipant', 'clearParticipant', + 'drawImage', 'clearImage', + 'drawWebView', 'clearWebView', + 'postMessage', 'onMessage', + 'sendAppInvitationToAllParticipants', + 'onMyMediaChange', + 'onRenderedAppOpened' + ], + version: '0.16' +}); +``` + +> **Gotcha:** The official guide lists `clearWebview` (lowercase 'v') in one config example. Use `clearWebView` (camelCase) to match the actual method name. + +## Types + +### PixelValue + +All position/size parameters accept three formats: + +```typescript +type PixelValue = `${string}px` | `${string}%` | number; +``` + +| Format | Example | Meaning | +|--------|---------|---------| +| `"Npx"` | `"100px"` | CSS reference pixels | +| `"N%"` | `"50%"` | Percentage of container/view | +| `number` | `1280` | Raw physical pixels | + +### ParticipantCutoutShape + +```typescript +type ParticipantCutoutShape = + | "person" // v5.9.3+ — Cut out background (AI segmentation) + | "standard" // v5.11.3+ — Full uncropped video (squared corners) + | "rectangle" // v5.11.0+ — Rounded rectangle (30px radius) + | "circle" // v5.11.3+ — Circle + | "square" // v5.11.3+ — Square (30px radius) + | "verticalRectangle" // v5.11.3+ — Vertical rectangle (30px radius) +``` + +All shapes have 30px rounded corners except `"standard"` which has squared corners. + +### RenderingContextView + +```typescript +type RenderingContextView = "immersive" | "camera"; +``` + +## Lifecycle + +### Starting a Rendering Context + +```javascript +// Team mode (person cutout — removes backgrounds) +await zoomSdk.runRenderingContext({ + view: 'immersive', + defaultCutout: 'person' +}); + +// Presentation mode (rectangle cutout — keeps backgrounds) +await zoomSdk.runRenderingContext({ + view: 'immersive', + defaultCutout: 'rectangle' +}); + +// Camera mode (affects only your video stream) +await zoomSdk.runRenderingContext({ view: 'camera' }); +``` + +**`runRenderingContext(options)`:** +- `view` (required): `"immersive"` | `"camera"` +- `defaultCutout` (optional): Sets the default cutout shape for all `drawParticipant()` calls in this context + +### Running Context Values + +| Context | Meaning | +|---------|---------| +| `inMeeting` | Default sidebar panel | +| `inImmersive` | Running in immersive mode (team or presentation) | +| `inCamera` | Running as virtual camera (off-screen rendering) | + +```javascript +const { runningContext } = await zoomSdk.getRunningContext(); +// runningContext changes automatically when runRenderingContext() is called +``` + +### Updating Content + +To move, resize, or adjust a drawn element: clear it first, then redraw. + +```javascript +// Move a participant +await zoomSdk.clearParticipant({ participantUUID: uuid }); +await zoomSdk.drawParticipant({ participantUUID: uuid, x: 100, y: 200, width: 640, height: 480, zIndex: 1 }); +``` + +> There is no in-place update — always clear + redraw. + +### Closing + +```javascript +await zoomSdk.closeRenderingContext(); +// Returns app to sidebar, runningContext becomes "inMeeting" +``` + +### Constraints + +- Only a **meeting host** can set the rendering context to immersive +- Only **one immersive context** can exist at a time (second attempt fails with error) +- **Camera mode + Presentation mode can run simultaneously** +- Host must use `sendAppInvitationToAllParticipants` to transition other participants +- If `aomhost` package needs download, `runRenderingContext` returns non-success + +## Drawing Methods + +### drawParticipant + +Position a participant's video feed on the canvas. + +```typescript +drawParticipant(options: DrawParticipantOptions): Promise +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `participantUUID` | `string` | — | Meeting-specific participant identifier | +| ~~`participantId`~~ | ~~`string`~~ | — | **DEPRECATED** — use `participantUUID` | +| `x` | `PixelValue` | `"0px"` | Horizontal position | +| `y` | `PixelValue` | `"0px"` | Vertical position | +| `width` | `PixelValue` | `"100%"` | Width (aspect ratio maintained) | +| `height` | `PixelValue` | `"100%"` | Height (aspect ratio maintained) | +| `zIndex` | `number` | `1` | Stacking order (higher = on top) | +| `cutout` | `ParticipantCutoutShape` | context default | Cutout behavior (v5.9.3+) | +| `cameraModeMirroring` | `boolean` | `false` | Mirror video in camera mode (v5.13.5+) | + +**Mode differences:** +- **Immersive:** Can draw any participant +- **Camera:** Can only draw current user (self) + +```javascript +// Immersive — draw any participant with person cutout +await zoomSdk.drawParticipant({ + participantUUID: 'uuid-from-getMeetingParticipants', + x: 40, y: 100, + width: 580, height: 500, + zIndex: 1, + cutout: 'person' +}); + +// Camera — draw self with mirroring +await zoomSdk.drawParticipant({ + participantUUID: myUUID, + x: 0, y: 0, + width: 1280, height: 720, + zIndex: 1, + cameraModeMirroring: true // v5.13.5+ +}); +``` + +### drawImage + +Draw static images (backgrounds, overlays, borders). + +```typescript +drawImage(options: DrawImageOptions): Promise +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `imageData` | `ImageData` | — | **Required.** Standard JS ImageData object (width, height, pixel bytes) | +| `x` | `PixelValue` | `"0px"` | Horizontal position | +| `y` | `PixelValue` | `"0px"` | Vertical position | +| `zIndex` | `number` | `1` | Stacking order | + +**Returns:** `{ imageId: string }` — use this ID with `clearImage()`. + +> **Important:** `imageData` is a standard JavaScript `ImageData` object (from `canvas.getImageData()`), NOT a base64 data URL. + +```javascript +const canvas = document.createElement('canvas'); +canvas.width = 1280; +canvas.height = 720; +const ctx = canvas.getContext('2d'); +// ... draw on canvas ... +const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + +const { imageId } = await zoomSdk.drawImage({ + imageData, + x: 0, y: 0, + zIndex: 0 +}); +``` + +#### HiDPI Constraints + +`drawImage()` does **not** directly support HiDPI image sizes. For HiDPI/Retina: + +1. Draw to canvas using the scaling ratio (`window.devicePixelRatio`) +2. Divide out the ratio for x/y coordinates when passing to `drawImage` +3. Keep the ratio for width and height +4. You may need to **tile** the screen for full-screen images + +```javascript +const dpr = window.devicePixelRatio || 1; +const canvas = document.createElement('canvas'); +canvas.width = 1280 * dpr; +canvas.height = 720 * dpr; +const ctx = canvas.getContext('2d'); +ctx.scale(dpr, dpr); +// ... draw at logical pixels ... +const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + +await zoomSdk.drawImage({ + imageData, + x: 0, y: 0, + zIndex: 0 +}); +``` + +### drawWebView + +Position the app's OSR (Off-Screen Rendering) webview within the Layers canvas. + +```typescript +drawWebView(options: DrawWebViewOptions): Promise +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `x` | `PixelValue` | `0` | Horizontal position in OSR target area | +| `y` | `PixelValue` | `0` | Vertical position in OSR target area | +| `width` | `PixelValue` | full rendering width | Width in OSR target area | +| `height` | `PixelValue` | full rendering height | Height in OSR target area | +| `zIndex` | `number` | `1` | Stacking order | + +> **⚠ Documentation inconsistency:** The official Zoom guides show a `webviewId` parameter in examples, but the TypeDoc type definition (v0.16.36) does **not** include it. Since there is only one webview per app, this parameter may be vestigial. If in doubt, omit it. + +**What the webview renders:** Your app's home URL as configured in `zoomSdk.config()`. It's an off-screen rendering of your app — not a configurable URL. + +**Only one webview per rendering context.** There is no multi-webview support. + +```javascript +// Full-screen webview in camera mode +const config = await zoomSdk.config({ /* ... */ }); +await zoomSdk.runRenderingContext({ view: 'camera' }); + +await zoomSdk.drawWebView({ + x: 0, + y: 0, + width: config.media.renderTarget.width, // Default: 1280 + height: config.media.renderTarget.height, // Default: 720 + zIndex: 2 +}); +``` + +```javascript +// Partial webview overlay (bottom third of camera) +await zoomSdk.drawWebView({ + x: 0, + y: 480, + width: 1280, + height: 240, + zIndex: 2 +}); +``` + +#### Webview Communication + +The sidebar app and the camera/immersive app are separate instances. Use `postMessage()` and `onMessage` to communicate between them: + +```javascript +// Sidebar instance → Camera instance (no connect() required) +zoomSdk.postMessage({ command: 'update-overlay', text: 'Q&A Time' }); + +// Camera instance listens +zoomSdk.addEventListener('onMessage', (eventInfo) => { + if (eventInfo.command === 'update-overlay') { + document.getElementById('overlay-text').textContent = eventInfo.text; + } +}); +``` + +> **Note:** `connect()` is NOT required for app-to-app messaging in Layers. `postMessage` works between instances of the same app. + +### Clearing + +```javascript +// Clear participant (use participantUUID, not the deprecated participantId) +await zoomSdk.clearParticipant({ participantUUID: 'uuid' }); + +// Clear image (use imageId from drawImage response) +await zoomSdk.clearImage({ imageId: 'id-from-drawImage' }); + +// Clear webview (hides it — app continues running) +await zoomSdk.clearWebView(); +// Note: TypeDoc v0.16.36 shows no parameters. +// Guide examples show { webviewId: "xxx" } but this may be outdated. +``` + +## Coordinate System + +- **Origin:** Top-left corner (0, 0) +- **X:** Increases rightward +- **Y:** Increases downward +- **Units:** PixelValue — supports `"Npx"`, `"N%"`, or raw `number` + +### Immersive Mode + +- Coordinates are CSS pixels relative to the meeting canvas +- Automatic scaling for different window sizes + +### Camera Mode + +- Coordinates are raw pixels relative to `renderTarget` dimensions +- Default renderTarget: 1280×720 (configurable) +- Access via: `config.media.renderTarget.width` / `.height` + +```javascript +const config = await zoomSdk.config({ /* ... */ }); +const rtWidth = config.media.renderTarget.width; // e.g. 1280 +const rtHeight = config.media.renderTarget.height; // e.g. 720 +``` + +## Z-Index Layering + +``` +zIndex: 2+ ─ WebViews, interactive overlays (top) +zIndex: 1 ─ Participant videos +zIndex: 0 ─ Background images (bottom) +``` + +Higher zIndex values render on top. All three element types (participant, image, webview) share the same z-index space and can overlap. + +## Events + +### onRenderedAppOpened + +Fires when the rendering context is ready. Best signal that CEF is initialized in camera mode. + +```javascript +zoomSdk.addEventListener('onRenderedAppOpened', () => { + // Safe to call drawParticipant, drawImage, drawWebView +}); +``` + +### onMyMediaChange + +Fires when the user's video changes (camera switch, "Original ratio" toggle, "HD" toggle). Returns device pixel dimensions of the source video. + +```javascript +zoomSdk.addEventListener('onMyMediaChange', (event) => { + // event.media.video.width / height — device pixels of source video + // Redraw your layout if needed +}); +``` + +### Window Resize (Immersive Only) + +When the Zoom meeting window is resized, the app must move and resize participants/images. Not relevant to Camera Mode (fixed renderTarget). + +## Immersive Mode vs Camera Mode + +| Aspect | Immersive | Camera | +|--------|-----------|--------| +| Scope | Entire meeting view | User's camera only | +| drawParticipant | Any participant | Self only | +| drawImage | Yes | Yes | +| drawWebView | Yes | Yes | +| Who sees it | All participants | All see it on this user's feed | +| Browser engine | Standard WebView | CEF (Chromium Embedded Framework) | +| Rendering | On-screen | Off-screen (OSR) | +| Coordinate space | CSS pixels | Raw pixels (renderTarget) | +| Simultaneous | One immersive at a time | Can run with Presentation mode | + +## Camera Mode: CEF Race Condition + +Camera mode uses CEF which takes time to initialize. Draw calls may fail if called too early. + +**Best approach: Listen for `onRenderedAppOpened`:** + +```javascript +zoomSdk.addEventListener('onRenderedAppOpened', async () => { + await zoomSdk.drawWebView({ x: 0, y: 0, width: 1280, height: 720, zIndex: 2 }); +}); +``` + +**Fallback: Retry with exponential backoff:** + +```javascript +async function drawWithRetry(drawFn, maxRetries = 5) { + for (let i = 0; i < maxRetries; i++) { + try { + await drawFn(); + return; + } catch (error) { + if (i === maxRetries - 1) throw error; + await new Promise(r => setTimeout(r, 200 * Math.pow(2, i))); + } + } +} +``` + +**Alternative: Check running context:** + +```javascript +const { runningContext } = await zoomSdk.getRunningContext(); +if (runningContext === 'inCamera') { + // CEF is ready, safe to draw +} +``` + +## Performance Tips + +- Use `requestAnimationFrame` for animations +- Minimize draw calls (batch updates when possible) +- Pre-render complex backgrounds to a single canvas ImageData +- Keep zIndex values low (0-10 range) +- Clear unused elements to free resources +- Test on lower-end hardware +- For full-screen images: tile the screen (HiDPI limitation) + +## Example: Two-Person Podcast Layout + +```javascript +// Background +const canvas = document.createElement('canvas'); +canvas.width = 1280; +canvas.height = 720; +const ctx = canvas.getContext('2d'); +const gradient = ctx.createLinearGradient(0, 0, 1280, 720); +gradient.addColorStop(0, '#1a1a2e'); +gradient.addColorStop(1, '#16213e'); +ctx.fillStyle = gradient; +ctx.fillRect(0, 0, 1280, 720); +const imageData = ctx.getImageData(0, 0, 1280, 720); + +await zoomSdk.drawImage({ imageData, x: 0, y: 0, zIndex: 0 }); + +// Host (left) — person cutout removes background +await zoomSdk.drawParticipant({ + participantUUID: hostUUID, + x: 40, y: 100, width: 580, height: 500, + zIndex: 1, cutout: 'person' +}); + +// Guest (right) +await zoomSdk.drawParticipant({ + participantUUID: guestUUID, + x: 660, y: 100, width: 580, height: 500, + zIndex: 1, cutout: 'person' +}); +``` + +## Version History + +| Feature | Client Version | SDK Version | +|---------|---------------|-------------| +| Core Layers API | 5.9.0 | 0.16 | +| `cutout: "person"` | 5.9.3 | 0.16 | +| `cutout: "rectangle"` | 5.11.0 | 0.16 | +| `cutout: "circle"`, `"square"`, `"verticalRectangle"` | 5.11.3 | 0.16 | +| `drawWebView()` / `clearWebView()` | 5.10.6 | 0.16.11+ | +| Camera Mode | 5.13.1 | 0.16 | +| `cameraModeMirroring` | 5.13.5 | 0.16 | + +## Resources + +- **Layers API docs**: https://developers.zoom.us/docs/zoom-apps/guides/layers-api/ +- **Using the API**: https://developers.zoom.us/docs/zoom-apps/guides/layers-using-api/ +- **Manipulating UI**: https://developers.zoom.us/docs/zoom-apps/guides/layers-manipulating-ui/ +- **Camera Mode docs**: https://developers.zoom.us/docs/zoom-apps/guides/camera-mode/ +- **Sample app**: https://github.com/zoom/zoomapps-customlayout-js +- **SDK TypeDoc**: https://appssdk.zoom.us/classes/ZoomSdk.ZoomSdk.html +- **Immersive example**: [../examples/layers-immersive.md](../examples/layers-immersive.md) +- **Camera example**: [../examples/layers-camera.md](../examples/layers-camera.md) diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/references/oauth.md b/plugins/zoom-developers/skills/zoom-apps-sdk/references/oauth.md new file mode 100644 index 00000000..239fc02d --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/references/oauth.md @@ -0,0 +1,202 @@ +# Zoom Apps SDK - OAuth Reference + +OAuth flows for Zoom Apps: web-based redirect, In-Client, and third-party. + +## Three OAuth Flows + +| Flow | UX | When to Use | +|------|----|-------------| +| **Web-based redirect** | Opens browser, redirect back | Initial install from Marketplace | +| **In-Client OAuth** | Popup inside Zoom, no redirect | Subsequent authorizations (best UX) | +| **Third-party OAuth** | External provider (Auth0, Google) | When your app needs non-Zoom auth | + +## PKCE (Required for All Flows) + +All Zoom Apps OAuth must use PKCE (Proof Key for Code Exchange): + +```javascript +const crypto = require('crypto'); + +// Generate PKCE pair +const verifier = crypto.randomBytes(32).toString('hex'); +const challenge = crypto.createHash('sha256') + .update(verifier) + .digest('base64url'); + +// verifier: stored server-side (never exposed to client) +// challenge: sent with authorization request +``` + +## Flow 1: Web-Based OAuth (Initial Install) + +``` +User clicks "Add" in Marketplace + | + v +GET https://zoom.us/oauth/authorize + ?client_id=YOUR_CLIENT_ID + &response_type=code + &redirect_uri=YOUR_REDIRECT_URI + &code_challenge=CHALLENGE + &code_challenge_method=S256 + &state=RANDOM_STATE + | + v +User authorizes -> Zoom redirects to YOUR_REDIRECT_URI?code=AUTH_CODE&state=STATE + | + v +Backend validates state, exchanges code for tokens + | + v +Backend gets deeplink, redirects user to Zoom client +``` + +### Server Route Handler + +```javascript +app.get('/auth', async (req, res) => { + const { code, state } = req.query; + + // Validate state (CSRF protection) + if (state !== req.session.state) { + return res.status(403).send('Invalid state'); + } + + // Exchange code for tokens + const tokenResponse = await axios.post('https://zoom.us/oauth/token', null, { + params: { + grant_type: 'authorization_code', + code, + redirect_uri: process.env.ZOOM_APP_REDIRECT_URI, + code_verifier: req.session.codeVerifier + }, + headers: { + 'Authorization': 'Basic ' + Buffer.from( + `${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}` + ).toString('base64') + } + }); + + const { access_token, refresh_token, expires_in } = tokenResponse.data; + + // Store tokens securely + req.session.tokens = { access_token, refresh_token, expires_at: Date.now() + expires_in * 1000 }; + + // Get deeplink to open app in Zoom + const deeplink = await axios.post('https://api.zoom.us/v2/zoomapp/deeplink', + { action: '' }, + { headers: { 'Authorization': `Bearer ${access_token}` } } + ); + + res.redirect(deeplink.data.deeplink); +}); +``` + +## Flow 2: In-Client OAuth (Best UX) + +No browser redirect - authorization happens inside Zoom: + +```javascript +// Frontend +const { codeChallenge, state } = await fetch('/api/auth/challenge').then(r => r.json()); + +zoomSdk.addEventListener('onAuthorized', async (event) => { + await fetch('/api/auth/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: event.code, state: event.state }) + }); +}); + +await zoomSdk.authorize({ codeChallenge, state }); +``` + +See **[In-Client OAuth example](../examples/in-client-oauth.md)** for complete implementation. + +## Token Exchange Endpoint + +``` +POST https://zoom.us/oauth/token + +Headers: + Authorization: Basic base64(CLIENT_ID:CLIENT_SECRET) + +Parameters: + grant_type=authorization_code + code=AUTH_CODE + redirect_uri=YOUR_REDIRECT_URI + code_verifier=PKCE_VERIFIER +``` + +Response: +```json +{ + "access_token": "...", + "token_type": "bearer", + "refresh_token": "...", + "expires_in": 3600, + "scope": "zoomapp:inmeeting" +} +``` + +## Token Refresh + +Access tokens expire in 1 hour. Use refresh token to get new ones: + +```javascript +async function refreshTokens(refreshToken) { + const response = await axios.post('https://zoom.us/oauth/token', null, { + params: { + grant_type: 'refresh_token', + refresh_token: refreshToken + }, + headers: { + 'Authorization': 'Basic ' + Buffer.from( + `${process.env.ZOOM_APP_CLIENT_ID}:${process.env.ZOOM_APP_CLIENT_SECRET}` + ).toString('base64') + } + }); + + return response.data; // { access_token, refresh_token, expires_in } +} +``` + +**Note:** Refresh tokens are single-use. Each refresh returns a new refresh_token. + +## Deep Linking + +After web OAuth, get a deeplink to open your app in Zoom: + +```javascript +const response = await axios.post('https://api.zoom.us/v2/zoomapp/deeplink', + { action: '' }, + { headers: { 'Authorization': `Bearer ${accessToken}` } } +); + +const { deeplink } = response.data; +// Redirect user to this URL to open app in Zoom client +``` + +## Required Scopes + +| Scope | Description | +|-------|-------------| +| `zoomapp:inmeeting` | In-meeting functionality (most common) | +| `user:read` | Read user profile | +| `meeting:read` | Read meeting details | +| `meeting:write` | Create/modify meetings | + +## Token Storage Patterns + +| Pattern | When to Use | +|---------|-------------| +| **Redis** | Multi-instance production servers | +| **Session cookie** | Simple single-server apps | +| **Firestore** | Serverless (Firebase) | +| **Encrypted database** | Complex apps with user accounts | + +## Resources + +- **Auth docs**: https://developers.zoom.us/docs/zoom-apps/authentication/ +- **In-Client OAuth example**: [../examples/in-client-oauth.md](../examples/in-client-oauth.md) +- **oauth skill**: [../../oauth/SKILL.md](../../oauth/SKILL.md) diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/references/zmail-sdk.md b/plugins/zoom-developers/skills/zoom-apps-sdk/references/zmail-sdk.md new file mode 100644 index 00000000..f330e961 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/references/zmail-sdk.md @@ -0,0 +1,214 @@ +# Zoom Mail Integration + +Integrate with Zoom Mail via REST API and Zoom Apps SDK for mail plugins. + +## Overview + +**Important**: There is no standalone "ZMail JS SDK". Zoom Mail integration uses: +1. **Zoom Mail REST API** - Server-side email operations +2. **Zoom Apps SDK** - Build mail plugins that run inside Zoom client + +## Prerequisites + +- Zoom Workplace Pro, Standard Pro, Business, or Enterprise account +- Zoom Mail enabled (Pro accounts have it by default; Business/Enterprise must enable) +- OAuth app with mail scopes + +## Zoom Mail REST API + +### Base URL + +``` +https://api.zoom.us/v2/emails +``` + +### Key Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/emails/mailboxes/{email}/messages/send` | Send a message | +| POST | `/emails/mailboxes/{email}/messages` | Create/add message to mailbox | +| POST | `/emails/mailboxes/{email}/drafts` | Create draft | +| POST | `/emails/mailboxes/{email}/labels` | Create label | +| GET | `/emails/mailboxes/me/profile` | Get mailbox profile | + +### Send Email Example + +Emails must be in RFC 2822 format, base64 encoded: + +```javascript +// Generate RFC 2822 compliant message +function generateEmailMessage(from, to, subject, body) { + const message = `From: ${from}\nTo: ${to}\nSubject: ${subject}\n\n${body}`; + return btoa(message); // Base64 encode +} + +// Send email via API +async function sendEmail(mailbox, toEmail, subject, body) { + const encodedMessage = generateEmailMessage(mailbox, toEmail, subject, body); + + const response = await fetch( + `https://api.zoom.us/v2/emails/mailboxes/${mailbox}/messages/send`, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + raw: encodedMessage + }) + } + ); + + return response.json(); +} + +// Usage +await sendEmail( + 'sender@company.zmail.com', + 'recipient@example.com', + 'Meeting Follow-up', + 'Thank you for attending today\'s meeting.' +); +``` + +### Create Draft + +```javascript +async function createDraft(mailbox, to, subject, body) { + const encodedMessage = generateEmailMessage(mailbox, to, subject, body); + + await fetch( + `https://api.zoom.us/v2/emails/mailboxes/${mailbox}/drafts`, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + raw: encodedMessage + }) + } + ); +} +``` + +### Get Mailbox Profile + +```javascript +const profile = await fetch( + 'https://api.zoom.us/v2/emails/mailboxes/me/profile', + { + headers: { 'Authorization': `Bearer ${accessToken}` } + } +).then(r => r.json()); + +console.log('Email:', profile.email); +``` + +## Zoom Apps SDK - Mail Plugins + +Build apps that run inside Zoom Mail tab using Zoom Apps SDK. + +### Installation + +```bash +npm install @zoom/appssdk +``` + +Or via CDN: +```html + +``` + +### Initialize for Mail Context + +```javascript +// IMPORTANT: Do NOT declare "let zoomSdk" - causes redeclaration error +// The SDK defines window.zoomSdk globally +let sdk = window.zoomSdk; + +async function init() { + try { + const configResponse = await sdk.config({ + popout: true, + capabilities: ['insertContentToMailActiveEditor'], + version: '0.16' + }); + + console.log('Running in context:', configResponse.runningContext); + // Will be "mailTab" when running in Zoom Mail + } catch (error) { + console.error('Not running inside Zoom client:', error); + } +} +``` + +### Insert Content to Mail Editor + +Insert HTML content into the active mail composer: + +```javascript +// Insert content into mail editor (must comply with Tiptap HTML specs) +await sdk.insertContentToMailActiveEditor({ + html: '

This content will be inserted into the email body.

' +}); +``` + +### Mail Plugin Use Cases + +| Use Case | Description | +|----------|-------------| +| **Email Templates** | Insert pre-built email templates | +| **Signature Manager** | Dynamic email signatures | +| **Meeting Links** | Auto-insert Zoom meeting links | +| **CRM Integration** | Pull contact info into emails | +| **Translation** | Translate email content | + +## Authentication + +### Server-to-Server OAuth (for REST API) + +```javascript +async function getAccessToken() { + const response = await fetch('https://zoom.us/oauth/token', { + method: 'POST', + headers: { + 'Authorization': 'Basic ' + btoa(`${clientId}:${clientSecret}`), + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: 'grant_type=client_credentials' + }); + + const data = await response.json(); + return data.access_token; // Valid for 1 hour +} +``` + +## Required Scopes + +| Scope | Description | +|-------|-------------| +| `mail:read` | Read mailbox data | +| `mail:write` | Send emails, create drafts | +| `mail:read:admin` | Admin read access | +| `mail:write:admin` | Admin write access | + +## Limitations + +| Limitation | Notes | +|------------|-------| +| Account requirement | Zoom Workplace Pro+ with Zoom Mail | +| Email format | Must be RFC 2822 compliant, base64 encoded | +| Plugin context | Zoom Apps SDK mail features only work in Mail tab | +| HTML in editor | Must comply with Tiptap specifications | + +## Resources + +- **Zoom Mail API Docs**: https://developers.zoom.us/docs/api/rest/zoom-mail/ +- **Zoom Mail Overview**: https://developers.zoom.us/docs/mail/ +- **Zoom Apps SDK**: https://github.com/zoom/appssdk +- **NPM Package**: https://www.npmjs.com/package/@zoom/appssdk +- **Sample Implementation**: https://github.com/Wrightlab1/ZoomMailAPI diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/troubleshooting/common-issues.md b/plugins/zoom-developers/skills/zoom-apps-sdk/troubleshooting/common-issues.md new file mode 100644 index 00000000..39816fb8 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/troubleshooting/common-issues.md @@ -0,0 +1,85 @@ +# Common Issues + +Quick diagnostics for Zoom Apps SDK problems. + +## Diagnostic Table + +| Issue | Cause | Solution | +|-------|-------|----------| +| App shows blank panel | Domain not in allowlist | Marketplace > Feature > Add Allow List > add your domain | +| `zoomSdk is not defined` | SDK script not loaded | Add `` | +| `SyntaxError: redeclaration of non-configurable global property` | Declared `let zoomSdk` | Use `let sdk = window.zoomSdk` instead | +| `config()` throws error | Not running in Zoom client | Wrap in try/catch, show browser fallback UI | +| `config()` hangs forever | SDK not initialized | Add 3-second timeout fallback | +| API call fails silently | Missing OAuth scope | Add required scopes in Marketplace > Scopes tab | +| `authorize()` doesn't work | Wrong OAuth flow | Use In-Client OAuth: `authorize()` + `onAuthorized` | +| `postMessage` not received | Instances not connected | Call `connect()` first, wait for `onConnect` | +| `runRenderingContext` fails | Missing capability | Add `'runRenderingContext'` to config() capabilities | +| App works then stops | Tunnel URL changed (ngrok/cloudflared/etc.) | Update every Marketplace URL that points at your tunnel domain (home page, redirect URLs, etc.) | +| CORS errors | Backend URL mismatch | Ensure frontend fetches from same-origin or configure CORS | +| Cookies not persisting | Wrong cookie settings | Set `sameSite: 'none', secure: true` | +| OAuth token exchange 400 | Wrong redirect URI | Ensure redirect URI matches exactly in Marketplace and code | +| 403 from Zoom REST API | Token expired | Implement token refresh with `refresh_token` | +| App loads in browser but not in Zoom | CSP headers wrong | Add `frame-ancestors 'self' zoom.us *.zoom.us` | +| `unsupportedApis` contains your API | Old Zoom client | User needs to update Zoom client | +| Collaborate/Layers APIs missing | Host privileges or client/version mismatch | Check `unsupportedApis` + `clientVersion`; ensure required features enabled in Marketplace; test as meeting host where required | +| drawImage fails in camera mode | CEF not ready | Add retry with exponential backoff (see camera-mode example) | + +## Error Codes + +Common SDK error codes from `config()` and API calls: + +| Code | Meaning | Fix | +|------|---------|-----| +| `INVALID_PARAMETERS` | Wrong argument format | Check API docs for correct parameter shape | +| `NOT_SUPPORTED` | API not available in this context | Check running context and unsupportedApis | +| `PERMISSION_DENIED` | Missing capability or scope | Add to config() capabilities AND Marketplace scopes | +| `INTERNAL_ERROR` | SDK internal failure | Retry, check Zoom client version | +| `NOT_CONFIGURED` | config() not called yet | Call config() before any other SDK method | + +## Quick Diagnostic Workflow + +``` +App doesn't work? + │ + ├─ Blank panel, no errors? + │ └─ Domain allowlist. Check Marketplace > Feature > Add Allow List + │ + ├─ JavaScript error in console? + │ ├─ "redeclaration" → Use `let sdk = window.zoomSdk` + │ ├─ "not defined" → SDK script not loaded + │ └─ "not configured" → Call config() first + │ + ├─ config() fails? + │ ├─ In browser → Expected. Not running in Zoom. + │ └─ In Zoom → Check capabilities match scopes + │ + ├─ API call returns error? + │ ├─ PERMISSION_DENIED → Add scope in Marketplace + │ ├─ NOT_SUPPORTED → Wrong running context + │ └─ INVALID_PARAMETERS → Check argument format + │ + └─ Works locally, fails after deploy? + ├─ Domain allowlist updated? + ├─ HTTPS configured? + └─ Cookies: SameSite=None, Secure=true? +``` + +## Enable SDK Debug Logging + +```javascript +// Check supported APIs at runtime +const { supportedApis } = await zoomSdk.getSupportedJsApis(); +console.log('Supported APIs:', supportedApis); + +// Check what was unsupported after config +const config = await zoomSdk.config({...}); +console.log('Unsupported:', config.unsupportedApis); +console.log('Client version:', config.clientVersion); +console.log('Running context:', config.runningContext); +``` + +## Resources + +- **Debugging guide**: [debugging.md](debugging.md) +- **Migration guide**: [migration.md](migration.md) diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/troubleshooting/debugging.md b/plugins/zoom-developers/skills/zoom-apps-sdk/troubleshooting/debugging.md new file mode 100644 index 00000000..5417beb7 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/troubleshooting/debugging.md @@ -0,0 +1,150 @@ +# Debugging Guide + +Local development setup, ngrok configuration, and browser preview. + +## ngrok Setup + +ngrok provides an HTTPS tunnel to your local server. Required because Zoom Apps need HTTPS. + +```bash +# Install ngrok (https://ngrok.com) +# Then start tunnel: +ngrok http 3000 +``` + +Copy the `https://xxxxx.ngrok.io` URL. + +### ngrok Free Tier Limitation + +Free ngrok URLs **change every restart**. You must update 4 places in Marketplace each time: + +1. **Home URL**: `https://xxxxx.ngrok.io` +2. **Redirect URL**: `https://xxxxx.ngrok.io/auth` +3. **OAuth Allow List**: `https://xxxxx.ngrok.io` +4. **Domain Allow List**: `xxxxx.ngrok.io` (no protocol) + +**Tip:** Get ngrok paid plan ($8/mo) for a stable subdomain (e.g., `https://myapp.ngrok.io`). + +## Marketplace Configuration for Local Dev + +In [Zoom Marketplace](https://marketplace.zoom.us/) -> Your App: + +### Feature tab -> Zoom App +- **Home URL**: `https://xxxxx.ngrok.io` +- **Share URL** (optional): `https://xxxxx.ngrok.io` + +### Feature tab -> OAuth Redirect URL +- **Redirect URL for OAuth**: `https://xxxxx.ngrok.io/auth` +- **Add Allow List**: `https://xxxxx.ngrok.io` + +### Feature tab -> Domain Allow List +- `xxxxx.ngrok.io` +- `appssdk.zoom.us` (if using CDN) + +### Scopes tab +- `zoomapp:inmeeting` (minimum required) + +## Browser Preview Mode + +Test your UI outside Zoom by implementing a fallback: + +```javascript +import zoomSdk from '@zoom/appssdk'; + +let isInZoom = false; + +async function init() { + try { + const config = await zoomSdk.config({ + capabilities: ['getMeetingContext', 'getUserContext'], + version: '0.16' + }); + isInZoom = true; + initZoomApp(config); + } catch (error) { + isInZoom = false; + initBrowserPreview(); + } +} + +function initBrowserPreview() { + // Show mock UI with sample data for development + const mockMeeting = { meetingID: '123456789', meetingTopic: 'Test Meeting' }; + const mockUser = { screenName: 'Dev User', role: 'host' }; + renderApp(mockMeeting, mockUser); + console.log('Running in browser preview mode'); +} +``` + +## Opening DevTools in Zoom + +The Zoom client's embedded browser supports DevTools: + +### Windows +1. Open Zoom client +2. Open your Zoom App +3. Right-click inside the app panel +4. Select "Inspect Element" (if available) + +### Alternative: Remote Debugging +1. Start Zoom with remote debugging flag +2. Open `chrome://inspect` in Chrome +3. Find your app's WebView + +**Note:** DevTools availability depends on Zoom client version and settings. Not all surfaces support it. + +## Environment Variables + +Use `.env` file with `dotenv`: + +```bash +# .env (never commit this file) +ZOOM_APP_CLIENT_ID=abc123 +ZOOM_APP_CLIENT_SECRET=xyz789 +ZOOM_APP_REDIRECT_URI=https://xxxxx.ngrok.io/auth +SESSION_SECRET=random-secret-string-here +ZOOM_HOST=https://zoom.us +``` + +```javascript +// server.js +require('dotenv').config(); +// process.env.ZOOM_APP_CLIENT_ID is now available +``` + +Add `.env` to `.gitignore`: +``` +.env +.env.local +``` + +## Common Dev Workflow + +```bash +# Terminal 1: Start server +npm run dev + +# Terminal 2: Start ngrok +ngrok http 3000 + +# Then: +# 1. Copy ngrok URL +# 2. Update Marketplace URLs (if changed) +# 3. Open Zoom client +# 4. Click your app in sidebar or during meeting +# 5. Check the ngrok local request inspector for request logs +``` + +## ngrok Request Inspector + +ngrok provides a local web UI that shows: +- All HTTP requests to your tunnel +- Request/response headers and bodies +- Replay failed requests + +This is invaluable for debugging OAuth redirects and API calls. + +## Resources + +- **Testing guide**: https://developers.zoom.us/docs/zoom-apps/guides/testing/ +- **ngrok docs**: https://ngrok.com/docs diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/troubleshooting/forum-top-questions.md b/plugins/zoom-developers/skills/zoom-apps-sdk/troubleshooting/forum-top-questions.md new file mode 100644 index 00000000..4e26d838 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/troubleshooting/forum-top-questions.md @@ -0,0 +1,39 @@ +--- +title: "Forum-Derived Top Questions (Zoom Apps)" +--- + +# Forum-Derived Top Questions (Zoom Apps) + +This is a checklist of the most common forum questions for **Zoom Apps SDK**. + +## Fast Routing Questions (Ask First) + +- Running context: `inMeeting` vs `inMainClient` vs `inWebinar` vs `inImmersive` etc. +- SDK loading style: **NPM import** vs **CDN** (`window.zoomSdk`) +- Marketplace config: domain allowlist, scopes, and required capabilities + +## “App won’t load / blank panel” + +Most common causes: +- domain not in Marketplace allowlist +- trying to run the app in a normal browser (needs preview/demo mode) +- blocked mixed-content or missing HTTPS in dev tunnel + +## “zoomSdk redeclaration” (CDN) + +Common failure: +- redeclaring `let zoomSdk = ...` when CDN already defines `window.zoomSdk` + +Answer pattern: +- use `const sdk = window.zoomSdk` or NPM import + +## Auth Confusion + +Common asks: +- “Do I use OAuth redirects?” +- “How does In-Client OAuth work?” + +Answer pattern: +- explain In-Client OAuth (PKCE) and required scopes +- differentiate from REST API OAuth flows + diff --git a/plugins/zoom-developers/skills/zoom-apps-sdk/troubleshooting/migration.md b/plugins/zoom-developers/skills/zoom-apps-sdk/troubleshooting/migration.md new file mode 100644 index 00000000..8c000f18 --- /dev/null +++ b/plugins/zoom-developers/skills/zoom-apps-sdk/troubleshooting/migration.md @@ -0,0 +1,106 @@ +# SDK Version Migration + +Notes on upgrading @zoom/appssdk versions and handling deprecations. + +## Current Version + +**Recommended:** `@zoom/appssdk` v0.16.26+ (latest stable) + +```json +{ + "dependencies": { + "@zoom/appssdk": "^0.16.26" + } +} +``` + +## Version Pinning Strategy + +| Strategy | package.json | Risk | Use When | +|----------|-------------|------|----------| +| **Exact** | `"0.16.26"` | Lowest | Production apps, critical stability | +| **Patch** | `"~0.16.26"` | Low | Most apps | +| **Minor** | `"^0.16.26"` | Medium | Active development | + +## Checking API Availability at Runtime + +Not all APIs are available in all Zoom client versions. Always check: + +```javascript +const { supportedApis } = await zoomSdk.getSupportedJsApis(); + +// Check before using an API +if (supportedApis.includes('authorize')) { + // Safe to use In-Client OAuth + await zoomSdk.authorize({...}); +} else { + // Fall back to web redirect OAuth + window.location.href = '/install'; +} +``` + +Also check `unsupportedApis` after `config()`: + +```javascript +const config = await zoomSdk.config({ + capabilities: ['authorize', 'getMeetingContext', 'newFeature'], + version: '0.16' +}); + +if (config.unsupportedApis.includes('newFeature')) { + console.log('newFeature not available in this client version'); + // Graceful degradation +} +``` + +## Config Version Parameter + +The `version` parameter in `config()` indicates which SDK API version you expect: + +```javascript +await zoomSdk.config({ + capabilities: [...], + version: '0.16' // API version, not NPM package version +}); +``` + +This helps Zoom maintain backward compatibility. Use the latest supported version. + +## Sample App SDK Versions + +Status of official sample repositories: + +| Repository | SDK Version | Status | Notes | +|-----------|-------------|--------|-------| +| zoomapps-sample-js | ^0.16.26 | Current | Best reference | +| zoomapps-advancedsample-react | 0.16.0 | Outdated | Works but update recommended | +| zoomapps-customlayout-js | ^0.16.8 | Outdated | Layers API may differ | +| zoomapps-texteditor-vuejs | ^0.16.7 | Outdated | Y.js pattern still valid | +| zoomapps-serverless-vuejs | ^0.16.21 | Slightly outdated | Firebase pattern still valid | + +## Deprecation Pattern + +Zoom typically deprecates APIs gradually: +1. API marked deprecated in docs +2. `unsupportedApis` starts returning it in newer clients +3. API stops working in future client versions + +**Best practice:** Check `getSupportedJsApis()` at startup and implement fallbacks. + +## Migration Checklist + +When upgrading SDK version: + +- [ ] Update `@zoom/appssdk` in package.json +- [ ] Run `npm install` +- [ ] Check changelog for breaking changes +- [ ] Test all capabilities in Zoom client +- [ ] Verify `getSupportedJsApis()` includes your APIs +- [ ] Test in both meeting and main client contexts +- [ ] Test browser preview fallback still works +- [ ] Update `version` parameter in `config()` if needed + +## Resources + +- **NPM package**: https://www.npmjs.com/package/@zoom/appssdk +- **Changelog**: https://github.com/zoom/appssdk/releases