Skip to main content

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.

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 mediaStreamController to capture a screenshare

For foundational concepts, please review:

The screenshare implementation uses:

  1. useScreensharePlayer Hook: Manages the complete screenshare lifecycle
  2. Separate Stream Architecture: Screenshare broadcasts as "screenshare" stream, camera as "default" stream
  3. Browser Screen Capture API: Triggers native screen selection dialog
  4. Independent Management: Start/stop screenshare without affecting the main camera broadcast
  5. Error Handling: Gracefully handles user cancellation and browser stop button

Screenshare Component

Imports

Key imports explained:

  • The Video component 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 later
  • requestedScreenshare: 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 stream
  • screensharePlayer: The player instance for the screenshare preview
  • startScreenshare: Function to initiate screen capture
  • stopScreenshare: Function to end screen capture

Key features:

  • Automatically sets videoDeviceId to "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:

  1. Browser displays native screen selection dialog

  2. User chooses entire screen, window, or browser tab

  3. Browser provides a MediaStream with the screen content

  4. The callback receives the configured media stream controller

  5. You can then broadcast the stream to the call

    The start screenshare flow:

  6. Mark screenshare as requested (triggers loading state)

  7. Call startScreenshare which triggers the browser's screen selection dialog

  8. After user selects a screen, the callback receives the media stream controller

  9. Listen for device errors (e.g., user clicks "Stop Sharing" in browser UI)

  10. Create a broadcast on the call with stream name "screenshare"

  11. Handle any errors by cleaning up

Stop Screenshare

The stop screenshare flow:

  1. Mark screenshare as not requested (triggers button state)
  2. Dispose the broadcast to stop streaming to peers
  3. Call stopScreenshare to 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:

  1. Initial State: "Start Screenshare" button
  2. Loading State: "Loading..." message while waiting for screen selection
  3. 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