Customize Your Streaming Application
Build custom controls using the MediaStreamController API.
While Native Frame provides pre-built components for common use cases, you may need to create custom controls that match your design system or implement specialized functionality. This guide demonstrates how to build custom components that interact directly with the MediaStreamController API, giving you full control over camera, microphone, and broadcasting behavior.
- 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 customizing components, specifically:
- How to create your own components
- Interacting with the MediaStreamController context
- React to MediaStreamController events
For foundational concepts, please review:
- The
usePreviewPlayeranduseAuthClienthooks. - The
useCallControlscustom hook. - The
PreviewPlayercomponent for device controls.
Custom Button Component
Next, you'll likely want to interact with the Encoder API to handle custom functionality.
In this example, we'll create a custom button that will disable the microphone of the encoder, and allow for custom callback logic.
Imports
import React, { memo, useCallback } from "react";
import { context, hooks } from "@video/video-client-react";
/**
* IMPORT EXPLANATIONS:
*
* 1. React imports:
* - memo: Prevents unnecessary re-renders for performance
* - useCallback: Memoizes callback functions to prevent recreation
*
* 2. @video/video-client-react imports:
* - context: Provides hooks to access video client Context
* - hooks: Provides custom hooks for video client functionality
*/
// Extract the hook to access media stream controller from Context
const { useMediaStreamControllerAPI } = context;
// Extract the hook to listen for video client events
const { useEvent } = hooks;
Hooks
Use the useMediaStreamControllerAPI hook to access the MediaStreamControllerAPIContext and the useEvent hook to listen for the audioDisabled event
/**
* STEP 1: Access the Media Stream Controller
*
* The `useMediaStreamControllerAPI` hook retrieves the media stream
* controller instance from React Context. This controller manages:
* - Camera on/off state
* - Microphone on/off state
* - Device selection (which camera/mic to use)
* - Media permissions
*
* IMPORTANT: This hook must be used inside a MediaStreamControllerAPIProvider.
* The parent component (CustomizeYourStreamingApplication) provides this context.
*/
const msc = useMediaStreamControllerAPI();
/**
* STEP 2: Listen for Audio State Changes
*
* The `useEvent` hook subscribes to events from the media stream controller.
* When you call this hook with "audioDisabled", the component will re-render
* whenever the audio enabled/disabled state changes.
*
* Why is this useful?
* - Your UI can update to reflect the current audio state
* - The button can show different states (muted/unmuted)
* - You can react to audio changes from other components
*
* The event name "audioDisabled" matches the property on the controller.
* Other available events: "videoDisabled", "deviceError", etc.
*/
useEvent(msc, "audioDisabled");
Click Hanlder
/**
* STEP 3: Handle Button Click
*
* This function runs when the user clicks the button.
* It demonstrates how to control the microphone programmatically.
*
* What happens:
* 1. Set audioDisabled to true (mutes the microphone)
* 2. You can add custom logic here (analytics, notifications, etc.)
*
* useCallback optimization:
* - Prevents the function from being recreated on every render
* - Only recreates if 'msc' changes
* - Improves performance, especially important for frequently rendered components
*
* CUSTOMIZATION IDEAS:
* - Toggle instead of just disabling: msc.audioDisabled = !msc.audioDisabled
* - Show a toast notification when muting
* - Log analytics events
* - Trigger other UI changes
* - Control video: msc.videoDisabled = true
* - Change devices: msc.audioDeviceId = "device-id"
*/
const handleClick = useCallback(() => {
// Disable audio (mute the microphone)
msc.audioDisabled = true;
// Add your custom logic here!
// Examples:
// - console.log('Audio muted');
// - showNotification('Microphone muted');
// - trackAnalyticsEvent('audio_muted');
}, [msc]);
Full Component Code
// CustomButton.tsx
/**
* CustomButton Component
*
* OVERVIEW:
* This component demonstrates how to create custom UI controls that
* interact with the video client's media stream controller.
*
* WHAT YOU'LL LEARN:
* - How to access video client state from context
* - How to listen for and react to video client events
* - How to control camera and microphone programmatically
* - How to create custom UI components that integrate with the video client
*
* KEY CONCEPTS:
* 1. CONTEXT: Accessing shared video client instances from React Context
* 2. EVENTS: Listening for state changes in the video client
* 3. MEDIA CONTROL: Programmatically controlling camera/microphone
*
* USE CASES:
* - Creating custom styled buttons
* - Adding additional functionality beyond built-in components
* - Integrating video controls with your existing UI design system
* - Implementing complex control logic (e.g., toggle multiple devices)
*
* @example
* ```tsx
* // Must be used inside MediaStreamControllerAPIProvider
* <MediaStreamControllerAPIProvider mediaStreamControllerAPI={msc}>
* <CustomButton />
* </MediaStreamControllerAPIProvider>
* ```
*/
import React, { memo, useCallback } from "react";
import { context, hooks } from "@video/video-client-react";
/**
* IMPORT EXPLANATIONS:
*
* 1. React imports:
* - memo: Prevents unnecessary re-renders for performance
* - useCallback: Memoizes callback functions to prevent recreation
*
* 2. @video/video-client-react imports:
* - context: Provides hooks to access video client Context
* - hooks: Provides custom hooks for video client functionality
*/
// Extract the hook to access media stream controller from Context
const { useMediaStreamControllerAPI } = context;
// Extract the hook to listen for video client events
const { useEvent } = hooks;
function CustomButton(): JSX.Element | null {
/**
* STEP 1: Access the Media Stream Controller
*
* The `useMediaStreamControllerAPI` hook retrieves the media stream
* controller instance from React Context. This controller manages:
* - Camera on/off state
* - Microphone on/off state
* - Device selection (which camera/mic to use)
* - Media permissions
*
* IMPORTANT: This hook must be used inside a MediaStreamControllerAPIProvider.
* The parent component (CustomizeYourStreamingApplication) provides this context.
*/
const msc = useMediaStreamControllerAPI();
/**
* STEP 2: Listen for Audio State Changes
*
* The `useEvent` hook subscribes to events from the media stream controller.
* When you call this hook with "audioDisabled", the component will re-render
* whenever the audio enabled/disabled state changes.
*
* Why is this useful?
* - Your UI can update to reflect the current audio state
* - The button can show different states (muted/unmuted)
* - You can react to audio changes from other components
*
* The event name "audioDisabled" matches the property on the controller.
* Other available events: "videoDisabled", "deviceError", etc.
*/
useEvent(msc, "audioDisabled");
/**
* STEP 3: Handle Button Click
*
* This function runs when the user clicks the button.
* It demonstrates how to control the microphone programmatically.
*
* What happens:
* 1. Set audioDisabled to true (mutes the microphone)
* 2. You can add custom logic here (analytics, notifications, etc.)
*
* useCallback optimization:
* - Prevents the function from being recreated on every render
* - Only recreates if 'msc' changes
* - Improves performance, especially important for frequently rendered components
*
* CUSTOMIZATION IDEAS:
* - Toggle instead of just disabling: msc.audioDisabled = !msc.audioDisabled
* - Show a toast notification when muting
* - Log analytics events
* - Trigger other UI changes
* - Control video: msc.videoDisabled = true
* - Change devices: msc.audioDeviceId = "device-id"
*/
const handleClick = useCallback(() => {
// Disable audio (mute the microphone)
msc.audioDisabled = true;
// Add your custom logic here!
// Examples:
// - console.log('Audio muted');
// - showNotification('Microphone muted');
// - trackAnalyticsEvent('audio_muted');
}, [msc]);
/**
* STEP 4: Render the Button
*
* This is a simple button with inline styles. In a real application,
* you would likely use CSS classes or a component library.
*
* CUSTOMIZATION IDEAS:
* - Change button text based on audio state:
* {msc.audioDisabled ? "Unmute" : "Mute"}
* - Add icons (mic icon, muted mic icon)
* - Apply different styling based on state
* - Add hover/active states
* - Use your design system's button component
* - Add accessibility attributes (aria-label, aria-pressed)
*
* @example Advanced Button:
* ```tsx
* <button
* onClick={handleClick}
* aria-label={msc.audioDisabled ? "Unmute microphone" : "Mute microphone"}
* aria-pressed={msc.audioDisabled}
* className={msc.audioDisabled ? "btn-muted" : "btn-active"}
* >
* {msc.audioDisabled ? <MicOffIcon /> : <MicOnIcon />}
* </button>
* ```
*/
return (
<button
type="button"
onClick={handleClick}
style={{
color: "white",
background: "green",
padding: ".5rem",
borderRadius: "5px",
}}
>
Custom Button
</button>
);
}
/**
* EXPORT WITH MEMOIZATION
*
* We wrap the component in React.memo() to prevent unnecessary re-renders.
* The component only re-renders when its props change or when events it
* listens to (via useEvent) are triggered.
*
* This is a performance optimization that's especially valuable for
* UI controls that might be rendered frequently.
*/
export default memo(CustomButton);
Broadcast Component
As mentioned above, this is almost identical to this Broadcasting Component. The only difference is the <CustomButton/> included in the return.
// #14
/**
* MAIN UI RENDERING
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<div className="w-full flex flex-col overflow-hidden mb-4">
{/* Video preview showing what your camera sees */}
<Encoder />
{/* Control buttons for managing call and broadcast */}
<div className="flex flex-row my-2 gap-2 ">
{/* Example custom button - you can add your own controls here */}
<CustomButton />
{/* Built-in controls from useCallControls hook */}
{renderControls()}
</div>
</div>
</PlayerAPIProvider>
</MediaStreamControllerAPIProvider>
);
Full Code
Customize Your Streaming Application Components
// CustomButton.tsx
/**
* CustomButton Component
*
* OVERVIEW:
* This component demonstrates how to create custom UI controls that
* interact with the video client's media stream controller.
*
* WHAT YOU'LL LEARN:
* - How to access video client state from context
* - How to listen for and react to video client events
* - How to control camera and microphone programmatically
* - How to create custom UI components that integrate with the video client
*
* KEY CONCEPTS:
* 1. CONTEXT: Accessing shared video client instances from React Context
* 2. EVENTS: Listening for state changes in the video client
* 3. MEDIA CONTROL: Programmatically controlling camera/microphone
*
* USE CASES:
* - Creating custom styled buttons
* - Adding additional functionality beyond built-in components
* - Integrating video controls with your existing UI design system
* - Implementing complex control logic (e.g., toggle multiple devices)
*
* @example
* ```tsx
* // Must be used inside MediaStreamControllerAPIProvider
* <MediaStreamControllerAPIProvider mediaStreamControllerAPI={msc}>
* <CustomButton />
* </MediaStreamControllerAPIProvider>
* ```
*/
import React, { memo, useCallback } from "react";
import { context, hooks } from "@video/video-client-react";
/**
* IMPORT EXPLANATIONS:
*
* 1. React imports:
* - memo: Prevents unnecessary re-renders for performance
* - useCallback: Memoizes callback functions to prevent recreation
*
* 2. @video/video-client-react imports:
* - context: Provides hooks to access video client Context
* - hooks: Provides custom hooks for video client functionality
*/
// Extract the hook to access media stream controller from Context
const { useMediaStreamControllerAPI } = context;
// Extract the hook to listen for video client events
const { useEvent } = hooks;
function CustomButton(): JSX.Element | null {
/**
* STEP 1: Access the Media Stream Controller
*
* The `useMediaStreamControllerAPI` hook retrieves the media stream
* controller instance from React Context. This controller manages:
* - Camera on/off state
* - Microphone on/off state
* - Device selection (which camera/mic to use)
* - Media permissions
*
* IMPORTANT: This hook must be used inside a MediaStreamControllerAPIProvider.
* The parent component (CustomizeYourStreamingApplication) provides this context.
*/
const msc = useMediaStreamControllerAPI();
/**
* STEP 2: Listen for Audio State Changes
*
* The `useEvent` hook subscribes to events from the media stream controller.
* When you call this hook with "audioDisabled", the component will re-render
* whenever the audio enabled/disabled state changes.
*
* Why is this useful?
* - Your UI can update to reflect the current audio state
* - The button can show different states (muted/unmuted)
* - You can react to audio changes from other components
*
* The event name "audioDisabled" matches the property on the controller.
* Other available events: "videoDisabled", "deviceError", etc.
*/
useEvent(msc, "audioDisabled");
/**
* STEP 3: Handle Button Click
*
* This function runs when the user clicks the button.
* It demonstrates how to control the microphone programmatically.
*
* What happens:
* 1. Set audioDisabled to true (mutes the microphone)
* 2. You can add custom logic here (analytics, notifications, etc.)
*
* useCallback optimization:
* - Prevents the function from being recreated on every render
* - Only recreates if 'msc' changes
* - Improves performance, especially important for frequently rendered components
*
* CUSTOMIZATION IDEAS:
* - Toggle instead of just disabling: msc.audioDisabled = !msc.audioDisabled
* - Show a toast notification when muting
* - Log analytics events
* - Trigger other UI changes
* - Control video: msc.videoDisabled = true
* - Change devices: msc.audioDeviceId = "device-id"
*/
const handleClick = useCallback(() => {
// Disable audio (mute the microphone)
msc.audioDisabled = true;
// Add your custom logic here!
// Examples:
// - console.log('Audio muted');
// - showNotification('Microphone muted');
// - trackAnalyticsEvent('audio_muted');
}, [msc]);
/**
* STEP 4: Render the Button
*
* This is a simple button with inline styles. In a real application,
* you would likely use CSS classes or a component library.
*
* CUSTOMIZATION IDEAS:
* - Change button text based on audio state:
* {msc.audioDisabled ? "Unmute" : "Mute"}
* - Add icons (mic icon, muted mic icon)
* - Apply different styling based on state
* - Add hover/active states
* - Use your design system's button component
* - Add accessibility attributes (aria-label, aria-pressed)
*
* @example Advanced Button:
* ```tsx
* <button
* onClick={handleClick}
* aria-label={msc.audioDisabled ? "Unmute microphone" : "Mute microphone"}
* aria-pressed={msc.audioDisabled}
* className={msc.audioDisabled ? "btn-muted" : "btn-active"}
* >
* {msc.audioDisabled ? <MicOffIcon /> : <MicOnIcon />}
* </button>
* ```
*/
return (
<button
type="button"
onClick={handleClick}
style={{
color: "white",
background: "green",
padding: ".5rem",
borderRadius: "5px",
}}
>
Custom Button
</button>
);
}
/**
* EXPORT WITH MEMOIZATION
*
* We wrap the component in React.memo() to prevent unnecessary re-renders.
* The component only re-renders when its props change or when events it
* listens to (via useEvent) are triggered.
*
* This is a performance optimization that's especially valuable for
* UI controls that might be rendered frequently.
*/
export default memo(CustomButton);
// BroadcastComponent.tsx
/**
* Broadcast Component
*
* WHAT YOU'LL LEARN:
* - How to use React Context providers to share video client state
* - How to integrate custom UI controls with the video client
*
* @example
* ```tsx
* <BroadcastComponent
* backendEndpoint="https://api.example.com"
* token="your-auth-token"
* streamKey="unique-stream-key"
* cbBroadcast={(broadcast) => console.log('Broadcast state:', broadcast)}
* cbCallId={(callId) => console.log('Call ID:', callId)}
* />
* ```
*/
import React, { memo, useEffect} from "react";
import { context, types, hooks } from "@video/video-client-react";
import CustomButton from "./CustomButton";
import Encoder from "../../../components/PreviewPlayer";
import { useCallControls } from "../../../components/CallControls";
/**
* IMPORT EXPLANATIONS:
*
* 1. React imports:
* - memo: Optimizes component re-renders by memoizing
* - useEffect: Runs side effects (like notifying parent of state changes)
*
* 2. @video/video-client-react imports:
* - context: Provides React Context providers for sharing state
* - types: TypeScript type definitions for video client APIs
* - hooks: Custom React hooks for video functionality
*
* 3. Local imports:
* - CustomButton: Example custom UI control component
* - Encoder: Reusable video preview component
* - useCallControls: Custom hook that manages call/broadcast lifecycle
*/
// Extract Context Providers for sharing video client state across components
const { MediaStreamControllerAPIProvider, PlayerAPIProvider } = context;
// Extract hooks for initializing video functionality
const { usePreviewPlayer, useAuthClient } = hooks;
/**
* Props Interface
*
* These are the values your component needs to function properly.
* Think of them as the "settings" or "configuration" for your encoder.
*/
interface CustomizeYourStreamingApplicationProps {
backendEndpoint: string;
token: string;
streamKey: string;
cbBroadcast: (broadcast: types.BroadcastAPI | null) => void;
cbCallId: (callId: string) => void;
}
function BroadcastComponent({
backendEndpoint,
token,
streamKey,
cbBroadcast,
cbCallId
}: CustomizeYourStreamingApplicationProps): JSX.Element {
/**
* STEP 1: Initialize Media Stream and Preview Player
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});
/**
* STEP 2: Set Up Authentication
*/
const authClient = useAuthClient(token);
/**
* STEP 3: Configure Call Options
*/
const callOptions: types.CallOptions = {
streamKey,
user: { userId: "123", displayName: "John Doe" },
backendEndpoints: [backendEndpoint],
auth: authClient,
}
/**
* STEP 4: Configure Broadcast Options
*/
const broadcastOptions: types.BroadcastOptions = {
streamName: "default",
}
/**
* STEP 5: Use the Call Controls Hook
*
*/
const { renderControls, call, broadcast } = useCallControls({
callOptions,
broadcastOptions,
type: "owner"
});
/**
* STEP 6 (Optional): Notify Parent Component of Broadcast Changes
*
*/
useEffect(() => {
cbBroadcast(broadcast);
}, [broadcast, cbBroadcast]);
/**
* STEP 7 (Optional): Notify Parent Component of Call ID
*
*/
useEffect(() => {
cbCallId(call?.id ?? "");
}, [call, cbCallId]);
/**
* LOADING STATE CHECK
*
*/
if (!mediaStreamController || !previewPlayer || authClient == null || backendEndpoint == null) {
return <div className="w-full h-full bg-gray-200" />;
}
// #14
/**
* MAIN UI RENDERING
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<div className="w-full flex flex-col overflow-hidden mb-4">
{/* Video preview showing what your camera sees */}
<Encoder />
{/* Control buttons for managing call and broadcast */}
<div className="flex flex-row my-2 gap-2 ">
{/* Example custom button - you can add your own controls here */}
<CustomButton />
{/* Built-in controls from useCallControls hook */}
{renderControls()}
</div>
</div>
</PlayerAPIProvider>
</MediaStreamControllerAPIProvider>
);
}
/**
* EXPORT WITH MEMOIZATION
*/
export default memo(BroadcastComponent);
Supporting Components
The following components are used in this demo are documented in 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 };
Next Steps
To learn more about the video-client-core library and advanced features:
- Learn how to customize your player
- Set up a private broadcast
- Create a modern look with an echo pillarbox