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

@instincthub/video-player

v0.1.0

Published

HLS-first video player with MP4 fallback for React

Downloads

10

Readme

@instincthub/video-player

HLS-first video player for React with automatic MP4 fallback, adaptive bitrate streaming, and full playback controls.

Built on hls.js with lazy loading — hls.js is only downloaded when needed and never loaded on Safari (native HLS) or for MP4-only sources.

Installation

npm install @instincthub/video-player

Peer dependencies: React 18 or 19.

Quick Start

import { VideoPlayer } from "@instincthub/video-player";
import "@instincthub/video-player/styles";

function Page() {
  return (
    <VideoPlayer
      src="https://example.com/stream/master.m3u8"
      fallbackSrc="https://example.com/video.mp4"
      poster="https://example.com/poster.jpg"
    />
  );
}

Features

  • HLS streaming with adaptive bitrate quality switching via hls.js
  • Automatic MP4 fallback when HLS fails (network, media, or codec errors)
  • Native HLS on Safari/iOS — hls.js is never loaded
  • Lazy loading — hls.js bundle only downloads when an .m3u8 source is used
  • Full playback controls — play/pause, seek, volume, speed, quality selector, fullscreen
  • Keyboard shortcuts — Space/K, J/L, arrows, M, F
  • Accessible — ARIA labels, focus-visible outlines, 44px tap targets
  • Themeable — CSS custom properties for colors, fonts, radii
  • Headless modeuseVideoPlayer hook for building custom UIs
  • Imperative ref API — control the player programmatically
  • TypeScript — full type definitions included
  • Idle tracking — auto-pause on inactivity with customizable "Continue watching?" overlay
  • Lightweight — ~42KB player core (before gzip), no runtime CSS-in-JS

How It Works

The player automatically selects the best playback strategy:

  1. MP4 source — plays directly via <video>
  2. M3U8 + Safari/iOS — plays natively via <video> (no hls.js loaded)
  3. M3U8 + Chrome/Firefox/Edge — lazy-loads hls.js for ABR streaming
  4. HLS failure — falls back to fallbackSrc MP4, resuming from last position

Component Props

<VideoPlayer
  // Source (required)
  src="video.m3u8"              // Primary URL (.m3u8 or .mp4)
  fallbackSrc="video.mp4"       // MP4 fallback when HLS fails
  poster="poster.jpg"           // Poster image before playback
  type="auto"                   // Force type: "auto" | "hls" | "mp4"

  // Playback
  autoplay={false}              // Autoplay (must be muted for browser policy)
  muted={false}                 // Start muted
  loop={false}                  // Loop playback
  volume={0.5}                  // Initial volume (0-1)
  playbackRate={1}              // Initial speed
  preload="metadata"            // "none" | "metadata" | "auto"
  startTime={30}                // Resume position in seconds

  // HLS
  hlsConfig={{                  // hls.js config overrides
    maxBufferLength: 60,
    startLevel: -1,
    capLevelToPlayerSize: true,
  }}

  // Idle tracking
  idleTimeout={5}               // Auto-pause after 5 min of inactivity (0 = disabled)
  idleMessage="Still watching?" // Custom idle overlay message

  // UI
  controls={true}               // Show built-in controls
  className="my-player"         // Container CSS class
  style={{ maxWidth: 800 }}     // Container inline styles
  width="100%"                  // Container width
  height="auto"                 // Container height (16:9 default)

  // Events
  onReady={() => {}}
  onPlay={() => {}}
  onPause={() => {}}
  onEnded={() => {}}
  onTimeUpdate={(time, duration) => {}}
  onError={(error) => {}}
  onBufferStart={() => {}}
  onBufferEnd={() => {}}
  onQualityChange={(level) => {}}
  onFallback={(reason) => {}}
/>

Props Reference

| Prop | Type | Default | Description | |------|------|---------|-------------| | src | string | — | Primary video URL (.m3u8 or .mp4). Required. | | fallbackSrc | string | — | MP4 fallback URL when HLS fails | | poster | string | — | Poster image URL | | type | "auto" \| "hls" \| "mp4" | "auto" | Force source type detection | | autoplay | boolean | false | Autoplay (must be muted for browser policy) | | muted | boolean | false | Start muted | | loop | boolean | false | Loop playback | | volume | number | 0.5 | Initial volume (0–1) | | playbackRate | number | 1 | Initial playback speed | | preload | "none" \| "metadata" \| "auto" | "metadata" | Preload strategy | | startTime | number | — | Resume position in seconds | | hlsConfig | Partial<HlsConfig> | — | hls.js configuration overrides | | idleTimeout | number | — | Idle timeout in minutes (0 or undefined disables) | | idleMessage | string | "Video paused. Continue watching?" | Idle overlay message | | controls | boolean | true | Show built-in controls | | className | string | — | Container CSS class | | style | CSSProperties | — | Container inline styles | | width | string \| number | "100%" | Container width | | height | string \| number | "auto" | Container height |

