5.7 KiB
5.7 KiB
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
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
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
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
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
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:
// 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:
// 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
- Sample app: https://github.com/zoom/zoomapps-customlayout-js