Skip to main content

Create a Group Call

Build multi-participant video calls with owner and participant roles.

What is a Group Call?

A group call is a multi-participant video conference where multiple users can join a single call and both send and receive video and audio data in real-time. Unlike direct streaming (1:1), group calls support many-to-many communication.

Key Features:

  • One user creates the call (the Owner)
  • Multiple users join using the call ID (the Participants)
  • All participants can see and hear each other
  • Everyone controls their own audio/video settings
  • Participants can start/stop broadcasting independently

Use Cases:

  • Video conferencing (team meetings, standups)
  • Online learning (virtual classrooms)
  • Digital community building (social video calls)
  • Remote collaboration (pair programming, design reviews)
  • Virtual events (webinars with multiple speakers)

Implementation Overview

Creating a group call involves two main components:

  1. GroupCallOwner - Creates and hosts the call
  2. GroupCallParticipant - Joins an existing call

Both components share similar structure but have different responsibilities and authentication scopes.

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:

Key Differences: Owner vs Participant

FeatureOwnerParticipant
Authentication Scopeconference-ownerconference-participant
Call ManagementCreates call with create buttonJoins call with join button
Call IDGenerated and shared with participantsReceives from owner
PermissionsCan manage call settingsStandard participant permissions
useCallControls type"owner""participant"
CallOptionsNo callId (creating new call)Includes callId (joining existing)

Automatic Peer Discovery

Both owner and participant use the <Peers/> component for automatic peer discovery:

  • Listens for streamAdded and streamRemoved events
  • Creates a player for each broadcasting peer
  • Displays peer video with name and muted badge
  • Updates automatically when peers join or leave
  • No manual participant management required

The <Peers/> component abstracts away all the complexity of managing multiple participants, making it easy to build scalable group calls.

Group Call Owner Component

The owner creates the call and manages participants.

Imports

import React, { useEffect } from "react";

/**
* IMPORT VIDEO CLIENT LIBRARIES
*
* - hooks: Custom React hooks (useAuthClient, usePreviewPlayer)
* - context: React Context providers for sharing instances
*/
import { hooks, context } from "@video/video-client-react";

/**
* REUSABLE COMPONENTS
*
* - useCallControls: Custom hook for managing call and broadcast state
* - Peers: Component that automatically discovers and displays all participants
* - Encoder: Owner's video preview with device controls
* - KickPeerButton: Optional button to remove participants
*
* Documentation:
* - Encoder: /articles/broadcasting-a-livestream/streaming-with-a-web-browser
* - Peers: /articles/consuming-a-livestream/view-a-stream#peers-component
* - useCallControls: /articles/broadcasting-a-livestream/set-up-a-livestream-video#callcontrols-hook
*/
import { useCallControls } from "../../components/CallControls";
import Peers from "../../components/Peers";
import KickPeerButton from "./KickPeerButton";
import PreviewPlayer from "../../components/PreviewPlayer";

const { useAuthClient, usePreviewPlayer } = hooks;

const { MediaStreamControllerAPIProvider, PlayerAPIProvider, CallAPIProvider } = context;

Render UI

Split-panel layout with owner's view and participants grid:

Layout:

  • Left Panel: Owner's preview player with create/broadcast controls
  • Right Panel: Participants grid (Peers component)
    • Automatically shows all participants
    • Updates when participants join/leave

Owner Flow:

  1. Click "Create Call" → Call is created, ID is shared via cbCallId
  2. Click "Start Broadcast" → Owner starts streaming
  3. Participants join → Peers component shows them automatically
  4. Click "End Broadcast" → Owner stops streaming
  5. Click "End Call" → Call ends, all participants disconnected
/**
* RENDER: CONDITIONAL UI STATES
*/

/**
* STATE 1: LOADING
*
* Show loading screen while media devices are being initialized.
* Once usePreviewPlayer completes, the UI will render.
*/
if (!mediaStreamController || !previewPlayer) {
return <div className={classNames.loadingScreenClassName} />;
}

/**
* STATE 2: ACTIVE GROUP CALL
*
* Split-panel layout for group call:
*
* LEFT PANEL: Owner's View
* - Encoder: Own video preview with device controls
* - "Conference Owner" badge
* - renderControls: Create call and start/stop broadcast buttons
*
* RIGHT PANEL: Participants Grid
* - Peers component: Automatically discovers and displays all participants
* - Only shown when call exists
* - Wraps Peers in CallAPIProvider to provide call context
*
* PARTICIPANT DISCOVERY:
* The Peers component automatically:
* 1. Listens for streamAdded/streamRemoved events
* 2. Creates a player for each participant
* 3. Displays participant video with name and muted badge
* 4. Updates when participants join or leave
*
* OWNER FLOW:
* 1. Owner clicks "Create Call" → Call is created
* 2. Owner clicks "Start Broadcast" → Owner starts streaming
* 3. Participants join → Peers component shows them automatically
* 4. Owner clicks "End Broadcast" → Owner stops streaming
* 5. Owner clicks "End Call" → Call ends, all participants disconnected
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<div className={classNames.wrapperClassName}>
{/* Left Panel: Owner's Encoder */}
<div className={classNames.leftPanelClassName}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<PreviewPlayer classNames={classNames.previewPlayerClassNames}/>
<span className={classNames.callOwnerClassName}>Conference Owner</span>
{renderControls()}
</PlayerAPIProvider>
</div>

