Skip to main content

View a Call Stream

Join live WebRTC calls for ultra-low latency viewing.

There are two ways to view video streams:

  1. WebRTC Call Viewer (this guide): Join live calls for ultra-low latency (less than 1s)
  2. Manifest Player: View transcoded streams via HLS/FLV (see Play a Stream Using a Manifest)

This guide covers joining and viewing WebRTC calls, which is ideal for:

  • Live interactive streaming
  • Real-time collaboration
  • Scenarios requiring sub-second latency

Prerequisites

This guide assumes basic knowledge of React concepts. The component uses pre-built components (JoinCallButton, Peers) to simplify the implementation.

Peers Component

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. The PeersComponent is a building block that will be used in subsquent examples throught the documentation site.

Imports

/**
* 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;

Props

/**
* 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>;
}

Hooks

The Peers component uses two key hooks:

  • useCallAPI(): Retrieves the call instance from React Context (provided by CallAPIProvider wrapper)
  • useCallPeers(call): Automatically discovers all broadcasting peers in the call and returns an array of peer objects. This hook handles:
    • Listening for peer join/leave events
    • Managing peer lifecycle
    • Tracking which peers are broadcasting
    • Cleaning up when peers disconnect
/**
* 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);

Render UI

The component renders dynamically based on peer availability:

  • No peers: Returns null (renders nothing) when no broadcasters are in the call
  • Active peers: Maps over the peers array to render each broadcaster with:
    • PeerAPIProvider: Makes peer instance available to child components
    • Player: Displays the peer's video stream
    • PeerMutedBadge: Shows muted/unmuted audio status
    • Display name: Shows the peer's name overlay
    • Custom children: Any additional overlays passed as props

The UI automatically updates when peers join or leave the 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>
))}
</>
);

Full Component Code

// 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);

Call Viewer Component

Imports

In other examples we use the custom <CallControls/> component or useCallControlsHook; however, since this is a simpler implementation (simply viewing a call rather than viewing and broadcasting), we will use local state and components from the @video/video-client-react package directly.

Key imports:

  • JoinCallButton
  • EndCallButton
  • CallAPIProvider
/**
* IMPORT VIDEO CLIENT CORE LIBRARIES
*
* These imports provide all the building blocks for joining and viewing calls:
* - components: Pre-built UI components (buttons, badges, controls)
* - types: TypeScript type definitions
* - hooks: Custom hooks for video functionality
* - context: React Context providers for sharing state
*/
import { components, types, hooks, context } from "@video/video-client-react";

/**
* EXTRACT PRE-BUILT COMPONENTS AND HOOKS
*
* - JoinCallButton: Button component that joins a call when clicked
* - EndCallButton: Button component that leaves/ends a call when clicked
* - useAuthClient: Hook to create authentication client
* - CallAPIProvider: Context provider that shares call instance with children
*/
const { JoinCallButton, EndCallButton } = components;
const { useAuthClient } = hooks;
const { CallAPIProvider } = context;

Props

Since we are viewing an existing call, a callId is required (in addition to authentication and backendUrl)

/**
* PROPS INTERFACE
*
* Configuration values needed to join and view a call.
*/
interface ViewAStreamProps {
/**
* The unique identifier of the call to join
* Usually provided by your backend or shared by the broadcaster
*/
callId: string;

/**
* Backend API endpoint URL (provided by Native Frame)
* Example: "https://api.nativeframe.com"
*/
backendEndpoint: string;

/**
* Authentication token from your auth system
* Must have "viewer" or "private-viewer" scope to join calls
*/
token: string;
}

State

Set up state to track the call instance and create an authentication client:

Key Points:

  • The call state tracks whether the user has joined the call
  • The useAuthClient hook handles authentication and token refresh
  • Token must have "viewer" or "private-viewer" scope
/**
* STEP 1: Manage Call State
*
* We use useState to track the call instance. The call lifecycle:
* 1. Initially null (not joined)
* 2. Set to CallAPI instance when user joins (via JoinCallButton)
* 3. Set back to null when user leaves (via EndCallButton)
*
* This state determines which UI to show:
* - null: Show JoinCallButton
* - CallAPI: Show Peers and EndCallButton
*/
const [call, setCall] = useState<types.CallAPI | null>(null);

