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/video-sdk/web/examples/event-handling.md
# Event Handling

Complete guide to handling events in the Zoom Video SDK for Web.

## Event Registration Pattern

```javascript
// Register event listener
client.on('event-name', (payload) => {
  // Handle event
});

// Remove event listener
client.off('event-name', handlerFunction);
```

## Critical Events (Must Handle)

These events are essential for any Video SDK application.

### Connection Events

```javascript
// Connection state changes - REQUIRED
client.on('connection-change', (payload) => {
  const { state, reason } = payload;

  switch (state) {
    case 'Connected':
      console.log('Successfully connected to session');
      break;
    case 'Reconnecting':
      console.log('Connection lost, attempting to reconnect...', reason);
      showReconnectingUI();
      break;
    case 'Closed':
      console.log('Disconnected from session', reason);
      handleDisconnect(reason);
      break;
    case 'Fail':
      console.error('Connection failed', reason);
      break;
  }
});

function handleDisconnect(reason) {
  // Reasons: 'ended by host', 'kicked by host', 'session ended', etc.
  cleanup();
  showDisconnectMessage(reason);
}
```

### Participant Events

```javascript
// New participant joined
client.on('user-added', (payload) => {
  // payload is array of participants
  payload.forEach(user => {
    console.log('Joined:', user.displayName, 'ID:', user.userId);
    createParticipantUI(user);
  });
});

// Participant left
client.on('user-removed', (payload) => {
  payload.forEach(user => {
    console.log('Left:', user.displayName);
    removeParticipantUI(user.userId);
    // Clean up their video
    stream.detachVideo(user.userId).catch(() => {});
  });
});

// Participant updated (name, mute status, video status, etc.)
client.on('user-updated', (payload) => {
  payload.forEach(user => {
    console.log('Updated:', user);
    updateParticipantUI(user);
  });
});
```

### Video Events

```javascript
// Remote participant video state changed - CRITICAL
client.on('peer-video-state-change', async (payload) => {
  const { action, userId } = payload;

  if (action === 'Start') {
    // User started video - render it
    const element = await stream.attachVideo(userId, VideoQuality.Video_360P);
    document.getElementById(`video-${userId}`)?.appendChild(element);
  } else if (action === 'Stop') {
    // User stopped video - clean up
    await stream.detachVideo(userId);
    const container = document.getElementById(`video-${userId}`);
    if (container) container.innerHTML = '';
  }
});

// Local video capture state
client.on('video-capturing-change', (payload) => {
  const { state } = payload;
  // state: 'Started' | 'Stopped' | 'Failed'
  console.log('Video capture:', state);
});

// Video dimension changed
client.on('video-dimension-change', (payload) => {
  const { width, height, type } = payload;
  console.log('Video dimension:', width, 'x', height, type);
});
```

### Audio Events

```javascript
// Current user audio state changed
client.on('current-audio-change', (payload) => {
  const { action, type, source } = payload;

  switch (action) {
    case 'join':
      console.log('Joined audio via:', type); // 'computer' or 'phone'
      break;
    case 'leave':
      console.log('Left audio');
      break;
    case 'muted':
      console.log('Audio muted', source); // 'active', 'passive(mute all)', etc.
      updateMuteUI(true);
      break;
    case 'unmuted':
      console.log('Audio unmuted');
      updateMuteUI(false);
      break;
  }
});

// Host asks you to unmute
client.on('host-ask-unmute-audio', (payload) => {
  console.log('Host requested unmute:', payload.reason);
  showUnmutePrompt();
});

// Active speaker detection
client.on('active-speaker', (payload) => {
  // Array sorted by volume (loudest first)
  if (payload.length > 0) {
    const activeSpeaker = payload[0];
    highlightActiveSpeaker(activeSpeaker.userId);
  }
});

// Auto-play blocked by browser
client.on('auto-play-audio-failed', () => {
  // Show button for user to click to enable audio
  showAudioEnableButton();
});
```

### Screen Share Events

```javascript
// Someone started/stopped sharing
client.on('active-share-change', async (payload) => {
  const { state, userId } = payload;

  if (state === 'Active') {
    console.log('User', userId, 'started sharing');
    const element = await stream.attachShareView(userId);
    document.getElementById('share-container').appendChild(element);
    showShareView();
  } else {
    console.log('Sharing stopped');
    await stream.detachShareView(userId);
    hideShareView();
  }
});

// You were forced to stop sharing
client.on('passively-stop-share', (payload) => {
  console.log('Forced to stop sharing:', payload);
  // Reasons: 'PrivilegeChange', 'AnotherShareStarted', etc.
});

// Share privilege changed
client.on('share-privilege-change', (payload) => {
  console.log('Share privilege:', payload.privilege);
});
```

## Chat Events

