Skip to content

Latest commit

 

History

History
1246 lines (972 loc) · 35.4 KB

File metadata and controls

1246 lines (972 loc) · 35.4 KB

TrueTime Meetings™ by Red5

A real-time video conferencing application built with React and Red5 Pro, featuring multi-participant video calls, screen sharing, chat messaging, and various interactive features.

Features

Core Video Conferencing

  • Multi-participant video calls with up to 30 participants by default
  • Real-time audio/video streaming using Red5 Pro WebRTC technology
  • Screen sharing with audio support
  • Play-only mode for viewers without publishing capabilities
  • Automatic device switching when devices become unavailable

Interactive Features

  • Live chat messaging with real-time delivery
  • Emoji reactions with floating animations
  • Raise hand functionality for meeting management
  • Participant list with status indicators
  • Audio level detection and visual talker indicators
  • External Streams Drawer for viewing additional media sources

Media Controls

  • Camera on/off with device selection
  • Microphone mute/unmute with device selection
  • Speaker selection for audio output
  • Video quality control with configurable bitrate settings

Layout Options

  • Auto layout - Automatically adjusts based on participant count
  • Tiled layout - Equal-sized video tiles for all participants
  • Pin/unpin participants for focused viewing

User Experience

  • Lobby page for pre-meeting setup and device testing
  • Permission handling for camera and microphone access
  • Responsive design with drawer-based UI components
  • Internationalization support with react-i18next
  • Dark/light theme support with Material-UI

Tech Stack

  • Frontend: React 18+ with Hooks
  • UI Library: Material-UI (MUI)
  • WebRTC: Red5 Pro Conference Client
  • State Management: React useState/useRef
  • Routing: React Router
  • Notifications: Notistack
  • Styling: Material-UI theming
  • Internationalization: react-i18next

Installation

# Clone the repository
git clone https://github.com/red5pro/red5-truetime-meetings
cd truetime-meetings

# Install dependencies
npm install --legacy-peer-deps

# Start the development server
npm start

Configuration

Runtime Configuration (Recommended)

The application supports runtime configuration via a JSON file, allowing you to modify settings after the build without rebuilding the application. This is the recommended approach for production deployments.

Setup

  1. Create config.json in the public folder (or use the provided config.json_prototype as a template):
{
  "VITE_TURN_SERVER_URL": "stun:stun2.l.google.com:19302",
  "VITE_TURN_SERVER_USERNAME": "",
  "VITE_TURN_SERVER_CREDENTIAL": "",
  "VITE_HOST": "xxx.red5pro.net",
  "VITE_NODE_GROUP": "default",
  "VITE_BACKEND_HOST": "",
  "VITE_ENABLE_CLOSED_CAPTION": "false",
  "VITE_ENABLE_RECORDING": "false",
  "VITE_DEFAULT_THEME": "black",
  "VITE_LOGO_URL": "/logo.svg"
}
  1. Update values in config.json to match your Red5 Pro deployment

  2. Build and deploy - The application will load configuration from /config.json at runtime

How It Works

  • The application fetches /config.json on startup (see src/main.tsx)
  • Configuration values are stored in the runtime config store (src/utils/configStore.ts)
  • Runtime config values override build-time environment variables
  • You can update config.json in your deployed application without rebuilding

Available Configuration Options

Parameter Type Description Example
VITE_HOST string Red5 Pro host URL "xxx.red5pro.net"
VITE_BACKEND_HOST string Backend API URL "xxx.red5pro.net"
VITE_NODE_GROUP string Red5 Pro node group "default"
VITE_TURN_SERVER_URL string TURN/STUN server URL "stun:stun2.l.google.com:19302"
VITE_TURN_SERVER_USERNAME string TURN server username ""
VITE_TURN_SERVER_CREDENTIAL string TURN server credential ""
VITE_ENABLE_RECORDING string Enable recording feature "true" or "false"
VITE_ENABLE_CLOSED_CAPTION string Enable closed captions "true" or "false"
VITE_DEFAULT_THEME string Default UI theme "black" or "blue"
VITE_LOGO_URL string Custom logo URL "/path/to/logo.svg"

