Set Up a Livestream Video
Learn how to broadcast audio and video from your application.
In order to livestream video, we must first set up a preview video. The preview video allows broadcasters to see exactly what their viewers will see before and during the livestream. It displays the output from the selected camera and provides controls for managing audio and video devices, ensuring everything looks and sounds correct before going live.
Prerequisites
Before getting started, in order to broadcast a livestream you will need the following:
/**
* Authentication token from your auth system
* This proves the user has permission to create calls and broadcast
*/
token: string;
/**
* Backend API endpoint URL (provided by Native Frame)
* Example: "https://api.nativeframe.com"
*/
backendEndpoint: string;
/**
* Unique identifier for this broadcast stream
* Should be unique per broadcaster
*/
streamKey: string;
Key Concepts
MediaStreamController
Manages access to camera and microphone:
- Controls device selection
- Toggles camera on/off (
videoPausedproperty) - Toggles microphone mute (
audioMutedproperty) - Switches between multiple devices
PreviewPlayer
Displays the local video feed before/during broadcast:
- Automatically plays when attached to a video element
- Should be muted to prevent audio feedback
- Must be disposed when done
Call
Represents the connection to the Native Frame backend:
- Created with
createCall(options) - Required before broadcasting can start
- Handles automatic reconnection and failover
- Disposes of active broadcasts when disposed
Broadcast
Represents an active broadcast stream:
- Created with
call.broadcast(mediaStreamController, options) - Sends audio/video to viewers
- Can be stopped without ending the call
- Identified by
streamName(usually "default")
Shared Components
These are components that will be used as a developoment starting block introducing concepts and reusable patterns that will be used throught documentation examples on this site.
Preview Player Component
The <PreviewPlayer/> component is a reusable video preview component that displays what your camera sees and provides standard device controls. This reusable component will be used as a building block for other documentation examples:
// 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);
Call Controls Hook
The useCallControls hook and CallControls component manage call and broadcast state with automatic UI controls. This reusable hook and component will be used as a building block for other documentation examples:
This hook abstracts away complex state management, so you don't have to manually track call and broadcast state or conditionally render buttons within individual components.
// 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}`);
}
}
// << use-broadcaster-call-controls
/**
* 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 };
Broadcasting Component
This is a streamlined example that demonstrates the essential patterns for setting up a livestream. It uses pre-built components (PreviewPlayer, CallControls) to abstract away UI complexity, letting you focus on the video client logic.
Required Imports
You'll need to import the following namespaces and constructors from the @video/video-client-react package:
/**
* CORE VIDEO CLIENT IMPORTS
*
* These are the fundamental building blocks from @video/video-client-react:
*
* 1. context: Provides React Context providers for sharing state
* - Think of Context as a way to share data across your component tree
* - Without Context, you'd need to pass props through every level
*
* 2. hooks: Custom React hooks for video functionality
* - Hooks are reusable functions that manage state and side effects
* - Video client hooks handle complex video/audio logic for you
*
* 3. types: TypeScript type definitions
* - These help catch errors at compile-time
* - Provide autocomplete and documentation in your IDE
*/
import { context, hooks, types } from "@video/video-client-react";
/**
* EXTRACT CONTEXT PROVIDERS
*
* These providers make video client instances available to child components:
*
* - MediaStreamControllerAPIProvider: Shares camera/mic control with children
* - PlayerAPIProvider: Shares video player with children
*
* Children can access these using hooks like:
* - useMediaStreamControllerAPI() - to control camera/mic
* - usePlayerAPI() - to control video playback
*/
const { MediaStreamControllerAPIProvider, PlayerAPIProvider } = context;
/**
* EXTRACT VIDEO CLIENT HOOKS
*
* These hooks initialize and manage video functionality:
*
* - usePreviewPlayer: Creates media stream controller + preview player
* Returns: { mediaStreamController, previewPlayer }
*
* - useAuthClient: Creates authentication client for API requests
* Takes a token, returns an auth client that handles token refresh
*/
const { usePreviewPlayer, useAuthClient } = hooks;
usePreviewPlayer
The usePreviewPlayer hook requests permission to access camera and microphone, and creates:
- A
mediaStreamControllerto manage these devices - A
previewPlayerto display what the camera sees
/**
* STEP 1: Initialize Media Stream and Preview Player
*
* The usePreviewPlayer hook:
* - Requests permission to access camera and microphone
* - Creates a mediaStreamController to manage these devices
* - Creates a previewPlayer to display what the camera sees
* - Handles device initialization and error states
*
* What you get:
* - mediaStreamController: Control camera/mic (on/off, device selection)
* - previewPlayer: Display the local video preview
*
* Think of this as "opening your camera app" - you need to access
* the camera and see what it's recording before you can broadcast.
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});
useAuthClient
The useAuthClient hook creates an authentication client which gives a user permission to connect to a call and broadcast a stream. The AuthClient:
- Validates your auth token,
- Handles automatic token refresh, and
- Provides credentials for all API requests
Only one AuthClient is required per session, so you will want to instatiate this at the top of your application.
/**
* STEP 2: Set Up Authentication
*
* The useAuthClient hook creates an authentication client that:
* - Validates your token with the backend
* - Handles automatic token refresh when it expires
* - Provides credentials for all API requests
*
* This is like showing your ID before entering a secure building.
* Without valid authentication, you can't create calls or broadcast.
*/
const authClient = useAuthClient(token);
Configure Options
Define how your call will be created and how your broadcast will be configured:
- Call Options: Specify streamKey, user information, backend endpoints, and authentication
- Broadcast Options: Specify the stream name (e.g., "default" for main camera/mic)
/**
* STEP 3: Configure Call Options
*
* These options define HOW your call will be created:
*
* - streamKey: Your unique broadcast identifier
* - user: Information about who's broadcasting
* - userId: Unique ID (can be any string, often from your auth system)
* - displayName: Name shown to other participants
* - backendEndpoints: Array of server URLs to connect to
* - auth: Authentication client (proves you have permission)
*
* Think of this like filling out a registration form before
* joining a meeting.
*/
const callOptions: types.CallOptions = {
streamKey,
user: { userId: "123", displayName: "John Doe" },
backendEndpoints: [backendEndpoint],
auth: authClient,
};
/**
* STEP 4: Configure Broadcast Options
*
* These options control your broadcast stream:
*
* - streamName: Name for this broadcast stream
* - "default" is the standard name for main camera/mic
* - You can have multiple streams (e.g., "screenshare")
* - Each stream name must be unique within a call
*
* The streamName helps viewers distinguish between different
* types of content (main camera vs. screen share).
*/
const broadcastOptions: types.BroadcastOptions = {
streamName: "default",
}
useCallControls
The useCallControls hook (created above) manages all call and broadcast logic automatically:
- Creating/joining calls
- Starting/stopping broadcasts
- Tracking call and broadcast state
- Providing UI controls (buttons)
Optional Callbacks
Other components in your application may need access to the call or broadcast, in this example we are using callbacks passed in as props to handle this as a React-only solution.
Alternatively, this could be handled in your application with state-management (React Context, Redux, etc.).
/**
* STEP 6 (OPTIONAL): Notify Parent of Broadcast Changes
*
* Whenever broadcast state changes (starts or stops), notify the parent
* component through the callback.
*
* Why? The parent might want to:
* - Show a "Broadcasting" indicator
* - Enable/disable other features
* - Track analytics
* - Display broadcast duration
*/
useEffect(() => {
cbBroadcast(broadcast ?? undefined);
}, [broadcast, cbBroadcast]);
/**
* STEP 7 (OPTIONAL): Notify Parent of Call ID
*
* When the call is created and we have a call ID, pass it to the parent.
*
* Why? The parent might want to:
* - Share the call ID with other users (so they can join)
* - Display it in the UI
* - Store it for later reference
* - Use it for analytics or logging
*
* The "??" operator provides an empty string if call is null or
* call.id is undefined (safer than accessing call?.id directly).
*/
useEffect(() => {
cbCallId(call?.id ?? "");
}, [call, cbCallId]);
Render UI
Construct your UI using Context Providers to share mediaStreamController and previewPlayer instances with child components:
- MediaStreamControllerAPIProvider: Makes camera/mic control available to children
- PlayerAPIProvider: Makes video player available to children
- Preview Player Component: Pre-built component showing video preview with device controls
- Call Controls: Buttons that automatically show/hide based on call/broadcast state
/**
* LOADING STATE CHECK
*
* Before rendering the UI, verify all required resources are ready.
* If any are missing, show a loading placeholder instead.
*/
if (!mediaStreamController || !previewPlayer || authClient == null || backendEndpoint == null) {
return <div className="w-full h-full bg-gray-200" />;
}
/**
* MAIN UI RENDERING
*
* Now that all resources are ready, render the encoder UI.
*
* STRUCTURE:
* 1. Context Providers: Share video client instances with children
* - MediaStreamControllerAPIProvider: Makes camera/mic control available
* - PlayerAPIProvider: Makes video player available
*
* 2. Encoder Component: Pre-built component showing:
* - Video preview (what your camera sees)
* - Device controls (camera on/off, mic on/off)
* - Device selectors (which camera, which mic, resolution)
*
* 3. Call Controls: Buttons for:
* - Creating/ending calls
* - Starting/stopping broadcasts
* - Automatically shown/hidden based on state
*
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<div className="w-full flex flex-col overflow-hidden mb-4">
{/* Video preview + device controls component */}
<Encoder/>
{/* Call/broadcast management buttons */}
<div className="mt-4">
{renderControls()}
</div>
</div>
</PlayerAPIProvider>
</MediaStreamControllerAPIProvider>
);
Full Code
// SetupALivestreamVideo.tsx
/**
* SetupALivestreamVideo Component
*
* OVERVIEW:
* This is a streamlined video broadcasting encoder component that demonstrates
* the essential patterns for setting up a livestream. It's designed as an
* introductory example for developers new to video streaming.
*
* WHAT YOU'LL LEARN:
* - How to initialize media devices (camera/microphone)
* - How to set up video call authentication
* - How to create and manage video calls
* - How to start/stop broadcasting
* - How to use reusable components for common patterns
*
* COMPONENT ARCHITECTURE:
* - SetupALivestreamVideo (this file): Main coordination and state management
* - Encoder: Reusable video preview + device controls component
* - CallControls: Reusable call/broadcast button management
*
* @example
* ```tsx
* <SetupALivestreamVideo
* token="your-auth-token"
* backendEndpoint="https://api.example.com"
* streamKey="unique-stream-key"
* cbBroadcast={(broadcast) => console.log('Broadcast state:', broadcast)}
* cbCallId={(callId) => console.log('Call ID:', callId)}
* />
* ```
*/
import React, { memo, useEffect } from "react";
// << imports
/**
* CORE VIDEO CLIENT IMPORTS
*
* These are the fundamental building blocks from @video/video-client-react:
*
* 1. context: Provides React Context providers for sharing state
* - Think of Context as a way to share data across your component tree
* - Without Context, you'd need to pass props through every level
*
* 2. hooks: Custom React hooks for video functionality
* - Hooks are reusable functions that manage state and side effects
* - Video client hooks handle complex video/audio logic for you
*
* 3. types: TypeScript type definitions
* - These help catch errors at compile-time
* - Provide autocomplete and documentation in your IDE
*/
import { context, hooks, types } from "@video/video-client-react";
// << end-imports
/**
* LOCAL IMPORTS
*
* These are reusable components we've created:
*
* - Encoder: Pre-built component showing video preview with device controls
* (camera on/off, mic on/off, device selection, resolution)
*
* - CallControls/useCallControls: Handles all call and broadcast button logic
* (create call, end call, start broadcast, end broadcast)
*
* Using these components lets you focus on your application logic instead
* of rebuilding common video UI patterns from scratch.
*/
import Encoder from "../../components/PreviewPlayer";
import { useCallControls } from "../../components/CallControls";
// << destructured-imports
/**
* EXTRACT CONTEXT PROVIDERS
*
* These providers make video client instances available to child components:
*
* - MediaStreamControllerAPIProvider: Shares camera/mic control with children
* - PlayerAPIProvider: Shares video player with children
*
* Children can access these using hooks like:
* - useMediaStreamControllerAPI() - to control camera/mic
* - usePlayerAPI() - to control video playback
*/
const { MediaStreamControllerAPIProvider, PlayerAPIProvider } = context;
/**
* EXTRACT VIDEO CLIENT HOOKS
*
* These hooks initialize and manage video functionality:
*
* - usePreviewPlayer: Creates media stream controller + preview player
* Returns: { mediaStreamController, previewPlayer }
*
* - useAuthClient: Creates authentication client for API requests
* Takes a token, returns an auth client that handles token refresh
*/
const { usePreviewPlayer, useAuthClient } = hooks;
// << end-destructured-imports
/**
* PROPS INTERFACE
*
* Configuration values needed for the encoder to function.
* These define what information the component needs from its parent.
*/
interface SetupALivestreamVideoProps {
// << prerequisites
/**
* Authentication token from your auth system
* This proves the user has permission to create calls and broadcast
*/
token: string;
/**
* Backend API endpoint URL (provided by Native Frame)
* Example: "https://api.nativeframe.com"
*/
backendEndpoint: string;
/**
* Unique identifier for this broadcast stream
* Should be unique per broadcaster
*/
streamKey: string;
// << end-prerequisites
/**
* Callback invoked when broadcast state changes
* Allows parent to track when broadcasting starts/stops
*/
cbBroadcast: (broadcast: types.BroadcastAPI | undefined) => void;
/**
* Callback invoked when call ID becomes available
* Use this to share the call ID with other participants
*/
cbCallId: (callId: string) => void;
}
function SetupALivestreamVideo({ token, backendEndpoint, streamKey, cbBroadcast, cbCallId }: SetupALivestreamVideoProps): React.ReactElement {
// << use-preview-player
/**
* STEP 1: Initialize Media Stream and Preview Player
*
* The usePreviewPlayer hook:
* - Requests permission to access camera and microphone
* - Creates a mediaStreamController to manage these devices
* - Creates a previewPlayer to display what the camera sees
* - Handles device initialization and error states
*
* What you get:
* - mediaStreamController: Control camera/mic (on/off, device selection)
* - previewPlayer: Display the local video preview
*
* Think of this as "opening your camera app" - you need to access
* the camera and see what it's recording before you can broadcast.
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});
// << end-use-preview-player
// << use-auth-client
/**
* STEP 2: Set Up Authentication
*
* The useAuthClient hook creates an authentication client that:
* - Validates your token with the backend
* - Handles automatic token refresh when it expires
* - Provides credentials for all API requests
*
* This is like showing your ID before entering a secure building.
* Without valid authentication, you can't create calls or broadcast.
*/
const authClient = useAuthClient(token);
// << end-use-auth-client
// << options
/**
* STEP 3: Configure Call Options
*
* These options define HOW your call will be created:
*
* - streamKey: Your unique broadcast identifier
* - user: Information about who's broadcasting
* - userId: Unique ID (can be any string, often from your auth system)
* - displayName: Name shown to other participants
* - backendEndpoints: Array of server URLs to connect to
* - auth: Authentication client (proves you have permission)
*
* Think of this like filling out a registration form before
* joining a meeting.
*/
const callOptions: types.CallOptions = {
streamKey,
user: { userId: "123", displayName: "John Doe" },
backendEndpoints: [backendEndpoint],
auth: authClient,
};
/**
* STEP 4: Configure Broadcast Options
*
* These options control your broadcast stream:
*
* - streamName: Name for this broadcast stream
* - "default" is the standard name for main camera/mic
* - You can have multiple streams (e.g., "screenshare")
* - Each stream name must be unique within a call
*
* The streamName helps viewers distinguish between different
* types of content (main camera vs. screen share).
*/
const broadcastOptions: types.BroadcastOptions = {
streamName: "default",
}
// << end-options
// << use-call-controls
/**
* STEP 5: Use the Call Controls Hook
*
* The useCallControls hook manages all call and broadcast logic:
* - Creating/joining calls
* - Starting/stopping broadcasts
* - Tracking call and broadcast state
* - Providing UI controls (buttons)
*
* Parameters:
* - callOptions: How to create the call
* - broadcastOptions: How to configure the broadcast
* - type: "owner" means you're creating/hosting the call
*
* Returns:
* - renderControls(): Function that renders call/broadcast buttons
* - call: Active call instance (or null)
* - broadcast: Active broadcast instance (or null)
*
* This hook is a huge time-saver! It handles all the complex
* state management so you don't have to.
*/
const { renderControls, call, broadcast } = useCallControls({callOptions, broadcastOptions, type: "owner"});
// << end-use-call-controls
// << optional-callbacks
/**
* STEP 6 (OPTIONAL): Notify Parent of Broadcast Changes
*
* Whenever broadcast state changes (starts or stops), notify the parent
* component through the callback.
*
* Why? The parent might want to:
* - Show a "Broadcasting" indicator
* - Enable/disable other features
* - Track analytics
* - Display broadcast duration
*/
useEffect(() => {
cbBroadcast(broadcast ?? undefined);
}, [broadcast, cbBroadcast]);
/**
* STEP 7 (OPTIONAL): Notify Parent of Call ID
*
* When the call is created and we have a call ID, pass it to the parent.
*
* Why? The parent might want to:
* - Share the call ID with other users (so they can join)
* - Display it in the UI
* - Store it for later reference
* - Use it for analytics or logging
*
* The "??" operator provides an empty string if call is null or
* call.id is undefined (safer than accessing call?.id directly).
*/
useEffect(() => {
cbCallId(call?.id ?? "");
}, [call, cbCallId]);
// << end-optional-callbacks
// << return
/**
* LOADING STATE CHECK
*
* Before rendering the UI, verify all required resources are ready.
* If any are missing, show a loading placeholder instead.
*/
if (!mediaStreamController || !previewPlayer || authClient == null || backendEndpoint == null) {
return <div className="w-full h-full bg-gray-200" />;
}
/**
* MAIN UI RENDERING
*
* Now that all resources are ready, render the encoder UI.
*
* STRUCTURE:
* 1. Context Providers: Share video client instances with children
* - MediaStreamControllerAPIProvider: Makes camera/mic control available
* - PlayerAPIProvider: Makes video player available
*
* 2. Encoder Component: Pre-built component showing:
* - Video preview (what your camera sees)
* - Device controls (camera on/off, mic on/off)
* - Device selectors (which camera, which mic, resolution)
*
* 3. Call Controls: Buttons for:
* - Creating/ending calls
* - Starting/stopping broadcasts
* - Automatically shown/hidden based on state
*
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<div className="w-full flex flex-col overflow-hidden mb-4">
{/* Video preview + device controls component */}
<Encoder/>
{/* Call/broadcast management buttons */}
<div className="mt-4">
{renderControls()}
</div>
</div>
</PlayerAPIProvider>
</MediaStreamControllerAPIProvider>
);
// << end-return
}
/**
* EXPORT WITH MEMOIZATION
*
* React.memo() optimizes performance by preventing unnecessary re-renders.
* The component only re-renders when its props actually change.
*
* This is especially important for video components because rendering
* can be expensive (video processing, canvas operations, etc.).
*/
export default memo(SetupALivestreamVideo);
Full Code
// 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}`);
}
}
// << use-broadcaster-call-controls
/**
* 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 };
// SetupALivestreamVideo.tsx
/**
* SetupALivestreamVideo Component
*
* OVERVIEW:
* This is a streamlined video broadcasting encoder component that demonstrates
* the essential patterns for setting up a livestream. It's designed as an
* introductory example for developers new to video streaming.
*
* WHAT YOU'LL LEARN:
* - How to initialize media devices (camera/microphone)
* - How to set up video call authentication
* - How to create and manage video calls
* - How to start/stop broadcasting
* - How to use reusable components for common patterns
*
* COMPONENT ARCHITECTURE:
* - SetupALivestreamVideo (this file): Main coordination and state management
* - Encoder: Reusable video preview + device controls component
* - CallControls: Reusable call/broadcast button management
*
* @example
* ```tsx
* <SetupALivestreamVideo
* token="your-auth-token"
* backendEndpoint="https://api.example.com"
* streamKey="unique-stream-key"
* cbBroadcast={(broadcast) => console.log('Broadcast state:', broadcast)}
* cbCallId={(callId) => console.log('Call ID:', callId)}
* />
* ```
*/
import React, { memo, useEffect } from "react";
// << imports
/**
* CORE VIDEO CLIENT IMPORTS
*
* These are the fundamental building blocks from @video/video-client-react:
*
* 1. context: Provides React Context providers for sharing state
* - Think of Context as a way to share data across your component tree
* - Without Context, you'd need to pass props through every level
*
* 2. hooks: Custom React hooks for video functionality
* - Hooks are reusable functions that manage state and side effects
* - Video client hooks handle complex video/audio logic for you
*
* 3. types: TypeScript type definitions
* - These help catch errors at compile-time
* - Provide autocomplete and documentation in your IDE
*/
import { context, hooks, types } from "@video/video-client-react";
// << end-imports
/**
* LOCAL IMPORTS
*
* These are reusable components we've created:
*
* - Encoder: Pre-built component showing video preview with device controls
* (camera on/off, mic on/off, device selection, resolution)
*
* - CallControls/useCallControls: Handles all call and broadcast button logic
* (create call, end call, start broadcast, end broadcast)
*
* Using these components lets you focus on your application logic instead
* of rebuilding common video UI patterns from scratch.
*/
import Encoder from "../../components/PreviewPlayer";
import { useCallControls } from "../../components/CallControls";
// << destructured-imports
/**
* EXTRACT CONTEXT PROVIDERS
*
* These providers make video client instances available to child components:
*
* - MediaStreamControllerAPIProvider: Shares camera/mic control with children
* - PlayerAPIProvider: Shares video player with children
*
* Children can access these using hooks like:
* - useMediaStreamControllerAPI() - to control camera/mic
* - usePlayerAPI() - to control video playback
*/
const { MediaStreamControllerAPIProvider, PlayerAPIProvider } = context;
/**
* EXTRACT VIDEO CLIENT HOOKS
*
* These hooks initialize and manage video functionality:
*
* - usePreviewPlayer: Creates media stream controller + preview player
* Returns: { mediaStreamController, previewPlayer }
*
* - useAuthClient: Creates authentication client for API requests
* Takes a token, returns an auth client that handles token refresh
*/
const { usePreviewPlayer, useAuthClient } = hooks;
// << end-destructured-imports
/**
* PROPS INTERFACE
*
* Configuration values needed for the encoder to function.
* These define what information the component needs from its parent.
*/
interface SetupALivestreamVideoProps {
// << prerequisites
/**
* Authentication token from your auth system
* This proves the user has permission to create calls and broadcast
*/
token: string;
/**
* Backend API endpoint URL (provided by Native Frame)
* Example: "https://api.nativeframe.com"
*/
backendEndpoint: string;
/**
* Unique identifier for this broadcast stream
* Should be unique per broadcaster
*/
streamKey: string;
// << end-prerequisites
/**
* Callback invoked when broadcast state changes
* Allows parent to track when broadcasting starts/stops
*/
cbBroadcast: (broadcast: types.BroadcastAPI | undefined) => void;
/**
* Callback invoked when call ID becomes available
* Use this to share the call ID with other participants
*/
cbCallId: (callId: string) => void;
}
function SetupALivestreamVideo({ token, backendEndpoint, streamKey, cbBroadcast, cbCallId }: SetupALivestreamVideoProps): React.ReactElement {
// << use-preview-player
/**
* STEP 1: Initialize Media Stream and Preview Player
*
* The usePreviewPlayer hook:
* - Requests permission to access camera and microphone
* - Creates a mediaStreamController to manage these devices
* - Creates a previewPlayer to display what the camera sees
* - Handles device initialization and error states
*
* What you get:
* - mediaStreamController: Control camera/mic (on/off, device selection)
* - previewPlayer: Display the local video preview
*
* Think of this as "opening your camera app" - you need to access
* the camera and see what it's recording before you can broadcast.
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});
// << end-use-preview-player
// << use-auth-client
/**
* STEP 2: Set Up Authentication
*
* The useAuthClient hook creates an authentication client that:
* - Validates your token with the backend
* - Handles automatic token refresh when it expires
* - Provides credentials for all API requests
*
* This is like showing your ID before entering a secure building.
* Without valid authentication, you can't create calls or broadcast.
*/
const authClient = useAuthClient(token);
// << end-use-auth-client
// << options
/**
* STEP 3: Configure Call Options
*
* These options define HOW your call will be created:
*
* - streamKey: Your unique broadcast identifier
* - user: Information about who's broadcasting
* - userId: Unique ID (can be any string, often from your auth system)
* - displayName: Name shown to other participants
* - backendEndpoints: Array of server URLs to connect to
* - auth: Authentication client (proves you have permission)
*
* Think of this like filling out a registration form before
* joining a meeting.
*/
const callOptions: types.CallOptions = {
streamKey,
user: { userId: "123", displayName: "John Doe" },
backendEndpoints: [backendEndpoint],
auth: authClient,
};
/**
* STEP 4: Configure Broadcast Options
*
* These options control your broadcast stream:
*
* - streamName: Name for this broadcast stream
* - "default" is the standard name for main camera/mic
* - You can have multiple streams (e.g., "screenshare")
* - Each stream name must be unique within a call
*
* The streamName helps viewers distinguish between different
* types of content (main camera vs. screen share).
*/
const broadcastOptions: types.BroadcastOptions = {
streamName: "default",
}
// << end-options
// << use-call-controls
/**
* STEP 5: Use the Call Controls Hook
*
* The useCallControls hook manages all call and broadcast logic:
* - Creating/joining calls
* - Starting/stopping broadcasts
* - Tracking call and broadcast state
* - Providing UI controls (buttons)
*
* Parameters:
* - callOptions: How to create the call
* - broadcastOptions: How to configure the broadcast
* - type: "owner" means you're creating/hosting the call
*
* Returns:
* - renderControls(): Function that renders call/broadcast buttons
* - call: Active call instance (or null)
* - broadcast: Active broadcast instance (or null)
*
* This hook is a huge time-saver! It handles all the complex
* state management so you don't have to.
*/
const { renderControls, call, broadcast } = useCallControls({callOptions, broadcastOptions, type: "owner"});
// << end-use-call-controls
// << optional-callbacks
/**
* STEP 6 (OPTIONAL): Notify Parent of Broadcast Changes
*
* Whenever broadcast state changes (starts or stops), notify the parent
* component through the callback.
*
* Why? The parent might want to:
* - Show a "Broadcasting" indicator
* - Enable/disable other features
* - Track analytics
* - Display broadcast duration
*/
useEffect(() => {
cbBroadcast(broadcast ?? undefined);
}, [broadcast, cbBroadcast]);
/**
* STEP 7 (OPTIONAL): Notify Parent of Call ID
*
* When the call is created and we have a call ID, pass it to the parent.
*
* Why? The parent might want to:
* - Share the call ID with other users (so they can join)
* - Display it in the UI
* - Store it for later reference
* - Use it for analytics or logging
*
* The "??" operator provides an empty string if call is null or
* call.id is undefined (safer than accessing call?.id directly).
*/
useEffect(() => {
cbCallId(call?.id ?? "");
}, [call, cbCallId]);
// << end-optional-callbacks
// << return
/**
* LOADING STATE CHECK
*
* Before rendering the UI, verify all required resources are ready.
* If any are missing, show a loading placeholder instead.
*/
if (!mediaStreamController || !previewPlayer || authClient == null || backendEndpoint == null) {
return <div className="w-full h-full bg-gray-200" />;
}
/**
* MAIN UI RENDERING
*
* Now that all resources are ready, render the encoder UI.
*
* STRUCTURE:
* 1. Context Providers: Share video client instances with children
* - MediaStreamControllerAPIProvider: Makes camera/mic control available
* - PlayerAPIProvider: Makes video player available
*
* 2. Encoder Component: Pre-built component showing:
* - Video preview (what your camera sees)
* - Device controls (camera on/off, mic on/off)
* - Device selectors (which camera, which mic, resolution)
*
* 3. Call Controls: Buttons for:
* - Creating/ending calls
* - Starting/stopping broadcasts
* - Automatically shown/hidden based on state
*
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<div className="w-full flex flex-col overflow-hidden mb-4">
{/* Video preview + device controls component */}
<Encoder/>
{/* Call/broadcast management buttons */}
<div className="mt-4">
{renderControls()}
</div>
</div>
</PlayerAPIProvider>
</MediaStreamControllerAPIProvider>
);
// << end-return
}
/**
* EXPORT WITH MEMOIZATION
*
* React.memo() optimizes performance by preventing unnecessary re-renders.
* The component only re-renders when its props actually change.
*
* This is especially important for video components because rendering
* can be expensive (video processing, canvas operations, etc.).
*/
export default memo(SetupALivestreamVideo);
Next Steps
To learn more about the video-client-core library and advanced features:
- Learn about customizing your streaming app
- Set up a screenshare
- Build out components to view a livestream