Screenshare
Add screen sharing capabilities to video calls.
Screenshare functionality allows participants in a video call to broadcast their screen as a separate stream. This is essential for presentations, demos, code reviews, and collaborative work. The screenshare is broadcast as a distinct stream (separate from the camera stream), allowing viewers to subscribe to either or both streams independently.
This implementation demonstrates how to add screenshare capabilities to a video call using the useScreensharePlayer hook from @video/video-client-react.
- React
Prerequisites
This guide assumes you have a basic understanding of setting up an <PreviewPlayer/> with @video/video-client-react. We will be focusing on the key differences for creating an echo pillarbox, specifically:
- How to configure the
mediaStreamControllerto capture a screenshare
For foundational concepts, please review:
- The
usePreviewPlayeranduseAuthClienthooks. - The
useCallControlscustom hook. - The
PreviewPlayercomponent for device controls.
The screenshare implementation uses:
- useScreensharePlayer Hook: Manages the complete screenshare lifecycle
- Separate Stream Architecture: Screenshare broadcasts as "screenshare" stream, camera as "default" stream
- Browser Screen Capture API: Triggers native screen selection dialog
- Independent Management: Start/stop screenshare without affecting the main camera broadcast
- Error Handling: Gracefully handles user cancellation and browser stop button
Screenshare Component
Imports
Key imports explained:
- The
Videocomponent for displaying the screenshare preview - useScreensharePlayer - Specialized hook for screenshare functionality that creates the screenshare media stream controller and player
import {components, context, hooks, types } from "@video/video-client-react";
// Extract Video component for displaying the screenshare
const { Video } = components;
// Extract context providers for passing down video client instances
const { MediaStreamControllerAPIProvider, PlayerAPIProvider } = context;
// Extract the specialized screenshare hook
const { useScreensharePlayer } = hooks;
Props
The component requires an active call instance to broadcast the screenshare to:
/**
* Props interface for Screenshare component
*/
interface ScreenshareProps {
/** Optional custom class names to override default styles */
classNames?: Partial<ScreenshareClassNames>;
/** The active call instance to broadcast screenshare to */
call: types.CallAPI;
}
State
Manage state for the broadcast and UI:
State variables:
broadcast: Tracks the active screenshare broadcast to properly dispose it laterrequestedScreenshare: Controls UI state (button vs video player)videoElement: Reference to the screenshare preview video element
/**
* State management for broadcast instance
* Tracks the active screenshare broadcast to properly dispose it later
*/
const [broadcast, setBroadcast] = useState<types.BroadcastAPI | null>(null);
/**
* State tracking whether user has requested screenshare
* Used to show appropriate UI (start button vs video player)
*/
const [requestedScreenshare, setRequestedScreenshare] = useState(false);
/**
* Reference to the video element for the screenshare preview
* Currently unused but available for future enhancements
*/
const videoElement = useRef<HTMLVideoElement>(null);
Hooks
Use the useScreensharePlayer hook to manage screenshare resources:
This hook provides:
mediaStreamController: Controls the screen capture streamscreensharePlayer: The player instance for the screenshare previewstartScreenshare: Function to initiate screen capturestopScreenshare: Function to end screen capture
Key features:
- Automatically sets
videoDeviceIdto "screencapture" - Creates a player for local preview
- Handles resource cleanup
- Supports callback pattern for custom logic
/**
* Initialize the screenshare hook
* - mediaStreamController: Manages the screen capture stream
* - screensharePlayer: Displays the local preview of the screenshare
* - startScreenshare: Function to initiate screen capture
* - stopScreenshare: Function to end screen capture
*/
const { mediaStreamController, screensharePlayer, startScreenshare, stopScreenshare } = useScreensharePlayer();
Click Handlers
Start Screenshare
When startScreenshare is called:
-
Browser displays native screen selection dialog
-
User chooses entire screen, window, or browser tab
-
Browser provides a MediaStream with the screen content
-
The callback receives the configured media stream controller
-
You can then broadcast the stream to the call
The start screenshare flow:
-
Mark screenshare as requested (triggers loading state)
-
Call
startScreensharewhich triggers the browser's screen selection dialog -
After user selects a screen, the callback receives the media stream controller
-
Listen for device errors (e.g., user clicks "Stop Sharing" in browser UI)
-
Create a broadcast on the call with stream name "screenshare"
-
Handle any errors by cleaning up
Stop Screenshare
The stop screenshare flow:
- Mark screenshare as not requested (triggers button state)
- Dispose the broadcast to stop streaming to peers
- Call
stopScreenshareto release screen capture resources
Independent Stream Management
Screenshare operates independently from the main camera broadcast:
- Starting screenshare doesn't affect the camera broadcast
- Stopping screenshare doesn't affect the camera broadcast
- Each stream can be controlled separately
- Viewers can subscribe to one or both streams
/**
* Handles the start screenshare flow
*
* Process:
* 1. Mark screenshare as requested
* 2. Trigger browser screen capture dialog via startScreenshare
* 3. Once user selects a screen, create a broadcast on the call
* 4. Listen for media device errors (e.g., user stops sharing via browser UI)
* 5. Handle any errors by cleaning up the screenshare
*
* The callback receives the media stream controller and player after successful
* screen capture, allowing us to broadcast it to the call.
*/
const handleStartScreenshare = useCallback(async () => {
setRequestedScreenshare(true);
// Callback invoked after user selects a screen to share
const callback = async (msc: types.MediaStreamControllerAPI, _player: types.PlayerAPI) => {
// Listen for device errors (e.g., user clicked "Stop Sharing" in browser)
msc.on("error", (error) => {
if (error.code === types.ErrorCode.MediaDeviceChangingFailed) {
handleStopScreenshare();
}
});
// Create a new broadcast on the call with the screenshare stream
// Stream name "screenshare" distinguishes it from the main camera stream
const newBroadcast = await call.broadcast(msc, {
streamName: "screenshare",
});
setBroadcast(newBroadcast);
};
try {
// Initiate screen capture - this triggers the browser's screen selection UI
await startScreenshare(callback);
} catch (error) {
// If user cancels or an error occurs, clean up
handleStopScreenshare();
}
}, [startScreenshare, call, setBroadcast, setRequestedScreenshare]);
/**
* Handles the stop screenshare flow
*
* Process:
* 1. Mark screenshare as not requested
* 2. Dispose the broadcast to stop sending the screenshare to peers
* 3. Stop the screen capture via stopScreenshare hook
*
* This ensures proper cleanup of all resources and notifies the call
* that the screenshare stream is no longer available.
*/
const handleStopScreenshare = useCallback(async () => {
setRequestedScreenshare(false);
// Callback to dispose the broadcast before stopping screen capture
const callback = async () => {
if (broadcast == null) return;
// Dispose the broadcast to stop streaming to peers
broadcast.dispose("stopScreenshareCallback");
setBroadcast(null);
};
// Stop the screen capture and release resources
await stopScreenshare(callback);
}, [stopScreenshare, broadcast, setBroadcast, setRequestedScreenshare]);
Render UI
Render the screenshare interface with video preview and controls.
The component renders three states:
- Initial State: "Start Screenshare" button
- Loading State: "Loading..." message while waiting for screen selection
- Active State: Video preview with "Stop Screenshare" button
/**
* Render Condition 1: Initial State
* Show "Start Screenshare" button when screenshare is not active
*/
if (!requestedScreenshare) {
return (
<div className={mergedClassNames?.wrapperClassName}>
<button className={mergedClassNames?.buttonClassName} onClick={handleStartScreenshare}>Start Screenshare</button>
</div>
)
}
/**
* Render Condition 2: Loading State
* Show loading message while waiting for screen capture to initialize
* This occurs between user clicking "Start" and selecting a screen
*/
if (mediaStreamController == null || screensharePlayer == null) {
return <div className={mergedClassNames?.wrapperClassName}>Loading...</div>;
}
/**
* Render Condition 3: Active Screenshare
* Show the screenshare preview video and stop button
*
* The MediaStreamControllerAPIProvider and PlayerAPIProvider make the
* screenshare instances available to the Video component via React context.
*/
return (
<div className={mergedClassNames?.wrapperClassName}>
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<PlayerAPIProvider playerAPI={screensharePlayer}>
<div className={mergedClassNames.wrapperClassName}>
<div className={mergedClassNames.videoContainerClassName}>
{/* Display the screenshare video preview */}
<Video ref={videoElement} className={mergedClassNames.videoClassName} />
{/* Button to stop the screenshare */}
<button className={mergedClassNames?.buttonClassName} onClick={handleStopScreenshare}>Stop Screenshare</button>
</div>
</div>
</PlayerAPIProvider>
</MediaStreamControllerAPIProvider>
</div>
);
}
Full Component Code
// in Screenshare.tsx
/**
* Screenshare Component
*
* A self-contained component that manages screenshare functionality in a video call.
* This component handles the complete lifecycle of screensharing including:
* - Requesting screen capture from the browser
* - Broadcasting the screen as a separate stream
* - Managing the screenshare player for preview
* - Gracefully handling errors and cleanup
*
* The screenshare is broadcast as a separate stream named "screenshare" to distinguish
* it from the main camera broadcast. This allows viewers to subscribe to either or both streams.
*
* @example
* ```tsx
* <Screenshare
* call={callInstance}
* classNames={{
* wrapperClassName: "custom-wrapper",
* buttonClassName: "custom-button"
* }}
* />
* ```
*/
import React, { memo, useState, useRef, useCallback } from "react";
import {components, context, hooks, types } from "@video/video-client-react";
// Extract Video component for displaying the screenshare
const { Video } = components;
// Extract context providers for passing down video client instances
const { MediaStreamControllerAPIProvider, PlayerAPIProvider } = context;
// Extract the specialized screenshare hook
const { useScreensharePlayer } = hooks;
/**
* CSS class names interface for styling customization
* Allows consumers to override default styles
*/
interface ScreenshareClassNames {
wrapperClassName: string;
videoContainerClassName: string;
videoClassName: string;
controlBarClassName: string;
controlBarItemClassName: string;
}
/**
* Default styling configuration
* Uses Tailwind CSS classes for responsive layout
*/
const defaultClassNames = {
wrapperClassName: "gap-4 flex flex-row",
videoContainerClassName: "md:w-1/2",
videoClassName: "w-full h-full",
buttonClassName: "bg-blue-500 text-white px-4 py-2 rounded-md cursor-pointer hover:bg-blue-600 transition-all duration-300 my-2",
};
/**
* Props interface for Screenshare component
*/
interface ScreenshareProps {
/** Optional custom class names to override default styles */
classNames?: Partial<ScreenshareClassNames>;
/** The active call instance to broadcast screenshare to */
call: types.CallAPI;
}
function Screenshare({classNames, call}: ScreenshareProps): JSX.Element {
// Merge custom class names with defaults
const mergedClassNames = { ...defaultClassNames,...classNames };
/**
* Initialize the screenshare hook
* - mediaStreamController: Manages the screen capture stream
* - screensharePlayer: Displays the local preview of the screenshare
* - startScreenshare: Function to initiate screen capture
* - stopScreenshare: Function to end screen capture
*/
const { mediaStreamController, screensharePlayer, startScreenshare, stopScreenshare } = useScreensharePlayer();
/**
* State management for broadcast instance
* Tracks the active screenshare broadcast to properly dispose it later
*/
const [broadcast, setBroadcast] = useState<types.BroadcastAPI | null>(null);
/**
* State tracking whether user has requested screenshare
* Used to show appropriate UI (start button vs video player)
*/
const [requestedScreenshare, setRequestedScreenshare] = useState(false);
/**
* Reference to the video element for the screenshare preview
* Currently unused but available for future enhancements
*/
const videoElement = useRef<HTMLVideoElement>(null);
/**
* Handles the start screenshare flow
*
* Process:
* 1. Mark screenshare as requested
* 2. Trigger browser screen capture dialog via startScreenshare
* 3. Once user selects a screen, create a broadcast on the call
* 4. Listen for media device errors (e.g., user stops sharing via browser UI)
* 5. Handle any errors by cleaning up the screenshare
*
* The callback receives the media stream controller and player after successful
* screen capture, allowing us to broadcast it to the call.
*/
const handleStartScreenshare = useCallback(async () => {
setRequestedScreenshare(true);
// Callback invoked after user selects a screen to share
const callback = async (msc: types.MediaStreamControllerAPI, _player: types.PlayerAPI) => {
// Listen for device errors (e.g., user clicked "Stop Sharing" in browser)
msc.on("error", (error) => {
if (error.code === types.ErrorCode.MediaDeviceChangingFailed) {
handleStopScreenshare();
}
});
// Create a new broadcast on the call with the screenshare stream
// Stream name "screenshare" distinguishes it from the main camera stream
const newBroadcast = await call.broadcast(msc, {
streamName: "screenshare",
});
setBroadcast(newBroadcast);
};
try {
// Initiate screen capture - this triggers the browser's screen selection UI
await startScreenshare(callback);
} catch (error) {
// If user cancels or an error occurs, clean up
handleStopScreenshare();
}
}, [startScreenshare, call, setBroadcast, setRequestedScreenshare]);
/**
* Handles the stop screenshare flow
*
* Process:
* 1. Mark screenshare as not requested
* 2. Dispose the broadcast to stop sending the screenshare to peers
* 3. Stop the screen capture via stopScreenshare hook
*
* This ensures proper cleanup of all resources and notifies the call
* that the screenshare stream is no longer available.
*/
const handleStopScreenshare = useCallback(async () => {
setRequestedScreenshare(false);
// Callback to dispose the broadcast before stopping screen capture
const callback = async () => {
if (broadcast == null) return;
// Dispose the broadcast to stop streaming to peers
broadcast.dispose("stopScreenshareCallback");
setBroadcast(null);
};
// Stop the screen capture and release resources
await stopScreenshare(callback);
}, [stopScreenshare, broadcast, setBroadcast, setRequestedScreenshare]);
/**
* Render Condition 1: Initial State
* Show "Start Screenshare" button when screenshare is not active
*/
if (!requestedScreenshare) {
return (
<div className={mergedClassNames?.wrapperClassName}>
<button className={mergedClassNames?.buttonClassName} onClick={handleStartScreenshare}>Start Screenshare</button>
</div>
)
}
/**
* Render Condition 2: Loading State
* Show loading message while waiting for screen capture to initialize
* This occurs between user clicking "Start" and selecting a screen
*/
if (mediaStreamController == null || screensharePlayer == null) {
return <div className={mergedClassNames?.wrapperClassName}>Loading...</div>;
}
/**
* Render Condition 3: Active Screenshare
* Show the screenshare preview video and stop button
*
* The MediaStreamControllerAPIProvider and PlayerAPIProvider make the
* screenshare instances available to the Video component via React context.
*/
return (
<div className={mergedClassNames?.wrapperClassName}>
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<PlayerAPIProvider playerAPI={screensharePlayer}>
<div className={mergedClassNames.wrapperClassName}>
<div className={mergedClassNames.videoContainerClassName}>
{/* Display the screenshare video preview */}
<Video ref={videoElement} className={mergedClassNames.videoClassName} />
{/* Button to stop the screenshare */}
<button className={mergedClassNames?.buttonClassName} onClick={handleStopScreenshare}>Stop Screenshare</button>
</div>
</div>
</PlayerAPIProvider>
</MediaStreamControllerAPIProvider>
</div>
);
}
/**
* Export as memoized component to prevent unnecessary re-renders
* The component only re-renders when props change
*/
export default memo(Screenshare);
Group Call Owner With Screenshare Component
To integrate screenshare into a complete group call interface, you can create a component that combines:
- Main camera broadcast
- Screenshare functionality
- Peer viewing
This component should look almost identical to the Group Call Owner Component, except for the addition of the <Screenshare/>.
// in GroupCallOwnerWithScreenshare.tsx
/**
* GroupCallOwnerWithScreenshare Component
*
* This component demonstrates a complete implementation of a group video call owner
* with integrated screenshare capabilities. It combines the main camera broadcast,
* peer viewing, and screenshare functionality in a unified interface.
*
* Key Features:
* - Owner can broadcast their camera to the call
* - Owner can start/stop screenshare as a separate stream
* - View all connected peers
* - Real-time connection management with proper cleanup
*
* @example
* ```tsx
* <GroupCallOwnerWithScreenshare
* backendEndpoint="https://api.example.com"
* token="your-auth-token"
* streamKey="unique-stream-key"
* cbCallId={(callId) => console.log('Call ID:', callId)}
* />
* ```
*/
import React, { useEffect } from "react";
import { hooks, context } from "@video/video-client-react";
import { useCallControls } from "../../components/CallControls";
import Peers from "../../components/Peers";
import PreviewPlayer from "../../components/PreviewPlayer";
import Screenshare from "./Screenshare";
// Extract hooks from video-client-react for authentication and preview functionality
const { useAuthClient, usePreviewPlayer } = hooks;
// Extract context providers for passing down video client instances
const { MediaStreamControllerAPIProvider, PlayerAPIProvider, CallAPIProvider } = context;
/**
* CSS class name configuration for the component layout
* Organized in a modular structure for easy customization
*/
const classNames = {
wrapperClassName: "flex flex-row gap-4",
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",
},
topPanelClassName: "flex flex-row gap-4",
bottomPanelClassName: "flex flex-row gap-4",
};
/**
* Props interface for GroupCallOwnerWithScreenshare component
*/
interface GroupCallOwnerWithScreenshareProps {
/** Backend API endpoint for the video service */
backendEndpoint: string;
/** Authentication token for the user */
token: string;
/** Unique stream key identifying this broadcast */
streamKey: string;
/** Callback function invoked when the call ID is available */
cbCallId: (callId: string) => void;
}
function GroupCallOwnerWithScreenshare({ backendEndpoint, token, streamKey, cbCallId }: GroupCallOwnerWithScreenshareProps): React.ReactElement | null {
/**
* Initialize the media stream controller and preview player
* These handle the main camera broadcast for the call owner
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});
/**
* Create an authenticated client using the provided token
*/
const authClient = useAuthClient(token);
/**
* Configure call options with authentication, backend endpoints, and user info
* The default broadcast stream is named "default" to distinguish it from screenshare
*/
const callOptions = { streamKey, user: { userId: "123", displayName: "John Doe" }, backendEndpoints: [backendEndpoint], auth: authClient, }
const broadcastOptions = {streamName: "default"};
/**
* Custom hook that manages call lifecycle and provides control UI
*/
const { renderControls, call, setCall, setBroadcast } = useCallControls({callOptions, broadcastOptions, type: "owner"});
/**
* Notify parent component when call ID becomes available
* This allows parent components to track the call state
*/
useEffect(() => {
cbCallId(call?.id ?? "");
}, [call, cbCallId]);
/**
* Cleanup effect for the call instance
* Ensures proper disposal of resources when the component unmounts
* or when the call changes
*/
useEffect(() => {
return () => {
if (call != null) {
call.dispose("Disposed by useEffect 1 return - Broadcaster");
setCall(null);
setBroadcast(null);
}
};
}, [call]);
/**
* Cleanup effect for the media stream controller
* Releases camera and microphone resources when component unmounts
*/
useEffect(() => {
return () => {
if (mediaStreamController != null) {
mediaStreamController.dispose("Disposed by useEffect");
}
};
}, [mediaStreamController]);
/**
* Cleanup effect for the preview player
* Stops the preview video playback when component unmounts
*/
useEffect(() => {
return () => {
if (previewPlayer != null) {
previewPlayer.dispose("Disposed by useEffect");
}
};
}, [previewPlayer]);
/**
* Return Condition 1: Loading State
* Show a loading screen while waiting for media resources to initialize
*/
if (!mediaStreamController || !previewPlayer) {
return <div className={classNames.loadingScreenClassName} />;
}
/**
* Return Condition 2: Main UI
* Render the complete interface with three main sections:
* 1. Left Panel: Owner's camera preview with controls
* 2. Right Panel: Screenshare interface
* 3. Bottom Panel: Connected peers grid
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<div className={classNames.wrapperClassName}>
{/* Top section with owner controls and screenshare */}
<div className={classNames.topPanelClassName}>
{/* Left panel: Owner's camera preview and broadcast controls */}
<div className={classNames.leftPanelClassName}>
<PlayerAPIProvider playerAPI={previewPlayer}>
{/* Video encoder displaying the owner's camera feed */}
<PreviewPlayer classNames={classNames.previewPlayerClassNames}/>
<span className={classNames.callOwnerClassName}>Conference Owner</span>
{/* Render start/stop broadcast buttons and other controls */}
{renderControls()}
</PlayerAPIProvider>
</div>
{/* Right panel: Screenshare component */}
<div className={classNames.rightPanelClassName}>
{/* Only render screenshare controls once the call is active */}
{call && <Screenshare call={call}/>}
</div>
</div>
{/* Bottom section: Grid of connected peer videos */}
<div className={classNames.bottomPanelClassName}>
<div className={classNames.peersWrapperClassName}>
{/* Provide call context to peers component for stream management */}
{call != null &&
<CallAPIProvider callAPI={call}>
<Peers/>
</CallAPIProvider>
}
</div>
</div>
</div>
</MediaStreamControllerAPIProvider>
);
}
export default GroupCallOwnerWithScreenshare;
Full Code
Screenshare Components
// in Screenshare.tsx
/**
* Screenshare Component
*
* A self-contained component that manages screenshare functionality in a video call.
* This component handles the complete lifecycle of screensharing including:
* - Requesting screen capture from the browser
* - Broadcasting the screen as a separate stream
* - Managing the screenshare player for preview
* - Gracefully handling errors and cleanup
*
* The screenshare is broadcast as a separate stream named "screenshare" to distinguish
* it from the main camera broadcast. This allows viewers to subscribe to either or both streams.
*
* @example
* ```tsx
* <Screenshare
* call={callInstance}
* classNames={{
* wrapperClassName: "custom-wrapper",
* buttonClassName: "custom-button"
* }}
* />
* ```
*/
import React, { memo, useState, useRef, useCallback } from "react";
import {components, context, hooks, types } from "@video/video-client-react";
// Extract Video component for displaying the screenshare
const { Video } = components;
// Extract context providers for passing down video client instances
const { MediaStreamControllerAPIProvider, PlayerAPIProvider } = context;
// Extract the specialized screenshare hook
const { useScreensharePlayer } = hooks;
/**
* CSS class names interface for styling customization
* Allows consumers to override default styles
*/
interface ScreenshareClassNames {
wrapperClassName: string;
videoContainerClassName: string;
videoClassName: string;
controlBarClassName: string;
controlBarItemClassName: string;
}
/**
* Default styling configuration
* Uses Tailwind CSS classes for responsive layout
*/
const defaultClassNames = {
wrapperClassName: "gap-4 flex flex-row",
videoContainerClassName: "md:w-1/2",
videoClassName: "w-full h-full",
buttonClassName: "bg-blue-500 text-white px-4 py-2 rounded-md cursor-pointer hover:bg-blue-600 transition-all duration-300 my-2",
};
/**
* Props interface for Screenshare component
*/
interface ScreenshareProps {
/** Optional custom class names to override default styles */
classNames?: Partial<ScreenshareClassNames>;
/** The active call instance to broadcast screenshare to */
call: types.CallAPI;
}
function Screenshare({classNames, call}: ScreenshareProps): JSX.Element {
// Merge custom class names with defaults
const mergedClassNames = { ...defaultClassNames,...classNames };
/**
* Initialize the screenshare hook
* - mediaStreamController: Manages the screen capture stream
* - screensharePlayer: Displays the local preview of the screenshare
* - startScreenshare: Function to initiate screen capture
* - stopScreenshare: Function to end screen capture
*/
const { mediaStreamController, screensharePlayer, startScreenshare, stopScreenshare } = useScreensharePlayer();
/**
* State management for broadcast instance
* Tracks the active screenshare broadcast to properly dispose it later
*/
const [broadcast, setBroadcast] = useState<types.BroadcastAPI | null>(null);
/**
* State tracking whether user has requested screenshare
* Used to show appropriate UI (start button vs video player)
*/
const [requestedScreenshare, setRequestedScreenshare] = useState(false);
/**
* Reference to the video element for the screenshare preview
* Currently unused but available for future enhancements
*/
const videoElement = useRef<HTMLVideoElement>(null);
/**
* Handles the start screenshare flow
*
* Process:
* 1. Mark screenshare as requested
* 2. Trigger browser screen capture dialog via startScreenshare
* 3. Once user selects a screen, create a broadcast on the call
* 4. Listen for media device errors (e.g., user stops sharing via browser UI)
* 5. Handle any errors by cleaning up the screenshare
*
* The callback receives the media stream controller and player after successful
* screen capture, allowing us to broadcast it to the call.
*/
const handleStartScreenshare = useCallback(async () => {
setRequestedScreenshare(true);
// Callback invoked after user selects a screen to share
const callback = async (msc: types.MediaStreamControllerAPI, _player: types.PlayerAPI) => {
// Listen for device errors (e.g., user clicked "Stop Sharing" in browser)
msc.on("error", (error) => {
if (error.code === types.ErrorCode.MediaDeviceChangingFailed) {
handleStopScreenshare();
}
});
// Create a new broadcast on the call with the screenshare stream
// Stream name "screenshare" distinguishes it from the main camera stream
const newBroadcast = await call.broadcast(msc, {
streamName: "screenshare",
});
setBroadcast(newBroadcast);
};
try {
// Initiate screen capture - this triggers the browser's screen selection UI
await startScreenshare(callback);
} catch (error) {
// If user cancels or an error occurs, clean up
handleStopScreenshare();
}
}, [startScreenshare, call, setBroadcast, setRequestedScreenshare]);
/**
* Handles the stop screenshare flow
*
* Process:
* 1. Mark screenshare as not requested
* 2. Dispose the broadcast to stop sending the screenshare to peers
* 3. Stop the screen capture via stopScreenshare hook
*
* This ensures proper cleanup of all resources and notifies the call
* that the screenshare stream is no longer available.
*/
const handleStopScreenshare = useCallback(async () => {
setRequestedScreenshare(false);
// Callback to dispose the broadcast before stopping screen capture
const callback = async () => {
if (broadcast == null) return;
// Dispose the broadcast to stop streaming to peers
broadcast.dispose("stopScreenshareCallback");
setBroadcast(null);
};
// Stop the screen capture and release resources
await stopScreenshare(callback);
}, [stopScreenshare, broadcast, setBroadcast, setRequestedScreenshare]);
/**
* Render Condition 1: Initial State
* Show "Start Screenshare" button when screenshare is not active
*/
if (!requestedScreenshare) {
return (
<div className={mergedClassNames?.wrapperClassName}>
<button className={mergedClassNames?.buttonClassName} onClick={handleStartScreenshare}>Start Screenshare</button>
</div>
)
}
/**
* Render Condition 2: Loading State
* Show loading message while waiting for screen capture to initialize
* This occurs between user clicking "Start" and selecting a screen
*/
if (mediaStreamController == null || screensharePlayer == null) {
return <div className={mergedClassNames?.wrapperClassName}>Loading...</div>;
}
/**
* Render Condition 3: Active Screenshare
* Show the screenshare preview video and stop button
*
* The MediaStreamControllerAPIProvider and PlayerAPIProvider make the
* screenshare instances available to the Video component via React context.
*/
return (
<div className={mergedClassNames?.wrapperClassName}>
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<PlayerAPIProvider playerAPI={screensharePlayer}>
<div className={mergedClassNames.wrapperClassName}>
<div className={mergedClassNames.videoContainerClassName}>
{/* Display the screenshare video preview */}
<Video ref={videoElement} className={mergedClassNames.videoClassName} />
{/* Button to stop the screenshare */}
<button className={mergedClassNames?.buttonClassName} onClick={handleStopScreenshare}>Stop Screenshare</button>
</div>
</div>
</PlayerAPIProvider>
</MediaStreamControllerAPIProvider>
</div>
);
}
/**
* Export as memoized component to prevent unnecessary re-renders
* The component only re-renders when props change
*/
export default memo(Screenshare);
// in GroupCallOwnerWithScreenshare.tsx
/**
* GroupCallOwnerWithScreenshare Component
*
* This component demonstrates a complete implementation of a group video call owner
* with integrated screenshare capabilities. It combines the main camera broadcast,
* peer viewing, and screenshare functionality in a unified interface.
*
* Key Features:
* - Owner can broadcast their camera to the call
* - Owner can start/stop screenshare as a separate stream
* - View all connected peers
* - Real-time connection management with proper cleanup
*
* @example
* ```tsx
* <GroupCallOwnerWithScreenshare
* backendEndpoint="https://api.example.com"
* token="your-auth-token"
* streamKey="unique-stream-key"
* cbCallId={(callId) => console.log('Call ID:', callId)}
* />
* ```
*/
import React, { useEffect } from "react";
import { hooks, context } from "@video/video-client-react";
import { useCallControls } from "../../components/CallControls";
import Peers from "../../components/Peers";
import PreviewPlayer from "../../components/PreviewPlayer";
import Screenshare from "./Screenshare";
// Extract hooks from video-client-react for authentication and preview functionality
const { useAuthClient, usePreviewPlayer } = hooks;
// Extract context providers for passing down video client instances
const { MediaStreamControllerAPIProvider, PlayerAPIProvider, CallAPIProvider } = context;
/**
* CSS class name configuration for the component layout
* Organized in a modular structure for easy customization
*/
const classNames = {
wrapperClassName: "flex flex-row gap-4",
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",
},
topPanelClassName: "flex flex-row gap-4",
bottomPanelClassName: "flex flex-row gap-4",
};
/**
* Props interface for GroupCallOwnerWithScreenshare component
*/
interface GroupCallOwnerWithScreenshareProps {
/** Backend API endpoint for the video service */
backendEndpoint: string;
/** Authentication token for the user */
token: string;
/** Unique stream key identifying this broadcast */
streamKey: string;
/** Callback function invoked when the call ID is available */
cbCallId: (callId: string) => void;
}
function GroupCallOwnerWithScreenshare({ backendEndpoint, token, streamKey, cbCallId }: GroupCallOwnerWithScreenshareProps): React.ReactElement | null {
/**
* Initialize the media stream controller and preview player
* These handle the main camera broadcast for the call owner
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});
/**
* Create an authenticated client using the provided token
*/
const authClient = useAuthClient(token);
/**
* Configure call options with authentication, backend endpoints, and user info
* The default broadcast stream is named "default" to distinguish it from screenshare
*/
const callOptions = { streamKey, user: { userId: "123", displayName: "John Doe" }, backendEndpoints: [backendEndpoint], auth: authClient, }
const broadcastOptions = {streamName: "default"};
/**
* Custom hook that manages call lifecycle and provides control UI
*/
const { renderControls, call, setCall, setBroadcast } = useCallControls({callOptions, broadcastOptions, type: "owner"});
/**
* Notify parent component when call ID becomes available
* This allows parent components to track the call state
*/
useEffect(() => {
cbCallId(call?.id ?? "");
}, [call, cbCallId]);
/**
* Cleanup effect for the call instance
* Ensures proper disposal of resources when the component unmounts
* or when the call changes
*/
useEffect(() => {
return () => {
if (call != null) {
call.dispose("Disposed by useEffect 1 return - Broadcaster");
setCall(null);
setBroadcast(null);
}
};
}, [call]);
/**
* Cleanup effect for the media stream controller
* Releases camera and microphone resources when component unmounts
*/
useEffect(() => {
return () => {
if (mediaStreamController != null) {
mediaStreamController.dispose("Disposed by useEffect");
}
};
}, [mediaStreamController]);
/**
* Cleanup effect for the preview player
* Stops the preview video playback when component unmounts
*/
useEffect(() => {
return () => {
if (previewPlayer != null) {
previewPlayer.dispose("Disposed by useEffect");
}
};
}, [previewPlayer]);
/**
* Return Condition 1: Loading State
* Show a loading screen while waiting for media resources to initialize
*/
if (!mediaStreamController || !previewPlayer) {
return <div className={classNames.loadingScreenClassName} />;
}
/**
* Return Condition 2: Main UI
* Render the complete interface with three main sections:
* 1. Left Panel: Owner's camera preview with controls
* 2. Right Panel: Screenshare interface
* 3. Bottom Panel: Connected peers grid
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<div className={classNames.wrapperClassName}>
{/* Top section with owner controls and screenshare */}
<div className={classNames.topPanelClassName}>
{/* Left panel: Owner's camera preview and broadcast controls */}
<div className={classNames.leftPanelClassName}>
<PlayerAPIProvider playerAPI={previewPlayer}>
{/* Video encoder displaying the owner's camera feed */}
<PreviewPlayer classNames={classNames.previewPlayerClassNames}/>
<span className={classNames.callOwnerClassName}>Conference Owner</span>
{/* Render start/stop broadcast buttons and other controls */}
{renderControls()}
</PlayerAPIProvider>
</div>
{/* Right panel: Screenshare component */}
<div className={classNames.rightPanelClassName}>
{/* Only render screenshare controls once the call is active */}
{call && <Screenshare call={call}/>}
</div>
</div>
{/* Bottom section: Grid of connected peer videos */}
<div className={classNames.bottomPanelClassName}>
<div className={classNames.peersWrapperClassName}>
{/* Provide call context to peers component for stream management */}
{call != null &&
<CallAPIProvider callAPI={call}>
<Peers/>
</CallAPIProvider>
}
</div>
</div>
</div>
</MediaStreamControllerAPIProvider>
);
}
export default GroupCallOwnerWithScreenshare;
Supporting Components
The following components are used by the Screenshare 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);
Key Concepts Explained
Stream Names
Each broadcast on a call requires a unique stream name:
"default" - Main camera broadcast
await call.broadcast(cameraController, { streamName: "default" });
"screenshare" - Screen capture broadcast
await call.broadcast(screenshareController, { streamName: "screenshare" });
This naming convention allows viewers to distinguish between different streams from the same participant.
Next Steps
- Explore Call Lobby for more advanced patterns
- Review Broadcasting a Livestream for core broadcasting concepts