{/* Right Panel: Participants Grid */}
<div className={classNames.rightPanelClassName}>
<div className={classNames.peersWrapperClassName}>
{call != null &&
<CallAPIProvider callAPI={call}>
<Peers>
{/* Optional: Add KickPeerButton as child to kick participants */}
{/* <KickPeerButton /> */}
</Peers>
</CallAPIProvider> }
</div>
</div>
</div>
</MediaStreamControllerAPIProvider>
);

Full Component Code

// GroupCallOwner.tsx

/**
* GroupCallOwner Component
*
* ADVANCED IMPLEMENTATION: Group Call (Owner/Host Side)
*
* This component enables creating and hosting multi-participant group calls
* where all participants can see and hear each other in real-time. The owner
* creates the call and participants join using the call ID.
*
* KEY CONCEPTS:
* 1. MULTI-PARTICIPANT CALLS: Multiple users in a single call
* 2. OWNER RESPONSIBILITIES: Create call, manage participants
* 3. AUTOMATIC PEER DISCOVERY: Peers component shows all participants
* 4. BIDIRECTIONAL STREAMING: Everyone can broadcast and receive
*
* USE CASES:
* - Video conferencing (team meetings, standups)
* - Online learning (virtual classrooms)
* - Digital community building (social video calls)
* - Remote collaboration (pair programming, design reviews)
* - Virtual events (webinars with multiple speakers)
*
* ARCHITECTURE:
* - Left panel: Owner's encoder (own video + call controls)
* - Right panel: Participants grid (all other participants)
*
* PREREQUISITES:
* Assumes familiarity with:
* - Encoder setup and device controls
* - Call and broadcast management
* - Peer discovery and rendering
* - Authentication with conference-owner scope
*/

import React, { useEffect } from "react";

/**
* IMPORT VIDEO CLIENT LIBRARIES
*
* - hooks: Custom React hooks (useAuthClient, usePreviewPlayer)
* - context: React Context providers for sharing instances
*/
import { hooks, context } from "@video/video-client-react";

/**
* REUSABLE COMPONENTS
*
* - useCallControls: Custom hook for managing call and broadcast state
* - Peers: Component that automatically discovers and displays all participants
* - Encoder: Owner's video preview with device controls
* - KickPeerButton: Optional button to remove participants
*
* Documentation:
* - Encoder: /articles/broadcasting-a-livestream/streaming-with-a-web-browser
* - Peers: /articles/consuming-a-livestream/view-a-stream#peers-component
* - useCallControls: /articles/broadcasting-a-livestream/set-up-a-livestream-video#callcontrols-hook
*/
import { useCallControls } from "../../components/CallControls";
import Peers from "../../components/Peers";
import KickPeerButton from "./KickPeerButton";
import PreviewPlayer from "../../components/PreviewPlayer";

const { useAuthClient, usePreviewPlayer } = hooks;

const { MediaStreamControllerAPIProvider, PlayerAPIProvider, CallAPIProvider } = context;

/**
* STYLING CONFIGURATION
*
* Split-panel layout:
* - Left: Owner's encoder (1/3 width)
* - Right: Participants grid (2/3 width)
*/
const classNames = {
wrapperClassName: "flex flex-row",
leftPanelClassName: "w-1/3 relative",
rightPanelClassName: "w-2/3",
playerWrapperClassName: "w-full",
callOwnerClassName: "text-sm text-white absolute top-1 right-1 z-[200] bg-black/50 px-2 py-1 rounded-md",
peersWrapperClassName: "w-full flex flex-row flex-wrap gap-2 px-4 overflow-auto",
loadingScreenClassName: "w-full h-full",
previewPlayerClassNames: {
wrapperClassName: "w-full",
videoContainerClassName: "w-full",
videoClassName: "w-full",
controlBarClassName: "flex flex-col",
controlBarItemClassName: "flex flex-col my-2 gap-2",
},
};

/**
* PROPS INTERFACE
*
* Configuration for creating and hosting a group call
*/
interface GroupCallOwnerProps {
/**
* Backend API endpoint URL
*/
backendEndpoint: string;

/**
* Authentication token (must have "conference-owner" scope)
*/
token: string;

/**
* Unique stream key for this call
*/
streamKey: string;

/**
* Callback to receive the created call ID
* Share this ID with participants so they can join
*/
cbCallId: (callId: string) => void;
}

