Livestream a Canvas Element
Stream videos with limitless possibility using an HTMLCanvasElement.
The mediaStreamController has many capabilities. Using it, a broadcaster may stream data from a video device, they may stream a video of their screen, and may even broadcast changes that occur on an HTML canvas element. This tutorial will guide you through the steps of creating a stream that broadcasts an HTML canvas element.
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 canvas broadcasting, specifically:
- How to configure the
mediaStreamControllerto capture a canvas element - The specific code required for canvas broadcasting
For foundational concepts, please review:
- The
usePreviewPlayeranduseAuthClienthooks. - The
useCallControlscustom hook. - The
PreviewPlayercomponent for device controls.
- React
Canvas Utils
The main difference between a broadcast that streams a video from a device or screen and a broadcast that streams an HTML canvas element is that the mediaStreamController either (1) has the option capturable: { element } passed into it when it is initialized or (2) sets the videoDeviceId to capturable after it is initialized. Refer to the code snippets below to see how to create a mediaStreamController that broadcasts an HTML canvas.
We need to create a canvas element that the mediaStreamController can use. Here we've created a mock canvas with an animated circle for demonstration purposes. In your application, you would use your own canvas (e.g., a game canvas, chart, or visualization).
/**
* Canvas Utilities for Canvas Broadcasting Demo
*
* This file provides a mock canvas with animated content for demonstration
* purposes. In a real application, you would replace this with your own
* canvas element.
*/
/**
* getMockCanvas: Creates a demo canvas with an animated circle
*
* OVERVIEW:
* This function creates an HTML canvas element with a simple animation
* (a pulsing circle) to demonstrate canvas broadcasting. It's used in
* the LivestreamACanvasElement component to show how to broadcast canvas content.
*
* WHAT IT DOES:
* 1. Creates a new canvas element programmatically
* 2. Sets up canvas dimensions (600x400px)
* 3. Starts an animation loop that draws a pulsing circle
* 4. Returns the canvas element for use with mediaStreamController
*
* IN YOUR APPLICATION:
* Replace this with your own canvas element. Common scenarios:
* - Game canvas: const canvas = document.getElementById('game-canvas') as HTMLCanvasElement
* - Chart canvas: const canvas = chartInstance.canvas
* - D3.js visualization: const canvas = d3.select('canvas').node() as HTMLCanvasElement
* - Three.js renderer: const canvas = renderer.domElement
* - Custom WebGL canvas: Your own canvas with WebGL context
*
* IMPORTANT NOTES:
* - The canvas must be actively rendering for the broadcast to show updates
* - Animation loops should run continuously while broadcasting
* - Canvas dimensions affect the broadcast resolution
* - The canvas doesn't need to be attached to the DOM to be captured
*
* @returns Promise<HTMLCanvasElement> A canvas element with animated content
*/
export const getMockCanvas = async (): Promise<HTMLCanvasElement> => {
// Create a new canvas element programmatically
const canvas: HTMLCanvasElement = window.document.createElement("canvas");
canvas.setAttribute("width", "600px");
canvas.setAttribute("height", "400px");
// Get the 2D rendering context for drawing
const context = canvas.getContext("2d");
// Store canvas dimensions for drawing calculations
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const circleColor = "#006699";
// Animation state: controls whether circle is growing or shrinking
let gain = true;
let radius = 0;
/**
* drawCircle: Animation loop that creates the pulsing circle effect
*
* This function runs continuously at ~60fps (every 16ms) to create
* smooth animation. Each frame:
* 1. Clears the previous frame
* 2. Draws a light gray background
* 3. Draws a circle that grows and shrinks
* 4. Schedules the next frame
*/
function drawCircle(): void {
// Clear the entire canvas for the new frame
context?.clearRect(0, 0, canvasWidth, canvasHeight);
// Draw light gray background
if (context != null) {
context.fillStyle = "#EEEEEE";
}
context?.fillRect(0, 0, canvasWidth, canvasHeight);
// Begin drawing the circle
context?.beginPath();
// Update radius for pulsing effect (grows from 0 to 100, then shrinks back)
const maxCircleWidth = 100;
gain = (gain && radius < maxCircleWidth) || radius === 0;
radius = gain ? radius + 1 : radius - 1;
// Draw circle centered in canvas
context?.arc(canvasWidth / 2, canvasHeight / 2, radius, 0, Math.PI * 2, false);
context?.closePath();
// Fill the circle with blue color
if (context != null) {
context.fillStyle = circleColor;
}
context?.fill();
// Schedule next frame (~60fps for smooth animation)
setTimeout(drawCircle, 16);
}
// Start the animation loop
drawCircle();
// Return the canvas element - it's now ready to be captured and broadcast!
return canvas;
};
Canvas Broadcasting Component
This component is nearly identical to Set up a Livestream Video. The main difference is configuring the mediaStreamController to capture a canvas instead of a camera.
Imports
Hooks
Use the usePreviewPlayer hook to create the mediaStreamController and previewPlayer, just like camera streaming:
Configure Canvas Capturing (The Key Difference!)
This is the novel code that makes canvas broadcasting work. After the mediaStreamController is initialized, configure it to capture your canvas:
/**
* Canvas Utilities for Canvas Broadcasting Demo
*
* This file provides a mock canvas with animated content for demonstration
* purposes. In a real application, you would replace this with your own
* canvas element.
*/
/**
* getMockCanvas: Creates a demo canvas with an animated circle
*
* OVERVIEW:
* This function creates an HTML canvas element with a simple animation
* (a pulsing circle) to demonstrate canvas broadcasting. It's used in
* the LivestreamACanvasElement component to show how to broadcast canvas content.
*
* WHAT IT DOES:
* 1. Creates a new canvas element programmatically
* 2. Sets up canvas dimensions (600x400px)
* 3. Starts an animation loop that draws a pulsing circle
* 4. Returns the canvas element for use with mediaStreamController
*
* IN YOUR APPLICATION:
* Replace this with your own canvas element. Common scenarios:
* - Game canvas: const canvas = document.getElementById('game-canvas') as HTMLCanvasElement
* - Chart canvas: const canvas = chartInstance.canvas
* - D3.js visualization: const canvas = d3.select('canvas').node() as HTMLCanvasElement
* - Three.js renderer: const canvas = renderer.domElement
* - Custom WebGL canvas: Your own canvas with WebGL context
*
* IMPORTANT NOTES:
* - The canvas must be actively rendering for the broadcast to show updates
* - Animation loops should run continuously while broadcasting
* - Canvas dimensions affect the broadcast resolution
* - The canvas doesn't need to be attached to the DOM to be captured
*
* @returns Promise<HTMLCanvasElement> A canvas element with animated content
*/
export const getMockCanvas = async (): Promise<HTMLCanvasElement> => {
// Create a new canvas element programmatically
const canvas: HTMLCanvasElement = window.document.createElement("canvas");
canvas.setAttribute("width", "600px");
canvas.setAttribute("height", "400px");
// Get the 2D rendering context for drawing
const context = canvas.getContext("2d");
// Store canvas dimensions for drawing calculations
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const circleColor = "#006699";
// Animation state: controls whether circle is growing or shrinking
let gain = true;
let radius = 0;
/**
* drawCircle: Animation loop that creates the pulsing circle effect
*
* This function runs continuously at ~60fps (every 16ms) to create
* smooth animation. Each frame:
* 1. Clears the previous frame
* 2. Draws a light gray background
* 3. Draws a circle that grows and shrinks
* 4. Schedules the next frame
*/
function drawCircle(): void {
// Clear the entire canvas for the new frame
context?.clearRect(0, 0, canvasWidth, canvasHeight);
// Draw light gray background
if (context != null) {
context.fillStyle = "#EEEEEE";
}
context?.fillRect(0, 0, canvasWidth, canvasHeight);
// Begin drawing the circle
context?.beginPath();
// Update radius for pulsing effect (grows from 0 to 100, then shrinks back)
const maxCircleWidth = 100;
gain = (gain && radius < maxCircleWidth) || radius === 0;
radius = gain ? radius + 1 : radius - 1;
// Draw circle centered in canvas
context?.arc(canvasWidth / 2, canvasHeight / 2, radius, 0, Math.PI * 2, false);
context?.closePath();
// Fill the circle with blue color
if (context != null) {
context.fillStyle = circleColor;
}
context?.fill();
// Schedule next frame (~60fps for smooth animation)
setTimeout(drawCircle, 16);
}
// Start the animation loop
drawCircle();
// Return the canvas element - it's now ready to be captured and broadcast!
return canvas;
};
/**
* STEP 2: Configure Canvas Capturing (THE KEY DIFFERENCE!)
*
* This is where canvas broadcasting differs from camera streaming.
* We use a useEffect to configure the mediaStreamController to capture
* a canvas element instead of a camera device.
*
* HOW IT WORKS:
* 1. Wait for mediaStreamController to be initialized
* 2. Get your canvas element (in this case, from getMockCanvas)
* 3. Set videoDeviceId to "capturable" (special keyword for canvas/element capture)
* 4. Provide the canvas element via the capturable property
*
* WHY useEffect?
* - mediaStreamController might not be ready immediately
* - We need to wait for it to initialize before configuring it
* - The dependency array [mediaStreamController] ensures this runs when it's ready
*
* IN YOUR APPLICATION:
* Replace getMockCanvas() with your own canvas element. For example:
* - A game canvas: document.getElementById('game-canvas')
* - A chart library canvas: chartInstance.canvas
* - A custom visualization: yourCustomCanvas
*
* IMPORTANT: The canvas must be a valid HTMLCanvasElement that's
* rendering content you want to broadcast.
*/
useEffect(() => {
if (mediaStreamController != null) {
const mockCanvas = async (): Promise<void> => {
// Get your canvas element (replace with your own canvas in production)
const canvas = await getMockCanvas();
// Configure the mediaStreamController to capture the canvas
// Step 1: Set videoDeviceId to "capturable" (tells the controller we're capturing an element)
mediaStreamController.videoDeviceId = "capturable";
// Step 2: Provide the canvas element via the capturable property
mediaStreamController.capturable = { element: canvas } as unknown as types.Capturable;
};
mockCanvas();
}
}, [mediaStreamController]);
Key Steps:
- Set
videoDeviceIdto"capturable"- this tells the controller you're capturing an element - Provide your canvas via the
capturableproperty
In your application, replace getMockCanvas() with your own canvas element.
Render UI
The rest of the component is identical to the Broadcasting Component for camera streaming - same providers, same controls, same workflow:
/**
* MAIN UI RENDERING
*
* The UI structure is identical to camera streaming:
* - Context providers to share video client instances
* - Encoder component to show the preview (of the canvas, in this case!)
* - Call controls to manage the call and broadcast
*
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<div className="w-full flex flex-col overflow-hidden mb-4">
{/* Video preview component - shows the canvas preview */}
<Encoder/>
{/* Call/broadcast management buttons */}
<div className="mt-4">
{renderControls()}
</div>
</div>
</PlayerAPIProvider>
</MediaStreamControllerAPIProvider>
);
Full Code
Canvas Components
/**
* Canvas Utilities for Canvas Broadcasting Demo
*
* This file provides a mock canvas with animated content for demonstration
* purposes. In a real application, you would replace this with your own
* canvas element.
*/
/**
* getMockCanvas: Creates a demo canvas with an animated circle
*
* OVERVIEW:
* This function creates an HTML canvas element with a simple animation
* (a pulsing circle) to demonstrate canvas broadcasting. It's used in
* the LivestreamACanvasElement component to show how to broadcast canvas content.
*
* WHAT IT DOES:
* 1. Creates a new canvas element programmatically
* 2. Sets up canvas dimensions (600x400px)
* 3. Starts an animation loop that draws a pulsing circle
* 4. Returns the canvas element for use with mediaStreamController
*
* IN YOUR APPLICATION:
* Replace this with your own canvas element. Common scenarios:
* - Game canvas: const canvas = document.getElementById('game-canvas') as HTMLCanvasElement
* - Chart canvas: const canvas = chartInstance.canvas
* - D3.js visualization: const canvas = d3.select('canvas').node() as HTMLCanvasElement
* - Three.js renderer: const canvas = renderer.domElement
* - Custom WebGL canvas: Your own canvas with WebGL context
*
* IMPORTANT NOTES:
* - The canvas must be actively rendering for the broadcast to show updates
* - Animation loops should run continuously while broadcasting
* - Canvas dimensions affect the broadcast resolution
* - The canvas doesn't need to be attached to the DOM to be captured
*
* @returns Promise<HTMLCanvasElement> A canvas element with animated content
*/
export const getMockCanvas = async (): Promise<HTMLCanvasElement> => {
// Create a new canvas element programmatically
const canvas: HTMLCanvasElement = window.document.createElement("canvas");
canvas.setAttribute("width", "600px");
canvas.setAttribute("height", "400px");
// Get the 2D rendering context for drawing
const context = canvas.getContext("2d");
// Store canvas dimensions for drawing calculations
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const circleColor = "#006699";
// Animation state: controls whether circle is growing or shrinking
let gain = true;
let radius = 0;
/**
* drawCircle: Animation loop that creates the pulsing circle effect
*
* This function runs continuously at ~60fps (every 16ms) to create
* smooth animation. Each frame:
* 1. Clears the previous frame
* 2. Draws a light gray background
* 3. Draws a circle that grows and shrinks
* 4. Schedules the next frame
*/
function drawCircle(): void {
// Clear the entire canvas for the new frame
context?.clearRect(0, 0, canvasWidth, canvasHeight);
// Draw light gray background
if (context != null) {
context.fillStyle = "#EEEEEE";
}
context?.fillRect(0, 0, canvasWidth, canvasHeight);
// Begin drawing the circle
context?.beginPath();
// Update radius for pulsing effect (grows from 0 to 100, then shrinks back)
const maxCircleWidth = 100;
gain = (gain && radius < maxCircleWidth) || radius === 0;
radius = gain ? radius + 1 : radius - 1;
// Draw circle centered in canvas
context?.arc(canvasWidth / 2, canvasHeight / 2, radius, 0, Math.PI * 2, false);
context?.closePath();
// Fill the circle with blue color
if (context != null) {
context.fillStyle = circleColor;
}
context?.fill();
// Schedule next frame (~60fps for smooth animation)
setTimeout(drawCircle, 16);
}
// Start the animation loop
drawCircle();
// Return the canvas element - it's now ready to be captured and broadcast!
return canvas;
};
// LivestreamACanvasElement.tsx
/**
* LivestreamACanvasElement Component
*
* OVERVIEW:
* This component demonstrates how to broadcast an HTML canvas element instead
* of a camera feed. This is useful for streaming games, animations, data
* visualizations, or any content that can be rendered to a canvas.
*
* WHAT YOU'LL LEARN:
* - How to capture and broadcast an HTML canvas element
* - How to set up the mediaStreamController to use "capturable" mode
* - How canvas streaming differs from camera streaming
* - How to integrate canvas broadcasting with the standard video client workflow
*
* KEY DIFFERENCES FROM STANDARD CAMERA STREAMING:
* The main difference is setting up the mediaStreamController to capture
* a canvas instead of a camera device:
* 1. Set videoDeviceId to "capturable"
* 2. Provide the canvas element via the capturable property
*
* USE CASES:
* - Streaming browser-based games
* - Broadcasting data visualizations and charts
* - Sharing animated content or graphics
* - Screen-like streaming without actual screen capture API
* - Creative applications with custom rendering
*
* PREREQUISITES:
* This component builds on the patterns from SetupALivestreamVideo.tsx.
* It uses the same authentication, call management, and broadcast setup,
* with the key addition of canvas capturing.
*
* @example
* ```tsx
* <LivestreamACanvasElement
* 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";
/**
* CORE VIDEO CLIENT IMPORTS
*
* These imports provide the same foundational building blocks as
* SetupALivestreamVideo, but we'll use them to broadcast a canvas
* instead of a camera feed.
*/
import { context, types, hooks } from "@video/video-client-react";
/**
* CANVAS UTILITY IMPORT
*
* getMockCanvas: A utility function that creates and returns a canvas element
* with animated content. In a real application, you would provide your own
* canvas element - this could be a game canvas, a chart, a visualization, etc.
*/
import { getMockCanvas } from "./canvas-utils";
/**
* REUSABLE COMPONENT IMPORTS
*
* - useCallControls: Manages call and broadcast state automatically
* - Encoder: Displays video preview with device controls
*
* These are the same components used in SetupALivestreamVideo,
* showing that canvas broadcasting integrates seamlessly with
* the standard video client workflow.
*/
import { useCallControls } from "../../components/CallControls";
import Encoder from "../../components/PreviewPlayer";
/**
* EXTRACT CONTEXT PROVIDERS
*
* These providers make video client instances available to child components.
* Even though we're broadcasting a canvas, we still use the same provider
* architecture for consistency and compatibility.
*/
const { MediaStreamControllerAPIProvider, PlayerAPIProvider } = context;
/**
* EXTRACT VIDEO CLIENT HOOKS
*
* - usePreviewPlayer: Creates media stream controller + preview player
* - useAuthClient: Creates authentication client for API requests
*/
const { usePreviewPlayer, useAuthClient } = hooks;
/**
* PROPS INTERFACE
*
* Configuration values needed for the canvas broadcaster to function.
* These are identical to SetupALivestreamVideo props, showing that
* canvas broadcasting doesn't require any special authentication or
* API configuration - it's just a different media source.
*/
interface LivestreamACanvasElementProps {
/**
* Authentication token from your auth system
* 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;
/**
* Callback invoked when broadcast state changes
* Allows parent to track when broadcasting starts/stops
*/
cbBroadcast: (broadcast: types.BroadcastAPI | null) => void;
/**
* Callback invoked when call ID becomes available
* Use this to share the call ID with viewers
*/
cbCallId: (callId: string) => void;
}
function LivestreamACanvasElement({ backendEndpoint, token, streamKey, cbBroadcast, cbCallId }: LivestreamACanvasElementProps): React.ReactElement {
/**
* STEP 1: Initialize Media Stream and Preview Player
*
* This step is identical to camera streaming. The usePreviewPlayer hook:
* - Creates a mediaStreamController to manage media input
* - Creates a previewPlayer to display the stream preview
*
* At this point, it doesn't matter whether we'll be capturing a camera
* or a canvas - we set that up in the next step.
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});
/**
* STEP 2: Configure Canvas Capturing (THE KEY DIFFERENCE!)
*
* This is where canvas broadcasting differs from camera streaming.
* We use a useEffect to configure the mediaStreamController to capture
* a canvas element instead of a camera device.
*
* HOW IT WORKS:
* 1. Wait for mediaStreamController to be initialized
* 2. Get your canvas element (in this case, from getMockCanvas)
* 3. Set videoDeviceId to "capturable" (special keyword for canvas/element capture)
* 4. Provide the canvas element via the capturable property
*
* WHY useEffect?
* - mediaStreamController might not be ready immediately
* - We need to wait for it to initialize before configuring it
* - The dependency array [mediaStreamController] ensures this runs when it's ready
*
* IN YOUR APPLICATION:
* Replace getMockCanvas() with your own canvas element. For example:
* - A game canvas: document.getElementById('game-canvas')
* - A chart library canvas: chartInstance.canvas
* - A custom visualization: yourCustomCanvas
*
* IMPORTANT: The canvas must be a valid HTMLCanvasElement that's
* rendering content you want to broadcast.
*/
useEffect(() => {
if (mediaStreamController != null) {
const mockCanvas = async (): Promise<void> => {
// Get your canvas element (replace with your own canvas in production)
const canvas = await getMockCanvas();
// Configure the mediaStreamController to capture the canvas
// Step 1: Set videoDeviceId to "capturable" (tells the controller we're capturing an element)
mediaStreamController.videoDeviceId = "capturable";
// Step 2: Provide the canvas element via the capturable property
mediaStreamController.capturable = { element: canvas } as unknown as types.Capturable;
};
mockCanvas();
}
}, [mediaStreamController]);
/**
* STEP 3: Set Up Authentication
*
* This is identical to camera streaming. Canvas broadcasting uses
* the same authentication system as any other video client operation.
*/
const authClient = useAuthClient(token);
/**
* STEP 4: Configure Call Options
*
* Define how the call will be created. This is identical to camera streaming.
* The call system doesn't need to know or care whether you're broadcasting
* a camera or a canvas - it just handles the connection and routing.
*/
const callOptions: types.CallOptions = {
streamKey,
user: { userId: "123", displayName: "John Doe" },
backendEndpoints: [backendEndpoint],
auth: authClient,
};
/**
* STEP 5: Configure Broadcast Options
*
* Define the stream name for this broadcast. Again, identical to camera streaming.
* The broadcast system treats canvas streams the same as camera streams.
*/
const broadcastOptions: types.BroadcastOptions = {
streamName: "default",
}
/**
* STEP 6: Use the Call Controls Hook
*
* Manages call and broadcast state automatically. This works identically
* for canvas broadcasting as it does for camera broadcasting - the hook
* doesn't need to know about the media source type.
*/
const { renderControls, call, broadcast } = useCallControls({callOptions, broadcastOptions, type: "owner"});
/**
* STEP 7 (Optional): Notify Parent of Broadcast Changes
*
* Pass broadcast state changes to the parent component via callback.
*/
useEffect(() => {
cbBroadcast(broadcast);
}, [broadcast, cbBroadcast]);
/**
* STEP 8 (Optional): Notify Parent of Call ID
*
* Pass the call ID to the parent component when available.
*/
useEffect(() => {
cbCallId(call?.id ?? "");
}, [call, cbCallId]);
/**
* LOADING STATE CHECK
*
* Verify all required resources are ready before rendering the UI.
*/
if (!mediaStreamController || !previewPlayer || authClient == null || backendEndpoint == null) {
return <div className="w-full h-full bg-gray-200" />;
}
/**
* MAIN UI RENDERING
*
* The UI structure is identical to camera streaming:
* - Context providers to share video client instances
* - Encoder component to show the preview (of the canvas, in this case!)
* - Call controls to manage the call and broadcast
*
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<div className="w-full flex flex-col overflow-hidden mb-4">
{/* Video preview component - shows the canvas preview */}
<Encoder/>
{/* Call/broadcast management buttons */}
<div className="mt-4">
{renderControls()}
</div>
</div>
</PlayerAPIProvider>
</MediaStreamControllerAPIProvider>
);
}
/**
* EXPORT WITH MEMOIZATION
*
*/
export default memo(LivestreamACanvasElement);
Supporting Components
The following components are used by Livestream a Canvas Element and 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 };