Environment Variables (Development)

For development purposes, you can also use environment variables. Create an .env file in the root of your project:

VITE_HOST="xxx-xxx-x-oci.red5pro.net"
VITE_BACKEND_HOST="xxx-xxx-x-oci.red5pro.net"
VITE_NODE_GROUP="xxxxxx"
VITE_TURN_SERVER_URL="stun:stun2.l.google.com:19302"
VITE_TURN_SERVER_USERNAME=""
VITE_TURN_SERVER_CREDENTIAL=""
VITE_PUBNUB_PUBLISH_KEY="pub-x-xxxxxx-xxxxxx-xxxxxx-xxxxxx-xxxxxx"
VITE_PUBNUB_SUBSCRIBE_KEY="sub-x-xxxxxx-xxxxxx-xxxxxx-xxxxxx-xxxxxx"
VITE_VIRTUAL_BACKGROUND_IMAGES="https://xxx,https://xxx,https://xxx"
VITE_ENABLE_RECORDING="false"
VITE_ENABLE_CLOSED_CAPTION="false"
VITE_ENABLE_EXTERNAL_STREAMS="false"
VITE_DEFAULT_THEME='black'
VITE_LOGO_URL="https://xxx"
VITE_BASENAME="/"
VITE_CONFIG_SERVICE_URL="http://localhost:3001"

Note: Runtime configuration from config.json takes precedence over build-time environment variables.

Docker Deployment

Docker Setup

The application includes Docker support for containerized deployment with runtime environment variable configuration.

Files Structure

  • Dockerfile - Multi-stage build configuration for React app with nginx
  • ci/docker_startup_script.sh - Runtime script for environment variable replacement
  • env.docker - Template file with placeholder values for build-time configuration
  • nginx.conf - Nginx configuration for serving the React app

Environment Variables

The Docker setup supports 4 configurable environment variables that are replaced at runtime:

Variable Description Example
VITE_HOST Frontend host URL your-frontend-domain.com
VITE_BACKEND_HOST Backend API URL your-backend-api.com
VITE_ENABLE_RECORDING Enable recording feature true or false
VITE_ENABLE_CLOSED_CAPTION Enable closed captions true or false

Build Process

  1. Build Time: The app is built with template variables from env.docker
  2. Runtime: The startup script replaces template variables with actual environment values

Usage

Build the Docker image:

docker build -t truetime-meetings .

Run with environment variables:

docker run -p 3000:80 \
  -e VITE_HOST=https://your-frontend-domain.com \
  -e VITE_BACKEND_HOST=https://your-backend-api.com \
  -e VITE_ENABLE_RECORDING=true \
  -e VITE_ENABLE_CLOSED_CAPTION=true \
  truetime-meetings

Using docker-compose:

version: '3.8'
services:
  truetime-meetings:
    build: .
    ports:
      - '3000:80'
    environment:
      - VITE_HOST=https://your-frontend-domain.com
      - VITE_BACKEND_HOST=https://your-backend-api.com
      - VITE_ENABLE_RECORDING=true
      - VITE_ENABLE_CLOSED_CAPTION=true
    restart: unless-stopped

How It Works

  1. Template Variables: The app is built with TEMPLATE_VITE_* placeholders
  2. Runtime Replacement: The startup script finds and replaces these placeholders with actual environment values
  3. Nginx Serving: The app is served by nginx with optimized configuration (gzip, caching, security headers)

Customization