function GroupCallOwner({ backendEndpoint, token, streamKey, cbCallId }: GroupCallOwnerProps): React.ReactElement | null {
/**
* STATE MANAGEMENT
*
* The owner manages media devices and call/broadcast state through hooks.
*/

/**
* INITIALIZE OWNER'S MEDIA
*
* The usePreviewPlayer hook sets up:
* - mediaStreamController: Controls owner's camera and microphone
* - previewPlayer: Displays owner's video preview
*
* This is the standard pattern for any component that needs to
* broadcast video.
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});

/**
* AUTHENTICATION
*
* Create auth client with "conference-owner" scope.
* This scope grants permissions to:
* - Create calls
* - Manage participants
* - Control call settings
*/
const authClient = useAuthClient(token);

/**
* CALL AND BROADCAST CONFIGURATION
*
* Configure options for call creation and broadcasting:
* - callOptions: Defines how to create the call
* - streamKey: Unique identifier for this call
* - user: Owner's display information
* - backendEndpoints: Server URLs
* - auth: Authentication client with conference-owner scope
* - broadcastOptions: Defines broadcast settings
* - streamName: Name of the broadcast stream
*/
const callOptions = { streamKey, user: { userId: "123", displayName: "John Doe" }, backendEndpoints: [backendEndpoint], auth: authClient, }
const broadcastOptions = {streamName: "default"};

/**
* USE CALL CONTROLS HOOK
*
* The useCallControls hook provides:
* - renderControls: Function to render create/join and start/stop broadcast buttons
* - call: The active call instance
* - setCall/setBroadcast: Functions to update call and broadcast state
*
* Type "owner" indicates this is the call creator (vs "participant" for joiners).
*/
const { renderControls, call, setCall, setBroadcast } = useCallControls({callOptions, broadcastOptions, type: "owner"});

/**
* NOTIFY PARENT OF CALL ID
*
* Once the call is created, send the call ID to the parent component.
* The parent can then share this ID with participants so they can join.
*
* FLOW:
* 1. Owner clicks "Create Call" button (from renderControls)
* 2. Call is created
* 3. This effect fires with the new call ID
* 4. Parent receives call ID via cbCallId callback
* 5. Parent displays/shares call ID for participants
*/
useEffect(() => {
cbCallId(call?.id ?? "");
}, [call, cbCallId]);



/**
* CLEANUP EFFECTS
*
* Proper cleanup is critical for group calls to avoid:
* - Memory leaks from undisposed resources
* - Lingering WebRTC connections
* - Camera/microphone not being released
* - Stale call instances
*
* We dispose in separate effects to handle each resource independently.
*/

// Cleanup call and broadcast
useEffect(() => {
return () => {
if (call != null) {
call.dispose("Disposed by useEffect - call cleanup");
setCall(null);
setBroadcast(null);
}
};
}, [call, setCall, setBroadcast]);

// Cleanup media stream controller
useEffect(() => {
return () => {
if (mediaStreamController != null) {
mediaStreamController.dispose("Disposed by useEffect - mediaStreamController cleanup");
}
};
}, [mediaStreamController]);

// Cleanup preview player
useEffect(() => {
return () => {
if (previewPlayer != null) {
previewPlayer.dispose("Disposed by useEffect - previewPlayer cleanup");
}
};
}, [previewPlayer]);

/**
* RENDER: CONDITIONAL UI STATES
*/

/**
* STATE 1: LOADING
*
* Show loading screen while media devices are being initialized.
* Once usePreviewPlayer completes, the UI will render.
*/
if (!mediaStreamController || !previewPlayer) {
return <div className={classNames.loadingScreenClassName} />;
}

/**
* STATE 2: ACTIVE GROUP CALL
*
* Split-panel layout for group call:
*
* LEFT PANEL: Owner's View
* - Encoder: Own video preview with device controls
* - "Conference Owner" badge
* - renderControls: Create call and start/stop broadcast buttons
*
* RIGHT PANEL: Participants Grid
* - Peers component: Automatically discovers and displays all participants
* - Only shown when call exists
* - Wraps Peers in CallAPIProvider to provide call context
*
* PARTICIPANT DISCOVERY:
* The Peers component automatically:
* 1. Listens for streamAdded/streamRemoved events
* 2. Creates a player for each participant
* 3. Displays participant video with name and muted badge
* 4. Updates when participants join or leave
*
* OWNER FLOW:
* 1. Owner clicks "Create Call" → Call is created
* 2. Owner clicks "Start Broadcast" → Owner starts streaming
* 3. Participants join → Peers component shows them automatically
* 4. Owner clicks "End Broadcast" → Owner stops streaming
* 5. Owner clicks "End Call" → Call ends, all participants disconnected
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<div className={classNames.wrapperClassName}>
{/* Left Panel: Owner's Encoder */}
<div className={classNames.leftPanelClassName}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<PreviewPlayer classNames={classNames.previewPlayerClassNames}/>
<span className={classNames.callOwnerClassName}>Conference Owner</span>
{renderControls()}
</PlayerAPIProvider>
</div>

{/* Right Panel: Participants Grid */}
<div className={classNames.rightPanelClassName}>
<div className={classNames.peersWrapperClassName}>
{call != null &&
<CallAPIProvider callAPI={call}>
<Peers>
{/* Optional: Add KickPeerButton as child to kick participants */}
{/* <KickPeerButton /> */}
</Peers>
</CallAPIProvider> }
</div>
</div>
</div>
</MediaStreamControllerAPIProvider>
);
}
export default GroupCallOwner;

Group Call Participant Component

The participant joins an existing call using the owner's call ID.

Props

  • callId: The call ID from the owner (required to join)

Full Component Code

// GroupCallParticipant.tsx

/**
* GroupCallParticipant Component
*
* ADVANCED IMPLEMENTATION: Group Call (Participant Side)
*
* This component enables joining existing multi-participant group calls.
* Participants can see and interact with the owner and all other participants
* in real-time.
*
* KEY CONCEPTS:
* 1. JOIN EXISTING CALLS: Participant joins using owner's call ID
* 2. PARTICIPANT SCOPE: Limited permissions vs owner
* 3. AUTOMATIC PEER DISCOVERY: Sees owner and all other participants
* 4. BIDIRECTIONAL STREAMING: Can broadcast and receive simultaneously
*
* USE CASES:
* - Joining team meetings
* - Attending virtual classes
* - Participating in community video calls
* - Collaborating remotely
* - Attending virtual events
*
* ARCHITECTURE:
* - Left panel: Participant's encoder (own video + broadcast controls)
* - Right panel: Other participants grid (owner + other participants)
*
* PREREQUISITES:
* Assumes familiarity with:
* - Encoder setup and device controls
* - Call joining and broadcast management
* - Peer discovery and rendering
* - Authentication with conference-participant scope
*/

