Skip to main content

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 mediaStreamController to capture a canvas element
  • The specific code required for canvas broadcasting

For foundational concepts, please review:

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:

  1. Set videoDeviceId to "capturable" - this tells the controller you're capturing an element
  2. Provide your canvas via the capturable property

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 };