Event Callbacks

| Event | Signature | Description | |-------|-----------|-------------| | onReady | () => void | Player is ready (metadata loaded) | | onPlay | () => void | Playback starts or resumes | | onPause | () => void | Playback pauses | | onEnded | () => void | Video reaches the end | | onTimeUpdate | (time: number, duration: number) => void | Current time changes (~4x/s) | | onError | (error: PlayerError) => void | Any playback error | | onBufferStart | () => void | Buffering begins | | onBufferEnd | () => void | Buffering ends | | onQualityChange | (level: QualityLevel) => void | HLS quality level changes | | onFallback | (reason: FallbackReason) => void | HLS falls back to MP4 | | onIdlePause | () => void | Video auto-paused due to inactivity |

Idle Tracking

Auto-pause the video when the user is inactive. A "Continue watching?" overlay appears with a resume button.

<VideoPlayer
  src="video.mp4"
  idleTimeout={5}                // Pause after 5 min of inactivity
  idleMessage="Still watching?"  // Custom message (optional)
  onIdlePause={() => console.log("User went idle")}
/>
  • idleTimeout is in minutes (e.g., 2, 5, 10). Set to 0 or omit to disable.
  • Timer resets on any user activity (mouse, keyboard, touch, scroll) and only runs while playing.

Imperative Ref API

Control the player programmatically using a ref:

import { useRef } from "react";
import { VideoPlayer, VideoPlayerRef } from "@instincthub/video-player";

function Page() {
  const playerRef = useRef<VideoPlayerRef>(null);

  return (
    <>
      <VideoPlayer src="video.mp4" ref={playerRef} />

      <button onClick={() => playerRef.current?.play()}>Play</button>
      <button onClick={() => playerRef.current?.seek(30)}>Jump to 30s</button>
      <button onClick={() => playerRef.current?.setPlaybackRate(2)}>2x</button>
    </>
  );
}

| Method | Description | |--------|-------------| | play() | Start or resume playback | | pause() | Pause playback | | seek(time) | Seek to time in seconds | | setVolume(vol) | Set volume (0–1) | | setMuted(muted) | Set mute state | | setPlaybackRate(rate) | Set playback speed | | setQuality(index) | Set HLS quality level (-1 for auto) | | getQualityLevels() | Get available HLS quality levels | | getCurrentTime() | Get current playback time | | getDuration() | Get total duration | | requestFullscreen() | Enter fullscreen | | exitFullscreen() | Exit fullscreen | | destroy() | Clean up all resources |

Headless Mode (useVideoPlayer)

Build a fully custom UI using the hook directly:

import { useVideoPlayer, formatTime } from "@instincthub/video-player";

function CustomPlayer() {
  const { containerRef, videoRef, state, controls } = useVideoPlayer({
    src: "https://example.com/video.mp4",
    controls: false,
  });

  return (
    <div ref={containerRef}>
      <video ref={videoRef} playsInline />

      <div className="my-controls">
        <button onClick={controls.togglePlay}>
          {state.status === "playing" ? "Pause" : "Play"}
        </button>
        <span>{formatTime(state.currentTime)} / {formatTime(state.duration)}</span>
        <button onClick={() => controls.seekRelative(-10)}>-10s</button>
        <button onClick={() => controls.seekRelative(10)}>+10s</button>
        <button onClick={controls.toggleMute}>
          {state.muted ? "Unmute" : "Mute"}
        </button>
        <button onClick={controls.toggleFullscreen}>Fullscreen</button>
      </div>
    </div>
  );
}

Hook Return

| Property | Type | Description | |----------|------|-------------| | containerRef | RefObject<HTMLDivElement> | Attach to outermost container (for fullscreen + keyboard) | | videoRef | RefObject<HTMLVideoElement> | Attach to the <video> element | | state | PlayerState | Reactive player state | | controls | PlayerControls | Imperative control methods |

PlayerState

| Field | Type | Description | |-------|------|-------------| | status | PlayerStatus | "idle" | "loading" | "ready" | "playing" | "paused" | "buffering" | "seeking" | "ended" | "error" | | currentTime | number | Current position (seconds) | | duration | number | Total duration (seconds) | | buffered | number | Buffered ahead (seconds) | | volume | number | Current volume (0–1) | | muted | boolean | Mute state | | playbackRate | number | Current speed | | isFullscreen | boolean | Fullscreen state | | qualityLevels | QualityLevel[] | Available HLS quality levels | | currentQuality | number | Current quality index (-1 = auto) | | autoQuality | boolean | Whether auto quality is enabled | | error | PlayerError \| null | Current error | | hasStarted | boolean | Whether playback has started |