import React, { useEffect, memo } from "react";

/**
* IMPORT VIDEO CLIENT LIBRARIES
*
* - types: TypeScript type definitions (CallOptions, etc.)
* - hooks: Custom React hooks (useAuthClient, usePreviewPlayer)
* - context: React Context providers for sharing instances
*/
import { types, hooks, context } from "@video/video-client-react";


/**
* REUSABLE COMPONENTS
*
* - useCallControls: Custom hook for managing call and broadcast state
* - Peers: Component that automatically discovers and displays all participants
* - Encoder: Participant's video preview with device controls
*
* Documentation:
* - Encoder: /articles/broadcasting-a-livestream/streaming-with-a-web-browser
* - Peers: /articles/consuming-a-livestream/view-a-stream#peers-component
* - useCallControls: /articles/broadcasting-a-livestream/set-up-a-livestream-video#callcontrols-hook
*/
import { useCallControls } from "../../components/CallControls";
import Peers from "../../components/Peers";
import PreviewPlayer from "../../components/PreviewPlayer";


const { useAuthClient, usePreviewPlayer } = hooks;

const { MediaStreamControllerAPIProvider, PlayerAPIProvider, CallAPIProvider } = context;

/**
* STYLING CONFIGURATION
*
* Split-panel layout (same as owner):
* - Left: Participant's encoder (1/3 width)
* - Right: Other participants grid (2/3 width)
*/
const classNames = {
wrapperClassName: "flex flex-row",
leftPanelClassName: "w-1/3 relative",
rightPanelClassName: "w-2/3",
playerWrapperClassName: "w-full",
callParticipantClassName: "text-sm text-white absolute top-1 right-1 z-[200] bg-black/50 px-2 py-1 rounded-md",
peersWrapperClassName: "w-full flex flex-row flex-wrap gap-2 px-4 overflow-auto",
loadingScreenClassName: "w-full h-full",
previewPlayerClassNames: {
wrapperClassName: "w-full",
videoContainerClassName: "w-full",
videoClassName: "w-full",
controlBarClassName: "flex flex-col",
controlBarItemClassName: "flex flex-col my-2 gap-2",
},
};

/**
* PROPS INTERFACE
*
* Configuration for joining a group call as a participant
*/
type Props = {
/**
* Backend API endpoint URL
*/
backendEndpoint: string;

/**
* Unique identifier for this participant
*/
userId: string;

/**
* Display name shown to other participants
*/
displayName: string;

/**
* Call ID to join (provided by the owner)
*/
callId: string;

/**
* Authentication token (must have "conference-participant" scope)
*/
token: string;
}

