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
- Core Connection Workflows
- Media Workflows
- Control Workflows
- Recording & Media Services
- Communication Workflows
- Resilience Workflows
- File Management Workflows
- 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
- API Reference - Complete method documentation
- Connection Sequence - Detailed initialization flow
- Device Validation - Device validation API
- File System Guide - File management patterns
Generated for Hiyve Client SDK v1.0