```javascript
// Received a chat message
client.on('chat-on-message', (payload) => {
  const { sender, receiver, message, timestamp, id } = payload;

  console.log(`[${sender.name}]: ${message}`);
  addChatMessage(payload);
});

// Chat privilege changed by host
client.on('chat-privilege-change', (payload) => {
  const { chatPrivilege } = payload;
  // ChatPrivilege.All, ChatPrivilege.NoOne, ChatPrivilege.EveryonePublicly
  updateChatUI(chatPrivilege);
});
```

## Recording Events

```javascript
// Cloud recording state changed
client.on('recording-change', (payload) => {
  const { state } = payload;
  // state: 'Recording', 'Paused', 'Stopped', etc.

  if (state === 'Recording') {
    showRecordingIndicator();
  } else {
    hideRecordingIndicator();
  }
});

// Individual recording consent
client.on('individual-recording-change', (payload) => {
  const { state, userId } = payload;

  if (state === 'Ask') {
    // Host is asking for recording consent
    showRecordingConsentDialog();
  }
});
```

## Live Transcription Events

```javascript
// Transcription status changed
client.on('caption-status', (payload) => {
  const { autoCaption, language } = payload;
  console.log('Caption enabled:', autoCaption, 'Language:', language);
});

// Received transcription text
client.on('caption-message', (payload) => {
  const { text, displayName, done, userId } = payload;

  // done=false means partial/interim result
  // done=true means final result
  updateTranscription(payload);
});

// Captions enabled/disabled
client.on('caption-enable', (isEnabled) => {
  console.log('Captions:', isEnabled ? 'enabled' : 'disabled');
});
```

## Device Events

```javascript
// Device plugged in or removed
client.on('device-change', () => {
  console.log('Device list changed');
  // Refresh device lists
  updateCameraList(stream.getCameraList());
  updateMicList(stream.getMicList());
  updateSpeakerList(stream.getSpeakerList());
});

// Device permission changed
client.on('device-permission-change', (payload) => {
  const { name, state } = payload;
  // name: 'microphone' | 'camera'
  // state: 'granted' | 'denied' | 'prompt'
  console.log(`${name} permission: ${state}`);
});
```

## Network Quality Events

```javascript
// Network quality changed
client.on('network-quality-change', (payload) => {
  const { userId, type, level } = payload;
  // type: 'uplink' | 'downlink'
  // level: 0-5 (0=unknown, 1=bad, 5=excellent)

  if (level <= 2) {
    showNetworkWarning(userId, type);
  }
});
```

## Statistics Events

```javascript
// Video statistics
client.on('video-statistic-data-change', (payload) => {
  const { data, type } = payload;
  // data.encoding: true=send, false=receive
  console.log('Video stats:', {
    fps: data.fps,
    resolution: `${data.width}x${data.height}`,
    bitrate: data.bitrate,
    packetLoss: data.avg_loss,
  });
});

// Audio statistics
client.on('audio-statistic-data-change', (payload) => {
  const { data, type } = payload;
  console.log('Audio stats:', {
    bitrate: data.bitrate,
    packetLoss: data.avg_loss,
    jitter: data.jitter,
  });
});
```

## Complete Event Handler Setup

