Files
2026-07-13 12:20:06 +08:00

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