# 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)