Customize Your Player
Venture beyond and create custom components that leverage our player API.
While Native Frame provides pre-built player controls, you may want to create custom components that match your design system or add specialized functionality. This guide demonstrates how to build custom player controls that interact directly with the PlayerAPI, giving you full control over playback behavior and state.
- React
Prerequisites
This guide assumes you have a basic understanding of setting up an <Player/> with @video/video-client-react. We will be focusing on the key differences for creating your own components that interact directly with the API.
- How to create your own components
- Interacting with the PlayerAPI context
- React to PlayerAPI events
For foundational concepts, please review:
- The
useRequestPlayerhook. - The
Playercomponent for stream playback.
Custom Button Component
As you build out your application, you'll likely want to interact with the Player API to handle custom functionality.
In this example, we'll create a custom button that play/pause button that demonstrates how to create your own player controls. This is useful when you want to match your design system or add custom functionality.
Imports
import React, { memo, useCallback } from "react";
/**
* IMPORT EXPLANATIONS:
*
* 1. React imports:
* - memo: Prevents unnecessary re-renders for performance
* - useCallback: Memoizes callback functions to prevent recreation
*
* 2. @video/video-client-react imports:
* - context: Provides hooks to access player context
* - hooks: Provides custom hooks for player functionality
*/
import { context, hooks } from "@video/video-client-react";
/**
* EXTRACT CONTEXT AND EVENT HOOKS
*
* - usePlayerAPI: Hook to access the player instance from context
* - useEvent: Hook to subscribe to player events and trigger re-renders
*/
const { usePlayerAPI } = context;
const { useEvent } = hooks;
Hooks
Use the usePlayerAPI hook to access the player instance and the useEvent hook to listen for state changes:
/**
* STEP 1: Access the Player Instance
*
* The usePlayerAPI hook retrieves the player instance from React Context.
* This player object provides methods and properties for controlling playback:
* - player.play(): Start playback
* - player.pause(): Pause playback
* - player.paused: Boolean indicating if player is currently paused
* - player.muted: Boolean indicating if audio is muted
* - player.volume: Current volume level (0-100)
* - And many more...
*
* IMPORTANT: This hook must be used inside a PlayerAPIProvider.
* The Player component we created earlier provides this context automatically.
*/
const player = usePlayerAPI();
/**
* STEP 2: Listen for Player State Changes
*
* The useEvent hook subscribes to player events. When these events fire,
* the component re-renders to reflect the new state.
*
* Events we're listening to:
* - "paused": Fired when playback is paused
* - "playing": Fired when playback starts/resumes
*
* Why is this useful?
* - Your UI can automatically update when player state changes
* - The button text will switch between "Play" and "Pause"
* - Changes from other controls will be reflected in this button
* - External events (like autoplay) will update the UI
*
* Other available events: "muted", "unmuted", "volumechange",
* "ended", "error", "loadedmetadata", etc.
*/
useEvent(player, "paused");
useEvent(player, "playing");
Handle Click
Create a click handler that toggles playback:
/**
* STEP 3: Handle Button Click
*
* This function runs when the user clicks the button.
* It toggles playback based on the current player state.
*
* HOW IT WORKS:
* 1. Check if player is currently paused (player.paused)
* 2. If paused: call player.play() to start playback
* 3. If playing: call player.pause() to pause playback
*
* NOTE: These methods are async, so we use await.
* They return promises that resolve when the operation completes.
*
* useCallback optimization:
* - Prevents the function from being recreated on every render
* - Only recreates if 'player' changes
* - Improves performance, especially for frequently rendered components
*
* CUSTOMIZATION IDEAS:
* - Add analytics tracking when user plays/pauses
* - Show a loading indicator during state transitions
* - Add keyboard shortcuts (spacebar to play/pause)
* - Implement double-click to fullscreen
* - Add haptic feedback on mobile devices
*/
const handleClick = useCallback(async () => {
if (player.paused) {
// Player is paused, so start playing
await player.play();
} else {
// Player is playing, so pause it
await player.pause();
}
}, [player]);
Full Component Code
// CustomPlayButton.tsx
/**
* CustomPlayButton Component
*
* OVERVIEW:
* This component demonstrates how to create custom player controls that
* interact with the video client's player API. It's a simple play/pause
* button that shows how to access player state and control playback.
*
* WHAT YOU'LL LEARN:
* - How to access the player instance from context
* - How to listen for and react to player events
* - How to control playback programmatically
* - How to create custom UI components that integrate with the player
*
* KEY CONCEPTS:
* 1. CONTEXT: Accessing the player instance via usePlayerAPI hook
* 2. EVENTS: Listening for state changes (paused, playing) via useEvent
* 3. PLAYBACK CONTROL: Using player.play() and player.pause() methods
* 4. REACTIVE UI: Button text changes based on player state
*
* USE CASES:
* - Creating custom styled buttons that match your design system
* - Adding additional functionality beyond built-in components
* - Implementing complex control logic
* - Building integrated player interfaces
*
* @example
* ```tsx
* // Must be used inside PlayerAPIProvider
* <PlayerAPIProvider playerAPI={player}>
* <CustomPlayButton />
* </PlayerAPIProvider>
* ```
*/
import React, { memo, useCallback } from "react";
/**
* IMPORT EXPLANATIONS:
*
* 1. React imports:
* - memo: Prevents unnecessary re-renders for performance
* - useCallback: Memoizes callback functions to prevent recreation
*
* 2. @video/video-client-react imports:
* - context: Provides hooks to access player context
* - hooks: Provides custom hooks for player functionality
*/
import { context, hooks } from "@video/video-client-react";
/**
* EXTRACT CONTEXT AND EVENT HOOKS
*
* - usePlayerAPI: Hook to access the player instance from context
* - useEvent: Hook to subscribe to player events and trigger re-renders
*/
const { usePlayerAPI } = context;
const { useEvent } = hooks;
function CustomButton(): JSX.Element | null {
/**
* STEP 1: Access the Player Instance
*
* The usePlayerAPI hook retrieves the player instance from React Context.
* This player object provides methods and properties for controlling playback:
* - player.play(): Start playback
* - player.pause(): Pause playback
* - player.paused: Boolean indicating if player is currently paused
* - player.muted: Boolean indicating if audio is muted
* - player.volume: Current volume level (0-100)
* - And many more...
*
* IMPORTANT: This hook must be used inside a PlayerAPIProvider.
* The Player component we created earlier provides this context automatically.
*/
const player = usePlayerAPI();
/**
* STEP 2: Listen for Player State Changes
*
* The useEvent hook subscribes to player events. When these events fire,
* the component re-renders to reflect the new state.
*
* Events we're listening to:
* - "paused": Fired when playback is paused
* - "playing": Fired when playback starts/resumes
*
* Why is this useful?
* - Your UI can automatically update when player state changes
* - The button text will switch between "Play" and "Pause"
* - Changes from other controls will be reflected in this button
* - External events (like autoplay) will update the UI
*
* Other available events: "muted", "unmuted", "volumechange",
* "ended", "error", "loadedmetadata", etc.
*/
useEvent(player, "paused");
useEvent(player, "playing");
/**
* STEP 3: Handle Button Click
*
* This function runs when the user clicks the button.
* It toggles playback based on the current player state.
*
* HOW IT WORKS:
* 1. Check if player is currently paused (player.paused)
* 2. If paused: call player.play() to start playback
* 3. If playing: call player.pause() to pause playback
*
* NOTE: These methods are async, so we use await.
* They return promises that resolve when the operation completes.
*
* useCallback optimization:
* - Prevents the function from being recreated on every render
* - Only recreates if 'player' changes
* - Improves performance, especially for frequently rendered components
*
* CUSTOMIZATION IDEAS:
* - Add analytics tracking when user plays/pauses
* - Show a loading indicator during state transitions
* - Add keyboard shortcuts (spacebar to play/pause)
* - Implement double-click to fullscreen
* - Add haptic feedback on mobile devices
*/
const handleClick = useCallback(async () => {
if (player.paused) {
// Player is paused, so start playing
await player.play();
} else {
// Player is playing, so pause it
await player.pause();
}
}, [player]);
/**
* STEP 4: Render the Button
*
* A simple button that shows different text based on player state.
* The text dynamically changes between "Play" and "Pause" thanks to
* the useEvent hooks that trigger re-renders on state changes.
*
* CUSTOMIZATION IDEAS:
* - Use icons instead of text (play/pause icons)
* - Add CSS transitions for smooth state changes
* - Show loading state while transitioning
* - Add different styling for playing vs paused states
* - Use your design system's button component
* - Add accessibility attributes (aria-label, aria-pressed)
* - Add keyboard support (Enter, Space)
*
* @example Advanced Button:
* ```tsx
* <button
* onClick={handleClick}
* aria-label={player.paused ? "Play video" : "Pause video"}
* aria-pressed={!player.paused}
* className={player.paused ? "btn-paused" : "btn-playing"}
* >
* {player.paused ? <PlayIcon /> : <PauseIcon />}
* </button>
* ```
*/
return (
<button type="button" onClick={handleClick} className="bg-blue-500 text-white p-2 rounded-md">
Custom Button: {player.paused ? "Play" : "Pause"}
</button>
);
}
/**
* EXPORT WITH MEMOIZATION
*
* We wrap the component in React.memo() to prevent unnecessary re-renders.
* The component only re-renders when:
* - Props change (this component has no props)
* - Events it listens to are triggered (paused, playing)
*
* This optimization is valuable for player controls because:
* - They may be rendered alongside many other UI elements
* - Player state can change frequently
* - Unnecessary re-renders can impact performance
*/
export default memo(CustomButton);
Customized Player Component
As mentioned above, this is almost identical to the component built in View A Stream. The only difference is the <CustomPlayButton/> included in the return.
// CustomizedPlayerComponent.tsx
import React from "react";
import { components } from "@video/video-client-react";
const { VolumeRange, QualitySelect, ToggleMuteButton, TogglePlayButton } = components;
/**
* IMPORT CUSTOM COMPONENTS
*
* - CustomPlayButton: A custom play/pause button demonstrating how to
* create your own player controls
* - Player: A wrapper component that manages manifest loading, player
* initialization, and provides context to child components
*/
import CustomPlayButton from "../../components/CustomPlayButton";
import Player from "../../components/Player";
/**
* IMPORT MANIFEST URL
*
* */
import { manifestUrl } from "../../utils";
/**
* STYLING CONFIGURATION
*/
const classNames = {
playerClassNames: {
videoClassName: "md:w-1/2 object-cover overflow-hidden rounded-xl",
playerContainerClassName: "relative flex flex-row gap-2",
},
};
function CustomizedPlayerComponent(): React.ReactElement | null {
/**
* RENDER THE MANIFEST PLAYER
*
* IMPORTANT: All control components must be children of the Player component
* to access the PlayerAPIProvider context.
*/
return (
<Player source={manifestUrl} classNames={classNames.playerClassNames} >
<div className="flex flex-col gap-2">
<QualitySelect />
<ToggleMuteButton />
<TogglePlayButton />
<VolumeRange />
{/* Custom play/pause button - demonstrates how to create custom controls */}
<CustomPlayButton />
</div>
</Player>
);
}
export default CustomizedPlayerComponent;
Full Code
Customize Your Player Components
// CustomPlayButton.tsx
/**
* CustomPlayButton Component
*
* OVERVIEW:
* This component demonstrates how to create custom player controls that
* interact with the video client's player API. It's a simple play/pause
* button that shows how to access player state and control playback.
*
* WHAT YOU'LL LEARN:
* - How to access the player instance from context
* - How to listen for and react to player events
* - How to control playback programmatically
* - How to create custom UI components that integrate with the player
*
* KEY CONCEPTS:
* 1. CONTEXT: Accessing the player instance via usePlayerAPI hook
* 2. EVENTS: Listening for state changes (paused, playing) via useEvent
* 3. PLAYBACK CONTROL: Using player.play() and player.pause() methods
* 4. REACTIVE UI: Button text changes based on player state
*
* USE CASES:
* - Creating custom styled buttons that match your design system
* - Adding additional functionality beyond built-in components
* - Implementing complex control logic
* - Building integrated player interfaces
*
* @example
* ```tsx
* // Must be used inside PlayerAPIProvider
* <PlayerAPIProvider playerAPI={player}>
* <CustomPlayButton />
* </PlayerAPIProvider>
* ```
*/
import React, { memo, useCallback } from "react";
/**
* IMPORT EXPLANATIONS:
*
* 1. React imports:
* - memo: Prevents unnecessary re-renders for performance
* - useCallback: Memoizes callback functions to prevent recreation
*
* 2. @video/video-client-react imports:
* - context: Provides hooks to access player context
* - hooks: Provides custom hooks for player functionality
*/
import { context, hooks } from "@video/video-client-react";
/**
* EXTRACT CONTEXT AND EVENT HOOKS
*
* - usePlayerAPI: Hook to access the player instance from context
* - useEvent: Hook to subscribe to player events and trigger re-renders
*/
const { usePlayerAPI } = context;
const { useEvent } = hooks;
function CustomButton(): JSX.Element | null {
/**
* STEP 1: Access the Player Instance
*
* The usePlayerAPI hook retrieves the player instance from React Context.
* This player object provides methods and properties for controlling playback:
* - player.play(): Start playback
* - player.pause(): Pause playback
* - player.paused: Boolean indicating if player is currently paused
* - player.muted: Boolean indicating if audio is muted
* - player.volume: Current volume level (0-100)
* - And many more...
*
* IMPORTANT: This hook must be used inside a PlayerAPIProvider.
* The Player component we created earlier provides this context automatically.
*/
const player = usePlayerAPI();
/**
* STEP 2: Listen for Player State Changes
*
* The useEvent hook subscribes to player events. When these events fire,
* the component re-renders to reflect the new state.
*
* Events we're listening to:
* - "paused": Fired when playback is paused
* - "playing": Fired when playback starts/resumes
*
* Why is this useful?
* - Your UI can automatically update when player state changes
* - The button text will switch between "Play" and "Pause"
* - Changes from other controls will be reflected in this button
* - External events (like autoplay) will update the UI
*
* Other available events: "muted", "unmuted", "volumechange",
* "ended", "error", "loadedmetadata", etc.
*/
useEvent(player, "paused");
useEvent(player, "playing");
/**
* STEP 3: Handle Button Click
*
* This function runs when the user clicks the button.
* It toggles playback based on the current player state.
*
* HOW IT WORKS:
* 1. Check if player is currently paused (player.paused)
* 2. If paused: call player.play() to start playback
* 3. If playing: call player.pause() to pause playback
*
* NOTE: These methods are async, so we use await.
* They return promises that resolve when the operation completes.
*
* useCallback optimization:
* - Prevents the function from being recreated on every render
* - Only recreates if 'player' changes
* - Improves performance, especially for frequently rendered components
*
* CUSTOMIZATION IDEAS:
* - Add analytics tracking when user plays/pauses
* - Show a loading indicator during state transitions
* - Add keyboard shortcuts (spacebar to play/pause)
* - Implement double-click to fullscreen
* - Add haptic feedback on mobile devices
*/
const handleClick = useCallback(async () => {
if (player.paused) {
// Player is paused, so start playing
await player.play();
} else {
// Player is playing, so pause it
await player.pause();
}
}, [player]);
/**
* STEP 4: Render the Button
*
* A simple button that shows different text based on player state.
* The text dynamically changes between "Play" and "Pause" thanks to
* the useEvent hooks that trigger re-renders on state changes.
*
* CUSTOMIZATION IDEAS:
* - Use icons instead of text (play/pause icons)
* - Add CSS transitions for smooth state changes
* - Show loading state while transitioning
* - Add different styling for playing vs paused states
* - Use your design system's button component
* - Add accessibility attributes (aria-label, aria-pressed)
* - Add keyboard support (Enter, Space)
*
* @example Advanced Button:
* ```tsx
* <button
* onClick={handleClick}
* aria-label={player.paused ? "Play video" : "Pause video"}
* aria-pressed={!player.paused}
* className={player.paused ? "btn-paused" : "btn-playing"}
* >
* {player.paused ? <PlayIcon /> : <PauseIcon />}
* </button>
* ```
*/
return (
<button type="button" onClick={handleClick} className="bg-blue-500 text-white p-2 rounded-md">
Custom Button: {player.paused ? "Play" : "Pause"}
</button>
);
}
/**
* EXPORT WITH MEMOIZATION
*
* We wrap the component in React.memo() to prevent unnecessary re-renders.
* The component only re-renders when:
* - Props change (this component has no props)
* - Events it listens to are triggered (paused, playing)
*
* This optimization is valuable for player controls because:
* - They may be rendered alongside many other UI elements
* - Player state can change frequently
* - Unnecessary re-renders can impact performance
*/
export default memo(CustomButton);
// CustomizedPlayerComponent.tsx
import React from "react";
import { components } from "@video/video-client-react";
const { VolumeRange, QualitySelect, ToggleMuteButton, TogglePlayButton } = components;
/**
* IMPORT CUSTOM COMPONENTS
*
* - CustomPlayButton: A custom play/pause button demonstrating how to
* create your own player controls
* - Player: A wrapper component that manages manifest loading, player
* initialization, and provides context to child components
*/
import CustomPlayButton from "../../components/CustomPlayButton";
import Player from "../../components/Player";
/**
* IMPORT MANIFEST URL
*
* */
import { manifestUrl } from "../../utils";
/**
* STYLING CONFIGURATION
*/
const classNames = {
playerClassNames: {
videoClassName: "md:w-1/2 object-cover overflow-hidden rounded-xl",
playerContainerClassName: "relative flex flex-row gap-2",
},
};
function CustomizedPlayerComponent(): React.ReactElement | null {
/**
* RENDER THE MANIFEST PLAYER
*
* IMPORTANT: All control components must be children of the Player component
* to access the PlayerAPIProvider context.
*/
return (
<Player source={manifestUrl} classNames={classNames.playerClassNames} >
<div className="flex flex-col gap-2">
<QualitySelect />
<ToggleMuteButton />
<TogglePlayButton />
<VolumeRange />
{/* Custom play/pause button - demonstrates how to create custom controls */}
<CustomPlayButton />
</div>
</Player>
);
}
export default CustomizedPlayerComponent;
Supporting Components
The following components are used in this demo and are documented in View A Stream:
// Player.tsx
/**
* Player Component
*
* OVERVIEW:
* A reusable video player component that handles manifest loading, player
* initialization, and provides context for control components. This component
* abstracts away the complexity of requesting a player, managing its lifecycle,
* and providing it to child components.
*
* WHAT IT DOES:
* - Loads and plays video streams from manifest URLs (HLS, FLV, DASH)
* - Initializes the player using the useRequestPlayer hook
* - Provides PlayerAPIProvider context to all children
* - Renders the video element with customizable styling
* - Manages player lifecycle (creation and disposal)
*
* KEY FEATURES:
* - Fully self-contained: Handles all player setup automatically
* - Customizable styling via classNames prop
* - Supports children for adding custom controls
* - Works with any manifest format supported by the video client
* - Automatic error handling and loading states
*
* REQUIREMENTS:
* - A valid manifest URL (HLS .m3u8, FLV .flv, or DASH .mpd)
* - No additional context providers needed (self-contained)
*
* @example Basic usage:
* ```tsx
* <Player source="https://example.com/stream.m3u8">
* <TogglePlayButton />
* <ToggleMuteButton />
* </Player>
* ```
*
* @example With custom styling:
* ```tsx
* <Player
* source={manifestUrl}
* classNames={{
* videoClassName: "w-full h-auto",
* playerContainerClassName: "relative"
* }}
* >
* <YourCustomControls />
* </Player>
* ```
*/
import React, {useRef} from "react";
/**
* IMPORT VIDEO CLIENT COMPONENTS AND HOOKS
*
* - components: Pre-built UI components (Video element wrapper)
* - context: React Context providers and hooks (PlayerAPIProvider)
* - hooks: Custom hooks for video functionality (useRequestPlayer)
*/
import { components, context, hooks } from "@video/video-client-react";
/**
* EXTRACT REQUIRED EXPORTS
*
* - PlayerAPIProvider: Context provider that makes player instance available to children
* - Video: Video element wrapper with built-in player integration
* - useRequestPlayer: Hook that requests and initializes a player from a manifest URL
*/
const { PlayerAPIProvider, usePeerAPI } = context;
const { Video } = components;
const { useRequestPlayer } = hooks;
/**
* STYLING INTERFACE
*
* Defines the CSS classes that can be customized for different parts
* of the player component. All fields are optional.
*/
export interface PlayerClassNames {
playerClassName: string;
videoClassName: string;
displayNameClassName: string;
playerContainerClassName: string;
}
/**
* DEFAULT STYLING
*
* Default styling using Tailwind CSS classes.
*/
const defaultClassNames: PlayerClassNames = {
playerClassName: "bg-black-200",
videoClassName: "h-56 w-56 object-cover overflow-hidden rounded-xl",
displayNameClassName: "text-sm text-white absolute bottom-1 left-1 z-[200] bg-black/50 px-2 py-1 rounded-md",
playerContainerClassName: "relative h-56 w-56",
};
/**
* PROPS INTERFACE
*
* Extends UseRequestPlayerOptions to inherit all player configuration options
* like source, requestPlayerOptions, and eventsMap.
*/
interface PlayerProps extends hooks.UseRequestPlayerOptions {
/** Child components (typically player controls) */
children?: React.ReactNode;
/** Optional custom class names (partial override of defaults) */
classNames?: Partial<PlayerClassNames>;
}
function Player({source, children, classNames, requestPlayerOptions, eventsMap}: PlayerProps): JSX.Element | null {
/**
* MERGE CUSTOM AND DEFAULT CLASS NAMES
*
* Combines default styling with any custom overrides provided via props.
*/
const mergedClassNames = { ...defaultClassNames, ...classNames };
/**
* VIDEO ELEMENT REF
*
* Creates a reference to the HTML video element.
* While not strictly required for basic playback (the Video component handles
* this internally), having a ref available can be useful for:
* - Direct DOM manipulation if needed
* - Integration with third-party libraries
* - Advanced video element access
*/
const videoElement = useRef<HTMLVideoElement>(null);
/**
* REQUEST AND INITIALIZE PLAYER
*
* The useRequestPlayer hook is the core of this component. It:
* 1. Takes the manifest URL (source) and loads it
* 2. Determines the appropriate player technology (HLS.js, FLV.js, native)
* 3. Creates a player instance configured for that technology
* 4. Handles errors and loading states automatically
* 5. Returns the player instance (or null if still loading/failed)
*
* The hook also handles cleanup when the component unmounts, ensuring
* the player is properly disposed and resources are freed.
*
* PARAMETERS:
* - source: The manifest URL to load (required)
* - requestPlayerOptions: Optional configuration for the player
* - eventsMap: Optional event listeners to attach to the player
*/
const player = useRequestPlayer({ source, requestPlayerOptions, eventsMap });
/**
* LOADING STATE CHECK
*
* If the player hasn't been created yet (still loading or failed),
* return null to render nothing.
*
* In a production app, you might want to show:
* - A loading spinner while player is initializing
* - An error message if loading failed
* - A placeholder image or poster frame
*/
if (player == null) return null;
/**
* RENDER THE PLAYER UI
*
* STRUCTURE:
* PlayerAPIProvider (makes player available to children)
* └── Outer container (playerClassName)
* └── Video container (playerContainerClassName)
* ├── Video element (videoClassName) - displays the stream
* └── Children - typically control buttons and UI elements
*
* HOW IT WORKS:
* 1. PlayerAPIProvider shares the player instance via React Context
* 2. All child components can access the player using usePlayerAPI()
* 3. The Video component connects to the player and displays the video
* 4. Children (controls) automatically sync with player state
*
* IMPORTANT: The PlayerAPIProvider is crucial - without it, child
* components wouldn't be able to access the player instance.
*/
return (
<PlayerAPIProvider playerAPI={player}>
{/* Outer wrapper for styling */}
<div className={mergedClassNames.playerClassName ?? ""}>
{/* Container for video and overlays */}
<div className={mergedClassNames.playerContainerClassName ?? ""}>
{/* Video element - displays the actual stream */}
<Video id="player-video" ref={videoElement} className={mergedClassNames.videoClassName ?? ""}/>
{/* Child components - typically controls */}
{children}
</div>
</div>
</PlayerAPIProvider>
);
}
/**
* EXPORT THE PLAYER COMPONENT
*
* This component is designed to be highly reusable. It can be used:
* - As-is for basic manifest playback
* - As a wrapper for custom player UIs
* - In galleries showing multiple streams
* - For VOD, live streams, or transcoded content
*/
export default Player;
To learn more about the video-client-core library and advanced features:
- Learn how to customize your preview player
- Set up a group call
- Create a private viewer component