To add more environment variables:

  1. Add the template variable to env.docker:

    VITE_NEW_VARIABLE=TEMPLATE_VITE_NEW_VARIABLE
  2. Update ci/docker_startup_script.sh to handle the new variable:

    # Add environment check
    if [ -z "$VITE_NEW_VARIABLE" ]; then
      echo "VITE_NEW_VARIABLE is not set"
      exit 1
    fi
    
    # Add replacement
    sed -i "s|TEMPLATE_VITE_NEW_VARIABLE|$VITE_NEW_VARIABLE|g" "$PATH_TO_FILE"
  3. Rebuild and run with the new environment variable:

    docker run -p 3000:80 \
      -e VITE_NEW_VARIABLE=your-value \
      truetime-meetings

Red5 Pro Conference SDK Documentation

Red5 Pro Conference SDK allows you to build advanced conference call applications with unlimited number of participants on top of your Red5 infrastructure(red5cloud-streammanager), supporting screen sharing, subscribe only participants, audio output/input device switching, publish quality changing and audio level monitoring.

Table of Contents

Overview

The Red5 Pro Conference SDK provides a high-level API for building video conferencing applications. It abstracts the complexity of WebRTC management, media handling, and connection management while providing robust features for production use.

Key Features

  • Real-time Video Conferencing: Full-duplex audio/video communication
  • Screen Sharing: Built-in screen capture and sharing capabilities
  • Audio Level Monitoring: Real-time audio level detection for participants
  • Media Device Management: Easy camera/microphone switching
  • Role-based Access: Support for publisher and subscriber roles
  • Chat Messaging: Built-in text chat functionality
  • Mute/Unmute Controls: Audio and video muting capabilities
  • Cross-browser Support: Works across modern browsers

Installation

Prerequisites

  • Red5 Pro Javascript WebRTC SDK
  • ConferenceClient.ts and MediaStreamManager.js
  • Red5 Pro Stream Manager(Cloud) deployment

NPM Installation

npm install red5pro-webrtc-sdk

Include the SDK Files ConferenceClient.ts and MediaStreamManager.js in same directory

import { ConferenceClient } from './ConferenceClient';

Quick Start

Here's a minimal example to get you started:

import { ConferenceClient } from './ConferenceClient';

// Initialize the client
client = new ConferenceClient({
  host: 'your-cloud-deployment-host.red5pro.net',
  nodeGroup: 'default',
  maxVideoBitrateKbps: 3000,
  iceServers: [{ urls: 'stun:stun2.l.google.com:19302' }],
});

// Get user media
const mediaStream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true,
});

// Join a room
await client.join('room-123', 'user-456', '', 'publisher', mediaStream, true, true);

// Listen for events
client.on('user-published', (data) => {
  console.log('Joined successfully:', data.participants);
});

client.on('new-participant', async (data) => {
  console.log('New participant:', data.participant);
  if (data.participant.role === 'publisher') {
    await client.subscribe(data.participant);
  }
});

Basic Usage

Joining a Room

Get user media stream through media stream manager. We will pass it to join()

var video = true;
var audio = true;
var mediaStream = client.mediaStreamManager.getCurrentStream(video, audio);
join(roomId, userId, token, role, mediaStream, videoEnabled, audioEnabled, metaData)

Join a conference room. If succesfull conference sdk will emit user-published event with already existing participants in the room. You can join as publisher or subscriber. Publishers can publish and subscribe(play), meanwhile subscriber role can only subscribe streams.

Parameters:

Parameter Type Default Description
roomId string - Unique room identifier
userId string - Unique user identifier (stream name)
token string '' Authentication token
role string 'publisher' User role ('publisher' or 'subscriber')
mediaStream MediaStream null User's media stream
videoEnabled boolean true Initial video state
audioEnabled boolean true Initial audio state
metaData string null Additional user metadata

Returns: Promise<boolean>

Example:

client.on('user-published', handleUserPublished);
client.on('subscribe-success', handleSubscribeSuccess);

await client.join(
  'conference-room-id',
  'user-id-123',
  'auth-token',
  'publisher',
  mediaStream,
  true,  // video enabled
  true,  // audio enabled
  { name: 'John Doe', avatar: 'https://...' }
);

