HEX
Server: Apache/2.4.46 (Win64) OpenSSL/1.1.1j PHP/8.4.25
System: Windows NT DESKTOP-4TAV2RJ 10.0 build 19045 (Windows 10) AMD64
User: fred (0)
PHP: 8.4.25
Disabled: NONE
Upload Files
File: C:/Users/fred/.codex/.tmp/plugins/plugins/zoom/skills/general/use-cases/custom-video.md
# Custom Video

Build fully branded video experiences with complete UI control.

## Overview

Use the Zoom Video SDK to create custom video applications with your own UI, branding, and user experience - powered by Zoom's infrastructure.

## Skills Needed

- **zoom-video-sdk** - Primary

## Meeting SDK vs Video SDK

| Aspect | Meeting SDK | Video SDK |
|--------|-------------|-----------|
| UI | Zoom's UI | Your custom UI |
| Branding | Zoom branding | Your branding |
| Experience | Zoom meetings | Video sessions |
| Features | Full Zoom features | Core video features |

## Platform Options

| Platform | Use Case | Guide |
|----------|----------|-------|
| Web | Browser-based custom video | [video-sdk/SKILL.md](../../video-sdk/SKILL.md) |
| Linux | Headless bots, raw media capture/injection | [video-sdk/linux/linux.md](../../video-sdk/linux/linux.md) |
| iOS | Custom video on iPhone/iPad | - |
| Android | Custom video on Android | - |
| Desktop | Custom desktop video apps | - |

## Quick Start (Web)

```javascript
import ZoomVideo from '@zoom/videosdk';

const client = ZoomVideo.createClient();
await client.init('en-US', 'CDN');
await client.join(topic, signature, userName, password);

const stream = client.getMediaStream();
await stream.startVideo();
await stream.startAudio();
```

## Common Tasks

### Building Custom Video Layouts

```javascript
// Initialize Video SDK
const client = ZoomVideo.createClient();
await client.init('en-US', 'CDN');
await client.join(topic, signature, userName, password);

const stream = client.getMediaStream();

// Start my video
await stream.startVideo();
await stream.renderVideo(
  document.querySelector('#my-video'),
  client.getSessionInfo().userId,
  1280, 720,  // width, height
  0, 0,       // x, y offset
  3           // quality (1-4)
);

// Listen for other participants
client.on('user-added', (payload) => {
  payload.forEach(async (user) => {
    if (user.bVideoOn) {
      await renderParticipantVideo(user.userId);
    }
  });
});

async function renderParticipantVideo(userId) {
  const container = createVideoContainer(userId);
  await stream.renderVideo(container, userId, 640, 360, 0, 0, 2);
}
```

### Gallery View Implementation

```javascript
class GalleryView {
  constructor(containerEl, maxPerPage = 25) {
    this.container = containerEl;
    this.maxPerPage = maxPerPage;
    this.currentPage = 0;
    this.participants = [];
  }

  updateLayout() {
    const count = Math.min(this.participants.length, this.maxPerPage);
    const cols = Math.ceil(Math.sqrt(count));
    const rows = Math.ceil(count / cols);

    const cellWidth = this.container.clientWidth / cols;
    const cellHeight = this.container.clientHeight / rows;

    // Render each participant in grid
    this.participants.slice(0, this.maxPerPage).forEach((userId, index) => {
      const x = (index % cols) * cellWidth;
      const y = Math.floor(index / cols) * cellHeight;

      stream.renderVideo(
        this.getCanvas(userId),
        userId,
        cellWidth, cellHeight,
        x, y, 2
      );
    });
  }

  addParticipant(userId) {
    this.participants.push(userId);
    this.updateLayout();
  }

  removeParticipant(userId) {
    this.participants = this.participants.filter(id => id !== userId);
    this.updateLayout();
  }
}
```

### Speaker View Implementation

```javascript
class SpeakerView {
  constructor(mainEl, stripEl) {
    this.mainVideo = mainEl;
    this.stripContainer = stripEl;
    this.activeSpeaker = null;
    this.participants = [];
  }

  setActiveSpeaker(userId) {
    if (this.activeSpeaker === userId) return;

    this.activeSpeaker = userId;

    // Render active speaker large
    stream.renderVideo(
      this.mainVideo,
      userId,
      1280, 720, 0, 0, 4  // High quality
    );

    // Update thumbnail strip
    this.updateStrip();
  }

  updateStrip() {
    const others = this.participants.filter(id => id !== this.activeSpeaker);
    const thumbWidth = 160;
    const thumbHeight = 90;

    others.forEach((userId, index) => {
      stream.renderVideo(
        this.getStripCanvas(userId),
        userId,
        thumbWidth, thumbHeight,
        index * thumbWidth, 0, 1  // Lower quality
      );
    });
  }
}

// Auto-switch to active speaker
client.on('active-speaker', (payload) => {
  if (payload.userId) {
    speakerView.setActiveSpeaker(payload.userId);
  }
});
```

### Custom Controls

```javascript
// Audio controls
async function toggleMute() {
  const stream = client.getMediaStream();
  const isMuted = stream.isAudioMuted();

  if (isMuted) {
    await stream.unmuteAudio();
  } else {
    await stream.muteAudio();
  }

  updateMuteButton(!isMuted);
}

// Video controls
async function toggleVideo() {
  const stream = client.getMediaStream();
  const isVideoOn = stream.isCapturingVideo();

  if (isVideoOn) {
    await stream.stopVideo();
  } else {
    await stream.startVideo();
    await stream.renderVideo(myCanvas, myUserId, 1280, 720, 0, 0, 3);
  }

  updateVideoButton(!isVideoOn);
}

// Device selection
async function switchCamera(deviceId) {
  const stream = client.getMediaStream();
  await stream.switchCamera(deviceId);
}

async function switchMicrophone(deviceId) {
  const stream = client.getMediaStream();
  await stream.switchMicrophone(deviceId);
}

// Get available devices
const cameras = await ZoomVideo.getCameras();
const mics = await ZoomVideo.getMicrophones();
const speakers = await ZoomVideo.getSpeakers();
```

## Resources

- **Video SDK docs**: https://developers.zoom.us/docs/video-sdk/
- **Web sample**: https://github.com/zoom/videosdk-web-sample
- **UI Toolkit**: https://developers.zoom.us/docs/video-sdk/web/ui-toolkit/