npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-helios

v2.3.3

Published

Production-grade React video player with HLS, quality selection, live streams, subtitles, and VTT sprite thumbnail preview

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-helios

Peer dependencies — install if not already in your project:

npm install react react-dom

Quick 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.tsx and 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,90

The 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 auto

Custom 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) | | 09 | 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:

  • timeupdate and progress events are handled by direct DOM mutation (refs), not React state.
  • ProgressBar and TimeDisplay self-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.ts

Development

# Install dependencies
npm install

# Build the library
npm run build

# Watch mode (rebuild on changes)
npm run dev

# Type-check only
npm run typecheck

To run the demo app against your local build:

cd examples/nextjs-demo
npm install
npm run dev

Publishing

prepublishOnly runs the build automatically:

npm publish

License

MIT