const handleUserPublished = async (data) => {
    console.log('User published/joined room success', data);
    await subscribeToParticipants(data.participants);
};

const subscribeToParticipants = async (participantsObj) => {
    for (const [userId, participant] of Object.entries(participantsObj)) {
        if(participant.role === "publisher") {
            await subscribeToParticipant(participant);
        }
    }
};

const subscribeToParticipant = async (participant) => {
    try {
        console.log(`Subscribing to participant: ${participant.uid}`);
        await client.subscribe(participant);
    } catch (error) {
        console.error(`Failed to subscribe to ${participant.uid}:`, error);
    }
};

// Add user to a state for rendering their video on screen.
const handleSubscribeSuccess = (data) => {
    console.log('Subscribe success:', data.uid);
    setSubscribedParticipants(prev => ({
        ...prev,
        [data.uid]: {
            participant: data.participant,
            mediaStream: data.mediaStream
        }
    }));
};

return(
{/* Subscribed participants */}
{Object.keys(subscribedParticipants).length > 0 && (
    <div style={{
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        gap: "15px",
        width: "100%"
    }}>
        <h3 style={{ margin: "0", color: "#333" }}>
            Subscribed Participants ({Object.keys(subscribedParticipants).length})
        </h3>
        <div style={{
            display: "grid",
            gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
            gap: "15px",
            width: "100%",
            maxWidth: "1200px"
        }}>
            {Object.entries(subscribedParticipants).map(([userId, subscribedData]) => (
                <div key={userId} style={{
                    display: "flex",
                    flexDirection: "column",
                    alignItems: "center",
                    gap: "5px"
                }}>
                    <span style={{
                        fontSize: "14px",
                        fontWeight: "bold",
                        color: "#666"
                    }}>
                        User: {userId.substring(0, 8)}...
                    </span>
                    <video
                        id={`red5pro-subscriber-${userId}`}
                        autoPlay
                        playsInline
                        ref={(videoElement) => {
                            if (videoElement && subscribedData.mediaStream && !videoElement.srcObject) {
                                videoElement.srcObject = subscribedData.mediaStream;
                            }
                        }}
                        style={{
                            width: "300px",
                            height: "225px",
                            border: "2px solid #28a745",
                            borderRadius: "8px",
                            backgroundColor: "#000"
                        }}
                    />
                </div>
            ))}
        </div>
    </div>
)}
)

Handling New Participants

When a new participant joins, conference sdk will emit a new event new-participant. Listen for this event and subscribe to new participant.

client.on('new-participant', handleNewParticipant);

const handleNewParticipant = async (data) => {
  console.log('New participant:', data.participant);
  setParticipants((prev) => ({
    ...prev,
    [data.participant.uid]: data.participant,
  }));

  if (data.participant.role === 'publisher') {
    setTimeout(async () => {
      await subscribeToParticipant(data.participant);
    }, 3000);
  }
};

Handle Disconnected Participants

When someones leaves the room, conference client will emit participant-disconnected event. Listen for it and remove the user from your participant lists.

client.on('participant-disconnected', handleParticipantDisconnected);

const handleParticipantDisconnected = (data) => {
  console.log('Participant disconnected:', data.participant);
  setParticipants((prev) => {
    const newParticipants = { ...prev };
    delete newParticipants[data.participant.uid];
    return newParticipants;
  });
  setSubscribedParticipants((prev) => {
    const newSubscribed = { ...prev };
    delete newSubscribed[data.participant.uid];
    return newSubscribed;
  });
  setAudioLevels((prev) => {
    // if you handle audio levels, remove from that list also.
    const newLevels = { ...prev };
    delete newLevels[data.participant.uid];
    return newLevels;
  });
};

Leave a Room

Leave a room by calling leave() method on conference client.

client.leave();

Mute Video/Audio

You can turn off/on camera or mute/unmute your microphone.

const handleMuteAudio = () => {
  if (isAudioMuted) {
    client.unmuteAudio();
  } else {
    client.muteAudio();
  }
};