function GroupCallParticipant({ backendEndpoint, token, userId, displayName, callId }: Props): React.ReactElement | null {
/**
* STATE MANAGEMENT
*
* The participant manages media devices and call/broadcast state through hooks.
*/

/**
* INITIALIZE PARTICIPANT'S MEDIA
*
* The usePreviewPlayer hook sets up:
* - mediaStreamController: Controls participant's camera and microphone
* - previewPlayer: Displays participant's video preview
*
* Same pattern as owner - all participants manage their own media.
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});

/**
* AUTHENTICATION
*
* Create auth client with "conference-participant" scope.
* This scope grants permissions to:
* - Join existing calls
* - Broadcast within calls
* - View other participants
*
* Key difference from owner: Cannot create calls or kick participants.
*/
const authClient = useAuthClient(token);

/**
* JOIN CALL CONFIGURATION
*
* Configure options for joining an existing call:
* - user: Participant's display information (userId, displayName)
* - auth: Authentication client with conference-participant scope
* - backendEndpoints: Server URLs
* - callId: The call ID to join (provided by owner)
*
* KEY DIFFERENCE FROM OWNER: Includes callId to join existing call.
*/
const joinCallOptions: types.CallOptions = {
user: { userId, displayName: `Participant: ${displayName}` },
auth: authClient,
backendEndpoints: [backendEndpoint],
callId: callId,
};
const broadcastOptions = {streamName: "default"};

/**
* USE CALL CONTROLS HOOK
*
* The useCallControls hook provides:
* - renderControls: Function to render join and start/stop broadcast buttons
* - call: The active call instance
* - setCall/setBroadcast: Functions to update call and broadcast state
*
* Type "participant" indicates this is joining an existing call (vs "owner" for creator).
*/
const { renderControls, call, setCall, setBroadcast } = useCallControls({callOptions: joinCallOptions, broadcastOptions, type: "participant"});



/**
* CLEANUP EFFECTS
*
* Proper cleanup is critical for participants to avoid:
* - Memory leaks from undisposed resources
* - Lingering WebRTC connections
* - Camera/microphone not being released
* - Stale call instances
*
* We dispose in separate effects to handle each resource independently.
*/

// Cleanup media stream controller
useEffect(() => {
return () => {
if (mediaStreamController != null) {
mediaStreamController.dispose("Disposed by useEffect - mediaStreamController cleanup");
}
};
}, [mediaStreamController]);

// Cleanup preview player
useEffect(() => {
return () => {
if (previewPlayer != null) {
previewPlayer.dispose("Disposed by useEffect - previewPlayer cleanup");
}
};
}, [previewPlayer]);

// Cleanup call and broadcast
useEffect(() => {
return () => {
if (call != null) {
call.dispose("Disposed by useEffect - call cleanup");
setCall(null);
setBroadcast(null);
}
};
}, [call, setCall, setBroadcast]);

/**
* RENDER: CONDITIONAL UI STATES
*/

/**
* STATE 1: LOADING OR MISSING REQUIRED PROPS
*
* Show loading screen while:
* - Media devices are being initialized
* - Required props (callId, userId, displayName) are missing
*
* Once all conditions are met, the UI will render.
*/
if (!mediaStreamController || !previewPlayer || !callId || !userId || !displayName) {
return <div className={classNames.loadingScreenClassName} />;
}

/**
* STATE 2: ACTIVE GROUP CALL PARTICIPATION
*
* Split-panel layout for participant:
*
* LEFT PANEL: Participant's View
* - Encoder: Own video preview with device controls
* - Display name badge showing "Participant: {name}"
* - renderControls: Join call and start/stop broadcast buttons
*
* RIGHT PANEL: Other Participants Grid
* - Peers component: Automatically discovers and displays ALL other participants
* - Includes the owner
* - Includes all other participants
* - Only shown when call exists
* - Wraps Peers in CallAPIProvider to provide call context
*
* PARTICIPANT FLOW:
* 1. Participant receives call ID from owner
* 2. Participant clicks "Join Call" → Joins the call
* 3. Participant clicks "Start Broadcast" → Starts streaming
* 4. Peers component shows owner + all other participants automatically
* 5. Other participants see this participant in their Peers grid
* 6. Participant clicks "End Broadcast" → Stops streaming (still in call)
* 7. Participant clicks "Leave Call" → Disconnects from call
*
* KEY POINT:
* From the participant's perspective, everyone else (owner + other participants)
* appears in the Peers grid. The Peers component doesn't distinguish between
* owner and other participants - it just shows all broadcasting peers.
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<div className={classNames.wrapperClassName}>
{/* Left Panel: Participant's Encoder */}
<div className={classNames.leftPanelClassName}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<PreviewPlayer classNames={classNames.previewPlayerClassNames}/>
<span className={classNames.callParticipantClassName}>Participant: {displayName}</span>
{renderControls()}
</PlayerAPIProvider>
</div>

{/* Right Panel: Other Participants Grid */}
<div className={classNames.rightPanelClassName}>
<div className={classNames.peersWrapperClassName}>
{call &&
<CallAPIProvider callAPI={call}>
<Peers/>
</CallAPIProvider>
}
</div>
</div>
</div>
</MediaStreamControllerAPIProvider>
);
}
export default memo(GroupCallParticipant);

Full Code

Group Call Components

// GroupCallParticipant.tsx

/**
* GroupCallParticipant Component
*
* ADVANCED IMPLEMENTATION: Group Call (Participant Side)
*
* This component enables joining existing multi-participant group calls.
* Participants can see and interact with the owner and all other participants
* in real-time.
*
* KEY CONCEPTS:
* 1. JOIN EXISTING CALLS: Participant joins using owner's call ID
* 2. PARTICIPANT SCOPE: Limited permissions vs owner
* 3. AUTOMATIC PEER DISCOVERY: Sees owner and all other participants
* 4. BIDIRECTIONAL STREAMING: Can broadcast and receive simultaneously
*
* USE CASES:
* - Joining team meetings
* - Attending virtual classes
* - Participating in community video calls
* - Collaborating remotely
* - Attending virtual events
*
* ARCHITECTURE:
* - Left panel: Participant's encoder (own video + broadcast controls)
* - Right panel: Other participants grid (owner + other participants)
*
* PREREQUISITES:
* Assumes familiarity with:
* - Encoder setup and device controls
* - Call joining and broadcast management
* - Peer discovery and rendering
* - Authentication with conference-participant scope
*/

import React, { useEffect, memo } from "react";

/**
* IMPORT VIDEO CLIENT LIBRARIES
*
* - types: TypeScript type definitions (CallOptions, etc.)
* - hooks: Custom React hooks (useAuthClient, usePreviewPlayer)
* - context: React Context providers for sharing instances
*/
import { types, hooks, context } from "@video/video-client-react";


/**
* REUSABLE COMPONENTS
*
* - useCallControls: Custom hook for managing call and broadcast state
* - Peers: Component that automatically discovers and displays all participants
* - Encoder: Participant's video preview with device controls
*
* Documentation:
* - Encoder: /articles/broadcasting-a-livestream/streaming-with-a-web-browser
* - Peers: /articles/consuming-a-livestream/view-a-stream#peers-component
* - useCallControls: /articles/broadcasting-a-livestream/set-up-a-livestream-video#callcontrols-hook
*/
import { useCallControls } from "../../components/CallControls";
import Peers from "../../components/Peers";
import PreviewPlayer from "../../components/PreviewPlayer";


const { useAuthClient, usePreviewPlayer } = hooks;

const { MediaStreamControllerAPIProvider, PlayerAPIProvider, CallAPIProvider } = context;

