Subchapter 157.4
references/layers-api.mdMarkdown16 KBView on GitHub
Build immersive video layouts and camera overlays using the Layers API.
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.
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. UseclearWebView(camelCase) to match the actual method name.
All position/size parameters accept three formats:
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 |
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.
type RenderingContextView = "immersive" | "camera";// 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| Context | Meaning |
|---|---|
inMeeting | Default sidebar panel |
inImmersive | Running in immersive mode (team or presentation) |
inCamera | Running as virtual camera (off-screen rendering) |
const { runningContext } = await zoomSdk.getRunningContext();
// runningContext changes automatically when runRenderingContext() is calledTo move, resize, or adjust a drawn element: clear it first, then redraw.
// 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.
await zoomSdk.closeRenderingContext();
// Returns app to sidebar, runningContext becomes "inMeeting"sendAppInvitationToAllParticipants to transition other participantsaomhost package needs download, runRenderingContext returns non-successPosition a participant’s video feed on the canvas.
drawParticipant(options: DrawParticipantOptions): Promise<GeneralMessageResponse>| 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 — 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+
});Draw static images (backgrounds, overlays, borders).
drawImage(options: DrawImageOptions): Promise<DrawImageResponse>| 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:
imageDatais a standard JavaScriptImageDataobject (fromcanvas.getImageData()), NOT a base64 data URL.
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
});drawImage() does not directly support HiDPI image sizes. For HiDPI/Retina:
window.devicePixelRatio)drawImageconst 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
});Position the app’s OSR (Off-Screen Rendering) webview within the Layers canvas.
drawWebView(options: DrawWebViewOptions): Promise<GeneralMessageResponse>| 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
webviewIdparameter 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.
// 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
});// Partial webview overlay (bottom third of camera)
await zoomSdk.drawWebView({
x: 0,
y: 480,
width: 1280,
height: 240,
zIndex: 2
});The sidebar app and the camera/immersive app are separate instances. Use postMessage() and onMessage to communicate between them:
// 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.postMessageworks between instances of the same app.
// 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."Npx", "N%", or raw numberrenderTarget dimensionsconfig.media.renderTarget.width / .heightconst config = await zoomSdk.config({ /* ... */ });
const rtWidth = config.media.renderTarget.width; // e.g. 1280
const rtHeight = config.media.renderTarget.height; // e.g. 720zIndex: 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.
Fires when the rendering context is ready. Best signal that CEF is initialized in camera mode.
zoomSdk.addEventListener('onRenderedAppOpened', () => {
// Safe to call drawParticipant, drawImage, drawWebView
});Fires when the user’s video changes (camera switch, “Original ratio” toggle, “HD” toggle). Returns device pixel dimensions of the source video.
zoomSdk.addEventListener('onMyMediaChange', (event) => {
// event.media.video.width / height — device pixels of source video
// Redraw your layout if needed
});When the Zoom meeting window is resized, the app must move and resize participants/images. Not relevant to Camera Mode (fixed renderTarget).
| 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 uses CEF which takes time to initialize. Draw calls may fail if called too early.
Best approach: Listen for onRenderedAppOpened:
zoomSdk.addEventListener('onRenderedAppOpened', async () => {
await zoomSdk.drawWebView({ x: 0, y: 0, width: 1280, height: 720, zIndex: 2 });
});Fallback: Retry with exponential backoff:
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:
const { runningContext } = await zoomSdk.getRunningContext();
if (runningContext === 'inCamera') {
// CEF is ready, safe to draw
}requestAnimationFrame for animations// 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'
});| 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 |