const handleMuteVideo = () => {
  if (isVideoMuted) {
    client.unmuteVideo();
  } else {
    client.muteVideo();
  }
};

Handle Participant Media Updates

When a participant in room turn on/turn off their camera or mute/unmute their microphone conference sdk will emit participant-media-update event. By listening to this event you can update ui accordingly like showing a mic muted icon on participant or puting an overlay to their video if they turned off their camera.

client.on('participant-media-update', handleParticipantMediaUpdate);

const handleParticipantMediaUpdate = (data) => {
  console.log('Participant media update:', data);
  const { streamName, videoEnabled, audioEnabled } = data;
  setParticipantMuteStates((prev) => ({
    ...prev,
    [streamName]: {
      videoMuted: !videoEnabled,
      audioMuted: !audioEnabled,
    },
  }));
};

Advanced Usage

Screen Sharing

Call startScreenShare() method on conference client.

const handleStartScreenShare = async () => {
  try {
    await client.startScreenShare({
      width: 1920,
      height: 1080,
      frameRate: 30,
      includeAudio: true,
    });
  } catch (error) {
    console.error('Failed to start screen share:', error);
    setIsStartingScreenShare(false);
    alert('Failed to start screen share: ' + error.message);
  }
};

Sdk will emit events screen-share-started, screen-share-stopped You may use such events like below

client.on('screen-share-started', handleScreenShareStarted);
client.on('screen-share-stopped', handleScreenShareStopped);

const handleScreenShareStarted = (data) => {
  console.log('Screen share started:', data);
  setIsScreenSharing(true);
  setIsStartingScreenShare(false);
};

const handleScreenShareStopped = () => {
  console.log('Screen share stopped');
  setIsScreenSharing(false);
  setIsStartingScreenShare(false);
};

Audio Level Monitoring

Conference client will emit audio level event for each subscribed participant periodically. You can use this event to display audio levels of participants on screen or as more real world example, you can use it to make a talking indicator.

client.on('audio-level', handleAudioLevel);
const handleAudioLevel = (data) => {
  setAudioLevels((prev) => ({
    ...prev,
    [data.userId]: data.level.normalized,
  }));
};

Audio level data includes normalized, rms, dcb. You can use the one you want.

Sending/Receiving Chat Message

You can send and receive chat message through red5 pro stream manager without relying any 3rd party messaging service. You can use this to exchange any message, build a emoji functionality or room chat box.

client.sendChatMessage('Hello world!');
client.on('chat-message', handleChatMessage);
const handleChatMessage = (chatMessage) => {
  console.log('Received chat message', chatMessage);
};

Change Video Publish Quality

You can set maximum video publish bitrate before joining a room or when you are in room and publishing.

While creating conference client, you can set maximum publish quality by passing maxVideoBitrateKbps.

client = new ConferenceClient({
  host: 'your-cloud-deployment-host.red5pro.net',
  nodeGroup: 'default',
  maxVideoBitrateKbps: 3000,
  iceServers: [{ urls: 'stun:stun2.l.google.com:19302' }],
});

If you have inited the sdk but not started to publish, you can still change it by reaching config object of conference client and set quality by

client.config.maxVideoBitrateKbps = 1000;

During publishing, you can change publish quality by calling

client.setMaxVideoPublishKbps(1500);

Remote participants should see quality increase/drop as soon as you call this.

Switch Video/Audio Output Devices

Through conference client media stream manager, you can allow users to switch audio input/output and camera devices.

Switch Video Device

Use switchVideoDevice(), refreshPublisherStream(), cleanupCurrentMediaStream() and setCurrentStream() methods to achieve this on air or before publishing.

See this react example:

const cameraSelected = React.useCallback(
  (value) => {
    if (selectedCamera !== value) {
      setSelectedDevices({ videoDeviceId: value });
      try {
        conferenceClient.current.mediaStreamManager.switchVideoDevice(value).then((mediaStream) => {
          const tempLocalVideo = document.getElementById('red5pro-publisher');
          tempLocalVideo.srcObject = mediaStream;
          conferenceClient.current.refreshPublisherStream(mediaStream).then((r) => {
            conferenceClient.current.mediaStreamManager.cleanupCurrentMediaStream();
            conferenceClient.current.mediaStreamManager.setCurrentStream(mediaStream);
          });
        });
      } catch (e) {
        log.log('Local stream is not ready yet.');
      }
    }
  },
  [selectedCamera, setSelectedDevices],
);

Switch Microphone Device

Call switchAudioDevice() and refresh users stream.

React example:

const microphoneSelected = React.useCallback(
  (value) => {
    if (selectedMicrophone !== value) {
      setSelectedDevices({ audioDeviceId: value });

      conferenceClient.current.mediaStreamManager.switchAudioDevice(value).then((mediaStream) => {
        const tempLocalVideo = document.getElementById('red5pro-publisher');
        tempLocalVideo.srcObject = mediaStream;
        conferenceClient.current.refreshPublisherStream(mediaStream).then((r) => {
          conferenceClient.current.mediaStreamManager.cleanupCurrentMediaStream();
          conferenceClient.current.mediaStreamManager.setCurrentStream(mediaStream);
        });
      });
    }
  },
  [selectedMicrophone, setSelectedDevices],
);

Switch Audio Output Device

You can even switch audio output device of conference while in a room.

React example:

const speakerSelected = React.useCallback(
  (value) => {
    if (selectedSpeaker !== value) {
      setSelectedDevices({ speakerDeviceId: value });
      updateAudioOutput(value);
    }
  },
  [selectedSpeaker, setSelectedDevices],
);

const updateAudioOutput = React.useCallback(
  async (selectedAudioOutput = selectedSpeaker) => {
    console.log('updateAudioOutput', selectedAudioOutput);
    if (!('setSinkId' in HTMLMediaElement.prototype) || isNull(selectedAudioOutput)) return;

    for (let key of Object.keys(participants)) {
      const videoElement = document.getElementById(`red5pro-subscriber-${key}`);
      if (!videoElement) {
        console.warn(`No video element found for participant ${key}`);
        subscribeToParticipants({ [key]: participants[key] });
        continue;
      }

      if (typeof videoElement.setSinkId !== 'function') {
        console.error('setSinkId is not supported in this browser.');
        continue;
      }

      try {
        await videoElement.setSinkId(selectedAudioOutput);
        console.log('Audio output set for', key, 'to', selectedAudioOutput);
      } catch (error) {
        console.error('Error setting audio output for', key, ':', error);
      }
    }
  },
  [selectedSpeaker, participants],
);

Events

import ConferenceClient from './ConferenceClient';

const client = new ConferenceClient({
  host: 'your-host.com',
  nodeGroup: 'default',
  iceServers: [{ urls: 'stun:stun2.l.google.com:19302' }],
});

// Set up event listeners
client.on('join-failed', handleJoinFail);
client.on('user-published', handleUserPublished);
client.on('new-participant', handleNewParticipant);
client.on('participant-disconnected', handleParticipantDisconnected);
client.on('subscribe-success', handleSubscribeSuccess);
client.on('subscribe-failed', handleSubscribeFailed);
client.on('subscribe-stop', handleSubscribeStop);
client.on('audio-level', handleAudioLevel);
client.on('audio-muted', handleAudioMuted);
client.on('video-muted', handleVideoMuted);
client.on('participant-media-update', handleParticipantMediaUpdate);
client.on('connection-closed', handleConnectionClosed);
client.on('connect-fail', handleConnectFail);
client.on('chat-message', handleChatMessage);
client.on('screen-share-started', handleScreenShareStarted);
client.on('screen-share-stopped', handleScreenShareStopped);

Connection Events

join-failed

Emitted when joining a room fails.