/**
* STYLING CONFIGURATION
*
* Split-panel layout (same as owner):
* - Left: Participant's encoder (1/3 width)
* - Right: Other participants grid (2/3 width)
*/
const classNames = {
wrapperClassName: "flex flex-row",
leftPanelClassName: "w-1/3 relative",
rightPanelClassName: "w-2/3",
playerWrapperClassName: "w-full",
callParticipantClassName: "text-sm text-white absolute top-1 right-1 z-[200] bg-black/50 px-2 py-1 rounded-md",
peersWrapperClassName: "w-full flex flex-row flex-wrap gap-2 px-4 overflow-auto",
loadingScreenClassName: "w-full h-full",
previewPlayerClassNames: {
wrapperClassName: "w-full",
videoContainerClassName: "w-full",
videoClassName: "w-full",
controlBarClassName: "flex flex-col",
controlBarItemClassName: "flex flex-col my-2 gap-2",
},
};

/**
* PROPS INTERFACE
*
* Configuration for joining a group call as a participant
*/
type Props = {
/**
* Backend API endpoint URL
*/
backendEndpoint: string;

/**
* Unique identifier for this participant
*/
userId: string;

/**
* Display name shown to other participants
*/
displayName: string;

/**
* Call ID to join (provided by the owner)
*/
callId: string;

/**
* Authentication token (must have "conference-participant" scope)
*/
token: string;
}

function GroupCallParticipant({ backendEndpoint, token, userId, displayName, callId }: Props): React.ReactElement | null {
/**
* STATE MANAGEMENT
*
* The participant manages media devices and call/broadcast state through hooks.
*/

/**
* INITIALIZE PARTICIPANT'S MEDIA
*
* The usePreviewPlayer hook sets up:
* - mediaStreamController: Controls participant's camera and microphone
* - previewPlayer: Displays participant's video preview
*
* Same pattern as owner - all participants manage their own media.
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});

/**
* AUTHENTICATION
*
* Create auth client with "conference-participant" scope.
* This scope grants permissions to:
* - Join existing calls
* - Broadcast within calls
* - View other participants
*
* Key difference from owner: Cannot create calls or kick participants.
*/
const authClient = useAuthClient(token);

/**
* JOIN CALL CONFIGURATION
*
* Configure options for joining an existing call:
* - user: Participant's display information (userId, displayName)
* - auth: Authentication client with conference-participant scope
* - backendEndpoints: Server URLs
* - callId: The call ID to join (provided by owner)
*
* KEY DIFFERENCE FROM OWNER: Includes callId to join existing call.
*/
const joinCallOptions: types.CallOptions = {
user: { userId, displayName: `Participant: ${displayName}` },
auth: authClient,
backendEndpoints: [backendEndpoint],
callId: callId,
};
const broadcastOptions = {streamName: "default"};

/**
* USE CALL CONTROLS HOOK
*
* The useCallControls hook provides:
* - renderControls: Function to render join and start/stop broadcast buttons
* - call: The active call instance
* - setCall/setBroadcast: Functions to update call and broadcast state
*
* Type "participant" indicates this is joining an existing call (vs "owner" for creator).
*/
const { renderControls, call, setCall, setBroadcast } = useCallControls({callOptions: joinCallOptions, broadcastOptions, type: "participant"});



/**
* CLEANUP EFFECTS
*
* Proper cleanup is critical for participants to avoid:
* - Memory leaks from undisposed resources
* - Lingering WebRTC connections
* - Camera/microphone not being released
* - Stale call instances
*
* We dispose in separate effects to handle each resource independently.
*/

// Cleanup media stream controller
useEffect(() => {
return () => {
if (mediaStreamController != null) {
mediaStreamController.dispose("Disposed by useEffect - mediaStreamController cleanup");
}
};
}, [mediaStreamController]);

// Cleanup preview player
useEffect(() => {
return () => {
if (previewPlayer != null) {
previewPlayer.dispose("Disposed by useEffect - previewPlayer cleanup");
}
};
}, [previewPlayer]);

// Cleanup call and broadcast
useEffect(() => {
return () => {
if (call != null) {
call.dispose("Disposed by useEffect - call cleanup");
setCall(null);
setBroadcast(null);
}
};
}, [call, setCall, setBroadcast]);

/**
* RENDER: CONDITIONAL UI STATES
*/

/**
* STATE 1: LOADING OR MISSING REQUIRED PROPS
*
* Show loading screen while:
* - Media devices are being initialized
* - Required props (callId, userId, displayName) are missing
*
* Once all conditions are met, the UI will render.
*/
if (!mediaStreamController || !previewPlayer || !callId || !userId || !displayName) {
return <div className={classNames.loadingScreenClassName} />;
}

/**
* STATE 2: ACTIVE GROUP CALL PARTICIPATION
*
* Split-panel layout for participant:
*
* LEFT PANEL: Participant's View
* - Encoder: Own video preview with device controls
* - Display name badge showing "Participant: {name}"
* - renderControls: Join call and start/stop broadcast buttons
*
* RIGHT PANEL: Other Participants Grid
* - Peers component: Automatically discovers and displays ALL other participants
* - Includes the owner
* - Includes all other participants
* - Only shown when call exists
* - Wraps Peers in CallAPIProvider to provide call context
*
* PARTICIPANT FLOW:
* 1. Participant receives call ID from owner
* 2. Participant clicks "Join Call" → Joins the call
* 3. Participant clicks "Start Broadcast" → Starts streaming
* 4. Peers component shows owner + all other participants automatically
* 5. Other participants see this participant in their Peers grid
* 6. Participant clicks "End Broadcast" → Stops streaming (still in call)
* 7. Participant clicks "Leave Call" → Disconnects from call
*
* KEY POINT:
* From the participant's perspective, everyone else (owner + other participants)
* appears in the Peers grid. The Peers component doesn't distinguish between
* owner and other participants - it just shows all broadcasting peers.
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<div className={classNames.wrapperClassName}>
{/* Left Panel: Participant's Encoder */}
<div className={classNames.leftPanelClassName}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<PreviewPlayer classNames={classNames.previewPlayerClassNames}/>
<span className={classNames.callParticipantClassName}>Participant: {displayName}</span>
{renderControls()}
</PlayerAPIProvider>
</div>