PlayerControls

| Method | Description | |--------|-------------| | play() | Start playback | | pause() | Pause playback | | togglePlay() | Toggle play/pause | | seek(time) | Seek to absolute time | | seekRelative(offset) | Seek relative to current position | | setVolume(vol) | Set volume (0–1) | | toggleMute() | Toggle mute | | setPlaybackRate(rate) | Set playback speed | | setQuality(index) | Set HLS quality (-1 for auto) | | toggleFullscreen() | Toggle fullscreen |

Keyboard Shortcuts

Click the player to focus, then use:

| Key | Action | |-----|--------| | Space / K | Toggle play/pause | | J / ArrowLeft | Rewind 10 seconds | | L / ArrowRight | Forward 10 seconds | | ArrowUp | Volume up 10% | | ArrowDown | Volume down 10% | | M | Toggle mute | | F | Toggle fullscreen |

Theming

Override CSS custom properties on the player container or any ancestor:

.my-player {
  --ihub-player-primary: #ff6b6b;
  --ihub-player-accent: #ee5a24;
  --ihub-player-bg: #1a1a2e;
  --ihub-player-controls-bg: rgba(26, 26, 46, 0.8);
  --ihub-player-text: #ffffff;
  --ihub-player-progress: #ff6b6b;
  --ihub-player-progress-hover: #ee5a24;
  --ihub-player-buffer: rgba(255, 255, 255, 0.2);
  --ihub-player-error: #e74c3c;
  --ihub-player-border-radius: 12px;
  --ihub-player-font-family: "Inter", sans-serif;
  --ihub-player-heading-font: "Inter", sans-serif;
  --ihub-player-transition: 0.15s ease;
}
<VideoPlayer src="video.mp4" className="my-player" />

| Variable | Default | Description | |----------|---------|-------------| | --ihub-player-primary | #00838f | Primary color (progress thumb, buttons) | | --ihub-player-accent | #0fabbc | Hover/active color | | --ihub-player-bg | #000000 | Player background | | --ihub-player-controls-bg | rgba(0,0,0,0.7) | Controls bar gradient | | --ihub-player-text | #ffffff | Text and icon color | | --ihub-player-progress | #00838f | Progress bar fill | | --ihub-player-progress-hover | #0fabbc | Progress bar on hover | | --ihub-player-buffer | rgba(255,255,255,0.3) | Buffer indicator | | --ihub-player-error | #ea5f5e | Error text color | | --ihub-player-border-radius | 8px | Corner radius | | --ihub-player-font-family | "Nunito", sans-serif | Body font | | --ihub-player-heading-font | "Montserrat", sans-serif | Label font | | --ihub-player-transition | 0.2s ease | Animation speed |

HLS Configuration

The player ships with sensible defaults. Override any hls.js config option via the hlsConfig prop:

<VideoPlayer
  src="stream.m3u8"
  fallbackSrc="video.mp4"
  hlsConfig={{
    startLevel: -1,               // Auto-select start quality
    capLevelToPlayerSize: true,    // Don't load above display size
    maxBufferLength: 30,           // Buffer 30s ahead
    maxMaxBufferLength: 60,        // Hard cap at 60s
    abrEwmaDefaultEstimate: 500000, // 500kbps initial estimate
    manifestLoadingMaxRetry: 3,    // Retry manifest 3 times
    fragLoadingMaxRetry: 3,        // Retry segments 3 times
    lowLatencyMode: false,         // Disable for VOD
  }}
/>

Error Recovery

The player handles HLS errors automatically:

  1. Network errors — retries with exponential backoff (3 attempts), then falls back to MP4
  2. Media errors — calls recoverMediaError(), then swapAudioCodec(), then falls back to MP4
  3. Other errors — falls back to MP4 immediately

Fallback preserves the current playback position and is seamless to the user.

TypeScript

All types are exported:

import type {
  VideoPlayerProps,
  VideoPlayerRef,
  PlayerState,
  PlayerControls,
  UseVideoPlayerReturn,
  PlayerStatus,
  SourceType,
  QualityLevel,
  PlayerError,
  FallbackReason,
  VideoPlayerEventProps,
} from "@instincthub/video-player";

Browser Support

| Browser | HLS Strategy | Minimum Version | |---------|-------------|-----------------| | Safari (macOS/iOS) | Native <video> | 14+ | | Chrome | hls.js | 90+ | | Firefox | hls.js | 90+ | | Edge | hls.js | 90+ |

Development

# Install dependencies
npm install

# Run example app (Next.js)
npm run dev

# Build the library
npm run build

# Run tests
npm run test

# Type check
npm run typecheck

# Lint
npm run lint

License

MIT