Data:

  • error - Error message
  • statusCode - HTTP status code (401 for unauthorized)
client.on('join-failed', (data) => {
  console.log('Join failed:', data.error);
  if (data.statusCode === 401) {
    // Handle unauthorized access
  }
});

user-published

Emitted when the user successfully joins and publishes to the room.

Data:

  • participants - Object containing other participants in the room
  • roomState - Current room state information
client.on('user-published', (data) => {
  setIsPublished(true);
  setParticipants(data.participants);
});

connection-closed

Emitted when the publisher connection is closed.

client.on('connection-closed', () => {
  // Handle connection loss
  handleLeaveFromRoom();
});

connect-fail

Emitted when initial connection fails.

client.on('connect-fail', () => {
  setIsJoining(false);
  // Show connection error
});

Participant Events

new-participant

Emitted when a new participant joins the room.

Data:

  • participant - Participant information object
  • roomState - Updated room state
client.on('new-participant', (data) => {
  setParticipants((prev) => ({
    ...prev,
    [data.participant.uid]: data.participant,
  }));
});

participant-disconnected

Emitted when a participant leaves the room.

Data:

  • participant - Participant information object
  • roomState - Updated room state
client.on('participant-disconnected', (data) => {
  setParticipants((prev) => {
    const newParticipants = { ...prev };
    delete newParticipants[data.participant.uid];
    return newParticipants;
  });
});

participant-media-update

Emitted when a participant's media state changes (mute/unmute).

Data:

  • streamName - Participant's stream name
  • videoEnabled - Whether video is enabled
  • audioEnabled - Whether audio is enabled
  • timestamp - Update timestamp
client.on('participant-media-update', (data) => {
  setParticipants((prev) => ({
    ...prev,
    [data.streamName]: {
      ...prev[data.streamName],
      videoEnabled: data.videoEnabled,
      audioEnabled: data.audioEnabled,
    },
  }));
});

Subscription Events

subscribe-success

Emitted when successfully subscribing to a participant's stream.

Data:

  • uid - Participant's unique identifier
  • mediaStream - MediaStream object
  • participant - Participant information
client.on('subscribe-success', (data) => {
  setSubscribedParticipants((prev) => ({
    ...prev,
    [data.uid]: {
      participant: data.participant,
      mediaStream: data.mediaStream,
    },
  }));
});

subscribe-failed

Emitted when subscription to a participant fails.

Data:

  • user - User object with uid property
  • error - Error message
client.on('subscribe-failed', (data) => {
  console.error('Subscribe failed for:', data.user.uid, data.error);
  // Handle retry logic or remove participant
});

subscribe-stop

Emitted when a subscription stops.

Data:

  • uid - Participant's unique identifier
client.on('subscribe-stop', (data) => {
  setSubscribedParticipants((prev) => {
    const newSubscribed = { ...prev };
    delete newSubscribed[data.uid];
    return newSubscribed;
  });
});

Audio/Video Events

audio-level

Emitted periodically with audio level data for participants.

Data:

  • userId - Participant's user ID
  • level - Audio level object containing:
    • normalized - Level from 0-100
    • rms - Root Mean Square value
    • db - Decibel level
client.on('audio-level', (data) => {
  if (data.level.normalized > 75) {
    // Participant is talking
    setTalkers((prev) => [...prev, data.userId]);
  }
});

audio-muted

Emitted when local audio is muted/unmuted.

Data:

  • muted - Boolean indicating mute state
client.on('audio-muted', (data) => {
  setIsMyMicMuted(data.muted);
});

video-muted

Emitted when local video is muted/unmuted.

Data:

  • muted - Boolean indicating mute state
client.on('video-muted', (data) => {
  setIsMyCamTurnedOff(data.muted);
});

Screen Share Events

screen-share-started

Emitted when screen sharing starts successfully.

Data:

  • streamName - Screen share stream name (usually ends with '-screenshare')
  • stream - MediaStream object (optional)