{/* Right Panel: Other Participants Grid */}
<div className={classNames.rightPanelClassName}>
<div className={classNames.peersWrapperClassName}>
{call &&
<CallAPIProvider callAPI={call}>
<Peers/>
</CallAPIProvider>
}
</div>
</div>
</div>
</MediaStreamControllerAPIProvider>
);
}
export default memo(GroupCallParticipant);
// GroupCallOwner.tsx

/**
* GroupCallOwner Component
*
* ADVANCED IMPLEMENTATION: Group Call (Owner/Host Side)
*
* This component enables creating and hosting multi-participant group calls
* where all participants can see and hear each other in real-time. The owner
* creates the call and participants join using the call ID.
*
* KEY CONCEPTS:
* 1. MULTI-PARTICIPANT CALLS: Multiple users in a single call
* 2. OWNER RESPONSIBILITIES: Create call, manage participants
* 3. AUTOMATIC PEER DISCOVERY: Peers component shows all participants
* 4. BIDIRECTIONAL STREAMING: Everyone can broadcast and receive
*
* USE CASES:
* - Video conferencing (team meetings, standups)
* - Online learning (virtual classrooms)
* - Digital community building (social video calls)
* - Remote collaboration (pair programming, design reviews)
* - Virtual events (webinars with multiple speakers)
*
* ARCHITECTURE:
* - Left panel: Owner's encoder (own video + call controls)
* - Right panel: Participants grid (all other participants)
*
* PREREQUISITES:
* Assumes familiarity with:
* - Encoder setup and device controls
* - Call and broadcast management
* - Peer discovery and rendering
* - Authentication with conference-owner scope
*/

import React, { useEffect } from "react";

/**
* IMPORT VIDEO CLIENT LIBRARIES
*
* - hooks: Custom React hooks (useAuthClient, usePreviewPlayer)
* - context: React Context providers for sharing instances
*/
import { hooks, context } from "@video/video-client-react";

/**
* REUSABLE COMPONENTS
*
* - useCallControls: Custom hook for managing call and broadcast state
* - Peers: Component that automatically discovers and displays all participants
* - Encoder: Owner's video preview with device controls
* - KickPeerButton: Optional button to remove participants
*
* Documentation:
* - Encoder: /articles/broadcasting-a-livestream/streaming-with-a-web-browser
* - Peers: /articles/consuming-a-livestream/view-a-stream#peers-component
* - useCallControls: /articles/broadcasting-a-livestream/set-up-a-livestream-video#callcontrols-hook
*/
import { useCallControls } from "../../components/CallControls";
import Peers from "../../components/Peers";
import KickPeerButton from "./KickPeerButton";
import PreviewPlayer from "../../components/PreviewPlayer";

const { useAuthClient, usePreviewPlayer } = hooks;

const { MediaStreamControllerAPIProvider, PlayerAPIProvider, CallAPIProvider } = context;

/**
* STYLING CONFIGURATION
*
* Split-panel layout:
* - Left: Owner's encoder (1/3 width)
* - Right: Participants grid (2/3 width)
*/
const classNames = {
wrapperClassName: "flex flex-row",
leftPanelClassName: "w-1/3 relative",
rightPanelClassName: "w-2/3",
playerWrapperClassName: "w-full",
callOwnerClassName: "text-sm text-white absolute top-1 right-1 z-[200] bg-black/50 px-2 py-1 rounded-md",
peersWrapperClassName: "w-full flex flex-row flex-wrap gap-2 px-4 overflow-auto",
loadingScreenClassName: "w-full h-full",
previewPlayerClassNames: {
wrapperClassName: "w-full",
videoContainerClassName: "w-full",
videoClassName: "w-full",
controlBarClassName: "flex flex-col",
controlBarItemClassName: "flex flex-col my-2 gap-2",
},
};

/**
* PROPS INTERFACE
*
* Configuration for creating and hosting a group call
*/
interface GroupCallOwnerProps {
/**
* Backend API endpoint URL
*/
backendEndpoint: string;

/**
* Authentication token (must have "conference-owner" scope)
*/
token: string;

/**
* Unique stream key for this call
*/
streamKey: string;

/**
* Callback to receive the created call ID
* Share this ID with participants so they can join
*/
cbCallId: (callId: string) => void;
}

