Tutorial

MUZIERTCCLIENT_API_WORKFLOWS

Hiyve Client API Workflows

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

This document complements the API Reference 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
  2. Media Workflows
  3. Control Workflows
  4. Recording & Media Services
  5. Communication Workflows
  6. Resilience Workflows
  7. File Management Workflows
  8. Complete Application Examples

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).

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

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.

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

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.

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

// 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

// 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.

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

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.

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

// 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.

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

// 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

// 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

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)

// 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).

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

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

// 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.

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

// 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

// 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.

// 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.

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

// 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.

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

// 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.

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

// 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.

// 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.

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

// 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.

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

// 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.

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()
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

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.

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

// 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.

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

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.

// 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.

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.

<!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.

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.

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


Generated for Hiyve Client SDK v1.0