Call Lobby
Let users configure devices and preview video before joining a call.
A common implementation for a video streaming application is to have a "lobby" that allows users to configure their device settings and view a preview of their video before joining a call. Once the user is ready and the call is ready to start, they are redirected to the call room.
- React
Prerequisites
These docs are for an advanced implementation and assumes you have a basic understanding of @video/video-client-react.
If you are new to @video/video-client-react, we recommend familiarizing yourself with basic concepts first:
Specifically, we will be skimming over the following concepts and implementations:
For foundational concepts, please review:
- The
usePreviewPlayeranduseAuthClienthooks. - The
useCallControlscustom hook. - The
PreviewPlayercomponent for device controls.
Use Portal Hook
This custom hook is used to create a React portal for the Preview Player component.
// usePortal.tsx
/**
* Custom hook for creating a React portal target element
*
* This hook creates a ref to a div element and notifies a parent component
* when the ref is attached, allowing the parent to render content into this
* element using React's createPortal.
*
* Common Use Case: Rendering a video encoder component in a specific location
* while keeping its state managed at a higher level in the component tree.
*
* @param setContainerElement - Callback function to notify parent of the portal target element
* @returns A React ref to attach to the portal container div
*
* @example
* ```tsx
* // In child component:
* const portalRef = usePortal(setContainerElement);
* return <div ref={portalRef} />;
*
* // In parent component:
* const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);
* {containerElement && createPortal(<EncoderComponent />, containerElement)}
* ```
*/
import { useEffect, useRef } from "react";
export const usePortal = (
setContainerElement: (containerElement: HTMLDivElement | null) => void,
): React.RefObject<HTMLDivElement> => {
// Create a ref to the div element that will serve as the portal target
const portalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Once the ref is attached to a DOM element, notify the parent component
if (portalRef.current) {
setContainerElement(portalRef.current);
}
// Cleanup: notify parent that the portal target is being removed
return () => {
setContainerElement(null);
};
}, [setContainerElement]);
return portalRef;
};
Waiting Room Component
This component will be where the user can configure their device settings, view a preview of their video before joining a call, and join a call.
Imports
In this example we will not be using the <CallControls/> component or useCallControls hook that we have used in previous examples, but creating a custom API interaction.
For this we will need:
import { types, context, joinCall } from "@video/video-client-react";
// Extract context hooks for accessing video client API instances
const { useAuthClientAPI, useMediaStreamControllerAPI } = context;
Props
In order to join the call, you will need to pass in the following props:
callId: The ID of the call to join.
In order to set the call, broadcast, and connecting state in the parent component, you will need to pass in the following props:
setCall: A function to set the call state.setBroadcast: A function to set the broadcast state.setConnecting: A function to set the connecting state.
/**
* Props interface for CallLobby component
*/
interface WaitingRoomProps {
/** Callback to set the call instance in parent component after joining */
setCall: (call: types.CallAPI) => void;
/** Callback to set the broadcast instance in parent component after starting broadcast */
setBroadcast: (broadcast: types.BroadcastAPI) => void;
/** The ID of the call to join */
callId: string | null;
/** Callback to pass the portal container element to parent for rendering encoder */
setContainerElement: (containerElement: HTMLDivElement | null) => void;
/** Backend API endpoint URL */
backendEndpoint: string;
/** Unique stream key for this broadcast */
streamKey: string;
}
State
We will use local state to managing "connecting" state and render a loader when the call is asynchronously joining.
/**
* Local state to track whether the user is in the process of joining the call
* Used to display a loading spinner while the async join operation completes
*/
const [connecting, setConnecting] = useState(false);
Click Handler
We will create a custom button in the <WaitingRoom/> component that will be used to join the call as well as broadcast the user's video. This is a custom
implementation that is not included in the @video/video-client-react package and gives you the ability to customize these API calls to your needs.
Our click handler will manage the following logic all in one user interaction:
- Set the connecting state to true, this will be used to display a loading state to the user
- Join the call
- Create a broadcast, this will be used to broadcast the user's video to the call
- Set the broadcast and call state in the parent component
- Set the connecting state to false, this will be used to hide the loading state to the user
/**
* Handles the join call flow when user clicks the "Join Room" button
*
* This function performs several operations in sequence:
* 1. Validates that a call ID exists
* 2. Sets loading state to show spinner
* 3. Joins the call using the joinCall helper
* 4. Creates a broadcast to start streaming the user's camera/mic
* 5. Updates parent component state with the new call and broadcast instances
* 6. Clears loading state
*
* Note: This combines both joining the call AND starting the broadcast
* in a single user interaction, providing a seamless UX.
*/
const handleJoinCall = async () => {
if (callId == null) {
throw new Error("Call ID is required to Join Call");
}
try {
// Step 1: Enable loading state to show user that connection is in progress
setConnecting(true);
// Step 2: Join the call with authentication and user info
const joinCallOptions = {
streamKey, // Unique identifier for the stream
user: { userId: "123", displayName: "Waiting Room Participant" }, // User identity
backendEndpoints: [backendEndpoint], // API endpoints
auth: authClient, // Authentication client
};
const newCall = await joinCall(callId, joinCallOptions);
// Step 3: Create a broadcast to start streaming this user's video/audio to other participants
// The "default" stream name distinguishes the main camera from other streams (like screenshare)
const broadcastOptions = {
streamName: "default",
};
const newBroadcast = await newCall.broadcast(mediaStreamController, broadcastOptions);
// Step 4: Notify parent component of the new call and broadcast instances
// This allows the parent to transition from WaitingRoom to CallRoom
setBroadcast(newBroadcast);
setCall(newCall);
// Step 5: Clear loading state now that we've successfully connected
setConnecting(false);
} catch {
// If any step fails, clear the loading state to allow the user to retry
setConnecting(false);
}
};
Render UI
return (
<div className={classNames.wrapperClassName}>
<div ref={portalRef} />
{connecting && (
<div className={classNames.connectingClassName}>
<div className={classNames.connectingLoaderClassName} />
</div>
)}
<div>
{callId != null && (
<button
type="button"
onClick={handleJoinCall}
className={classNames.joinRoomButtonClassName}
>
Join Room
</button>
)}
</div>
</div>
);
}
Full Component Code
// CallLobby.tsx
/**
* CallLobby Component
*
* A pre-call waiting room that allows users to configure their device settings
* (camera, microphone, etc.) and preview their video before joining a call.
*
* This component demonstrates a common video conferencing pattern where users
* can prepare their setup before being fully connected to other participants.
*
* Key Features:
* - Preview camera/microphone with device selection controls
* - Join call button with loading state
* - Portal-based rendering for flexible UI composition
* - Automatic broadcast creation upon joining
*
* @example
* ```tsx
* <WaitingRoom
* setCall={setCall}
* setBroadcast={setBroadcast}
* callId="call-123"
* setContainerElement={setContainerElement}
* backendEndpoint="https://api.example.com"
* streamKey="unique-stream-key"
* />
* ```
*/
import React, { memo, useState } from "react";
import { types, context, joinCall } from "@video/video-client-react";
// Extract context hooks for accessing video client API instances
const { useAuthClientAPI, useMediaStreamControllerAPI } = context;
import { usePortal } from "./usePortal"; // Custom hook for portal rendering
/**
* CSS class names for styling the waiting room UI
* Uses Tailwind CSS utility classes for responsive design
*/
const classNames = {
wrapperClassName: "flex flex-col m-4",
connectingClassName: "flex justify-center",
connectingLoaderClassName: "animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-gray-900",
joinRoomButtonClassName: "text-white bg-green-500 px-4 py-2 rounded-md cursor-pointer my-4",
};
/**
* Props interface for CallLobby component
*/
interface WaitingRoomProps {
/** Callback to set the call instance in parent component after joining */
setCall: (call: types.CallAPI) => void;
/** Callback to set the broadcast instance in parent component after starting broadcast */
setBroadcast: (broadcast: types.BroadcastAPI) => void;
/** The ID of the call to join */
callId: string | null;
/** Callback to pass the portal container element to parent for rendering encoder */
setContainerElement: (containerElement: HTMLDivElement | null) => void;
/** Backend API endpoint URL */
backendEndpoint: string;
/** Unique stream key for this broadcast */
streamKey: string;
}
function WaitingRoom({ setCall, setBroadcast, callId, setContainerElement, backendEndpoint, streamKey }: WaitingRoomProps): JSX.Element {
/**
* Local state to track whether the user is in the process of joining the call
* Used to display a loading spinner while the async join operation completes
*/
const [connecting, setConnecting] = useState(false);
/**
* Access the media stream controller from context
* This manages the user's camera and microphone input
*/
const mediaStreamController = useMediaStreamControllerAPI();
/**
* Access the authentication client from context
* This provides the auth credentials needed to join the call
*/
const authClient = useAuthClientAPI();
/**
* Create a portal target for rendering the encoder component
* This allows the encoder (camera preview + controls) to be rendered
* in this component's DOM while being managed by the parent
*/
const portalRef = usePortal(setContainerElement);
/**
* Handles the join call flow when user clicks the "Join Room" button
*
* This function performs several operations in sequence:
* 1. Validates that a call ID exists
* 2. Sets loading state to show spinner
* 3. Joins the call using the joinCall helper
* 4. Creates a broadcast to start streaming the user's camera/mic
* 5. Updates parent component state with the new call and broadcast instances
* 6. Clears loading state
*
* Note: This combines both joining the call AND starting the broadcast
* in a single user interaction, providing a seamless UX.
*/
const handleJoinCall = async () => {
if (callId == null) {
throw new Error("Call ID is required to Join Call");
}
try {
// Step 1: Enable loading state to show user that connection is in progress
setConnecting(true);
// Step 2: Join the call with authentication and user info
const joinCallOptions = {
streamKey, // Unique identifier for the stream
user: { userId: "123", displayName: "Waiting Room Participant" }, // User identity
backendEndpoints: [backendEndpoint], // API endpoints
auth: authClient, // Authentication client
};
const newCall = await joinCall(callId, joinCallOptions);
// Step 3: Create a broadcast to start streaming this user's video/audio to other participants
// The "default" stream name distinguishes the main camera from other streams (like screenshare)
const broadcastOptions = {
streamName: "default",
};
const newBroadcast = await newCall.broadcast(mediaStreamController, broadcastOptions);
// Step 4: Notify parent component of the new call and broadcast instances
// This allows the parent to transition from WaitingRoom to CallRoom
setBroadcast(newBroadcast);
setCall(newCall);
// Step 5: Clear loading state now that we've successfully connected
setConnecting(false);
} catch {
// If any step fails, clear the loading state to allow the user to retry
setConnecting(false);
}
};
return (
<div className={classNames.wrapperClassName}>
<div ref={portalRef} />
{connecting && (
<div className={classNames.connectingClassName}>
<div className={classNames.connectingLoaderClassName} />
</div>
)}
<div>
{callId != null && (
<button
type="button"
onClick={handleJoinCall}
className={classNames.joinRoomButtonClassName}
>
Join Room
</button>
)}
</div>
</div>
);
}
export default memo(WaitingRoom);
Call Room Component
This component will be where a user interacts with a live conference call. In this view, the user will be able to:
- See the other participants in the call.
- See their own Preview Player video.
- Continue to be able to configure their device settings.
- Stop their broadcast and leave the call.
Imports
Similar to the Call Lobby component, since we will not be using the <CallControls/> component or useCallControls hook, we will have to directly interact with the Call and Broadcast Contexts.
For this we will need:
import React, { useState } from "react";
import { context } from "@video/video-client-react";
// Extract context hooks for accessing active call and broadcast instances
const { useBroadcastAPI, useCallAPI } = context;
Props
In order to reset the call and broadcast state in the parent component, you will need to pass in the following props:
setCall: A function to set the call state.setBroadcast: A function to set the broadcast state.setDisconnecting: A function to set the disconnecting state in the parent component.
/**
* Props interface for CallRoom component
*/
interface CallRoomProps {
/** Callback invoked when user leaves the room, used to reset parent state */
onLeaveRoom: () => void;
/** Callback to pass the portal container element to parent for rendering encoder */
setContainerElement: (containerElement: HTMLDivElement | null) => void;
}
Hooks
- Use the
useCallAPIanduseBroadcastAPIhooks to access the activecallandbroadcastinstances. These can be accessed from thehooksnamespace exported from@video/video-client-reactpackage. - Use the
usePortalhook to portal our Preview Player into this component.
/**
* Access the active call instance from context
* This is the CallAPI instance that was created in the WaitingRoom
*/
const call = useCallAPI();
/**
* Access the active broadcast instance from context
* This is the BroadcastAPI instance streaming the user's camera/mic
*/
const broadcast = useBroadcastAPI();
/**
* Create a portal target for rendering the encoder component
* The encoder (camera preview + controls) will be rendered here via React portal
*/
const portalRef = usePortal(setContainerElement);
Local state
We will use local state to managing "disconnecting" state and render a loader when the call is asynchronously joining.
/**
* Local state to track whether the user is in the process of leaving the call
* Used to display a loading spinner while the async disconnect operation completes
*/
const [disconnecting, setDisconnecting] = useState(false);
Click Handler
In this component we will also use a custom button to (1) end the broadcast and (2) leave the call. This is a custom
implementation that is not included in the @video/video-client-react package and gives you the ability to customize these API calls to your needs.
Our click handler will manage the following logic all in one user interaction:
- Set the disconnecting state to true, this will be used to display a loading state to the user
- Dispose the call and broadcast
- Set the broadcast and call state in the parent component to null
- Set the disconnecting state to false, this will be used to hide the loading state to the user
/**
* Handles the leave room flow when user clicks the "Leave Room" button
*
* This function performs cleanup operations in sequence:
* 1. Sets loading state to show spinner
* 2. Disposes the call instance (disconnects from other participants)
* 3. Disposes the broadcast instance (stops streaming camera/mic)
* 4. Notifies parent component to reset state (transition back to WaitingRoom)
* 5. Clears loading state
*
* Important: Proper disposal of call and broadcast instances is crucial
* to release WebRTC connections and free up system resources.
*/
const handleLeaveRoom = async () => {
try {
// Step 1: Enable loading state to show user that disconnection is in progress
setDisconnecting(true);
// Step 2: Dispose the call and broadcast instances
// This closes WebRTC connections and stops media streaming
call.dispose("Call disposed via onClick event of LeaveRoomButton");
broadcast.dispose("Broadcast disposed via onClick event of LeaveRoomButton");
// Step 3: Notify parent component to reset its state
// This typically transitions the UI back to the WaitingRoom view
onLeaveRoom();
// Step 4: Clear loading state now that we've successfully disconnected
setDisconnecting(false);
} catch {
throw new Error("Unable to dispose call or broadcast");
}
};
Full Component Code
// CallRoom.tsx
/**
* CallRoom Component
*
* The active call room interface where users interact during a live video call.
* This component displays the user's video encoder (camera preview) and provides
* controls for leaving the call.
*
* Key Features:
* - Shows user's own camera/video preview via portal rendering
* - Leave call button with loading state
* - Proper cleanup of call and broadcast resources
* - Integration with parent component for state management
*
* @example
* ```tsx
* <CallRoom
* onLeaveRoom={handleLeaveRoom}
* setContainerElement={setContainerElement}
* />
* ```
*/
import React, { useState } from "react";
import { context } from "@video/video-client-react";
// Extract context hooks for accessing active call and broadcast instances
const { useBroadcastAPI, useCallAPI } = context;
import { usePortal } from "./usePortal";
/**
* Props interface for CallRoom component
*/
interface CallRoomProps {
/** Callback invoked when user leaves the room, used to reset parent state */
onLeaveRoom: () => void;
/** Callback to pass the portal container element to parent for rendering encoder */
setContainerElement: (containerElement: HTMLDivElement | null) => void;
}
function CallRoom({
onLeaveRoom,
setContainerElement,
}: CallRoomProps): JSX.Element {
/**
* Local state to track whether the user is in the process of leaving the call
* Used to display a loading spinner while the async disconnect operation completes
*/
const [disconnecting, setDisconnecting] = useState(false);
/**
* Access the active call instance from context
* This is the CallAPI instance that was created in the WaitingRoom
*/
const call = useCallAPI();
/**
* Access the active broadcast instance from context
* This is the BroadcastAPI instance streaming the user's camera/mic
*/
const broadcast = useBroadcastAPI();
/**
* Create a portal target for rendering the encoder component
* The encoder (camera preview + controls) will be rendered here via React portal
*/
const portalRef = usePortal(setContainerElement);
/**
* Handles the leave room flow when user clicks the "Leave Room" button
*
* This function performs cleanup operations in sequence:
* 1. Sets loading state to show spinner
* 2. Disposes the call instance (disconnects from other participants)
* 3. Disposes the broadcast instance (stops streaming camera/mic)
* 4. Notifies parent component to reset state (transition back to WaitingRoom)
* 5. Clears loading state
*
* Important: Proper disposal of call and broadcast instances is crucial
* to release WebRTC connections and free up system resources.
*/
const handleLeaveRoom = async () => {
try {
// Step 1: Enable loading state to show user that disconnection is in progress
setDisconnecting(true);
// Step 2: Dispose the call and broadcast instances
// This closes WebRTC connections and stops media streaming
call.dispose("Call disposed via onClick event of LeaveRoomButton");
broadcast.dispose("Broadcast disposed via onClick event of LeaveRoomButton");
// Step 3: Notify parent component to reset its state
// This typically transitions the UI back to the WaitingRoom view
onLeaveRoom();
// Step 4: Clear loading state now that we've successfully disconnected
setDisconnecting(false);
} catch {
throw new Error("Unable to dispose call or broadcast");
}
};
return (
<div className="flex flex-col m-4">
<h1>Call Room</h1>
<div ref={portalRef} />
{disconnecting && (
<div className="flex justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-gray-900" />
</div>
)}
<div className="flex justify-center mt-10">
<button
type="button"
onClick={handleLeaveRoom}
style={{
color: "white",
background: "red",
padding: ".5rem",
borderRadius: "5px",
cursor: "pointer",
}}
>
Leave Room
</button>
</div>
</div>
);
}
export default CallRoom;
App Component
This is the top-level component that will be rendered in the index.html file. It will be responsible for:
- Creating an
AuthClientinstance. - Creating a
MediaStreamControllerinstance. - Creating a
PreviewPlayerinstance. - Rendering the
<CallLobby/>or<CallRoom/>component based on the state of the call and broadcast. - Rendering the
<PreviewPlayer/>portal.
Props
In this example, we will be passing in the required callId to the CallLobbyApp component. This will be used to join an existing call.
/**
* Props interface for App component
*/
type AppProps = {
/** The ID of the call to join */
callId: string;
/** Backend API endpoint URL */
backendEndpoint: string;
/** Authentication token for the user */
token: string;
/** Unique stream key for this broadcast */
streamKey: string;
};
Local state
We will be managing the following local state:
call: The CallAPI instance.broadcast: The BroadcastAPI instance.containerElement: The element to render the preview player into.
/**
* Global state for the active call instance
* Null when not in a call (WaitingRoom), populated when in a call (CallRoom)
*/
const [call, setCall] = useState<types.CallAPI | null>(null);
/**
* Global state for the active broadcast instance
* Null when not broadcasting, populated when streaming camera/mic to call
*/
const [broadcast, setBroadcast] = useState<types.BroadcastAPI | null>(null);
/**
* Portal target element for rendering the encoder component
* Child components (WaitingRoom/CallRoom) will set this via setContainerElement
*/
const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);
Click Handler
A custom click handler will handle when a user clicks a "Leave Room" button, and will cleanup both the call and the broadcast at the same time.
/**
* Handler for leaving the room
* Called by CallRoom component when user clicks "Leave Room"
* Disposes call and resets state to transition back to WaitingRoom
*/
const handleLeaveRoom = useCallback(() => {
if (call != null) {
call.dispose("Disposed by handleLeaveRoom");
setCall(null);
setBroadcast(null);
}
}, [call]);
Render UI
If a mediaStreamController, previewPlayer, and authClient are not available, we will return a loading state. Otherwise, we will render the <WaitingRoom/> or <CallRoom/> component based on the state of the call and broadcast.
We will also render the <PreviewPlayer/> component via React portal.
/**
* Return Condition 1: Loading State
* Wait for all required resources to be initialized before rendering
*/
if (!mediaStreamController || !authClient || !previewPlayer) {
return <div className="w-full h-full bg-gray-200" />;
}
/**
* Return Condition 2: Main Application UI
*
* The component tree provides context to child components:
* - AuthClientAPIProvider: Makes auth client available to children
* - MediaStreamControllerAPIProvider: Provides camera/mic control
* - PlayerAPIProvider: Provides video preview player
* - CallAPIProvider: Provides active call instance (only when in CallRoom)
* - BroadcastAPIProvider: Provides active broadcast instance (only when in CallRoom)
*
* Conditional Rendering:
* - If call/broadcast are null: Show WaitingRoom (pre-call setup)
* - If call/broadcast exist: Show CallRoom (active call)
*
* Portal Rendering:
* - The Encoder component is rendered via React portal into the containerElement
* - This allows WaitingRoom/CallRoom to control where the encoder appears
* - The encoder maintains access to PlayerAPIProvider context despite portal
*/
return (
<AuthClientAPIProvider authClient={authClient}>
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<div className="w-full h-full">
<div className="flex flex-row my-2 gap-2" />
{call == null || broadcast == null ? (
<WaitingRoom
setCall={setCall}
setBroadcast={setBroadcast}
callId={callId}
setContainerElement={setContainerElement}
backendEndpoint={backendEndpoint}
streamKey={streamKey}
/>
) : (
<CallAPIProvider callAPI={call}>
<BroadcastAPIProvider broadcastAPI={broadcast}>
<CallRoom
onLeaveRoom={handleLeaveRoom}
setContainerElement={setContainerElement}
/>
</BroadcastAPIProvider>
</CallAPIProvider>
)}
</div>
<PlayerAPIProvider playerAPI={previewPlayer}>
{/* Render encoder via portal into child-specified container */}
{containerElement != null && createPortal(<PreviewPlayer />, containerElement)}
</PlayerAPIProvider>
</MediaStreamControllerAPIProvider>
</AuthClientAPIProvider>
);
Full Component Code
// App.tsx
/**
* App Component - Waiting Room Implementation
*
* This is the root component for a call waiting room pattern, managing the full
* lifecycle of a video call from pre-call setup to active call participation.
*
* Component Architecture:
* - Manages global state for call, broadcast, and media resources
* - Provides context to child components via React Context providers
* - Handles transitions between WaitingRoom and CallRoom views
* - Uses React portals to render the encoder in flexible locations
*
* Flow:
* 1. Initialize media stream controller and preview player
* 2. Authenticate user with auth client
* 3. Render WaitingRoom for pre-call setup
* 4. User joins call → transition to CallRoom
* 5. User leaves call → transition back to WaitingRoom
*
* @example
* ```tsx
* <App
* callId="call-123"
* backendEndpoint="https://api.example.com"
* token="auth-token"
* streamKey="unique-stream-key"
* />
* ```
*/
import React, { useEffect, useState, useCallback } from "react";
import { context, types, hooks } from "@video/video-client-react";
import WaitingRoom from "./CallLobby";
import CallRoom from "./CallRoom";
import PreviewPlayer from "../../components/PreviewPlayer";
import { createPortal } from "react-dom";
// Extract context providers for managing different API instances
const { MediaStreamControllerAPIProvider, PlayerAPIProvider, CallAPIProvider, BroadcastAPIProvider, AuthClientAPIProvider } = context;
// Extract hooks for creating media and auth instances
const { usePreviewPlayer, useAuthClient } = hooks;
/**
* Props interface for App component
*/
type AppProps = {
/** The ID of the call to join */
callId: string;
/** Backend API endpoint URL */
backendEndpoint: string;
/** Authentication token for the user */
token: string;
/** Unique stream key for this broadcast */
streamKey: string;
};
function App({ callId, backendEndpoint, token, streamKey }: AppProps): JSX.Element {
/**
* Initialize the media stream controller and preview player
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});
/**
* Initialize authentication client
*/
const authClient = useAuthClient(token);
/**
* Global state for the active call instance
* Null when not in a call (WaitingRoom), populated when in a call (CallRoom)
*/
const [call, setCall] = useState<types.CallAPI | null>(null);
/**
* Global state for the active broadcast instance
* Null when not broadcasting, populated when streaming camera/mic to call
*/
const [broadcast, setBroadcast] = useState<types.BroadcastAPI | null>(null);
/**
* Portal target element for rendering the encoder component
* Child components (WaitingRoom/CallRoom) will set this via setContainerElement
*/
const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);
/**
* Cleanup effect for media stream controller
* Releases camera/microphone resources when component unmounts
*/
useEffect(() => {
return () => {
if (mediaStreamController != null) {
mediaStreamController.dispose("Disposed by useEffect 1 return - Broadcaster");
}
};
}, [mediaStreamController]);
/**
* Cleanup effect for preview player
* Stops video preview playback when component unmounts
*/
useEffect(() => {
return () => {
if (previewPlayer != null) {
previewPlayer.dispose("Disposed by useEffect 1 return - Broadcaster");
}
};
}, [previewPlayer]);
/**
* Cleanup effect for call and broadcast
* Disconnects from call and stops streaming when component unmounts
*/
useEffect(() => {
return () => {
if (call != null) {
call.dispose("Disposed by useEffect 1 return - Broadcaster");
setCall(null);
setBroadcast(null);
}
};
}, [call]);
/**
* Handler for leaving the room
* Called by CallRoom component when user clicks "Leave Room"
* Disposes call and resets state to transition back to WaitingRoom
*/
const handleLeaveRoom = useCallback(() => {
if (call != null) {
call.dispose("Disposed by handleLeaveRoom");
setCall(null);
setBroadcast(null);
}
}, [call]);
/**
* Return Condition 1: Loading State
* Wait for all required resources to be initialized before rendering
*/
if (!mediaStreamController || !authClient || !previewPlayer) {
return <div className="w-full h-full bg-gray-200" />;
}
/**
* Return Condition 2: Main Application UI
*
* The component tree provides context to child components:
* - AuthClientAPIProvider: Makes auth client available to children
* - MediaStreamControllerAPIProvider: Provides camera/mic control
* - PlayerAPIProvider: Provides video preview player
* - CallAPIProvider: Provides active call instance (only when in CallRoom)
* - BroadcastAPIProvider: Provides active broadcast instance (only when in CallRoom)
*
* Conditional Rendering:
* - If call/broadcast are null: Show WaitingRoom (pre-call setup)
* - If call/broadcast exist: Show CallRoom (active call)
*
* Portal Rendering:
* - The Encoder component is rendered via React portal into the containerElement
* - This allows WaitingRoom/CallRoom to control where the encoder appears
* - The encoder maintains access to PlayerAPIProvider context despite portal
*/
return (
<AuthClientAPIProvider authClient={authClient}>
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<div className="w-full h-full">
<div className="flex flex-row my-2 gap-2" />
{call == null || broadcast == null ? (
<WaitingRoom
setCall={setCall}
setBroadcast={setBroadcast}
callId={callId}
setContainerElement={setContainerElement}
backendEndpoint={backendEndpoint}
streamKey={streamKey}
/>
) : (
<CallAPIProvider callAPI={call}>
<BroadcastAPIProvider broadcastAPI={broadcast}>
<CallRoom
onLeaveRoom={handleLeaveRoom}
setContainerElement={setContainerElement}
/>
</BroadcastAPIProvider>
</CallAPIProvider>
)}
</div>
<PlayerAPIProvider playerAPI={previewPlayer}>
{/* Render encoder via portal into child-specified container */}
{containerElement != null && createPortal(<PreviewPlayer />, containerElement)}
</PlayerAPIProvider>
</MediaStreamControllerAPIProvider>
</AuthClientAPIProvider>
);
}
export default App;
Full Code
Group Call Components
// usePortal.tsx
/**
* Custom hook for creating a React portal target element
*
* This hook creates a ref to a div element and notifies a parent component
* when the ref is attached, allowing the parent to render content into this
* element using React's createPortal.
*
* Common Use Case: Rendering a video encoder component in a specific location
* while keeping its state managed at a higher level in the component tree.
*
* @param setContainerElement - Callback function to notify parent of the portal target element
* @returns A React ref to attach to the portal container div
*
* @example
* ```tsx
* // In child component:
* const portalRef = usePortal(setContainerElement);
* return <div ref={portalRef} />;
*
* // In parent component:
* const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);
* {containerElement && createPortal(<EncoderComponent />, containerElement)}
* ```
*/
import { useEffect, useRef } from "react";
export const usePortal = (
setContainerElement: (containerElement: HTMLDivElement | null) => void,
): React.RefObject<HTMLDivElement> => {
// Create a ref to the div element that will serve as the portal target
const portalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Once the ref is attached to a DOM element, notify the parent component
if (portalRef.current) {
setContainerElement(portalRef.current);
}
// Cleanup: notify parent that the portal target is being removed
return () => {
setContainerElement(null);
};
}, [setContainerElement]);
return portalRef;
};
// CallLobby.tsx
/**
* CallLobby Component
*
* A pre-call waiting room that allows users to configure their device settings
* (camera, microphone, etc.) and preview their video before joining a call.
*
* This component demonstrates a common video conferencing pattern where users
* can prepare their setup before being fully connected to other participants.
*
* Key Features:
* - Preview camera/microphone with device selection controls
* - Join call button with loading state
* - Portal-based rendering for flexible UI composition
* - Automatic broadcast creation upon joining
*
* @example
* ```tsx
* <WaitingRoom
* setCall={setCall}
* setBroadcast={setBroadcast}
* callId="call-123"
* setContainerElement={setContainerElement}
* backendEndpoint="https://api.example.com"
* streamKey="unique-stream-key"
* />
* ```
*/
import React, { memo, useState } from "react";
import { types, context, joinCall } from "@video/video-client-react";
// Extract context hooks for accessing video client API instances
const { useAuthClientAPI, useMediaStreamControllerAPI } = context;
import { usePortal } from "./usePortal"; // Custom hook for portal rendering
/**
* CSS class names for styling the waiting room UI
* Uses Tailwind CSS utility classes for responsive design
*/
const classNames = {
wrapperClassName: "flex flex-col m-4",
connectingClassName: "flex justify-center",
connectingLoaderClassName: "animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-gray-900",
joinRoomButtonClassName: "text-white bg-green-500 px-4 py-2 rounded-md cursor-pointer my-4",
};
/**
* Props interface for CallLobby component
*/
interface WaitingRoomProps {
/** Callback to set the call instance in parent component after joining */
setCall: (call: types.CallAPI) => void;
/** Callback to set the broadcast instance in parent component after starting broadcast */
setBroadcast: (broadcast: types.BroadcastAPI) => void;
/** The ID of the call to join */
callId: string | null;
/** Callback to pass the portal container element to parent for rendering encoder */
setContainerElement: (containerElement: HTMLDivElement | null) => void;
/** Backend API endpoint URL */
backendEndpoint: string;
/** Unique stream key for this broadcast */
streamKey: string;
}
function WaitingRoom({ setCall, setBroadcast, callId, setContainerElement, backendEndpoint, streamKey }: WaitingRoomProps): JSX.Element {
/**
* Local state to track whether the user is in the process of joining the call
* Used to display a loading spinner while the async join operation completes
*/
const [connecting, setConnecting] = useState(false);
/**
* Access the media stream controller from context
* This manages the user's camera and microphone input
*/
const mediaStreamController = useMediaStreamControllerAPI();
/**
* Access the authentication client from context
* This provides the auth credentials needed to join the call
*/
const authClient = useAuthClientAPI();
/**
* Create a portal target for rendering the encoder component
* This allows the encoder (camera preview + controls) to be rendered
* in this component's DOM while being managed by the parent
*/
const portalRef = usePortal(setContainerElement);
/**
* Handles the join call flow when user clicks the "Join Room" button
*
* This function performs several operations in sequence:
* 1. Validates that a call ID exists
* 2. Sets loading state to show spinner
* 3. Joins the call using the joinCall helper
* 4. Creates a broadcast to start streaming the user's camera/mic
* 5. Updates parent component state with the new call and broadcast instances
* 6. Clears loading state
*
* Note: This combines both joining the call AND starting the broadcast
* in a single user interaction, providing a seamless UX.
*/
const handleJoinCall = async () => {
if (callId == null) {
throw new Error("Call ID is required to Join Call");
}
try {
// Step 1: Enable loading state to show user that connection is in progress
setConnecting(true);
// Step 2: Join the call with authentication and user info
const joinCallOptions = {
streamKey, // Unique identifier for the stream
user: { userId: "123", displayName: "Waiting Room Participant" }, // User identity
backendEndpoints: [backendEndpoint], // API endpoints
auth: authClient, // Authentication client
};
const newCall = await joinCall(callId, joinCallOptions);
// Step 3: Create a broadcast to start streaming this user's video/audio to other participants
// The "default" stream name distinguishes the main camera from other streams (like screenshare)
const broadcastOptions = {
streamName: "default",
};
const newBroadcast = await newCall.broadcast(mediaStreamController, broadcastOptions);
// Step 4: Notify parent component of the new call and broadcast instances
// This allows the parent to transition from WaitingRoom to CallRoom
setBroadcast(newBroadcast);
setCall(newCall);
// Step 5: Clear loading state now that we've successfully connected
setConnecting(false);
} catch {
// If any step fails, clear the loading state to allow the user to retry
setConnecting(false);
}
};
return (
<div className={classNames.wrapperClassName}>
<div ref={portalRef} />
{connecting && (
<div className={classNames.connectingClassName}>
<div className={classNames.connectingLoaderClassName} />
</div>
)}
<div>
{callId != null && (
<button
type="button"
onClick={handleJoinCall}
className={classNames.joinRoomButtonClassName}
>
Join Room
</button>
)}
</div>
</div>
);
}
export default memo(WaitingRoom);
// CallRoom.tsx
/**
* CallRoom Component
*
* The active call room interface where users interact during a live video call.
* This component displays the user's video encoder (camera preview) and provides
* controls for leaving the call.
*
* Key Features:
* - Shows user's own camera/video preview via portal rendering
* - Leave call button with loading state
* - Proper cleanup of call and broadcast resources
* - Integration with parent component for state management
*
* @example
* ```tsx
* <CallRoom
* onLeaveRoom={handleLeaveRoom}
* setContainerElement={setContainerElement}
* />
* ```
*/
import React, { useState } from "react";
import { context } from "@video/video-client-react";
// Extract context hooks for accessing active call and broadcast instances
const { useBroadcastAPI, useCallAPI } = context;
import { usePortal } from "./usePortal";
/**
* Props interface for CallRoom component
*/
interface CallRoomProps {
/** Callback invoked when user leaves the room, used to reset parent state */
onLeaveRoom: () => void;
/** Callback to pass the portal container element to parent for rendering encoder */
setContainerElement: (containerElement: HTMLDivElement | null) => void;
}
function CallRoom({
onLeaveRoom,
setContainerElement,
}: CallRoomProps): JSX.Element {
/**
* Local state to track whether the user is in the process of leaving the call
* Used to display a loading spinner while the async disconnect operation completes
*/
const [disconnecting, setDisconnecting] = useState(false);
/**
* Access the active call instance from context
* This is the CallAPI instance that was created in the WaitingRoom
*/
const call = useCallAPI();
/**
* Access the active broadcast instance from context
* This is the BroadcastAPI instance streaming the user's camera/mic
*/
const broadcast = useBroadcastAPI();
/**
* Create a portal target for rendering the encoder component
* The encoder (camera preview + controls) will be rendered here via React portal
*/
const portalRef = usePortal(setContainerElement);
/**
* Handles the leave room flow when user clicks the "Leave Room" button
*
* This function performs cleanup operations in sequence:
* 1. Sets loading state to show spinner
* 2. Disposes the call instance (disconnects from other participants)
* 3. Disposes the broadcast instance (stops streaming camera/mic)
* 4. Notifies parent component to reset state (transition back to WaitingRoom)
* 5. Clears loading state
*
* Important: Proper disposal of call and broadcast instances is crucial
* to release WebRTC connections and free up system resources.
*/
const handleLeaveRoom = async () => {
try {
// Step 1: Enable loading state to show user that disconnection is in progress
setDisconnecting(true);
// Step 2: Dispose the call and broadcast instances
// This closes WebRTC connections and stops media streaming
call.dispose("Call disposed via onClick event of LeaveRoomButton");
broadcast.dispose("Broadcast disposed via onClick event of LeaveRoomButton");
// Step 3: Notify parent component to reset its state
// This typically transitions the UI back to the WaitingRoom view
onLeaveRoom();
// Step 4: Clear loading state now that we've successfully disconnected
setDisconnecting(false);
} catch {
throw new Error("Unable to dispose call or broadcast");
}
};
return (
<div className="flex flex-col m-4">
<h1>Call Room</h1>
<div ref={portalRef} />
{disconnecting && (
<div className="flex justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-gray-900" />
</div>
)}
<div className="flex justify-center mt-10">
<button
type="button"
onClick={handleLeaveRoom}
style={{
color: "white",
background: "red",
padding: ".5rem",
borderRadius: "5px",
cursor: "pointer",
}}
>
Leave Room
</button>
</div>
</div>
);
}
export default CallRoom;
// App.tsx
/**
* App Component - Waiting Room Implementation
*
* This is the root component for a call waiting room pattern, managing the full
* lifecycle of a video call from pre-call setup to active call participation.
*
* Component Architecture:
* - Manages global state for call, broadcast, and media resources
* - Provides context to child components via React Context providers
* - Handles transitions between WaitingRoom and CallRoom views
* - Uses React portals to render the encoder in flexible locations
*
* Flow:
* 1. Initialize media stream controller and preview player
* 2. Authenticate user with auth client
* 3. Render WaitingRoom for pre-call setup
* 4. User joins call → transition to CallRoom
* 5. User leaves call → transition back to WaitingRoom
*
* @example
* ```tsx
* <App
* callId="call-123"
* backendEndpoint="https://api.example.com"
* token="auth-token"
* streamKey="unique-stream-key"
* />
* ```
*/
import React, { useEffect, useState, useCallback } from "react";
import { context, types, hooks } from "@video/video-client-react";
import WaitingRoom from "./CallLobby";
import CallRoom from "./CallRoom";
import PreviewPlayer from "../../components/PreviewPlayer";
import { createPortal } from "react-dom";
// Extract context providers for managing different API instances
const { MediaStreamControllerAPIProvider, PlayerAPIProvider, CallAPIProvider, BroadcastAPIProvider, AuthClientAPIProvider } = context;
// Extract hooks for creating media and auth instances
const { usePreviewPlayer, useAuthClient } = hooks;
/**
* Props interface for App component
*/
type AppProps = {
/** The ID of the call to join */
callId: string;
/** Backend API endpoint URL */
backendEndpoint: string;
/** Authentication token for the user */
token: string;
/** Unique stream key for this broadcast */
streamKey: string;
};
function App({ callId, backendEndpoint, token, streamKey }: AppProps): JSX.Element {
/**
* Initialize the media stream controller and preview player
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});
/**
* Initialize authentication client
*/
const authClient = useAuthClient(token);
/**
* Global state for the active call instance
* Null when not in a call (WaitingRoom), populated when in a call (CallRoom)
*/
const [call, setCall] = useState<types.CallAPI | null>(null);
/**
* Global state for the active broadcast instance
* Null when not broadcasting, populated when streaming camera/mic to call
*/
const [broadcast, setBroadcast] = useState<types.BroadcastAPI | null>(null);
/**
* Portal target element for rendering the encoder component
* Child components (WaitingRoom/CallRoom) will set this via setContainerElement
*/
const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);
/**
* Cleanup effect for media stream controller
* Releases camera/microphone resources when component unmounts
*/
useEffect(() => {
return () => {
if (mediaStreamController != null) {
mediaStreamController.dispose("Disposed by useEffect 1 return - Broadcaster");
}
};
}, [mediaStreamController]);
/**
* Cleanup effect for preview player
* Stops video preview playback when component unmounts
*/
useEffect(() => {
return () => {
if (previewPlayer != null) {
previewPlayer.dispose("Disposed by useEffect 1 return - Broadcaster");
}
};
}, [previewPlayer]);
/**
* Cleanup effect for call and broadcast
* Disconnects from call and stops streaming when component unmounts
*/
useEffect(() => {
return () => {
if (call != null) {
call.dispose("Disposed by useEffect 1 return - Broadcaster");
setCall(null);
setBroadcast(null);
}
};
}, [call]);
/**
* Handler for leaving the room
* Called by CallRoom component when user clicks "Leave Room"
* Disposes call and resets state to transition back to WaitingRoom
*/
const handleLeaveRoom = useCallback(() => {
if (call != null) {
call.dispose("Disposed by handleLeaveRoom");
setCall(null);
setBroadcast(null);
}
}, [call]);
/**
* Return Condition 1: Loading State
* Wait for all required resources to be initialized before rendering
*/
if (!mediaStreamController || !authClient || !previewPlayer) {
return <div className="w-full h-full bg-gray-200" />;
}
/**
* Return Condition 2: Main Application UI
*
* The component tree provides context to child components:
* - AuthClientAPIProvider: Makes auth client available to children
* - MediaStreamControllerAPIProvider: Provides camera/mic control
* - PlayerAPIProvider: Provides video preview player
* - CallAPIProvider: Provides active call instance (only when in CallRoom)
* - BroadcastAPIProvider: Provides active broadcast instance (only when in CallRoom)
*
* Conditional Rendering:
* - If call/broadcast are null: Show WaitingRoom (pre-call setup)
* - If call/broadcast exist: Show CallRoom (active call)
*
* Portal Rendering:
* - The Encoder component is rendered via React portal into the containerElement
* - This allows WaitingRoom/CallRoom to control where the encoder appears
* - The encoder maintains access to PlayerAPIProvider context despite portal
*/
return (
<AuthClientAPIProvider authClient={authClient}>
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<div className="w-full h-full">
<div className="flex flex-row my-2 gap-2" />
{call == null || broadcast == null ? (
<WaitingRoom
setCall={setCall}
setBroadcast={setBroadcast}
callId={callId}
setContainerElement={setContainerElement}
backendEndpoint={backendEndpoint}
streamKey={streamKey}
/>
) : (
<CallAPIProvider callAPI={call}>
<BroadcastAPIProvider broadcastAPI={broadcast}>
<CallRoom
onLeaveRoom={handleLeaveRoom}
setContainerElement={setContainerElement}
/>
</BroadcastAPIProvider>
</CallAPIProvider>
)}
</div>
<PlayerAPIProvider playerAPI={previewPlayer}>
{/* Render encoder via portal into child-specified container */}
{containerElement != null && createPortal(<PreviewPlayer />, containerElement)}
</PlayerAPIProvider>
</MediaStreamControllerAPIProvider>
</AuthClientAPIProvider>
);
}
export default App;
Supporting Components
The following components are used by the Call Lobby component and are documented in View a Call Stream and Set Up A Livestream Video:
// PreviewPlayer.tsx
/**
* Preview Player Component
*
* OVERVIEW:
* A reusable preview player component that displays a video preview and
* provides standard device controls. This component is designed to be
* dropped into any video broadcasting application with minimal configuration.
*
* WHAT IT DOES:
* - Displays video preview (what your camera sees)
* - Provides toggle buttons for camera and microphone
* - Offers device selection dropdowns (which camera, which mic)
* - Allows resolution/quality selection
* - Automatically connects to video client context
*
* KEY FEATURES:
* - Fully self-contained: No state management needed in parent
* - Customizable styling via className props
* - Responsive layout (adapts to screen size)
* - Accessibility-friendly controls
* - Built-in error handling
*
* REQUIREMENTS:
* Must be used inside:
* - MediaStreamControllerAPIProvider (for device control)
* - PlayerAPIProvider (for video display)
*
* @example
* ```tsx
* <MediaStreamControllerAPIProvider mediaStreamControllerAPI={msc}>
* <PlayerAPIProvider playerAPI={player}>
* <Encoder />
* </PlayerAPIProvider>
* </MediaStreamControllerAPIProvider>
* ```
*
* @example With custom styling:
* ```tsx
* <Encoder
* classNames={{
* wrapperClassName: "my-custom-layout",
* videoClassName: "rounded-lg shadow-lg"
* }}
* />
* ```
*/
import React, { memo, useRef } from "react";
import {components, } from "@video/video-client-react";
/**
* EXTRACT UI COMPONENTS
*
* These are pre-built components from @video/video-client-react:
*
* - Video: Displays the video stream
* - AudioSourceSelect: Dropdown to select microphone
* - VideoSourceSelect: Dropdown to select camera
* - ResolutionSelect: Dropdown to select video quality
* - ToggleMicButton: Button to mute/unmute microphone
* - ToggleCameraButton: Button to enable/disable camera
*
* All these components automatically connect to the video client
* context, so they "just work" without prop drilling.
*/
const { Video, AudioSourceSelect, VideoSourceSelect, ResolutionSelect, ToggleMicButton, ToggleCameraButton } = components;
/**
* STYLING INTERFACE
*
* */
interface PreviewPlayerClassNames {
wrapperClassName: string;
videoContainerClassName: string;
videoClassName: string;
controlBarClassName: string;
cntrolBarItemClassName: string;
}
/**
* DEFAULT STYLING
*
* Default styling using Tailwind CSS classes.
*/
const defaultClassNames = {
wrapperClassName: "gap-4 flex flex-row",
videoContainerClassName: "md:w-1/2",
videoClassName: "w-full h-auto",
controlBarClassName: "flex flex-col",
controlBarItemClassName: "flex flex-col my-2 gap-2",
};
/**
* PROPS INTERFACE
*
*/
interface PreviewPlayerProps {
/** Optional custom class names (partial override of defaults) */
classNames?: Partial<PreviewPlayerClassNames>;
}
function PreviewPlayer({classNames}: PreviewPlayerProps): JSX.Element {
const mergedClassNames = { ...defaultClassNames,...classNames };
/**
* VIDEO ELEMENT REF
*
* This is a required prop for the Video component.
*/
const videoElement = useRef<HTMLVideoElement>(null);
/**
* RENDER THE PREVIEW PLAYER UI
*
*/
return (
<div className={mergedClassNames.wrapperClassName}>
{/* VIDEO PREVIEW SECTION */}
<div className={mergedClassNames.videoContainerClassName}>
{/*
The Video component displays what your camera sees.
It automatically connects to the PlayerAPI from context.
*/}
<Video ref={videoElement} className={mergedClassNames.videoClassName} />
</div>
{/* CONTROLS SECTION */}
<div className={mergedClassNames.controlBarClassName}>
{/*
Toggle Controls Group
Quick on/off buttons for camera and microphone
*/}
<div className={mergedClassNames.controlBarItemClassName}>
<ToggleMicButton />
<ToggleCameraButton />
</div>
{/*
Device Selection Group
Dropdowns for choosing which camera/mic and quality
*/}
<div className={mergedClassNames.controlBarItemClassName}>
<AudioSourceSelect />
<VideoSourceSelect />
<ResolutionSelect />
</div>
</div>
</div>
);
}
/**
* EXPORT WITH MEMOIZATION
*
*/
export default memo(PreviewPlayer);
// CallControls.tsx
/**
* CallControls Component & Hook
*
* OVERVIEW:
* This file provides both a component and a custom hook for managing
* call and broadcast state with automatic UI controls. It's a powerful
* abstraction that handles all the complexity of call/broadcast lifecycle.
*
* WHAT IT DOES FOR YOU:
* - Manages call and broadcast state automatically
* - Renders the right buttons based on current state
* - Handles state transitions (no call → call → broadcasting)
* - Provides callbacks for cleanup and notifications
* - Supports multiple usage patterns (owner, participant, broadcast-only)
*
* KEY BENEFITS:
* - No need to manually track call/broadcast state
* - No need to conditionally render buttons yourself
* - No need to write call creation/disposal logic
* - Consistent UX across your application
* - Easy to extend and customize
*
* USAGE PATTERNS:
* 1. "owner": Creates and hosts calls (shows CreateCallButton)
* 2. "participant": Joins existing calls (shows JoinCallButton)
* 3. "broadcast-controls-only": Only manages broadcast (requires existing call)
*
* @example As a hook:
* ```tsx
* const { renderControls, call, broadcast } = useCallControls({
* callOptions: { streamKey, auth, ... },
* broadcastOptions: { streamName: 'default' },
* type: 'owner'
* });
*
* return <div>{renderControls()}</div>;
* ```
*
* @example As a component:
* ```tsx
* <CallControls
* callOptions={{ streamKey, auth, ... }}
* broadcastOptions={{ streamName: 'default' }}
* type="owner"
* />
* ```
*/
import React, { useState } from "react";
import { components, types, context } from "@video/video-client-react";
/**
* EXTRACT PRE-BUILT BUTTON COMPONENTS
*
* These components handle button UI and click logic:
* - CreateCallButton: Creates a new call
* - JoinCallButton: Joins an existing call by ID
* - EndCallButton: Ends the current call
* - StartBroadcastButton: Starts broadcasting to a call
* - EndBroadcastButton: Stops broadcasting
*
* All buttons automatically handle loading states, errors, and cleanup.
*/
const { CreateCallButton, EndCallButton, StartBroadcastButton, EndBroadcastButton, JoinCallButton } = components;
/**
* EXTRACT CONTEXT PROVIDERS
*
* These make call and broadcast instances available to button components:
* - CallAPIProvider: Shares call instance with children
* - BroadcastAPIProvider: Shares broadcast instance with children
*/
const { CallAPIProvider, BroadcastAPIProvider } = context;
interface CallControlsOptions {
callOptions?: types.CallOptions;
broadcastOptions: types.BroadcastOptions;
type: 'owner' | 'participant' | 'broadcast-controls-only';
call?: types.CallAPI | null;
}
type CallState = 'no-call-owner' | 'no-call-participant' | 'call-no-broadcast' | 'call-with-broadcast' | 'start-broadcast-only' | 'end-broadcast-only';
interface CallControlsReturn {
call: types.CallAPI | null;
broadcast: types.BroadcastAPI | null;
setCall: (call: types.CallAPI | null) => void;
setBroadcast: (broadcast: types.BroadcastAPI | null) => void;
state: CallState;
renderControls: () => React.ReactElement;
}
/**
* Utility function to determine the current call/broadcast state
*/
function getCallState(call: types.CallAPI | null, broadcast: types.BroadcastAPI | null, type: 'owner' | 'participant' | 'broadcast-controls-only'): CallState {
switch (type) {
case 'owner':
if (call == null) {
return 'no-call-owner';
}
if (broadcast == null) {
return 'call-no-broadcast';
}
return 'call-with-broadcast';
case 'participant':
if (call == null) {
return 'no-call-participant';
}
if (broadcast == null) {
return 'call-no-broadcast';
}
return 'call-with-broadcast';
case 'broadcast-controls-only':
if (call == null) {
throw new Error('Call is required when type is broadcast-controls-only');
}
if (broadcast == null) {
return 'start-broadcast-only';
}
return 'end-broadcast-only';
default:
// This should never happen if TypeScript types are correct
const _exhaustive: never = type;
throw new Error(`Unknown type: ${_exhaustive}`);
}
}
/**
* Custom hook for managing broadcaster call and broadcast state with controls
*
* @param options - Call and broadcast configuration options
* @returns Object containing state, setters, and a render function for controls
*
* @example
* ```tsx
* function MyComponent() {
* const { renderControls } = useBroadcasterCallControls({
* callOptions: { streamKey: '...', auth: authClient, ... },
* broadcastOptions: { streamName: 'default' }
* });
*
* return <div>{renderControls()}</div>;
* }
* ```
*/
function useCallControls(options: CallControlsOptions): CallControlsReturn {
const { callOptions, broadcastOptions } = options;
const [call, setCall] = useState<types.CallAPI | null>(options.call ?? null);
const [broadcast, setBroadcast] = useState<types.BroadcastAPI | null>(null);
const state = getCallState(call, broadcast, options.type);
/**
* Renders the appropriate controls based on current call/broadcast state
*/
const renderControls = (): React.ReactElement => {
switch (state) {
case 'no-call-owner':
if (callOptions == null) {
throw new Error('Call options are required');
}
// No active call - show CreateCallButton
return (
<CreateCallButton
callOptions={callOptions}
setCall={setCall}
/>
);
case 'no-call-participant':
if (callOptions == null || callOptions.callId == null) {
throw new Error('Call options and call ID are required');
}
// No active call - show JoinCallButton
return <JoinCallButton callId={callOptions.callId} joinCallOptions={callOptions} setCall={setCall} />;
case 'call-no-broadcast':
// Call active, no broadcast - show EndCallButton and StartBroadcastButton
return (
<CallAPIProvider callAPI={call!}>
<EndCallButton onDisposed={() => setCall(null)} />
<StartBroadcastButton
broadcastOptions={broadcastOptions}
setBroadcast={setBroadcast}
/>
</CallAPIProvider>
);
case 'call-with-broadcast':
// Call and broadcast active - show EndCallButton and EndBroadcastButton
return (
<CallAPIProvider callAPI={call!}>
<EndCallButton onDisposed={() => setCall(null)} />
<BroadcastAPIProvider broadcastAPI={broadcast!}>
<EndBroadcastButton onDisposed={() => setBroadcast(null)} />
</BroadcastAPIProvider>
</CallAPIProvider>
);
case 'start-broadcast-only':
return (
<CallAPIProvider callAPI={call!}>
<StartBroadcastButton broadcastOptions={broadcastOptions} setBroadcast={setBroadcast} />
</CallAPIProvider>
);
case 'end-broadcast-only':
return (
<CallAPIProvider callAPI={call!}>
<BroadcastAPIProvider broadcastAPI={broadcast!}>
<EndBroadcastButton onDisposed={() => setBroadcast(null)} />
</BroadcastAPIProvider>
</CallAPIProvider>
);
default:
// Exhaustive check - TypeScript will error if we miss a case
const _exhaustive: CallState = state;
return _exhaustive;
}
};
return {
call,
broadcast,
setCall,
setBroadcast,
state,
renderControls,
};
}
/**
* Component wrapper for the broadcaster call controls hook
* Manages call and broadcast state internally
*/
function CallControls(options: CallControlsOptions): React.ReactElement {
const { renderControls } = useCallControls(options);
return renderControls();
}
export default CallControls;
export { useCallControls, getCallState };
export type { CallControlsOptions, CallControlsReturn, CallState };
// Player.tsx
/**
* Player Component
*
* OVERVIEW:
* A reusable video player component that handles manifest loading, player
* initialization, and provides context for control components. This component
* abstracts away the complexity of requesting a player, managing its lifecycle,
* and providing it to child components.
*
* WHAT IT DOES:
* - Loads and plays video streams from manifest URLs (HLS, FLV, DASH)
* - Initializes the player using the useRequestPlayer hook
* - Provides PlayerAPIProvider context to all children
* - Renders the video element with customizable styling
* - Manages player lifecycle (creation and disposal)
*
* KEY FEATURES:
* - Fully self-contained: Handles all player setup automatically
* - Customizable styling via classNames prop
* - Supports children for adding custom controls
* - Works with any manifest format supported by the video client
* - Automatic error handling and loading states
*
* REQUIREMENTS:
* - A valid manifest URL (HLS .m3u8, FLV .flv, or DASH .mpd)
* - No additional context providers needed (self-contained)
*
* @example Basic usage:
* ```tsx
* <Player source="https://example.com/stream.m3u8">
* <TogglePlayButton />
* <ToggleMuteButton />
* </Player>
* ```
*
* @example With custom styling:
* ```tsx
* <Player
* source={manifestUrl}
* classNames={{
* videoClassName: "w-full h-auto",
* playerContainerClassName: "relative"
* }}
* >
* <YourCustomControls />
* </Player>
* ```
*/
import React, {useRef} from "react";
/**
* IMPORT VIDEO CLIENT COMPONENTS AND HOOKS
*
* - components: Pre-built UI components (Video element wrapper)
* - context: React Context providers and hooks (PlayerAPIProvider)
* - hooks: Custom hooks for video functionality (useRequestPlayer)
*/
import { components, context, hooks } from "@video/video-client-react";
/**
* EXTRACT REQUIRED EXPORTS
*
* - PlayerAPIProvider: Context provider that makes player instance available to children
* - Video: Video element wrapper with built-in player integration
* - useRequestPlayer: Hook that requests and initializes a player from a manifest URL
*/
const { PlayerAPIProvider, usePeerAPI } = context;
const { Video } = components;
const { useRequestPlayer } = hooks;
/**
* STYLING INTERFACE
*
* Defines the CSS classes that can be customized for different parts
* of the player component. All fields are optional.
*/
export interface PlayerClassNames {
playerClassName: string;
videoClassName: string;
displayNameClassName: string;
playerContainerClassName: string;
}
/**
* DEFAULT STYLING
*
* Default styling using Tailwind CSS classes.
*/
const defaultClassNames: PlayerClassNames = {
playerClassName: "bg-black-200",
videoClassName: "h-56 w-56 object-cover overflow-hidden rounded-xl",
displayNameClassName: "text-sm text-white absolute bottom-1 left-1 z-[200] bg-black/50 px-2 py-1 rounded-md",
playerContainerClassName: "relative h-56 w-56",
};
/**
* PROPS INTERFACE
*
* Extends UseRequestPlayerOptions to inherit all player configuration options
* like source, requestPlayerOptions, and eventsMap.
*/
interface PlayerProps extends hooks.UseRequestPlayerOptions {
/** Child components (typically player controls) */
children?: React.ReactNode;
/** Optional custom class names (partial override of defaults) */
classNames?: Partial<PlayerClassNames>;
}
function Player({source, children, classNames, requestPlayerOptions, eventsMap}: PlayerProps): JSX.Element | null {
/**
* MERGE CUSTOM AND DEFAULT CLASS NAMES
*
* Combines default styling with any custom overrides provided via props.
*/
const mergedClassNames = { ...defaultClassNames, ...classNames };
/**
* VIDEO ELEMENT REF
*
* Creates a reference to the HTML video element.
* While not strictly required for basic playback (the Video component handles
* this internally), having a ref available can be useful for:
* - Direct DOM manipulation if needed
* - Integration with third-party libraries
* - Advanced video element access
*/
const videoElement = useRef<HTMLVideoElement>(null);
/**
* REQUEST AND INITIALIZE PLAYER
*
* The useRequestPlayer hook is the core of this component. It:
* 1. Takes the manifest URL (source) and loads it
* 2. Determines the appropriate player technology (HLS.js, FLV.js, native)
* 3. Creates a player instance configured for that technology
* 4. Handles errors and loading states automatically
* 5. Returns the player instance (or null if still loading/failed)
*
* The hook also handles cleanup when the component unmounts, ensuring
* the player is properly disposed and resources are freed.
*
* PARAMETERS:
* - source: The manifest URL to load (required)
* - requestPlayerOptions: Optional configuration for the player
* - eventsMap: Optional event listeners to attach to the player
*/
const player = useRequestPlayer({ source, requestPlayerOptions, eventsMap });
/**
* LOADING STATE CHECK
*
* If the player hasn't been created yet (still loading or failed),
* return null to render nothing.
*
* In a production app, you might want to show:
* - A loading spinner while player is initializing
* - An error message if loading failed
* - A placeholder image or poster frame
*/
if (player == null) return null;
/**
* RENDER THE PLAYER UI
*
* STRUCTURE:
* PlayerAPIProvider (makes player available to children)
* └── Outer container (playerClassName)
* └── Video container (playerContainerClassName)
* ├── Video element (videoClassName) - displays the stream
* └── Children - typically control buttons and UI elements
*
* HOW IT WORKS:
* 1. PlayerAPIProvider shares the player instance via React Context
* 2. All child components can access the player using usePlayerAPI()
* 3. The Video component connects to the player and displays the video
* 4. Children (controls) automatically sync with player state
*
* IMPORTANT: The PlayerAPIProvider is crucial - without it, child
* components wouldn't be able to access the player instance.
*/
return (
<PlayerAPIProvider playerAPI={player}>
{/* Outer wrapper for styling */}
<div className={mergedClassNames.playerClassName ?? ""}>
{/* Container for video and overlays */}
<div className={mergedClassNames.playerContainerClassName ?? ""}>
{/* Video element - displays the actual stream */}
<Video id="player-video" ref={videoElement} className={mergedClassNames.videoClassName ?? ""}/>
{/* Child components - typically controls */}
{children}
</div>
</div>
</PlayerAPIProvider>
);
}
/**
* EXPORT THE PLAYER COMPONENT
*
* This component is designed to be highly reusable. It can be used:
* - As-is for basic manifest playback
* - As a wrapper for custom player UIs
* - In galleries showing multiple streams
* - For VOD, live streams, or transcoded content
*/
export default Player;
// Peers.tsx
/**
* Peers Component
*
* OVERVIEW:
* A reusable component that automatically discovers and displays all
* broadcasting peers in a WebRTC call. This component abstracts away
* the complexity of peer management, player creation, and stream handling.
*
* WHAT IT DOES:
* - Discovers all peers (broadcasters) in the current call
* - Creates a player for each broadcasting peer
* - Displays peer information (display name, muted status)
* - Automatically updates when peers join or leave
* - Handles multiple simultaneous broadcasters
*
* KEY FEATURES:
* - Fully automatic peer discovery and management
* - Responsive to peer join/leave events
* - Customizable styling via classNames prop
* - Supports multiple broadcasters simultaneously
* - Shows muted badge for each peer
* - Displays peer display names
*
* REQUIREMENTS:
* Must be used inside CallAPIProvider to access the call instance.
*
* @example Basic usage:
* ```tsx
* <CallAPIProvider callAPI={call}>
* <Peers />
* </CallAPIProvider>
* ```
*
* @example With custom styling:
* ```tsx
* <Peers
* classNames={{
* playerClassNames: {
* videoClassName: "w-full h-auto"
* },
* displayNameClassName: "text-lg font-bold"
* }}
* />
* ```
*/
import React, { memo } from "react";
/**
* IMPORT VIDEO CLIENT LIBRARIES
*
* - hooks: Custom hooks for video functionality (useCallPeers, useCallAPI)
* - context: React Context providers and hooks (PeerAPIProvider, useCallAPI)
* - components: Pre-built UI components (PeerMutedBadge)
*/
import { hooks, context, components } from "@video/video-client-react";
/**
* EXTRACT REQUIRED EXPORTS
*
* - useCallPeers: Hook that returns all peers in the call
* - PeerAPIProvider: Context provider that makes peer instance available
* - useCallAPI: Hook to access the call instance from context
* - PeerMutedBadge: Component that shows muted/unmuted status
*/
const { useCallPeers } = hooks;
const { PeerAPIProvider, useCallAPI } = context;
const { PeerMutedBadge } = components;
/**
* IMPORT PLAYER COMPONENT
*
* The Player component we created earlier. Peers uses it to display
* each broadcaster's stream.
*/
import Player from "./Player";
import type { PlayerClassNames } from "./Player";
/**
* DEFAULT STYLING
*
* Provides sensible defaults for peer display:
* - Muted badges: Show audio status (muted in red, unmuted in white)
* - Display name: Peer's name shown at bottom-left of video
*
* All styles use absolute positioning to overlay on the video.
*/
const defaultClassNames = {
mutedBadgeClassNames: {
PeerMutedBadgeHasAudio:
"absolute z-[200] top-1 right-1 opacity-70 bg-black font-bold text-sm text-white px-2 py-1 rounded-md",
PeerMutedBadgeNoAudio:
"absolute z-[200] top-1 right-1 opacity-70 bg-black font-bold text-sm text-red-500 px-2 py-1 rounded-md",
},
displayNameClassName: "text-sm text-white absolute bottom-1 left-1 z-[200] bg-black/50 px-2 py-1 rounded-md",
};
/**
* STYLING INTERFACES
*
* Define the structure for customizing peer display appearance.
*/
interface PeersClassNames {
/** Styling for muted/unmuted badge */
mutedBadgeClassNames: {
PeerMutedBadgeHasAudio: string;
PeerMutedBadgeNoAudio: string;
};
/** Styling for peer display name overlay */
displayNameClassName: string;
/** Styling passed to Player component for each peer */
playerClassNames: Partial<PlayerClassNames>;
}
/**
* PROPS INTERFACE
*
* Configuration options for the Peers component.
*/
interface PeersProps {
/** Optional children rendered for each peer (in addition to default overlays). Allows customization in different areas of your application. */
children?: React.ReactNode;
/** Optional custom class names (partial override of defaults) */
classNames?: Partial<PeersClassNames>;
}
function Peers({ children, classNames }: PeersProps): React.ReactElement | null {
/**
* MERGE CUSTOM AND DEFAULT CLASS NAMES
*
* Combines default styling with any custom overrides provided via props.
*/
const mergedClassNames = { ...defaultClassNames, ...classNames };
/**
* STEP 1: Access the Call Instance
*
* The useCallAPI hook retrieves the call instance from React Context.
* This is provided by the CallAPIProvider that wraps this component.
*
* IMPORTANT: This component must be used inside CallAPIProvider,
* otherwise this hook will throw an error.
*/
const call = useCallAPI();
/**
* STEP 2: Discover All Peers
*
* The useCallPeers hook is the magic that makes this component work.
* It automatically:
* 1. Discovers all peers currently in the call
* 2. Returns an array of peer objects with their streams
* 3. Updates automatically when peers join or leave
* 4. Filters to only show peers that are broadcasting
*
* Each peer object contains:
* - peer: The peer instance (with methods and properties)
* - stream: The media stream being broadcast by this peer
* - peerParams: Additional peer information (displayName, etc.)
*
* This hook handles all the complexity of:
* - Listening for peer join/leave events
* - Managing peer lifecycle
* - Tracking which peers are broadcasting
* - Cleaning up when peers disconnect
*/
const callPeers = useCallPeers(call);
/**
* RETURN CONDITION 1: No Broadcasting Peers
*
* If there are no peers broadcasting (callPeers is empty), return null
* to render nothing.
*
* This happens when:
* - No broadcasters have joined the call yet
* - All broadcasters have stopped broadcasting
* - All broadcasters have left the call
*
* The component will automatically show peers once someone starts broadcasting.
*/
if (callPeers.length === 0) return null;
/**
* RETURN CONDITION 2: Display All Broadcasting Peers
*
* For each peer, render a complete player with overlays.
*
* STRUCTURE FOR EACH PEER:
* PeerAPIProvider (makes peer instance available to children)
* └── Player (displays the peer's video stream)
* ├── PeerMutedBadge (shows muted/unmuted status)
* ├── Display Name (shows peer's name)
* └── Children (any custom overlays passed as props)
*
* HOW IT WORKS:
* 1. Map over the callPeers array
* 2. For each peer, create a PeerAPIProvider with their peer instance
* 3. Inside, render a Player component with their stream
* 4. Add overlays: muted badge and display name
* 5. Include any custom children passed to Peers
*
* KEY DETAILS:
* - key={peer.peer.userId}: Ensures React can track each peer uniquely
* - source={peer.stream}: The Player uses the peer's media stream
* - PeerAPIProvider: Makes peer methods available to badge components
* - PeerMutedBadge: Automatically shows correct badge based on peer muted state
*
* AUTOMATIC UPDATES:
* When peers join/leave, useCallPeers triggers a re-render, and this
* map will automatically add/remove players as needed. No manual management required!
*/
return (
<>
{callPeers.map((peer) => (
<PeerAPIProvider peerAPI={peer.peer} key={peer.peer.userId}>
{/* Player component displays the peer's video stream */}
<Player source={peer.stream} classNames={mergedClassNames.playerClassNames} >
{/* Muted badge - shows audio status */}
<PeerMutedBadge classNames={mergedClassNames.mutedBadgeClassNames} />
{/* Display name overlay */}
<span className={mergedClassNames.displayNameClassName}>
{(peer as any)?.peerParams?.displayName}
</span>
{/* Any custom children passed to Peers */}
{children}
</Player>
</PeerAPIProvider>
))}
</>
);
}
/**
* EXPORT WITH MEMOIZATION
*
* We wrap the component in React.memo() to prevent unnecessary re-renders.
* The component only re-renders when:
* - Props change (children, classNames)
* - Call peers change (join/leave events)
*
* This optimization is important because:
* - There may be multiple peer players rendering simultaneously
* - Video rendering is computationally expensive
* - Peer changes should trigger targeted updates, not full re-renders
*/
export default memo(Peers);