function GroupCallOwner({ backendEndpoint, token, streamKey, cbCallId }: GroupCallOwnerProps): React.ReactElement | null {
/**
* STATE MANAGEMENT
*
* The owner manages media devices and call/broadcast state through hooks.
*/

/**
* INITIALIZE OWNER'S MEDIA
*
* The usePreviewPlayer hook sets up:
* - mediaStreamController: Controls owner's camera and microphone
* - previewPlayer: Displays owner's video preview
*
* This is the standard pattern for any component that needs to
* broadcast video.
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});

/**
* AUTHENTICATION
*
* Create auth client with "conference-owner" scope.
* This scope grants permissions to:
* - Create calls
* - Manage participants
* - Control call settings
*/
const authClient = useAuthClient(token);

/**
* CALL AND BROADCAST CONFIGURATION
*
* Configure options for call creation and broadcasting:
* - callOptions: Defines how to create the call
* - streamKey: Unique identifier for this call
* - user: Owner's display information
* - backendEndpoints: Server URLs
* - auth: Authentication client with conference-owner scope
* - broadcastOptions: Defines broadcast settings
* - streamName: Name of the broadcast stream
*/
const callOptions = { streamKey, user: { userId: "123", displayName: "John Doe" }, backendEndpoints: [backendEndpoint], auth: authClient, }
const broadcastOptions = {streamName: "default"};

/**
* USE CALL CONTROLS HOOK
*
* The useCallControls hook provides:
* - renderControls: Function to render create/join and start/stop broadcast buttons
* - call: The active call instance
* - setCall/setBroadcast: Functions to update call and broadcast state
*
* Type "owner" indicates this is the call creator (vs "participant" for joiners).
*/
const { renderControls, call, setCall, setBroadcast } = useCallControls({callOptions, broadcastOptions, type: "owner"});

/**
* NOTIFY PARENT OF CALL ID
*
* Once the call is created, send the call ID to the parent component.
* The parent can then share this ID with participants so they can join.
*
* FLOW:
* 1. Owner clicks "Create Call" button (from renderControls)
* 2. Call is created
* 3. This effect fires with the new call ID
* 4. Parent receives call ID via cbCallId callback
* 5. Parent displays/shares call ID for participants
*/
useEffect(() => {
cbCallId(call?.id ?? "");
}, [call, cbCallId]);



/**
* CLEANUP EFFECTS
*
* Proper cleanup is critical for group calls to avoid:
* - Memory leaks from undisposed resources
* - Lingering WebRTC connections
* - Camera/microphone not being released
* - Stale call instances
*
* We dispose in separate effects to handle each resource independently.
*/

// Cleanup call and broadcast
useEffect(() => {
return () => {
if (call != null) {
call.dispose("Disposed by useEffect - call cleanup");
setCall(null);
setBroadcast(null);
}
};
}, [call, setCall, setBroadcast]);

// Cleanup media stream controller
useEffect(() => {
return () => {
if (mediaStreamController != null) {
mediaStreamController.dispose("Disposed by useEffect - mediaStreamController cleanup");
}
};
}, [mediaStreamController]);

// Cleanup preview player
useEffect(() => {
return () => {
if (previewPlayer != null) {
previewPlayer.dispose("Disposed by useEffect - previewPlayer cleanup");
}
};
}, [previewPlayer]);

/**
* RENDER: CONDITIONAL UI STATES
*/

/**
* STATE 1: LOADING
*
* Show loading screen while media devices are being initialized.
* Once usePreviewPlayer completes, the UI will render.
*/
if (!mediaStreamController || !previewPlayer) {
return <div className={classNames.loadingScreenClassName} />;
}

/**
* STATE 2: ACTIVE GROUP CALL
*
* Split-panel layout for group call:
*
* LEFT PANEL: Owner's View
* - Encoder: Own video preview with device controls
* - "Conference Owner" badge
* - renderControls: Create call and start/stop broadcast buttons
*
* RIGHT PANEL: Participants Grid
* - Peers component: Automatically discovers and displays all participants
* - Only shown when call exists
* - Wraps Peers in CallAPIProvider to provide call context
*
* PARTICIPANT DISCOVERY:
* The Peers component automatically:
* 1. Listens for streamAdded/streamRemoved events
* 2. Creates a player for each participant
* 3. Displays participant video with name and muted badge
* 4. Updates when participants join or leave
*
* OWNER FLOW:
* 1. Owner clicks "Create Call" → Call is created
* 2. Owner clicks "Start Broadcast" → Owner starts streaming
* 3. Participants join → Peers component shows them automatically
* 4. Owner clicks "End Broadcast" → Owner stops streaming
* 5. Owner clicks "End Call" → Call ends, all participants disconnected
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<div className={classNames.wrapperClassName}>
{/* Left Panel: Owner's Encoder */}
<div className={classNames.leftPanelClassName}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<PreviewPlayer classNames={classNames.previewPlayerClassNames}/>
<span className={classNames.callOwnerClassName}>Conference Owner</span>
{renderControls()}
</PlayerAPIProvider>
</div>

{/* Right Panel: Participants Grid */}
<div className={classNames.rightPanelClassName}>
<div className={classNames.peersWrapperClassName}>
{call != null &&
<CallAPIProvider callAPI={call}>
<Peers>
{/* Optional: Add KickPeerButton as child to kick participants */}
{/* <KickPeerButton /> */}
</Peers>
</CallAPIProvider> }
</div>
</div>
</div>
</MediaStreamControllerAPIProvider>
);
}
export default GroupCallOwner;

Supporting Components

The following components are used by the Group Call 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);

Next Steps