Render UI

The component has three conditional states:

  1. Loading: Show loading message while authentication initializes
  2. Not Joined: Show JoinCallButton to join the call
  3. Joined: Show Peers component and EndCallButton

How it works:

  • JoinCallButton handles the complexity of joining the call
  • Once joined, the Peers component automatically discovers and displays all broadcasters
  • EndCallButton leaves the call and resets state
/**
* RETURN CONDITION 1: Loading State
*
* Wait for authentication to complete before showing the UI.
* The authClient will be null while initializing.
*
* In a production app, you might show:
* - A loading spinner
* - A skeleton UI
* - A splash screen
*/
if (authClient == null || callId == null) {
return <div>Loading...</div>;
}

/**
* RETURN CONDITION 2: Not Joined - Show Join Button
*
* When the user hasn't joined the call yet, show the JoinCallButton.
*
* THE JOIN FLOW:
* 1. User clicks the JoinCallButton
* 2. Button uses the provided joinCallOptions to join the call
* 3. Once joined, it calls setCall with the CallAPI instance
* 4. This triggers a re-render with call !== null
* 5. User then sees the viewing interface (RETURN CONDITION 3)
*
* JOIN CALL OPTIONS:
* - callId: Which call to join (required)
* - user: Viewer information (userId, displayName)
* - auth: Authentication client (proves permission to join)
* - backendEndpoints: Server URLs to connect to
*
* The JoinCallButton handles all the complexity of:
* - Establishing WebRTC connections
* - Negotiating media streams
* - Error handling
* - Loading states
*/
if (call == null) {
return (
<div className="w-full h-full">
<JoinCallButton
callId={callId}
joinCallOptions={{
user: { userId: "123", displayName: "John Doe" },
auth: authClient,
backendEndpoints: [backendEndpoint],
}}
setCall={setCall}
/>
</div>
);
}

/**
* RETURN CONDITION 3: Joined - Show Peers and Controls
*
* Once the user has joined the call, display:
* 1. Peers component: Shows all broadcasting peers
* 2. EndCallButton: Allows user to leave the call
*
* COMPONENT STRUCTURE:
* CallAPIProvider (makes call available to children)
* └── Container
* ├── Peers (discovers and displays all broadcasters)
* └── EndCallButton (leaves the call)
*
* HOW IT WORKS:
* 1. CallAPIProvider shares the call instance via React Context
* 2. Peers component accesses the call and discovers all broadcasting peers
* 3. For each peer, Peers creates a Player to display their stream
* 4. Peers automatically updates when broadcasters join/leave
* 5. EndCallButton disposes the call and resets state to null
*
* IMPORTANT: CallAPIProvider is required for Peers to work.
* Without it, Peers can't access the call to discover broadcasters.
*/
return (
<CallAPIProvider callAPI={call}>
<div className="w-full">
{/* Display all broadcasting peers */}
<Peers classNames={classNames}/>

{/* Leave call button */}
<div className="mt-4">
<EndCallButton onDisposed={() => setCall(null)} />
</div>
</div>
</CallAPIProvider>
);

Full Component Code

// ViewAStream.tsx

/**
* ViewAStream Component
*
* OVERVIEW:
* This component demonstrates how to join and view a live WebRTC call
* (as opposed to viewing a transcoded manifest). This is used for ultra-low
* latency viewing where viewers connect directly to a WebRTC call to watch
* broadcasters in real-time.
*
* WHAT YOU'LL LEARN:
* - How to join an existing WebRTC call as a viewer
* - How to authenticate and connect to calls
* - How to display all broadcasting peers in a call
* - How to manage call state (joining, viewing, leaving)
* - The difference between WebRTC calls and manifest playback
*
* KEY CONCEPTS:
* 1. WEBRTC CALL VIEWER vs MANIFEST VIEWER:
* - WebRTC Call: Direct peer-to-peer connection, ultra-low latency (<1s)
* - Manifest: CDN-delivered, higher latency but more scalable
*
* 2. CALL LIFECYCLE:
* - Not joined → Join call → Connected → View peers → Leave call
*
* 3. PEER MANAGEMENT:
* - Automatically displays all broadcasting peers
* - Updates when peers join or leave
* - Handles multiple simultaneous broadcasters
*
* USE CASES:
* - Live interactive streaming (gaming, talk shows)
* - Real-time collaboration
* - Live auctions or events requiring immediate feedback
* - Video conferencing
* - Any scenario where sub-second latency is critical
*
* @example
* ```tsx
* <ViewAStream
* callId="call-123"
* backendEndpoint="https://api.example.com"
* token="your-auth-token"
* />
* ```
*/