client.on('screen-share-started', (data) => {
  setIsScreenShared(true);
  if (data.stream) {
    // Handle screen share stream
  }
});

screen-share-stopped

Emitted when screen sharing stops.

client.on('screen-share-stopped', () => {
  setIsScreenShared(false);
});

Chat Events

chat-message

Emitted when receiving a chat message from another participant.

Data:

  • senderStreamName - Name of the message sender
  • chatMessageText - JSON string containing message data
client.on('chat-message', (chatMessage) => {
  const message = JSON.parse(chatMessage.chatMessageText);

  if (message.eventType === 'MESSAGE_RECEIVED') {
    setMessages((prev) => [...prev, message]);
  } else if (message.eventType === 'REACTIONS') {
    showReactions(message.senderStreamId, message.reaction);
  } else if (message.eventType === 'RAISED_HAND') {
    handleRaisedHand(message);
  }
});

Event Handler Examples

function handleJoinFail(data) {
  setIsJoining(false);
  if (data.statusCode === 401) {
    setUnAuthorizedDialogOpen(true);
  }
}

function handleUserPublished(data) {
  setIsJoining(false);
  setIsPublished(true);
  setParticipants(data.participants);
  setlobbyOrMeetingPage('meeting');
}

function handleNewParticipant(data) {
  setParticipants((prev) => ({
    ...prev,
    [data.participant.uid]: data.participant,
  }));
}

function handleParticipantDisconnected(data) {
  // Clean up UI references
  clearRemoteSubscriber(data.participant.uid);

  // Remove from participants
  setParticipants((prev) => {
    const newParticipants = { ...prev };
    delete newParticipants[data.participant.uid];
    return newParticipants;
  });
}

function handleAudioLevel(data) {
  // Update talking indicators
  if (data.level.normalized > 75) {
    setTalkers((prev) => [...prev, data.userId]);
  }
}

function handleChatMessage(chatMessage) {
  const message = JSON.parse(chatMessage.chatMessageText);

  switch (message.eventType) {
    case 'MESSAGE_RECEIVED':
      if (!messageDrawerOpen) {
        setNumberOfUnReadMessages((prev) => prev + 1);
        // Show notification
        enqueueSnackbar(message.message, {
          sender: message.name,
          variant: 'message',
        });
      }
      setMessages((prev) => [...prev, message]);
      break;

    case 'REACTIONS':
      showReactions(message.senderStreamId, message.reaction);
      break;

    case 'RAISED_HAND':
      if (message.isRaisedHand) {
        setRaisedHands((prev) => [...prev, message.senderStreamId]);
      } else {
        setRaisedHands((prev) => prev.filter((id) => id !== message.senderStreamId));
      }
      break;
  }
}

Removing Event Listeners

// Remove specific listener
client.off('join-failed', handleJoinFail);

// Clean up when component unmounts
useEffect(() => {
  return () => {
    // Remove all listeners by calling leave
    client.leave();
  };
}, []);

Common Patterns

Error Handling

client.on('subscribe-failed', (data) => {
  console.error('Subscribe failed:', data.error);
  // Implement retry logic or remove participant from UI
});

client.on('connect-fail', () => {
  setIsJoining(false);
  displayMessage('Failed to connect to the server');
});

State Management

// Update participants when they join/leave
client.on('new-participant', handleNewParticipant);
client.on('participant-disconnected', handleParticipantDisconnected);

// Track media states
client.on('participant-media-update', handleParticipantMediaUpdate);
client.on('audio-muted', (data) => setIsMyMicMuted(data.muted));
client.on('video-muted', (data) => setIsMyCamTurnedOff(data.muted));

UI Updates

// Show loading states
client.on('user-published', () => {
  setIsJoining(false);
  setlobbyOrMeetingPage('meeting');
});

// Handle talking indicators
client.on('audio-level', (data) => {
  updateTalkerLevel(data.userId, data.level.normalized);
});