# Hiyve Client API Workflows

> **Comprehensive guide to real-world workflow patterns for the Hiyve Client SDK**

This document complements the [API Reference](./docs.md) by showing how different features work together in complete, real-world scenarios. Each workflow includes sequence diagrams, code examples, and the events you'll handle at each step.

---

## Table of Contents

1. [Core Connection Workflows](#1-core-connection-workflows)
   - [1.1 Room Owner Creates Room](#11-room-owner-creates-room)
   - [1.2 Guest Joins Existing Room](#12-guest-joins-existing-room)
   - [1.3 Token-Based Invite Flow](#13-token-based-invite-flow)
   - [1.4 Wait for Host Flow](#14-wait-for-host-flow)
2. [Media Workflows](#2-media-workflows)
   - [2.1 Screen Sharing](#21-screen-sharing)
   - [2.2 Device Switching](#22-device-switching)
   - [2.3 Audio Processing Pipeline](#23-audio-processing-pipeline)
3. [Control Workflows](#3-control-workflows)
   - [3.1 Mute/Unmute Flow](#31-muteunmute-flow)
   - [3.2 Waiting Room Admission](#32-waiting-room-admission)
   - [3.3 Room Owner Controls](#33-room-owner-controls)
4. [Recording & Media Services](#4-recording--media-services)
   - [4.1 Recording with Live Transcription](#41-recording-with-live-transcription)
   - [4.2 Video Composition](#42-video-composition)
   - [4.3 Live Streaming](#43-live-streaming)
   - [4.4 Post-Meeting AI Summary](#44-post-meeting-ai-summary)
5. [Communication Workflows](#5-communication-workflows)
   - [5.1 Chat Messaging](#51-chat-messaging)
   - [5.2 Data Channel Messaging](#52-data-channel-messaging)
6. [Resilience Workflows](#6-resilience-workflows)
   - [6.1 Reconnection](#61-reconnection)
   - [6.2 iOS Orientation Handling](#62-ios-orientation-handling)
   - [6.3 Constraint Relaxation](#63-constraint-relaxation)
7. [File Management Workflows](#7-file-management-workflows)
   - [7.1 File Upload & Sharing](#71-file-upload--sharing)
   - [7.2 Room File System Cache](#72-room-file-system-cache)
8. [Complete Application Examples](#8-complete-application-examples)
   - [8.1 Basic Video Call](#81-basic-video-call)
   - [8.2 Recorded Meeting with Transcription](#82-recorded-meeting-with-transcription)
   - [8.3 Webinar with Waiting Room](#83-webinar-with-waiting-room)

---

## 1. Core Connection Workflows

### 1.1 Room Owner Creates Room

The room owner initiates the meeting, becoming the host with elevated privileges (recording, streaming, participant control).

```mermaid
sequenceDiagram
    participant Owner as Room Owner
    participant Client as Hiyve Client
    participant Server as Signaling Server
    participant Media as Media Server

    Note over Owner,Media: Phase 1: Initialize & Configure
    Owner->>Client: new Client({ roomToken })
    Owner->>Client: listVideoDevices()
    Owner->>Client: listAudioInputDevices()
    Owner->>Client: setLocalVideoDevice({ videoDeviceId })
    Owner->>Client: setLocalAudioInputDevice({ audioInputDeviceId })

    Note over Owner,Media: Phase 2: Create Room
    Owner->>Client: createRoom({ roomName, userId })
    Client->>Server: Connect WebSocket
    Client->>Server: create room request
    Server->>Media: Create room on media server
    Media-->>Server: Room created
    Server-->>Client: Room object
    Client-->>Owner: ROOM_JOINED event

    Note over Owner,Media: Phase 3: Connect Media
    Owner->>Client: connectTransports({ localVideoElementId })
    Client->>Server: Create send transport
    Client->>Server: Create receive transport
    Client->>Media: Produce audio track
    Client->>Media: Produce video track
    Client-->>Owner: CONNECTED event

    Note over Owner,Media: Room Ready - Waiting for Guests
```

#### Code Example

```javascript
import { Client, ClientEvents } from 'muziertcclient';

// Step 1: Initialize client with room token (obtained from your backend)
const client = new Client({ roomToken: 'your-server-generated-token' });

// Step 2: Set up event listeners BEFORE creating room
client.addEventListener(ClientEvents.ROOM_JOINED, (e) => {
  console.log('Room created:', e.detail);
  // { id: 'room-123', name: 'my-meeting', owner: 'user-1', users: ['user-1'] }
});

client.addEventListener(ClientEvents.CONNECTED, () => {
  console.log('Media connected - room is ready');
});

client.addEventListener(ClientEvents.USER_JOINED_ROOM, (e) => {
  console.log('New participant joined:', e.detail.userId);
});

client.addEventListener(ClientEvents.MEDIA_TRACK_ADDED, (e) => {
  const { userId, track, kind, paused, audioOnly } = e.detail;
  attachRemoteTrack(userId, track, kind, paused);
});

client.addEventListener(ClientEvents.ERROR, (e) => {
  console.error('Client error:', e.detail.error);
});

// Step 3: Configure devices
const cameras = await client.listVideoDevices();
const microphones = await client.listAudioInputDevices();

await client.setLocalVideoDevice({ videoDeviceId: cameras[0].deviceId });
await client.setLocalAudioInputDevice({ audioInputDeviceId: microphones[0].deviceId });

// Step 4: Create the room
const room = await client.createRoom({
  roomName: 'team-standup',
  userId: 'host-user-123',
  externalUserId: 'john@company.com', // Optional: your app's user ID
  options: {
    waitingRoom: true,  // Enable waiting room
    maxParticipants: 10
  }
});

console.log('Room created, ID:', room.id);

// Step 5: Connect media transports
await client.connectTransports({
  localVideoElementId: 'local-video', // DOM element ID for local preview
  options: {
    enableAudioInputMonitor: true,  // Enable VU meter
    enableAudioGainControl: true    // Enable volume control
  }
});

// Room is now live!
```

#### Events Emitted

| Event | When | Payload |
|-------|------|---------|
| `ROOM_JOINED` | Room successfully created | `{ id, name, owner, users[], created }` |
| `CONNECTED` | Media transports connected | None |
| `AUDIO_INPUT_MONITOR_CREATED` | If enabled in options | `AudioInputProcessor` instance |
| `AUDIO_GAIN_CONTROL_CREATED` | If enabled in options | `GainProcessor` instance |

---

### 1.2 Guest Joins Existing Room

Guests join an existing room created by the owner. They receive media tracks from all existing participants.

```mermaid
sequenceDiagram
    participant Guest as Guest User
    participant Client as Hiyve Client
    participant Server as Signaling Server
    participant Media as Media Server

    Note over Guest,Media: Phase 1: Initialize & Configure
    Guest->>Client: new Client({ roomToken })
    Guest->>Client: setLocalVideoDevice()
    Guest->>Client: setLocalAudioInputDevice()

    Note over Guest,Media: Phase 2: Join Room
    Guest->>Client: joinRoom({ roomName, userId })
    Client->>Server: Join room request
    Server-->>Client: Room object + existing users
    Client-->>Guest: ROOM_JOINED event

    Note over Guest,Media: Phase 3: Connect & Receive Media
    Guest->>Client: connectTransports()
    Client->>Server: Create transports
    Client->>Media: Produce audio/video
    Client->>Server: Get existing producers

    loop For each existing producer
        Server-->>Client: Producer info (with paused state)
        Client->>Media: Consume producer
        Client-->>Guest: MEDIA_TRACK_ADDED event
    end

    Client-->>Guest: CONNECTED event
```

#### Code Example

```javascript
import { Client, ClientEvents } from 'muziertcclient';

const client = new Client({ roomToken: 'guest-room-token' });

// Track remote participants
const remoteParticipants = new Map();

client.addEventListener(ClientEvents.MEDIA_TRACK_ADDED, (e) => {
  const { userId, track, kind, paused, audioOnly } = e.detail;

  // Get or create participant entry
  if (!remoteParticipants.has(userId)) {
    remoteParticipants.set(userId, { audio: null, video: null, audioOnly });
    createParticipantUI(userId, audioOnly);
  }

  const participant = remoteParticipants.get(userId);
  participant[kind] = track;

  // Attach track to DOM element
  const element = document.getElementById(`${kind}-${userId}`);
  if (element) {
    element.srcObject = new MediaStream([track]);

    // Reflect mute state in UI
    if (paused) {
      element.classList.add('muted');
      showMuteIcon(userId, kind);
    }
  }
});

client.addEventListener(ClientEvents.MEDIA_TRACK_REMOVED, (e) => {
  const { userId, kind } = e.detail;
  const element = document.getElementById(`${kind}-${userId}`);
  if (element) {
    element.srcObject = null;
  }
});

client.addEventListener(ClientEvents.USER_DISCONNECTED, (e) => {
  const { userId } = e.detail;
  remoteParticipants.delete(userId);
  removeParticipantUI(userId);
});

// Configure and join
await client.setLocalVideoDevice({ videoDeviceId: selectedCamera });
await client.setLocalAudioInputDevice({ audioInputDeviceId: selectedMic });

const room = await client.joinRoom({
  roomName: 'team-standup',
  userId: 'guest-user-456',
  externalUserId: 'jane@company.com'
});

console.log(`Joined room with ${room.users.length} participants`);

await client.connectTransports({
  localVideoElementId: 'local-video'
});
```

#### Events Emitted

| Event | When | Payload |
|-------|------|---------|
| `ROOM_JOINED` | Successfully joined room | `{ id, name, owner, users[], created }` |
| `MEDIA_TRACK_ADDED` | For each remote user's track | `{ userId, track, kind, paused, audioOnly }` |
| `CONNECTED` | Media transports connected | None |

---

### 1.3 Token-Based Invite Flow

Room owners can generate invite tokens to share with guests. Tokens support password protection, expiration, and single/multi-use types.

```mermaid
sequenceDiagram
    participant Owner as Room Owner
    participant Client as Owner's Client
    participant Server as Signaling Server
    participant Guest as Guest User
    participant GClient as Guest's Client

    Note over Owner,GClient: Phase 1: Owner Generates Token
    Owner->>Client: createJoinToken({ roomName, options })
    Client->>Server: Generate token request
    Server-->>Client: { token, expiresAt }
    Owner->>Owner: Share token URL with guest

    Note over Owner,GClient: Phase 2: Guest Uses Token
    Guest->>GClient: getRoomNameFromToken({ joinToken })
    GClient->>Server: Decode token
    Server-->>GClient: Room name

    Guest->>GClient: joinRoomWithToken({ joinToken, password? })
    GClient->>Server: Validate token + join
    Server-->>GClient: Room object
    GClient-->>Guest: ROOM_JOINED event
```

#### Code Example - Owner Generates Token

```javascript
// Owner creates an invite token
const tokenResult = await client.createJoinToken({
  userId: 'host-user-123',
  roomName: 'team-standup',
  joinUserId: 'expected-guest-id', // Optional: restrict to specific user
  password: 'meeting123',          // Optional: password protection
  expiresIn: '24h',                // Token expiration
  type: 'multi-use'                // 'single-use' or 'multi-use'
});

// Share this URL with guests
const inviteUrl = `https://yourapp.com/join?token=${tokenResult.token}`;
console.log('Share this link:', inviteUrl);
```

#### Code Example - Guest Joins with Token

```javascript
// Guest extracts token from URL
const urlParams = new URLSearchParams(window.location.search);
const joinToken = urlParams.get('token');

const client = new Client({ roomToken: 'guest-room-token' });

// Optionally get room info before joining
const roomName = await client.getRoomNameFromToken({
  joinToken,
  password: 'meeting123', // If token is password-protected
  userId: 'guest-user-456'
});
console.log('About to join:', roomName);

// Join the room
const room = await client.joinRoomWithToken({
  joinToken,
  password: 'meeting123',
  userId: 'guest-user-456',
  roomRegion: 'us-east-1' // Optional: specify region
});

await client.connectTransports({ localVideoElementId: 'local-video' });
```

#### Token Options

| Option | Type | Description |
|--------|------|-------------|
| `password` | `string` | Require password to use token |
| `expiresIn` | `string` | Token validity period (`'1h'`, `'24h'`, `'7d'`) |
| `type` | `string` | `'single-use'` (invalidated after use) or `'multi-use'` |
| `joinUserId` | `string` | Restrict token to specific user ID |

---

### 1.4 Wait for Host Flow

Guests can join before the host starts the meeting, waiting in a lobby until the room is active.

```mermaid
sequenceDiagram
    participant Guest as Guest User
    participant Client as Hiyve Client
    participant Server as Signaling Server

    Note over Guest,Server: Guest joins before host
    Guest->>Client: joinRoomWithTokenAndWait({ joinToken, timeout })
    Client-->>Guest: WAIT_FOR_HOST_STARTED event

    loop Polling until host joins or timeout
        Client->>Server: Check room status
        Server-->>Client: Room not ready
        Client->>Client: Wait pollingInterval
    end

    alt Host starts meeting
        Server-->>Client: Room is ready
        Client-->>Guest: WAIT_FOR_HOST_ROOM_READY event
        Client->>Server: Complete join
        Client-->>Guest: ROOM_JOINED event
    else Timeout expires
        Client-->>Guest: WAIT_FOR_HOST_TIMEOUT event
    else User cancels
        Client-->>Guest: WAIT_FOR_HOST_CANCELLED event
    end
```

#### Code Example

```javascript
const client = new Client({ roomToken: 'guest-room-token' });

// Listen for wait status events
client.addEventListener(ClientEvents.WAIT_FOR_HOST_STARTED, (e) => {
  const { roomName, timeout, pollingInterval } = e.detail;
  showWaitingUI(`Waiting for host to start ${roomName}...`);
});

client.addEventListener(ClientEvents.WAIT_FOR_HOST_ROOM_READY, (e) => {
  const { roomName, elapsedTime } = e.detail;
  console.log(`Host is ready! Waited ${elapsedTime}ms`);
  hideWaitingUI();
});

client.addEventListener(ClientEvents.WAIT_FOR_HOST_TIMEOUT, (e) => {
  const { roomName, timeout, elapsedTime } = e.detail;
  showError(`Meeting didn't start within ${timeout}ms. Please try again.`);
});

client.addEventListener(ClientEvents.WAIT_FOR_HOST_CANCELLED, (e) => {
  console.log('Stopped waiting for host');
});

// Join and wait for host
try {
  const room = await client.joinRoomWithTokenAndWait({
    joinToken: 'invite-token-xyz',
    password: 'meeting123',
    userId: 'guest-user-456',
    timeout: 300000,        // 5 minutes max wait
    pollingInterval: 3000   // Check every 3 seconds
  });

  // Host is ready, connect media
  await client.connectTransports({ localVideoElementId: 'local-video' });
} catch (error) {
  if (error.message.includes('timeout')) {
    showError('Host did not start the meeting in time');
  }
}
```

---

## 2. Media Workflows

### 2.1 Screen Sharing

Screen sharing uses **track replacement** instead of creating new producers, ensuring other participants see uninterrupted video.

```mermaid
sequenceDiagram
    participant User as Sharing User
    participant Client as Hiyve Client
    participant Others as Other Participants

    Note over User,Others: Why Track Replacement?
    Note over User,Others: Producer close/create would cause<br/>MEDIA_TRACK_REMOVED → MEDIA_TRACK_ADDED<br/>causing video flicker for all participants

    User->>Client: startScreenShare()
    Client->>Client: getDisplayMedia({ video: true, audio: true })
    Client->>Client: videoProducer.replaceTrack(screenTrack)
    Note over Client: Same producer ID maintained!
    Client-->>User: SCREEN_SHARE_STARTED event
    Note over Others: Video continues seamlessly<br/>with screen content

    opt System audio available
        Client->>Client: mergeAudioInput(screenAudioStream)
        Client-->>User: AUDIO_STREAM_ADDED event
    end

    User->>Client: stopScreenShare()
    Client->>Client: Stop screen track
    Client->>Client: getUserMedia(cameraConstraints)
    Client->>Client: videoProducer.replaceTrack(cameraTrack)
    Client-->>User: SCREEN_SHARE_STOPPED event
    Note over Others: Video continues seamlessly<br/>back to camera
```

#### Code Example

```javascript
// Start screen sharing
const startScreenShareBtn = document.getElementById('share-screen');
const stopScreenShareBtn = document.getElementById('stop-share');

client.addEventListener(ClientEvents.SCREEN_SHARE_STARTED, () => {
  startScreenShareBtn.disabled = true;
  stopScreenShareBtn.disabled = false;
  showScreenShareIndicator();
});

client.addEventListener(ClientEvents.SCREEN_SHARE_STOPPED, () => {
  startScreenShareBtn.disabled = false;
  stopScreenShareBtn.disabled = true;
  hideScreenShareIndicator();
});

startScreenShareBtn.addEventListener('click', async () => {
  try {
    await client.startScreenShare();
  } catch (error) {
    if (error.name === 'NotAllowedError') {
      showError('Screen share permission denied');
    }
  }
});

stopScreenShareBtn.addEventListener('click', async () => {
  await client.stopScreenShare();
});

// Check if currently sharing
if (client.isScreenShareActive()) {
  console.log('Screen share is active');
}
```

#### Why Track Replacement Matters

| Approach | What Happens | User Experience |
|----------|--------------|-----------------|
| **Track Replacement** (used) | `producer.replaceTrack()` | Seamless transition, same producer ID |
| Producer Close/Create (NOT used) | Close producer → Create new | Video disappears 300-500ms, flickers for all |

---

### 2.2 Device Switching

Switch cameras, microphones, or speakers without disconnecting from the room.

```mermaid
sequenceDiagram
    participant User as User
    participant Client as Hiyve Client
    participant Browser as Browser API

    Note over User,Browser: Video Device Switch
    User->>Client: setLocalVideoDevice({ videoDeviceId })
    Client->>Browser: getUserMedia({ video: { deviceId } })
    Browser-->>Client: New video stream
    Client->>Client: videoProducer.replaceTrack(newTrack)
    Client->>Client: Update local video element
    Client-->>User: VIDEO_INPUT_DEVICE_CHANGED event

    Note over User,Browser: Audio Input Switch
    User->>Client: setLocalAudioInputDevice({ audioInputDeviceId })
    Client->>Browser: getUserMedia({ audio: { deviceId } })
    Browser-->>Client: New audio stream
    Client->>Client: audioProducer.replaceTrack(newTrack)
    Client-->>User: AUDIO_INPUT_DEVICE_CHANGED event

    Note over User,Browser: Audio Output Switch
    User->>Client: setAudioOutputDevice({ audioOutputDeviceId })
    loop For each remote audio element
        Client->>Browser: element.setSinkId(deviceId)
    end
    Client-->>User: AUDIO_OUTPUT_DEVICE_CHANGED event
```

#### Code Example

```javascript
// Build device selector UI
async function populateDeviceSelectors() {
  const cameras = await client.listVideoDevices();
  const microphones = await client.listAudioInputDevices();
  const speakers = await client.listAudioOutputDevices();

  // Populate <select> elements
  populateSelect('camera-select', cameras);
  populateSelect('mic-select', microphones);

  // Audio output selection may not be supported in all browsers
  if (client.isAudioOutputSelectionSupported()) {
    populateSelect('speaker-select', speakers);
  } else {
    document.getElementById('speaker-select').disabled = true;
  }
}

// Handle device changes
document.getElementById('camera-select').addEventListener('change', async (e) => {
  const stream = await client.setLocalVideoDevice({
    videoDeviceId: e.target.value
  });
  // Local preview is automatically updated
});

document.getElementById('mic-select').addEventListener('change', async (e) => {
  const stream = await client.setLocalAudioInputDevice({
    audioInputDeviceId: e.target.value,
    constraints: {
      echoCancellation: true,
      noiseSuppression: true
    }
  });
});

document.getElementById('speaker-select').addEventListener('change', async (e) => {
  const result = await client.setAudioOutputDevice({
    audioOutputDeviceId: e.target.value
  });
  console.log(`Updated ${result.updatedElements} audio elements`);
});

// Listen for device change events
client.addEventListener(ClientEvents.AUDIO_OUTPUT_DEVICE_CHANGED, (e) => {
  const { previousDeviceId, newDeviceId, updatedElements } = e.detail;
  console.log(`Switched from ${previousDeviceId} to ${newDeviceId}`);
});
```

---

### 2.3 Audio Processing Pipeline

The audio processing pipeline enables VU meters, volume control, and audio mixing.

```
                                  ┌──────────────────────┐
                                  │  AudioInputProcessor │
                                  │  (VU Meter/Levels)   │
                                  └──────────┬───────────┘
                                             │ getVolume()
┌─────────────┐    ┌─────────────────┐       │
│ Microphone  │───▶│ MediaStreamNode │───────┼──────────────────────────┐
└─────────────┘    └─────────────────┘       │                          │
                                             ▼                          ▼
                                  ┌──────────────────┐       ┌──────────────────┐
                                  │  GainProcessor   │       │   AudioMerger    │
                                  │  (Volume Ctrl)   │       │ (System Audio)   │
                                  └────────┬─────────┘       └────────┬─────────┘
                                           │                          │
                                           ▼                          ▼
                                  ┌─────────────────────────────────────────────┐
                                  │              Audio Producer                  │
                                  │         (Sent to other participants)         │
                                  └─────────────────────────────────────────────┘
```

#### Code Example - VU Meter

```javascript
// Enable audio monitoring when connecting
await client.connectTransports({
  localVideoElementId: 'local-video',
  options: {
    enableAudioInputMonitor: true
  }
});

// Get the monitor when it's created
client.addEventListener(ClientEvents.AUDIO_INPUT_MONITOR_CREATED, (e) => {
  const monitor = e.detail;

  // Poll volume for VU meter
  const vuMeter = document.getElementById('vu-meter');
  setInterval(() => {
    const volume = monitor.getVolume(); // 0-255
    const percent = (volume / 255) * 100;
    vuMeter.style.width = `${percent}%`;

    // Color coding
    if (percent > 80) {
      vuMeter.style.backgroundColor = 'red'; // Clipping!
    } else if (percent > 60) {
      vuMeter.style.backgroundColor = 'yellow';
    } else {
      vuMeter.style.backgroundColor = 'green';
    }
  }, 50);
});

// Or get it directly after connection
const monitor = client.getAudioInputMonitor();
```

#### Code Example - Volume Control

```javascript
await client.connectTransports({
  localVideoElementId: 'local-video',
  options: {
    enableAudioGainControl: true
  }
});

client.addEventListener(ClientEvents.AUDIO_GAIN_CONTROL_CREATED, (e) => {
  const gainControl = e.detail;

  // Connect to volume slider
  const volumeSlider = document.getElementById('volume-slider');
  volumeSlider.addEventListener('input', (e) => {
    // Value: 0 = mute, 1 = normal, 2 = 2x gain
    gainControl.gain.value = parseFloat(e.target.value);
  });
});
```

#### Code Example - Audio Merging (Background Music)

```javascript
// Get background music stream
const bgMusic = document.getElementById('background-music');
const bgMusicStream = bgMusic.captureStream();

// Merge with microphone audio
client.mergeAudioInput({ stream: bgMusicStream });

client.addEventListener(ClientEvents.AUDIO_STREAM_ADDED, (e) => {
  console.log('Background music merged into call');
});

// Later, remove the background music
client.unmergeAudioInput({ stream: bgMusicStream });

client.addEventListener(ClientEvents.AUDIO_STREAM_REMOVED, (e) => {
  console.log('Background music removed from call');
});
```

---

## 3. Control Workflows

### 3.1 Mute/Unmute Flow

Muting has two types: **local mute** (user controls their own) and **remote mute** (room owner requests).

```mermaid
sequenceDiagram
    participant User as Local User
    participant Client as Client
    participant Server as Server
    participant Owner as Room Owner

    Note over User,Owner: Local Mute Flow
    User->>Client: muteLocalAudio(true)
    Client->>Client: audioProducer.pause()
    Client->>Server: Broadcast mute state
    Client-->>User: AUDIO_MUTED event { muted: true }
    Note over Server,Owner: Other users see<br/>user as muted

    Note over User,Owner: Remote Mute Flow (Owner → User)
    Owner->>Server: muteRemoteAudio({ userId, muteAudio: true })
    Server->>Client: Mute request

    alt User has not self-muted
        Client-->>User: REMOTE_AUDIO_MUTED event
        Note over User: User can comply or ignore
    else User already self-muted
        Note over Client: Request ignored (user has priority)
    end
```

#### Code Example - Local Mute

```javascript
const muteAudioBtn = document.getElementById('mute-audio');
const muteVideoBtn = document.getElementById('mute-video');

// Toggle audio mute
muteAudioBtn.addEventListener('click', async () => {
  const currentlyMuted = client.isLocalAudioPaused();
  await client.muteLocalAudio(!currentlyMuted);
});

// Toggle video mute
muteVideoBtn.addEventListener('click', async () => {
  const currentlyMuted = client.isLocalVideoPaused();
  await client.muteLocalVideo(!currentlyMuted);
});

// Update UI based on mute events
client.addEventListener(ClientEvents.AUDIO_MUTED, (e) => {
  const { muted } = e.detail;
  muteAudioBtn.textContent = muted ? 'Unmute Audio' : 'Mute Audio';
  muteAudioBtn.classList.toggle('muted', muted);
});

client.addEventListener(ClientEvents.VIDEO_MUTED, (e) => {
  const { muted } = e.detail;
  muteVideoBtn.textContent = muted ? 'Turn On Video' : 'Turn Off Video';

  // Show/hide local video
  const localVideo = document.getElementById('local-video');
  localVideo.classList.toggle('hidden', muted);
});
```

#### Code Example - Room Owner Remote Mute

```javascript
// Room owner mutes a participant
async function muteParticipant(userId, muteAudio = true, muteVideo = false) {
  if (!client.isLocalUserRoomOwner()) {
    console.error('Only room owner can mute others');
    return;
  }

  if (muteAudio) {
    await client.muteRemoteAudio({
      room: currentRoom,
      userId,
      muteAudio: true
    });
  }

  if (muteVideo) {
    await client.muteRemoteVideo({
      room: currentRoom,
      userId,
      muteVideo: true
    });
  }
}

// Participant handles remote mute request
client.addEventListener(ClientEvents.REMOTE_AUDIO_MUTED, (e) => {
  const { userId, muted } = e.detail;

  // Show notification
  showNotification(`Host has ${muted ? 'muted' : 'unmuted'} your microphone`);

  // UI can choose to auto-comply or let user decide
  // The event is informational - actual mute is handled by the client
});
```

---

### 3.2 Waiting Room Admission

Room owners review and admit/reject guests from the waiting room.

```mermaid
sequenceDiagram
    participant Guest as Guest User
    participant Server as Server
    participant Owner as Room Owner
    participant OClient as Owner's Client

    Guest->>Server: Request to join room

    alt Waiting room enabled
        Server->>OClient: Waiting room request
        OClient-->>Owner: ADMIT_WAITING_ROOM event
        Note over Owner: Owner sees guest<br/>in waiting list

        alt Owner admits guest
            Owner->>OClient: admitWaitingRoomUser({ userToAdmit })
            OClient->>Server: Admit request
            Server->>Guest: Admitted to room
        else Owner rejects guest
            Owner->>OClient: rejectWaitingRoomUser({ userToReject })
            OClient->>Server: Reject request
            Server->>Guest: Rejected
            Guest-->>Guest: REJECT_WAITING_ROOM event
        end
    else Waiting room disabled
        Server->>Guest: Direct admission
    end
```

#### Code Example - Room Owner

```javascript
// Track waiting room guests
const waitingGuests = new Map();

client.addEventListener(ClientEvents.ADMIT_WAITING_ROOM, (e) => {
  const { userId, externalUserId } = e.detail;

  // Add to waiting list UI
  waitingGuests.set(userId, { userId, externalUserId });
  updateWaitingRoomUI();
});

// Admit a guest
async function admitGuest(userId) {
  await client.admitWaitingRoomUser({ userToAdmit: userId });
  waitingGuests.delete(userId);
  updateWaitingRoomUI();
}

// Reject a guest
async function rejectGuest(userId) {
  await client.rejectWaitingRoomUser({ userToReject: userId });
  waitingGuests.delete(userId);
  updateWaitingRoomUI();
}

// Admit all waiting guests
async function admitAllGuests() {
  for (const [userId] of waitingGuests) {
    await client.admitWaitingRoomUser({ userToAdmit: userId });
  }
  waitingGuests.clear();
  updateWaitingRoomUI();
}
```

#### Code Example - Guest

```javascript
// Guest handles rejection
client.addEventListener(ClientEvents.REJECT_WAITING_ROOM, (e) => {
  const { userId } = e.detail;
  showError('You were not admitted to the meeting');
  redirectToHomePage();
});
```

---

### 3.3 Room Owner Controls

Room owners have elevated privileges for managing the meeting.

```javascript
// Check if current user is room owner
if (client.isLocalUserRoomOwner()) {
  showOwnerControls();
}

// Get all current attendees
const attendees = await client.getAttendees();
// Returns: [{ odId, externalUserId, isSelf, isOwner }]

// Mute all participants
async function muteAllParticipants() {
  const attendees = await client.getAttendees();

  for (const attendee of attendees) {
    if (!attendee.isSelf) {
      await client.muteRemoteAudio({
        room: currentRoom,
        userId: attendee.odId,
        muteAudio: true
      });
    }
  }
}

// Close the room (ends meeting for everyone)
async function endMeeting() {
  await client.closeConnection();
  // All participants will receive ROOM_CLOSED event
}

client.addEventListener(ClientEvents.ROOM_CLOSED, () => {
  showNotification('The host has ended the meeting');
  redirectToHomePage();
});
```

---

## 4. Recording & Media Services

### 4.1 Recording with Live Transcription

Recording spawns a bot that joins the room, captures all media streams, and optionally provides real-time transcription.

```mermaid
sequenceDiagram
    participant Owner as Room Owner
    participant Client as Client
    participant Server as Server
    participant Bot as Recording Bot
    participant S3 as AWS S3

    Owner->>Client: startRecording({ transcribe: true })
    Client->>Server: Start recording request
    Server->>Server: Send message
    Server-->>Client: recordingId
    Client-->>Owner: RECORDING_STARTED event

    Note over Bot,S3: Bot Process (AWS EC2)
    Bot->>Server: Poll queue
    Bot->>Server: Join room
    Bot->>Bot: Start recording

    loop During recording
        Bot->>Bot: Transcription
        Bot->>Server: Send transcription result
        Server->>Client: Transcription text
        Client-->>Owner: TRANSCRIPTION_RECEIVED event
    end

    Owner->>Client: stopRecording()
    Client->>Server: Stop recording request
    Server->>Bot: Stop signal
    Bot->>S3: Upload MKV files
    Bot->>Server: Recording complete
    Client-->>Owner: RECORDING_STOPPED event

    Owner->>Client: getRecordingUrls({ recordingId })
    Client->>Server: Request URLs
    Server-->>Client: S3 signed URLs
```

#### Code Example

```javascript
// Only room owner can start recording
if (!client.isLocalUserRoomOwner()) {
  console.error('Only room owner can record');
  return;
}

// Start recording with transcription
const recordingId = await client.startRecording({
  type: 'cloud',
  options: {
    transcribe: true,           // Enable live transcription
    autoCompose: true,          // Auto-compose after stop
    useContext: true,           // Include meeting context
    postMeetingSummary: true    // Generate AI summary
  }
});

console.log('Recording started:', recordingId);

// Listen for recording events
client.addEventListener(ClientEvents.RECORDING_STARTED, (e) => {
  const { recordingId, roomName } = e.detail;
  showRecordingIndicator();
});

client.addEventListener(ClientEvents.RECORDING_STOPPED, (e) => {
  const { recordingId } = e.detail;
  hideRecordingIndicator();
});

// Handle live transcription
const transcriptions = [];

client.addEventListener(ClientEvents.TRANSCRIPTION_RECEIVED, (e) => {
  const { userId, text, timestamp, isFinal } = e.detail;

  if (isFinal) {
    transcriptions.push({ userId, text, timestamp });
    updateTranscriptionUI(userId, text, timestamp);
  } else {
    // Interim result - show in progress
    showInterimTranscription(userId, text);
  }
});

// Stop recording
async function stopRecording() {
  await client.stopRecording();

  // If autoCompose was true, composition starts automatically
  // Otherwise, manually start composition:
  // await client.startComposition({ recordingId });
}

// Get recording files
async function downloadRecordings() {
  const urls = await client.getRecordingUrls({
    recordingId: client.getRecordingId(),
    roomName: currentRoom.name,
    userId: currentUserId
  });

  // urls is array of S3 signed URLs for MKV files
  for (const url of urls) {
    window.open(url, '_blank');
  }
}
```

---

### 4.2 Video Composition

After recording stops, compose individual recordings into a grid-style video.

```mermaid
sequenceDiagram
    participant Owner as Room Owner
    participant Client as Client
    participant Server as Server
    participant Composer as Composer Service
    participant S3 as AWS S3

    Owner->>Client: startComposition({ recordingId })
    Client->>Server: Composition request
    Server->>Server: Send message

    Note over Composer,S3: Composition Process
    Composer->>S3: Download individual recordings
    Composer->>Composer: Grid assembly
    Composer->>S3: Upload composed video

    loop Polling for status
        Owner->>Client: getCompositionStatus({ recordingId })
        Client->>Server: Status request
        Server-->>Client: { status: 'processing', progress: 45 }
    end

    Server-->>Client: { status: 'completed' }

    Owner->>Client: getCompositionUrl({ recordingId })
    Client->>Server: URL request
    Server-->>Client: S3 signed URL
```

#### Code Example

```javascript
// Start composition after recording stops
const recordingId = client.getRecordingId();

await client.startComposition({ recordingId });

// Poll for completion
async function waitForComposition() {
  const pollInterval = 5000; // 5 seconds

  while (true) {
    const status = await client.getCompositionStatus({
      recordingId,
      roomName: currentRoom.name,
      userId: currentUserId
    });

    console.log(`Composition status: ${status.status}, progress: ${status.progress}%`);
    updateProgressUI(status.progress);

    if (status.status === 'completed') {
      // Get the final video URL
      const url = await client.getCompositionUrl({
        recordingId,
        roomName: currentRoom.name,
        userId: currentUserId
      });

      // Get signed URL for download
      const signedUrl = await client.getSignedCompositionUrl({ url });

      return signedUrl;
    }

    if (status.status === 'failed') {
      throw new Error('Composition failed');
    }

    await new Promise(r => setTimeout(r, pollInterval));
  }
}

// Get list of all compositions for this room
const compositions = await client.getCompositionList({
  roomName: currentRoom.name,
  userId: currentUserId
});
```

---

### 4.3 Live Streaming

Stream the meeting to external platforms like YouTube or Twitch.

```mermaid
sequenceDiagram
    participant Owner as Room Owner
    participant Client as Client
    participant Server as Server
    participant Bot as Streaming Bot
    participant RTMP as YouTube/Twitch

    Owner->>Client: startStreaming({ type: 'cloud', options })
    Client->>Server: Start streaming request
    Server-->>Client: streamingId
    Client-->>Owner: STREAMING_STARTED event

    Note over Bot,RTMP: Streaming Active
    Bot->>RTMP: RTMP stream

    Owner->>Client: switchStreamingUser({ userToSwitchTo })
    Client->>Server: Switch request
    Server->>Bot: Change featured user
    Client-->>Owner: STREAMING_USER_CHANGED event

    Owner->>Client: stopStreaming()
    Client->>Server: Stop streaming request
    Server->>Bot: Stop signal
    Client-->>Owner: STREAMING_STOPPED event
```

#### Code Example

```javascript
// Start streaming to YouTube
const streamingId = await client.startStreaming({
  type: 'cloud',
  options: {
    rtmpUrl: 'rtmp://a.rtmp.youtube.com/live2',
    streamKey: 'your-youtube-stream-key',
    platform: 'youtube' // or 'twitch', 'custom'
  }
});

client.addEventListener(ClientEvents.STREAMING_STARTED, (e) => {
  showStreamingIndicator();
  console.log('Streaming started:', e.detail.streamingId);
});

// Switch featured user in stream
async function featureUser(userId) {
  await client.switchStreamingUser({ userToSwitchTo: userId });
}

client.addEventListener(ClientEvents.STREAMING_USER_CHANGED, (e) => {
  const { userId } = e.detail;
  highlightFeaturedUser(userId);
});

// Stop streaming
async function stopStream() {
  await client.stopStreaming();
}

client.addEventListener(ClientEvents.STREAMING_STOPPED, (e) => {
  hideStreamingIndicator();
});

// Get streaming URLs
const urls = await client.getStreamingUrls({
  streamingId: client.getStreamingId(),
  roomName: currentRoom.name,
  userId: currentUserId
});
```

---

### 4.4 Post-Meeting AI Summary

After recording, generate AI-powered summaries, action items, and insights.

```javascript
// After recording completes, start transcription processing
const recordingId = client.getRecordingId();

await client.startTranscription({ recordingId });

// Poll transcription status
async function waitForTranscription() {
  while (true) {
    const status = await client.getTranscriptionStatus({
      recordingId,
      roomName: currentRoom.name,
      userId: currentUserId
    });

    if (status.status === 'completed') {
      break;
    }

    await new Promise(r => setTimeout(r, 5000));
  }
}

await waitForTranscription();

// Get AI-generated summary
const summary = await client.getTranscriptionSummary({
  recordingId,
  userId: currentUserId
});

console.log('Meeting Summary:', summary);
// Returns:
// {
//   summary: "Key discussion points...",
//   actionItems: ["Follow up with...", "Schedule..."],
//   participants: [...],
//   duration: "45 minutes",
//   topics: ["Budget", "Timeline", "Resources"]
// }
```

---

## 5. Communication Workflows

### 5.1 Chat Messaging

Real-time text chat with history retrieval.

```mermaid
sequenceDiagram
    participant Sender as Sender
    participant Client as Sender's Client
    participant Server as Server
    participant Receiver as Receiver
    participant RClient as Receiver's Client

    Sender->>Client: sendChatMessage({ message })
    Client->>Server: Chat message
    Server->>Server: Store in database
    Server->>RClient: Broadcast message
    RClient-->>Receiver: RECEIVE_CHAT_MESSAGE event

    Note over Receiver,RClient: Later - Retrieve History
    Receiver->>RClient: getChatHistory({ cursor })
    RClient->>Server: History request
    Server-->>RClient: Chat messages array
```

#### Code Example

```javascript
// Send a chat message
const chatInput = document.getElementById('chat-input');
const sendBtn = document.getElementById('send-chat');

sendBtn.addEventListener('click', async () => {
  const message = chatInput.value.trim();
  if (message) {
    await client.sendChatMessage({ message });
    chatInput.value = '';
    // Message will come back via RECEIVE_CHAT_MESSAGE (including your own)
  }
});

// Receive chat messages
client.addEventListener(ClientEvents.RECEIVE_CHAT_MESSAGE, (e) => {
  const { userId, message, timestamp, externalUserId } = e.detail;

  addChatBubble({
    sender: externalUserId || userId,
    text: message,
    time: new Date(timestamp),
    isSelf: userId === currentUserId
  });
});

// Load chat history (e.g., when rejoining)
async function loadChatHistory() {
  let cursor = null;
  const allMessages = [];

  do {
    const result = await client.getChatHistory({ cursor });
    allMessages.push(...result.messages);
    cursor = result.nextCursor;
  } while (cursor);

  // Display historical messages
  allMessages.forEach(msg => {
    addChatBubble({
      sender: msg.externalUserId || msg.userId,
      text: msg.message,
      time: new Date(msg.timestamp),
      isSelf: msg.userId === currentUserId
    });
  });
}
```

---

### 5.2 Data Channel Messaging

Send arbitrary JSON data for custom features like reactions, polls, or whiteboard sync.

```mermaid
sequenceDiagram
    participant Sender as Sender
    participant Client as Sender's Client
    participant Server as Server
    participant All as All Participants
    participant Target as Target User

    Note over Sender,Target: Broadcast to All
    Sender->>Client: sendDataMessage({ userId: null, message })
    Client->>Server: Data message (broadcast)
    Server->>All: Broadcast to all
    All-->>All: DATA_MESSAGE event

    Note over Sender,Target: Send to Specific User
    Sender->>Client: sendDataMessage({ userId: 'target', message })
    Client->>Server: Data message (unicast)
    Server->>Target: Send to target only
    Target-->>Target: DATA_MESSAGE event
```

#### Code Example

```javascript
// Send reaction to all participants
async function sendReaction(emoji) {
  await client.sendDataMessage({
    userId: null, // null = broadcast to all
    message: {
      type: 'reaction',
      emoji: emoji,
      from: currentUserId
    },
    preventReturn: false // Also receive your own message
  });
}

// Send private message to specific user
async function sendPrivateNote(targetUserId, note) {
  await client.sendDataMessage({
    userId: targetUserId,
    message: {
      type: 'private-note',
      text: note,
      from: currentUserId
    }
  });
}

// Handle incoming data messages
client.addEventListener(ClientEvents.DATA_MESSAGE, (e) => {
  const { userId, data } = e.detail;

  switch (data.type) {
    case 'reaction':
      showReactionAnimation(data.emoji, data.from);
      break;

    case 'private-note':
      showPrivateNote(data.text, data.from);
      break;

    case 'poll-vote':
      recordVote(data.pollId, data.option, userId);
      break;

    case 'whiteboard-update':
      updateWhiteboard(data.strokes);
      break;

    default:
      console.log('Unknown data message type:', data.type);
  }
});

// Example: Live poll system
async function createPoll(question, options) {
  await client.sendDataMessage({
    userId: null,
    message: {
      type: 'poll-create',
      pollId: crypto.randomUUID(),
      question,
      options
    }
  });
}

async function votePoll(pollId, optionIndex) {
  await client.sendDataMessage({
    userId: null,
    message: {
      type: 'poll-vote',
      pollId,
      option: optionIndex
    }
  });
}
```

---

## 6. Resilience Workflows

### 6.1 Reconnection

Handle network interruptions gracefully while preserving mute states.

```mermaid
stateDiagram-v2
    [*] --> Connected: CONNECTED event
    Connected --> Disconnected: Network failure<br/>DISCONNECTED event
    Disconnected --> Reconnecting: App calls reconnect()
    Reconnecting --> Connected: Success<br/>CONNECTED event
    Reconnecting --> Disconnected: Failure
    Connected --> [*]: closeConnection()
```

```mermaid
sequenceDiagram
    participant User as User
    participant Client as Client
    participant Server as Server

    Note over User,Server: Connection Lost
    Client-->>User: DISCONNECTED event

    User->>Client: reconnect()

    Note over Client: Preserve mute states
    Client->>Client: Capture: audioMuted, videoMuted

    Client->>Client: Close old transports
    Client->>Server: Create new transports
    Client->>Server: Produce media (same mute state)

    Client->>Server: getExistingProducers()
    Server-->>Client: [{producerId, userId, kind, paused}]

    loop For each producer
        Client->>Server: Consume producer
        Client-->>User: MEDIA_TRACK_ADDED { paused }
    end

    Client-->>User: CONNECTED event
```

#### Code Example

```javascript
let isReconnecting = false;

client.addEventListener(ClientEvents.DISCONNECTED, async () => {
  console.log('Connection lost');
  showReconnectingUI();

  if (isReconnecting) return;
  isReconnecting = true;

  // Exponential backoff retry
  const maxRetries = 5;
  let delay = 1000;

  for (let i = 0; i < maxRetries; i++) {
    try {
      await client.reconnect({
        options: {
          // Preserve any connection options
        }
      });

      console.log('Reconnected successfully');
      hideReconnectingUI();
      isReconnecting = false;
      return;

    } catch (error) {
      console.log(`Reconnect attempt ${i + 1} failed:`, error.message);

      if (i < maxRetries - 1) {
        await new Promise(r => setTimeout(r, delay));
        delay *= 2; // Exponential backoff
      }
    }
  }

  // All retries failed
  isReconnecting = false;
  showError('Unable to reconnect. Please refresh the page.');
});

client.addEventListener(ClientEvents.CONNECTED, () => {
  console.log('Connection established');
  hideReconnectingUI();
});

// Re-track remote users after reconnection
// The MEDIA_TRACK_ADDED events will fire for each producer
// Note: `paused` field tells you if user is muted
client.addEventListener(ClientEvents.MEDIA_TRACK_ADDED, (e) => {
  const { userId, track, kind, paused, audioOnly } = e.detail;

  // Attach track and show correct mute state
  attachRemoteTrack(userId, track, kind);

  if (paused) {
    showMutedIndicator(userId, kind);
  } else {
    hideMutedIndicator(userId, kind);
  }
});
```

---

### 6.2 iOS Orientation Handling

iOS requires special handling when device orientation changes to maintain correct video orientation.

```mermaid
sequenceDiagram
    participant Device as iOS Device
    participant Client as Client
    participant Producer as Video Producer
    participant Others as Other Participants

    Device->>Client: orientationchange event

    alt Guard check passes
        Client->>Client: Set _orientationChangeInProgress = true
        Client->>Client: Calculate new video constraints

        Note over Client: Portrait: 480x640<br/>Landscape: 1280x720, 16:9

        Client->>Client: getUserMedia(newConstraints)
        Client->>Producer: replaceTrack(newVideoTrack)

        Note over Others: Video continues<br/>uninterrupted!

        Client->>Client: Update local video element
        Client->>Client: Set _orientationChangeInProgress = false
    else Guard check fails (in progress)
        Note over Client: Ignore duplicate event
    end
```

#### Why This Matters

On iOS devices:
- Video is captured in the device orientation at the time of `getUserMedia()`
- Orientation doesn't update dynamically on existing tracks
- Without this handling, video appears sideways after rotation
- Track replacement (not producer close/create) ensures seamless transition

#### Code Example

```javascript
// iOS orientation handling is automatic in the client
// But you can detect orientation for UI purposes

function isLandscape() {
  return window.matchMedia('(orientation: landscape)').matches;
}

// Update UI based on orientation
window.matchMedia('(orientation: portrait)').addEventListener('change', (e) => {
  if (e.matches) {
    // Portrait mode
    applyPortraitLayout();
  } else {
    // Landscape mode
    applyLandscapeLayout();
  }
});

// The client automatically handles video track replacement
// You'll see this in logs:
// "iOS orientation change complete in Xms"
```

---

### 6.3 Constraint Relaxation

When media constraints can't be satisfied, the client automatically relaxes them.

```mermaid
sequenceDiagram
    participant App as Application
    participant Client as Client
    participant Browser as Browser

    App->>Client: setLocalVideoDevice({ deviceId })
    Client->>Browser: getUserMedia({ video: { width: 1920, height: 1080 } })
    Browser-->>Client: OverconstrainedError

    Note over Client: Level 1: Relax resolution
    Client->>Browser: getUserMedia({ video: { width: 1280, height: 720 } })
    Browser-->>Client: OverconstrainedError

    Note over Client: Level 2: Relax further
    Client->>Browser: getUserMedia({ video: { width: 640, height: 480 } })
    Browser-->>Client: Success!

    Client-->>App: CONSTRAINTS_RELAXED event
```

#### Code Example

```javascript
client.addEventListener(ClientEvents.CONSTRAINTS_RELAXED, (e) => {
  const { level, originalConstraints, relaxedConstraints, failedConstraint, reason } = e.detail;

  console.log(`Constraints relaxed to level ${level}`);
  console.log('Original:', originalConstraints);
  console.log('Relaxed:', relaxedConstraints);
  console.log('Failed constraint:', failedConstraint);

  // Show user-friendly notification
  switch (level) {
    case 1:
      showNotification('Video quality reduced due to device limitations');
      break;
    case 2:
      showNotification('Video quality significantly reduced');
      break;
    case 3:
      showNotification('Using minimal video quality');
      break;
  }
});
```

---

## 7. File Management Workflows

### 7.1 File Upload & Sharing

Upload files to S3 and share with meeting participants.

```javascript
// Get upload URL
const uploadInfo = await client.signaling.getUploadUrl({
  fileName: 'presentation.pdf',
  contentType: 'application/pdf',
  fileSize: file.size
});

// Upload file to S3
await fetch(uploadInfo.uploadUrl, {
  method: 'PUT',
  body: file,
  headers: {
    'Content-Type': 'application/pdf'
  }
});

// Notify server that upload is complete
await client.signaling.fileUploaded({
  fileId: uploadInfo.fileId,
  fileName: 'presentation.pdf'
});

// Share file with room participants
await client.signaling.shareFile({
  fileId: uploadInfo.fileId,
  shareWith: 'room', // or specific userId
  roomName: currentRoom.name
});
```

---

### 7.2 Room File System Cache

Client-side caching of room files with IndexedDB.

```javascript
import { FileSystemCache } from 'muziertcclient';

// Initialize cache for this user and room
const cache = new FileSystemCache();
await cache.initialize(currentUserId, currentRoom.name);

// Load all files
const files = await cache.loadAllFiles();

// Get file tree structure
const tree = cache.getFileTree();
// Returns: { folders: {...}, files: [...] }

// Create folder
await cache.addFolder('/documents/reports');

// Delete file
await cache.deleteFile(fileId);

// Sync with server (get updates since last sync)
const updates = await cache.syncSince(lastSyncTimestamp);

// Clear cache and reinitialize
await cache.clearAndReinit(currentUserId, currentRoom.name);
```

---

## 8. Complete Application Examples

### 8.1 Basic Video Call

A minimal but complete video call implementation.

```html
<!DOCTYPE html>
<html>
<head>
  <title>Video Call</title>
  <style>
    .video-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 10px; }
    .video-container { position: relative; background: #000; }
    .video-container video { width: 100%; height: auto; }
    .muted-indicator { position: absolute; top: 10px; right: 10px; background: red; color: white; padding: 5px; }
    .hidden { display: none; }
  </style>
</head>
<body>
  <div id="local-container">
    <video id="local-video" autoplay muted playsinline></video>
  </div>

  <div id="remote-videos" class="video-grid"></div>

  <div id="controls">
    <button id="mute-audio">Mute Audio</button>
    <button id="mute-video">Mute Video</button>
    <button id="leave">Leave</button>
  </div>

  <script type="module">
    import { Client, ClientEvents } from 'https://cdn.muziemedia.com/muziertcclient/latest/muziertcclient.es.min.js';

    const roomToken = 'YOUR_ROOM_TOKEN'; // From your backend
    const roomName = 'test-room';
    const userId = 'user-' + Math.random().toString(36).substr(2, 9);

    const client = new Client({ roomToken });
    const remoteUsers = new Map();

    // Event handlers
    client.addEventListener(ClientEvents.MEDIA_TRACK_ADDED, (e) => {
      const { userId, track, kind, paused, audioOnly } = e.detail;

      let container = document.getElementById(`remote-${userId}`);
      if (!container) {
        container = document.createElement('div');
        container.id = `remote-${userId}`;
        container.className = 'video-container';
        container.innerHTML = `
          <video id="video-${userId}" autoplay playsinline></video>
          <audio id="audio-${userId}" autoplay></audio>
          <div id="mute-${userId}-audio" class="muted-indicator hidden">Muted</div>
        `;
        document.getElementById('remote-videos').appendChild(container);
        remoteUsers.set(userId, { audio: null, video: null });
      }

      const element = document.getElementById(`${kind}-${userId}`);
      if (element) {
        element.srcObject = new MediaStream([track]);
      }

      // Show mute indicator if producer is paused
      if (paused && kind === 'audio') {
        document.getElementById(`mute-${userId}-audio`).classList.remove('hidden');
      }
    });

    client.addEventListener(ClientEvents.MEDIA_TRACK_REMOVED, (e) => {
      const { userId, kind } = e.detail;
      const element = document.getElementById(`${kind}-${userId}`);
      if (element) {
        element.srcObject = null;
      }
    });

    client.addEventListener(ClientEvents.USER_DISCONNECTED, (e) => {
      const container = document.getElementById(`remote-${e.detail.userId}`);
      if (container) {
        container.remove();
      }
      remoteUsers.delete(e.detail.userId);
    });

    client.addEventListener(ClientEvents.ERROR, (e) => {
      console.error('Error:', e.detail.error);
      alert('Error: ' + e.detail.error);
    });

    // Control buttons
    document.getElementById('mute-audio').addEventListener('click', async () => {
      const muted = client.isLocalAudioPaused();
      await client.muteLocalAudio(!muted);
      document.getElementById('mute-audio').textContent = !muted ? 'Unmute Audio' : 'Mute Audio';
    });

    document.getElementById('mute-video').addEventListener('click', async () => {
      const muted = client.isLocalVideoPaused();
      await client.muteLocalVideo(!muted);
      document.getElementById('mute-video').textContent = !muted ? 'Unmute Video' : 'Mute Video';
    });

    document.getElementById('leave').addEventListener('click', async () => {
      await client.closeConnection();
      window.location.href = '/';
    });

    // Initialize
    async function init() {
      try {
        // Get devices
        const cameras = await client.listVideoDevices();
        const mics = await client.listAudioInputDevices();

        if (cameras.length) {
          await client.setLocalVideoDevice({ videoDeviceId: cameras[0].deviceId });
        }
        if (mics.length) {
          await client.setLocalAudioInputDevice({ audioInputDeviceId: mics[0].deviceId });
        }

        // Join or create room
        try {
          await client.joinRoom({ roomName, userId });
        } catch (e) {
          // Room doesn't exist, create it
          await client.createRoom({ roomName, userId });
        }

        // Connect media
        await client.connectTransports({ localVideoElementId: 'local-video' });

        console.log('Connected to room!');
      } catch (error) {
        console.error('Initialization failed:', error);
        alert('Failed to connect: ' + error.message);
      }
    }

    init();
  </script>
</body>
</html>
```

---

### 8.2 Recorded Meeting with Transcription

Complete recording workflow with AI summary.

```javascript
import { Client, ClientEvents } from 'muziertcclient';

class RecordedMeeting {
  constructor(roomToken) {
    this.client = new Client({ roomToken });
    this.recordingId = null;
    this.transcriptions = [];
    this.setupEventListeners();
  }

  setupEventListeners() {
    this.client.addEventListener(ClientEvents.RECORDING_STARTED, (e) => {
      this.recordingId = e.detail.recordingId;
      this.showRecordingIndicator(true);
    });

    this.client.addEventListener(ClientEvents.RECORDING_STOPPED, (e) => {
      this.showRecordingIndicator(false);
    });

    this.client.addEventListener(ClientEvents.TRANSCRIPTION_RECEIVED, (e) => {
      const { userId, text, timestamp, isFinal } = e.detail;

      if (isFinal) {
        this.transcriptions.push({ userId, text, timestamp });
        this.updateTranscriptionPanel(userId, text, timestamp);
      } else {
        this.showInterimText(userId, text);
      }
    });

    this.client.addEventListener(ClientEvents.TRANSCRIPTION_STARTED, () => {
      this.showTranscriptionPanel(true);
    });

    this.client.addEventListener(ClientEvents.TRANSCRIPTION_STOPPED, () => {
      this.showTranscriptionPanel(false);
    });
  }

  async startMeeting(roomName, userId) {
    // Configure devices
    const cameras = await this.client.listVideoDevices();
    const mics = await this.client.listAudioInputDevices();

    await this.client.setLocalVideoDevice({ videoDeviceId: cameras[0].deviceId });
    await this.client.setLocalAudioInputDevice({ audioInputDeviceId: mics[0].deviceId });

    // Create room
    await this.client.createRoom({ roomName, userId });
    await this.client.connectTransports({ localVideoElementId: 'local-video' });
  }

  async startRecording() {
    if (!this.client.isLocalUserRoomOwner()) {
      throw new Error('Only room owner can record');
    }

    await this.client.startRecording({
      type: 'cloud',
      options: {
        transcribe: true,
        autoCompose: true,
        postMeetingSummary: true
      }
    });
  }

  async stopRecording() {
    await this.client.stopRecording();
  }

  async getRecordingFiles() {
    if (!this.recordingId) {
      throw new Error('No recording in progress');
    }

    // Wait for composition to complete
    let status;
    do {
      status = await this.client.getCompositionStatus({
        recordingId: this.recordingId,
        roomName: this.client.room.name,
        userId: this.client.userId
      });

      if (status.status === 'processing') {
        await new Promise(r => setTimeout(r, 5000));
      }
    } while (status.status === 'processing');

    if (status.status === 'completed') {
      // Get composed video
      const videoUrl = await this.client.getCompositionUrl({
        recordingId: this.recordingId,
        roomName: this.client.room.name,
        userId: this.client.userId
      });

      // Get AI summary
      const summary = await this.client.getTranscriptionSummary({
        recordingId: this.recordingId,
        userId: this.client.userId
      });

      return {
        videoUrl: await this.client.getSignedCompositionUrl({ url: videoUrl }),
        summary,
        transcriptions: this.transcriptions
      };
    }

    throw new Error('Composition failed');
  }

  showRecordingIndicator(show) {
    document.getElementById('recording-indicator').classList.toggle('hidden', !show);
  }

  showTranscriptionPanel(show) {
    document.getElementById('transcription-panel').classList.toggle('hidden', !show);
  }

  updateTranscriptionPanel(userId, text, timestamp) {
    const panel = document.getElementById('transcription-content');
    const entry = document.createElement('div');
    entry.innerHTML = `<strong>${userId}</strong>: ${text}`;
    panel.appendChild(entry);
    panel.scrollTop = panel.scrollHeight;
  }

  showInterimText(userId, text) {
    const interim = document.getElementById('interim-transcription');
    interim.textContent = `${userId}: ${text}...`;
  }
}

// Usage
const meeting = new RecordedMeeting('room-token');
await meeting.startMeeting('recorded-meeting', 'host-123');

// Later...
await meeting.startRecording();

// When meeting ends...
await meeting.stopRecording();
const results = await meeting.getRecordingFiles();
console.log('Video:', results.videoUrl);
console.log('Summary:', results.summary);
```

---

### 8.3 Webinar with Waiting Room

Host controls participants with waiting room admission.

```javascript
import { Client, ClientEvents } from 'muziertcclient';

class Webinar {
  constructor(roomToken) {
    this.client = new Client({ roomToken });
    this.waitingRoom = new Map();
    this.participants = new Map();
    this.isHost = false;
    this.setupEventListeners();
  }

  setupEventListeners() {
    // Waiting room events
    this.client.addEventListener(ClientEvents.ADMIT_WAITING_ROOM, (e) => {
      const { userId, externalUserId } = e.detail;
      this.waitingRoom.set(userId, { userId, externalUserId, joinedAt: Date.now() });
      this.updateWaitingRoomUI();
    });

    this.client.addEventListener(ClientEvents.REJECT_WAITING_ROOM, (e) => {
      // Received by rejected guest
      this.showRejectionMessage();
    });

    // Participant events
    this.client.addEventListener(ClientEvents.USER_JOINED_ROOM, (e) => {
      this.waitingRoom.delete(e.detail.userId);
      this.participants.set(e.detail.userId, e.detail);
      this.updateWaitingRoomUI();
      this.updateParticipantList();
    });

    this.client.addEventListener(ClientEvents.USER_DISCONNECTED, (e) => {
      this.participants.delete(e.detail.userId);
      this.updateParticipantList();
    });

    // Media events
    this.client.addEventListener(ClientEvents.MEDIA_TRACK_ADDED, (e) => {
      this.attachParticipantMedia(e.detail);
    });

    // Streaming events
    this.client.addEventListener(ClientEvents.STREAMING_STARTED, () => {
      this.showStreamingIndicator(true);
    });

    this.client.addEventListener(ClientEvents.STREAMING_STOPPED, () => {
      this.showStreamingIndicator(false);
    });
  }

  async startAsHost(roomName, userId, rtmpConfig) {
    this.isHost = true;

    const cameras = await this.client.listVideoDevices();
    const mics = await this.client.listAudioInputDevices();

    await this.client.setLocalVideoDevice({ videoDeviceId: cameras[0].deviceId });
    await this.client.setLocalAudioInputDevice({ audioInputDeviceId: mics[0].deviceId });

    await this.client.createRoom({
      roomName,
      userId,
      options: {
        waitingRoom: true,
        maxParticipants: 100
      }
    });

    await this.client.connectTransports({
      localVideoElementId: 'host-video',
      options: {
        enableAudioInputMonitor: true
      }
    });

    // Start streaming if configured
    if (rtmpConfig) {
      await this.client.startStreaming({
        type: 'cloud',
        options: rtmpConfig
      });
    }

    this.showHostControls();
  }

  async joinAsGuest(joinToken, password, userId) {
    this.isHost = false;

    const cameras = await this.client.listVideoDevices();
    const mics = await this.client.listAudioInputDevices();

    await this.client.setLocalVideoDevice({ videoDeviceId: cameras[0].deviceId });
    await this.client.setLocalAudioInputDevice({ audioInputDeviceId: mics[0].deviceId });

    // Will be placed in waiting room
    await this.client.joinRoomWithToken({
      joinToken,
      password,
      userId
    });

    // Wait for admission...
  }

  // Host: Admit guest from waiting room
  async admitGuest(userId) {
    if (!this.isHost) return;
    await this.client.admitWaitingRoomUser({ userToAdmit: userId });
  }

  // Host: Reject guest from waiting room
  async rejectGuest(userId) {
    if (!this.isHost) return;
    await this.client.rejectWaitingRoomUser({ userToReject: userId });
  }

  // Host: Admit all waiting guests
  async admitAll() {
    if (!this.isHost) return;
    for (const [userId] of this.waitingRoom) {
      await this.client.admitWaitingRoomUser({ userToAdmit: userId });
    }
  }

  // Host: Mute all participants
  async muteAllParticipants() {
    if (!this.isHost) return;
    for (const [userId] of this.participants) {
      await this.client.muteRemoteAudio({
        room: this.client.room,
        userId,
        muteAudio: true
      });
    }
  }

  // Host: Feature a participant in the stream
  async featureParticipant(userId) {
    if (!this.isHost) return;
    await this.client.switchStreamingUser({ userToSwitchTo: userId });
  }

  updateWaitingRoomUI() {
    const waitingList = document.getElementById('waiting-room-list');
    waitingList.innerHTML = '';

    for (const [userId, guest] of this.waitingRoom) {
      const item = document.createElement('div');
      item.innerHTML = `
        <span>${guest.externalUserId || userId}</span>
        <button onclick="webinar.admitGuest('${userId}')">Admit</button>
        <button onclick="webinar.rejectGuest('${userId}')">Reject</button>
      `;
      waitingList.appendChild(item);
    }

    document.getElementById('waiting-count').textContent = this.waitingRoom.size;
  }

  updateParticipantList() {
    const participantList = document.getElementById('participant-list');
    participantList.innerHTML = '';

    for (const [userId, participant] of this.participants) {
      const item = document.createElement('div');
      item.innerHTML = `
        <span>${participant.externalUserId || userId}</span>
        ${this.isHost ? `
          <button onclick="webinar.featureParticipant('${userId}')">Feature</button>
          <button onclick="webinar.muteParticipant('${userId}')">Mute</button>
        ` : ''}
      `;
      participantList.appendChild(item);
    }
  }

  attachParticipantMedia({ userId, track, kind, paused }) {
    let container = document.getElementById(`participant-${userId}`);
    if (!container) {
      container = document.createElement('div');
      container.id = `participant-${userId}`;
      container.className = 'participant-video';
      container.innerHTML = `
        <video autoplay playsinline></video>
        <audio autoplay></audio>
      `;
      document.getElementById('participants-grid').appendChild(container);
    }

    const element = container.querySelector(kind === 'video' ? 'video' : 'audio');
    element.srcObject = new MediaStream([track]);
  }

  showHostControls() {
    document.getElementById('host-controls').classList.remove('hidden');
    document.getElementById('waiting-room-panel').classList.remove('hidden');
  }

  showStreamingIndicator(show) {
    document.getElementById('streaming-indicator').classList.toggle('hidden', !show);
  }

  showRejectionMessage() {
    alert('You were not admitted to the webinar.');
    window.location.href = '/';
  }
}

// Usage
const webinar = new Webinar('room-token');

// As host
await webinar.startAsHost('product-launch', 'host-123', {
  rtmpUrl: 'rtmp://a.rtmp.youtube.com/live2',
  streamKey: 'your-stream-key',
  platform: 'youtube'
});

// As guest
await webinar.joinAsGuest('invite-token', 'webinar123', 'guest-456');
```

---

## Event Reference

Quick reference for all events and their payloads.

| Event | Payload | Description |
|-------|---------|-------------|
| `CONNECTED` | None | WebRTC transport connected |
| `DISCONNECTED` | None | WebRTC transport lost |
| `ROOM_JOINED` | `{ id, name, owner, users[], created }` | Room joined successfully |
| `ROOM_CLOSED` | None | Room closed by owner |
| `USER_JOINED_ROOM` | `{ userId, externalUserId? }` | New participant joined |
| `USER_DISCONNECTED` | `{ userId }` | Participant left |
| `MEDIA_TRACK_ADDED` | `{ userId, track, kind, paused, audioOnly }` | Remote track available |
| `MEDIA_TRACK_REMOVED` | `{ userId, producerId, kind }` | Remote track removed |
| `AUDIO_MUTED` | `{ userId, muted }` | Local audio mute changed |
| `VIDEO_MUTED` | `{ userId, muted }` | Local video mute changed |
| `OUTPUT_MUTED` | `{ userId, muted }` | Local output mute changed |
| `REMOTE_AUDIO_MUTED` | `{ userId, muted }` | Owner requested audio mute |
| `REMOTE_VIDEO_MUTED` | `{ userId, muted }` | Owner requested video mute |
| `REMOTE_OUTPUT_MUTED` | `{ userId, muted }` | Owner requested output mute |
| `SCREEN_SHARE_STARTED` | None | Screen sharing started |
| `SCREEN_SHARE_STOPPED` | None | Screen sharing stopped |
| `RECORDING_STARTED` | `{ recordingId, roomName }` | Recording started |
| `RECORDING_STOPPED` | `{ recordingId }` | Recording stopped |
| `STREAMING_STARTED` | `{ streamingId }` | Streaming started |
| `STREAMING_STOPPED` | `{ streamingId }` | Streaming stopped |
| `STREAMING_USER_CHANGED` | `{ userId }` | Featured user changed |
| `TRANSCRIPTION_STARTED` | None | Transcription started |
| `TRANSCRIPTION_STOPPED` | None | Transcription stopped |
| `TRANSCRIPTION_RECEIVED` | `{ userId, text, timestamp?, isFinal? }` | Transcription text |
| `RECEIVE_CHAT_MESSAGE` | `{ userId, message, timestamp, externalUserId? }` | Chat message |
| `DATA_MESSAGE` | `{ userId, data }` | Custom data message |
| `ADMIT_WAITING_ROOM` | `{ userId, externalUserId? }` | Guest in waiting room |
| `REJECT_WAITING_ROOM` | `{ userId }` | Guest rejected |
| `WAIT_FOR_HOST_STARTED` | `{ roomName, timeout, pollingInterval }` | Waiting for host |
| `WAIT_FOR_HOST_ROOM_READY` | `{ roomName, elapsedTime }` | Host is ready |
| `WAIT_FOR_HOST_TIMEOUT` | `{ roomName, timeout, elapsedTime }` | Wait timed out |
| `WAIT_FOR_HOST_CANCELLED` | `{ roomName, elapsedTime }` | Wait cancelled |
| `AUDIO_INPUT_MONITOR_CREATED` | `AudioInputProcessor` | VU meter ready |
| `AUDIO_GAIN_CONTROL_CREATED` | `GainProcessor` | Volume control ready |
| `AUDIO_STREAM_ADDED` | `AudioMerger` | Audio merged |
| `AUDIO_STREAM_REMOVED` | `AudioMerger` | Audio unmerged |
| `AUDIO_INPUT_DEVICE_CHANGED` | Device info | Audio device switched |
| `VIDEO_INPUT_DEVICE_CHANGED` | Device info | Video device switched |
| `AUDIO_OUTPUT_DEVICE_CHANGED` | `{ previousDeviceId, newDeviceId, updatedElements }` | Speaker switched |
| `AUDIO_VALIDATION_FAILED` | `{ deviceId?, issues[], recommendations[], settings }` | Audio validation failed |
| `AUDIO_VALIDATION_WARNING` | `{ deviceId?, issues[], recommendations[], settings }` | Audio validation warning |
| `CONSTRAINTS_RELAXED` | `{ level, originalConstraints, relaxedConstraints, failedConstraint, reason? }` | Media constraints relaxed |
| `FEEDBACK_DETECTED` | `{ level?, frequency? }` | Audio feedback detected |
| `FEEDBACK_STOPPED` | None | Audio feedback stopped |
| `ERROR` | `{ error, originalError?, producerId? }` | Error occurred |

---

## Related Documentation

- [API Reference](./docs.md) - Complete method documentation
- [Connection Sequence](./Sequence.md) - Detailed initialization flow
- [Device Validation](./DeviceCheck.md) - Device validation API
- [File System Guide](./FILE_FOLDER_FILTERING_GUIDE.md) - File management patterns

---

*Generated for Hiyve Client SDK v1.0*