import React, { useState } from "react";


/**
* IMPORT VIDEO CLIENT CORE LIBRARIES
*
* These imports provide all the building blocks for joining and viewing calls:
* - components: Pre-built UI components (buttons, badges, controls)
* - types: TypeScript type definitions
* - hooks: Custom hooks for video functionality
* - context: React Context providers for sharing state
*/
import { components, types, hooks, context } from "@video/video-client-react";

/**
* EXTRACT PRE-BUILT COMPONENTS AND HOOKS
*
* - JoinCallButton: Button component that joins a call when clicked
* - EndCallButton: Button component that leaves/ends a call when clicked
* - useAuthClient: Hook to create authentication client
* - CallAPIProvider: Context provider that shares call instance with children
*/
const { JoinCallButton, EndCallButton } = components;
const { useAuthClient } = hooks;
const { CallAPIProvider } = context;
/**
* IMPORT PEERS COMPONENT
*
* The Peers component is a reusable component that:
* - Automatically discovers all peers (broadcasters) in the call
* - Renders a player for each broadcasting peer
* - Handles peer joining/leaving events
* - Displays peer information (name, muted status)
*
* This abstraction saves you from manually managing peer discovery
* and player creation for each broadcaster.
*/
import Peers from "../../components/Peers";

/**
* PROPS INTERFACE
*
* Configuration values needed to join and view a call.
*/
interface ViewAStreamProps {
/**
* The unique identifier of the call to join
* Usually provided by your backend or shared by the broadcaster
*/
callId: string;

/**
* Backend API endpoint URL (provided by Native Frame)
* Example: "https://api.nativeframe.com"
*/
backendEndpoint: string;

/**
* Authentication token from your auth system
* Must have "viewer" or "private-viewer" scope to join calls
*/
token: string;
}
/**
* STYLING CONFIGURATION
*
* CSS classes for customizing the player appearance.
* Passed to the Peers component which applies them to each player.
*/
const classNames = {
playerClassNames: {
videoClassName: "object-cover overflow-hidden rounded-xl",
playerContainerClassName: "relative md:w-1/2 ",
},
};