```javascript
class ZoomEventHandler {
  constructor(client, stream) {
    this.client = client;
    this.stream = stream;
    this.handlers = new Map();
  }

  init() {
    this.setupConnectionEvents();
    this.setupParticipantEvents();
    this.setupVideoEvents();
    this.setupAudioEvents();
    this.setupShareEvents();
    this.setupChatEvents();
    this.setupRecordingEvents();
    this.setupDeviceEvents();
  }

  setupConnectionEvents() {
    this.on('connection-change', this.handleConnectionChange.bind(this));
  }

  setupParticipantEvents() {
    this.on('user-added', this.handleUserAdded.bind(this));
    this.on('user-removed', this.handleUserRemoved.bind(this));
    this.on('user-updated', this.handleUserUpdated.bind(this));
  }

  setupVideoEvents() {
    this.on('peer-video-state-change', this.handlePeerVideoChange.bind(this));
    this.on('video-capturing-change', this.handleCapturingChange.bind(this));
  }

  setupAudioEvents() {
    this.on('current-audio-change', this.handleAudioChange.bind(this));
    this.on('host-ask-unmute-audio', this.handleUnmuteRequest.bind(this));
    this.on('active-speaker', this.handleActiveSpeaker.bind(this));
    this.on('auto-play-audio-failed', this.handleAutoPlayFailed.bind(this));
  }

  setupShareEvents() {
    this.on('active-share-change', this.handleShareChange.bind(this));
    this.on('passively-stop-share', this.handlePassiveStopShare.bind(this));
  }

  setupChatEvents() {
    this.on('chat-on-message', this.handleChatMessage.bind(this));
    this.on('chat-privilege-change', this.handleChatPrivilegeChange.bind(this));
  }

  setupRecordingEvents() {
    this.on('recording-change', this.handleRecordingChange.bind(this));
  }

  setupDeviceEvents() {
    this.on('device-change', this.handleDeviceChange.bind(this));
  }

  // Helper to track handlers for cleanup
  on(event, handler) {
    this.client.on(event, handler);
    this.handlers.set(event, handler);
  }

  // Clean up all handlers
  destroy() {
    this.handlers.forEach((handler, event) => {
      this.client.off(event, handler);
    });
    this.handlers.clear();
  }

  // Event handlers
  handleConnectionChange(payload) {
    console.log('Connection:', payload.state, payload.reason);
  }

  handleUserAdded(payload) {
    payload.forEach(user => console.log('User joined:', user.displayName));
  }

  handleUserRemoved(payload) {
    payload.forEach(user => {
      console.log('User left:', user.displayName);
      this.stream.detachVideo(user.userId).catch(() => {});
    });
  }

  handleUserUpdated(payload) {
    payload.forEach(user => console.log('User updated:', user));
  }

  async handlePeerVideoChange(payload) {
    const { action, userId } = payload;
    if (action === 'Start') {
      const element = await this.stream.attachVideo(userId, VideoQuality.Video_360P);
      document.getElementById(`video-${userId}`)?.appendChild(element);
    } else {
      await this.stream.detachVideo(userId);
    }
  }

  handleCapturingChange(payload) {
    console.log('Video capture:', payload.state);
  }

  handleAudioChange(payload) {
    console.log('Audio:', payload.action, payload.type);
  }

  handleUnmuteRequest(payload) {
    console.log('Host requested unmute');
  }

  handleActiveSpeaker(payload) {
    if (payload.length > 0) {
      console.log('Active speaker:', payload[0].userId);
    }
  }

  handleAutoPlayFailed() {
    console.log('Auto-play blocked - show enable audio button');
  }

  async handleShareChange(payload) {
    const { state, userId } = payload;
    if (state === 'Active') {
      const element = await this.stream.attachShareView(userId);
      document.getElementById('share-container')?.appendChild(element);
    } else {
      await this.stream.detachShareView(userId);
    }
  }

  handlePassiveStopShare(payload) {
    console.log('Forced to stop sharing:', payload);
  }

  handleChatMessage(payload) {
    console.log(`Chat from ${payload.sender.name}: ${payload.message}`);
  }

  handleChatPrivilegeChange(payload) {
    console.log('Chat privilege:', payload.chatPrivilege);
  }

  handleRecordingChange(payload) {
    console.log('Recording:', payload.state);
  }

  handleDeviceChange() {
    console.log('Device list changed');
  }
}

// Usage
const eventHandler = new ZoomEventHandler(client, stream);
eventHandler.init();

// Cleanup on leave
client.on('connection-change', (payload) => {
  if (payload.state === 'Closed') {
    eventHandler.destroy();
  }
});
```

## Event Reference Table

| Event | Category | When Triggered |
|-------|----------|----------------|
| `connection-change` | Session | Connection state changes |
| `user-added` | Participant | New participant joins |
| `user-removed` | Participant | Participant leaves |
| `user-updated` | Participant | Participant properties change |
| `peer-video-state-change` | Video | Remote video starts/stops |
| `video-capturing-change` | Video | Local capture state changes |
| `video-dimension-change` | Video | Video resolution changes |
| `current-audio-change` | Audio | Local audio state changes |
| `host-ask-unmute-audio` | Audio | Host requests unmute |
| `active-speaker` | Audio | Active speaker changes |
| `auto-play-audio-failed` | Audio | Browser blocked autoplay |
| `active-share-change` | Share | Someone starts/stops sharing |
| `passively-stop-share` | Share | Forced to stop sharing |
| `share-privilege-change` | Share | Share settings changed |
| `chat-on-message` | Chat | Message received |
| `recording-change` | Recording | Recording state changes |
| `caption-message` | Transcription | Transcription text received |
| `device-change` | Device | Device added/removed |
| `network-quality-change` | Network | Network quality changes |

## Key Points

1. **Always handle `connection-change`** - Essential for detecting disconnects
2. **`peer-video-state-change` is for remote video** - Not your own video
3. **Clean up on `user-removed`** - Detach their video
4. **Handle `auto-play-audio-failed`** - Browser may block audio autoplay
5. **Use `off()` to clean up** - Prevent memory leaks

## Related Documentation

- [Session Join](session-join-pattern.md) - Initial setup
- [Video Rendering](video-rendering.md) - Video handling
- [API Reference](../references/web-reference.md) - Full API