react-helios
v2.3.3
Published
Production-grade React video player with HLS, quality selection, live streams, subtitles, and VTT sprite thumbnail preview
Maintainers
Readme
react-helios
Production-grade React video player with HLS streaming, adaptive quality selection, live stream support, subtitle tracks, VTT sprite sheet thumbnail preview, Picture-in-Picture, and full keyboard control.
Installation
npm install react-heliosPeer dependencies — install if not already in your project:
npm install react react-domQuick Start
import { VideoPlayer } from "react-helios";
import "react-helios/styles";
export default function App() {
return (
<VideoPlayer
src="https://example.com/video.mp4"
controls
autoplay={false}
/>
);
}Next.js — import the styles in your root
layout.tsxand mark the component as"use client"or wrap it in a client component.
HLS Streaming
Pass any .m3u8 URL — HLS.js is initialised automatically:
<VideoPlayer
src="https://example.com/stream.m3u8"
controls
enableHLS // default: true
/>On Safari the browser's native HLS engine is used. A LIVE badge and GO LIVE button appear automatically for live streams.
Thumbnail Preview
Hover over the progress bar to see a time tooltip. For rich sprite-sheet thumbnails, pass a thumbnailVtt URL pointing to a WebVTT thumbnail file.
<VideoPlayer
src="https://example.com/video.mp4"
thumbnailVtt="https://example.com/thumbs/storyboard.vtt"
/>VTT format
Each cue in the .vtt file maps a time range to a rectangular region inside a sprite image using the #xywh=x,y,w,h fragment:
WEBVTT
00:00:00.000 --> 00:00:05.000
https://example.com/thumbs/sprite.jpg#xywh=0,0,160,90
00:00:05.000 --> 00:00:10.000
https://example.com/thumbs/sprite.jpg#xywh=160,0,160,90
00:00:10.000 --> 00:00:15.000
https://example.com/thumbs/sprite.jpg#xywh=320,0,160,90The player fetches the VTT file once, parses all cues, and uses CSS background-position to display the correct sprite cell during hover — no additional network requests per hover.
To disable the preview entirely:
<VideoPlayer src="..." enablePreview={false} />Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| src | string | — | Video URL (MP4, WebM, HLS .m3u8, …) |
| poster | string | — | Poster image shown before playback |
| controls | boolean | true | Show the built-in control bar |
| autoplay | boolean | false | Start playback on mount |
| muted | boolean | false | Start muted |
| loop | boolean | false | Loop the video |
| preload | "none" \| "metadata" \| "auto" | "metadata" | Native preload attribute |
| playbackRates | PlaybackRate[] | [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] | Available speed options |
| enableHLS | boolean | true | Enable HLS.js for .m3u8 sources |
| enablePreview | boolean | true | Show thumbnail / time tooltip on progress bar hover |
| thumbnailVtt | string | — | URL to a WebVTT sprite sheet file for rich thumbnail preview |
| hlsConfig | Partial<HlsConfig> | — | Override any hls.js configuration option |
| subtitles | SubtitleTrack[] | — | Subtitle / caption tracks |
| crossOrigin | "anonymous" \| "use-credentials" | — | CORS attribute for the video element |
| className | string | — | CSS class on the player container |
| onPlay | () => void | — | Fired when playback starts |
| onPause | () => void | — | Fired when playback pauses |
| onEnded | () => void | — | Fired when playback ends |
| onError | (error: VideoError) => void | — | Fired on playback or stream errors |
| onTimeUpdate | (time: number) => void | — | Fired every ~250 ms during playback |
| onDurationChange | (duration: number) => void | — | Fired when video duration becomes known |
| onBuffering | (isBuffering: boolean) => void | — | Fired when buffering starts / stops |
| onTheaterModeChange | (isTheater: boolean) => void | — | Fired when theater mode is toggled |
| contextMenuItems | ContextMenuItem[] | — | Extra items appended to the right-click context menu |
| controlBarItems | ControlBarItem[] | — | Extra icon buttons appended to the right side of the control bar |
Quality Selection
For HLS streams (.m3u8) the player automatically parses the available quality levels from the manifest. Once levels are available, the Settings (⚙) button in the control bar grows a Speed / Quality tab bar:
- Speed tab — always visible, lets you change playback rate.
- Quality tab — appears only for HLS streams. Lists all levels sorted by bitrate (e.g. 1080p, 720p, 480p) plus an Auto option that enables ABR (adaptive bitrate). The current auto-selected level is shown in parentheses next to "Auto".
For plain MP4/WebM files there are no quality levels to switch between, so the Quality tab never appears.
You can also switch quality programmatically via the ref:
playerRef.current?.setQualityLevel(0); // pin to highest level
playerRef.current?.setQualityLevel(-1); // back to ABR autoCustom Control Bar Buttons
Inject your own icon buttons into the right side of the control bar (between the settings gear and the PiP/Theater/Fullscreen buttons) using controlBarItems:
import { VideoPlayer, ControlBarItem } from "react-helios";
const items: ControlBarItem[] = [
{
key: "download",
label: "Download",
icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zm-8 2V5h2v6h1.17L12 13.17 9.83 11H11zm-6 7h14v2H5v-2z"/></svg>,
onClick: () => downloadVideo(),
},
{
key: "share",
label: "Share",
title: "Share this video",
icon: <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11A2.99 2.99 0 0 0 18 8a3 3 0 1 0-3-3c0 .24.04.47.09.7L8.04 9.81A2.99 2.99 0 0 0 6 9a3 3 0 1 0 3 3c0-.24-.04-.47-.09-.7l7.05-4.11c.52.47 1.2.77 1.96.77a3 3 0 0 0 3-3 3 3 0 0 0-3-3z"/></svg>,
onClick: () => openShareDialog(),
},
];
<VideoPlayer src="..." controlBarItems={items} />Buttons receive the same controlButton CSS class as built-in buttons (hover highlight, active press scale, no focus outline).
Context Menu
Right-clicking the player shows a built-in menu (Play/Pause, Loop, Copy URL, Picture-in-Picture). You can append your own items by passing contextMenuItems:
import { VideoPlayer, ContextMenuItem } from "react-helios";
const items: ContextMenuItem[] = [
{ label: "Add to Watchlist", onClick: () => addToWatchlist() },
{ label: "Share", onClick: () => openShareDialog() },
];
<VideoPlayer src="..." contextMenuItems={items} />Each item closes the menu automatically after its onClick is called.
Imperative API (Ref)
Use a ref to control the player programmatically:
import { useRef } from "react";
import { VideoPlayer, VideoPlayerRef } from "react-helios";
export default function App() {
const playerRef = useRef<VideoPlayerRef>(null);
return (
<>
<VideoPlayer ref={playerRef} src="..." controls />
<button onClick={() => playerRef.current?.play()}>Play</button>
<button onClick={() => playerRef.current?.pause()}>Pause</button>
<button onClick={() => playerRef.current?.seek(30)}>Jump to 30s</button>
<button onClick={() => playerRef.current?.setVolume(0.5)}>50% volume</button>
<button onClick={() => playerRef.current?.setPlaybackRate(1.5)}>1.5× speed</button>
</>
);
}VideoPlayerRef methods
| Method | Signature | Description |
|--------|-----------|-------------|
| play | () => Promise<void> | Start playback |
| pause | () => void | Pause playback |
| seek | (time: number) => void | Seek to a time in seconds |
| setVolume | (volume: number) => void | Set volume 0–1 |
| toggleMute | () => void | Toggle mute, restoring the pre-mute volume |
| setPlaybackRate | (rate: PlaybackRate) => void | Set playback speed |
| setQualityLevel | (level: number) => void | Set HLS quality level; -1 = auto ABR |
| seekToLive | () => void | Jump to the live edge (HLS live streams) |
| toggleFullscreen | () => Promise<void> | Toggle fullscreen |
| togglePictureInPicture | () => Promise<void> | Toggle Picture-in-Picture |
| toggleTheaterMode | () => void | Toggle theater (wide) mode |
| getState | () => PlayerState | Snapshot of current player state |
| getVideoElement | () => HTMLVideoElement \| null | Access the underlying <video> element |
Theater Mode
The player fires onTheaterModeChange when theater mode is toggled (via the T key, the control bar button, or playerRef.current?.toggleTheaterMode()). Wire it to your layout state to widen your container:
"use client";
import { useState } from "react";
import { VideoPlayer } from "react-helios";
export default function Page() {
const [isTheater, setIsTheater] = useState(false);
return (
<main
style={{ maxWidth: isTheater ? "1600px" : "1200px" }}
className="mx-auto px-6 transition-[max-width] duration-300"
>
<VideoPlayer
src="https://example.com/stream.m3u8"
controls
onTheaterModeChange={(t) => setIsTheater(t)}
/>
</main>
);
}The player itself does not manage your page layout — it only notifies you so you can adapt your design.
Subtitles
<VideoPlayer
src="https://example.com/video.mp4"
subtitles={[
{ id: "en", src: "/subs/en.vtt", label: "English", srclang: "en", default: true },
{ id: "es", src: "/subs/es.vtt", label: "Español", srclang: "es" },
]}
/>Subtitle files must be served with Access-Control-Allow-Origin if hosted on a different origin than the page.
Keyboard Shortcuts
Shortcuts activate when the player has focus (click the player or tab to it).
| Key | Action |
|-----|--------|
| Space / K | Play / Pause |
| ← / → | Seek −5 s / +5 s |
| ↑ / ↓ | Volume +10% / −10% |
| M | Toggle mute (restores previous volume) |
| F | Toggle fullscreen |
| T | Toggle theater mode |
| P | Toggle Picture-in-Picture |
| L | Seek to live edge (live streams only) |
| 0–9 | Jump to 0%–90% of duration |
Progress bar keyboard (when the progress bar has focus):
| Key | Action |
|-----|--------|
| ← / → | Seek −5 s / +5 s |
| Shift + ← / Shift + → | Seek −10 s / +10 s |
| Home | Jump to start |
| End | Jump to end |
TypeScript
All types are exported from the package:
import type {
VideoPlayerProps,
VideoPlayerRef,
PlayerState,
PlaybackRate,
HLSQualityLevel,
SubtitleTrack,
BufferedRange,
VideoError,
VideoErrorCode,
ThumbnailCue,
ContextMenuItem,
ControlBarItem,
} from "react-helios";
// VTT utilities (useful for server-side pre-parsing or custom UIs)
import { parseThumbnailVtt, findThumbnailCue } from "react-helios";PlayerState
interface PlayerState {
isPlaying: boolean;
currentTime: number;
duration: number;
volume: number;
isMuted: boolean;
playbackRate: number;
bufferedRanges: BufferedRange[];
isBuffering: boolean;
error: VideoError | null;
isFullscreen: boolean;
isPictureInPicture: boolean;
isTheaterMode: boolean;
isLive: boolean;
qualityLevels: HLSQualityLevel[];
currentQualityLevel: number; // -1 = ABR auto
}VideoError
type VideoErrorCode =
| "MEDIA_ERR_ABORTED"
| "MEDIA_ERR_NETWORK"
| "MEDIA_ERR_DECODE"
| "MEDIA_ERR_SRC_NOT_SUPPORTED"
| "HLS_NETWORK_ERROR"
| "HLS_FATAL_ERROR"
| "UNKNOWN";
interface VideoError {
code: VideoErrorCode;
message: string;
}ThumbnailCue
interface ThumbnailCue {
start: number; // seconds
end: number; // seconds
url: string; // absolute URL to the sprite image
x: number; // pixel offset within sprite
y: number;
w: number; // cell width in pixels
h: number; // cell height in pixels
}ControlBarItem
interface ControlBarItem {
key: string; // React reconciliation key
icon: ReactNode; // SVG, img, or any React node
label: string; // aria-label
title?: string; // tooltip (falls back to label)
onClick: () => void;
}ContextMenuItem
interface ContextMenuItem {
label: string;
onClick: () => void;
}Utility Functions
The package exports a few helper utilities used internally, exposed for custom integrations:
import { formatTime, isHLSUrl, getMimeType } from "react-helios";
formatTime(90); // "1:30"
formatTime(3661); // "1:01:01"
isHLSUrl("stream.m3u8"); // true
isHLSUrl("video.mp4"); // false
getMimeType("video.mp4"); // "video/mp4"
getMimeType("video.webm"); // "video/webm"For VTT parsing in custom UIs or server-side pre-processing:
import { parseThumbnailVtt, findThumbnailCue } from "react-helios";
import type { ThumbnailCue } from "react-helios";
const cues: ThumbnailCue[] = parseThumbnailVtt(vttText, baseUrl);
// Binary search — O(log n)
const cue = findThumbnailCue(cues, currentTime);
if (cue) {
// cue.url, cue.x, cue.y, cue.w, cue.h
}Performance
The player is architected to produce zero React re-renders during playback:
timeupdateandprogressevents are handled by direct DOM mutation (refs), not React state.ProgressBarandTimeDisplayself-subscribe to the video element — the parent tree never re-renders on seek or time change.- VTT sprite thumbnails are looked up via binary search (O(log n)) and rendered via CSS
background-position— no hidden<video>element, no canvas, no network requests per hover. - Buffered ranges are the only state that triggers a re-render (fires every few seconds during buffering, not 60× per second).
Project Structure
react-helios/
├── src/ # Library source
│ ├── components/ # VideoPlayer, Controls, control elements
│ ├── hooks/ # useVideoPlayer (state + HLS init)
│ ├── lib/ # Types, HLS utilities, VTT parser, format helpers
│ └── styles/ # CSS
├── examples/
│ └── nextjs-demo/ # Standalone Next.js demo app
├── dist/ # Build output (ESM + CJS + DTS)
├── package.json
├── tsconfig.json
└── tsup.config.tsDevelopment
# Install dependencies
npm install
# Build the library
npm run build
# Watch mode (rebuild on changes)
npm run dev
# Type-check only
npm run typecheckTo run the demo app against your local build:
cd examples/nextjs-demo
npm install
npm run devPublishing
prepublishOnly runs the build automatically:
npm publishLicense
MIT