function ViewAStream({ callId, backendEndpoint, token }: ViewAStreamProps): React.ReactElement | null {
/**
* STEP 1: Manage Call State
*
* We use useState to track the call instance. The call lifecycle:
* 1. Initially null (not joined)
* 2. Set to CallAPI instance when user joins (via JoinCallButton)
* 3. Set back to null when user leaves (via EndCallButton)
*
* This state determines which UI to show:
* - null: Show JoinCallButton
* - CallAPI: Show Peers and EndCallButton
*/
const [call, setCall] = useState<types.CallAPI | null>(null);
/**
* STEP 2: Set Up Authentication
*
*/
const authClient = useAuthClient(token);


/**
* RETURN CONDITION 1: Loading State
*
* Wait for authentication to complete before showing the UI.
* The authClient will be null while initializing.
*
* In a production app, you might show:
* - A loading spinner
* - A skeleton UI
* - A splash screen
*/
if (authClient == null || callId == null) {
return <div>Loading...</div>;
}

/**
* RETURN CONDITION 2: Not Joined - Show Join Button
*
* When the user hasn't joined the call yet, show the JoinCallButton.
*
* THE JOIN FLOW:
* 1. User clicks the JoinCallButton
* 2. Button uses the provided joinCallOptions to join the call
* 3. Once joined, it calls setCall with the CallAPI instance
* 4. This triggers a re-render with call !== null
* 5. User then sees the viewing interface (RETURN CONDITION 3)
*
* JOIN CALL OPTIONS:
* - callId: Which call to join (required)
* - user: Viewer information (userId, displayName)
* - auth: Authentication client (proves permission to join)
* - backendEndpoints: Server URLs to connect to
*
* The JoinCallButton handles all the complexity of:
* - Establishing WebRTC connections
* - Negotiating media streams
* - Error handling
* - Loading states
*/
if (call == null) {
return (
<div className="w-full h-full">
<JoinCallButton
callId={callId}
joinCallOptions={{
user: { userId: "123", displayName: "John Doe" },
auth: authClient,
backendEndpoints: [backendEndpoint],
}}
setCall={setCall}
/>
</div>
);
}

/**
* RETURN CONDITION 3: Joined - Show Peers and Controls
*
* Once the user has joined the call, display:
* 1. Peers component: Shows all broadcasting peers
* 2. EndCallButton: Allows user to leave the call
*
* COMPONENT STRUCTURE:
* CallAPIProvider (makes call available to children)
* └── Container
* ├── Peers (discovers and displays all broadcasters)
* └── EndCallButton (leaves the call)
*
* HOW IT WORKS:
* 1. CallAPIProvider shares the call instance via React Context
* 2. Peers component accesses the call and discovers all broadcasting peers
* 3. For each peer, Peers creates a Player to display their stream
* 4. Peers automatically updates when broadcasters join/leave
* 5. EndCallButton disposes the call and resets state to null
*
* IMPORTANT: CallAPIProvider is required for Peers to work.
* Without it, Peers can't access the call to discover broadcasters.
*/
return (
<CallAPIProvider callAPI={call}>
<div className="w-full">
{/* Display all broadcasting peers */}
<Peers classNames={classNames}/>

{/* Leave call button */}
<div className="mt-4">
<EndCallButton onDisposed={() => setCall(null)} />
</div>
</div>
</CallAPIProvider>
);
}

/**
* EXPORT THE COMPONENT
*
* This component provides a complete viewer experience:
* - Authentication
* - Joining calls
* - Viewing all broadcasters
* - Leaving calls
*
* It's designed to be easy to use while demonstrating best practices
* for WebRTC call viewing with the video client.
*/
export default ViewAStream;

Full Code

View A Call Stream Components

// 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);
// 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 View a Call Stream component and are documented in View a Stream and Set Up A Livestream Video:

// 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;
// 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 };

Key Concepts

WebRTC Call

A WebRTC call is a real-time connection between multiple participants:

  • Ultra-low latency: Less than 1 second delay
  • Peer-to-peer: Direct connection between participants
  • Automatic discovery: New peers are automatically detected via streamAdded events
  • Authentication required: Viewers need a token with "viewer" or "private-viewer" scope

Call ID

The callId uniquely identifies a live call:

  • Generated when a broadcaster creates a call
  • Shared with viewers who want to join
  • Required parameter for joinCall()
  • Can be obtained from URL, API, or broadcaster

Stream Events

The call instance fires events when peers join/leave:

streamAdded Event:

  • Fired when a peer starts broadcasting
  • Contains the stream, peer info, and stream name
  • Viewer creates a player to display the stream

streamRemoved Event:

  • Fired when a peer stops broadcasting or leaves
  • Contains information to identify which stream was removed
  • Viewer cleans up the corresponding player

Peers Array

The peers array tracks all broadcasting participants:

  • Each peer has their own player instance
  • Each peer's video is displayed independently
  • Peers can join and leave dynamically
  • UI updates automatically as peers change

WebRTC Call Viewer vs Manifest Player

WebRTC Call Viewer:

  • Connects directly to live calls
  • Requires authentication and callId
  • Ultra-low latency (< 1 second)
  • Ideal for real-time interaction
  • Automatic peer discovery
  • Dynamic participant management

Manifest Player:

  • Uses HLS/FLV/DASH URLs
  • No authentication required
  • Higher latency (3-30 seconds)
  • Better for VOD and CDN content
  • Adaptive bitrate streaming
  • Single stream playback

Next Steps

Now that you have a basic call viewer, you can:

  • Customize the Peers component styling
  • Add custom overlays for each peer
  • Implement additional player controls
  • Handle peer events for notifications