File: C:/Users/fred/.codex/.tmp/plugins/plugins/zoom/skills/zoom-apps-sdk/examples/layers-camera.md
# 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