Skip to main content

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.

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:

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:

  1. Set the connecting state to true, this will be used to display a loading state to the user
  2. Join the call
  3. Create a broadcast, this will be used to broadcast the user's video to the call
  4. Set the broadcast and call state in the parent component
  5. 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 useCallAPI and useBroadcastAPI hooks to access the active call and broadcast instances. These can be accessed from the hooks namespace exported from @video/video-client-react package.
  • Use the usePortal hook 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:

  1. Set the disconnecting state to true, this will be used to display a loading state to the user
  2. Dispose the call and broadcast
  3. Set the broadcast and call state in the parent component to null
  4. 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 AuthClient instance.
  • Creating a MediaStreamController instance.
  • Creating a PreviewPlayer instance.
  • 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);