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/shaka-player

v0.1.0

Published

Shaka Player-based video player with adaptive streaming for React

Downloads

14

Readme

@instincthub/shaka-player

A feature-rich React video player built on Shaka Player with adaptive bitrate streaming (HLS/DASH), automatic MP4 fallback, keyboard controls, subtitles, and a fully customizable UI.

Features

  • Adaptive streaming — HLS and DASH via Shaka Player with automatic ABR
  • Native HLS on Safari/iOS — uses the browser's built-in HLS support
  • MP4 fallback — seamless fallback when streaming fails, with position resume
  • Quality selector — auto (ABR) or manual quality switching
  • Subtitles/captions — VTT and SRT support with multi-language picker
  • Keyboard shortcuts — play/pause, seek, volume, fullscreen, captions
  • Idle detection — auto-pause after configurable inactivity timeout
  • Next video — end-screen with countdown and thumbnail
  • Stall recovery — 5-stage escalation from playhead nudge to full restart
  • CSS theming — customizable via CSS custom properties
  • Headless modeuseVideoPlayer hook for building custom UIs
  • TypeScript — full type definitions for all props, state, and events
  • Lightweight — < 55 kB (Shaka Player loaded on demand)

Installation

npm install @instincthub/shaka-player

Peer dependencies

React 18 or 19 is required:

npm install react react-dom

Quick Start

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

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

Props

Source & Playback

| Prop | Type | Default | Description | |------|------|---------|-------------| | src | string | required | Primary video URL (.m3u8, .mpd, or .mp4) | | fallbackSrc | string | — | MP4 fallback URL when adaptive streaming fails | | poster | string | — | Poster image shown before playback | | type | "auto" \| "hls" \| "dash" \| "mp4" | "auto" | Force source type detection | | autoplay | boolean | false | Start playback automatically | | 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 |

Streaming & Subtitles

| Prop | Type | Default | Description | |------|------|---------|-------------| | shakaConfig | Record<string, unknown> | — | Shaka Player configuration overrides | | subtitles | SubtitleTrack[] | — | Subtitle/caption tracks |

Next Video

| Prop | Type | Default | Description | |------|------|---------|-------------| | nextVideo | NextVideo | — | Next video info for end screen | | onNextVideo | () => void | — | Called when user clicks "next" or countdown finishes |

Idle Tracking

| Prop | Type | Default | Description | |------|------|---------|-------------| | idleTimeout | number | — | Idle timeout in minutes (0 or undefined disables) | | idleMessage | string | "Video paused. Continue watching?" | Message shown when auto-paused |

UI & Styling

| Prop | Type | Default | Description | |------|------|---------|-------------| | controls | boolean | true | Show built-in control bar | | className | string | — | CSS class on the container | | style | CSSProperties | — | Inline styles on the container | | width | string \| number | "100%" | Container width | | height | string \| number | "auto" | Container height |

Event Callbacks

| Prop | Type | Description | |------|------|-------------| | onReady | () => void | Player is ready for playback | | onPlay | () => void | Playback started | | onPause | () => void | Playback paused | | onEnded | () => void | Playback reached the end | | onTimeUpdate | (time: number, duration: number) => void | Current time changed | | onError | (error: PlayerError) => void | An error occurred | | onQualityChange | (level: QualityLevel) => void | Active quality level changed | | onFallback | (reason: FallbackReason) => void | Player fell back to MP4 | | onBufferStart | () => void | Buffering started | | onBufferEnd | () => void | Buffering ended | | onSubtitleChange | (track: SubtitleTrack \| null) => void | Active subtitle changed | | onIdlePause | () => void | Video auto-paused due to inactivity |

Imperative Ref API

Use a ref when you need programmatic control from a parent component:

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

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

  return (
    <>
      <VideoPlayer ref={playerRef} src="https://example.com/stream.m3u8" />
      <button onClick={() => playerRef.current?.seek(60)}>Jump to 1:00</button>
      <button onClick={() => playerRef.current?.setPlaybackRate(2)}>2x Speed</button>
    </>
  );
}

Ref Methods

| Method | Description | |--------|-------------| | play() | Start playback | | pause() | Pause playback | | seek(time) | Seek to time in seconds | | setVolume(volume) | Set volume (0–1) | | setMuted(muted) | Set muted state | | setPlaybackRate(rate) | Set playback speed | | setQuality(levelIndex) | Set quality level (-1 for auto) | | getQualityLevels() | Get available quality levels | | setSubtitle(trackIndex) | Set subtitle track (-1 for off) | | getSubtitleTracks() | Get available subtitle tracks | | getCurrentTime() | Get current time in seconds | | getDuration() | Get total duration in seconds | | requestFullscreen() | Enter fullscreen | | exitFullscreen() | Exit fullscreen | | destroy() | Clean up all resources |

Headless Mode

Use the useVideoPlayer hook to build a completely custom UI while the hook manages all streaming, state, and playback logic:

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

function CustomPlayer() {
  const { containerRef, videoRef, state, controls } = useVideoPlayer({
    src: "https://example.com/stream.m3u8",
    fallbackSrc: "https://example.com/video.mp4",
    onReady: () => console.log("Ready"),
  });

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

      <button onClick={controls.togglePlay}>
        {state.status === "playing" ? "Pause" : "Play"}
      </button>

      <input
        type="range"
        min={0}
        max={state.duration}
        value={state.currentTime}
        onChange={(e) => controls.seek(Number(e.target.value))}
      />

      <span>
        {formatTime(state.currentTime)} / {formatTime(state.duration)}
      </span>

      {state.qualityLevels.length > 0 && (
        <select
          value={state.currentQuality}
          onChange={(e) => controls.setQuality(Number(e.target.value))}
        >
          <option value={-1}>Auto</option>
          {state.qualityLevels.map((level) => (
            <option key={level.index} value={level.index}>
              {level.label}
            </option>
          ))}
        </select>
      )}
    </div>
  );
}

Hook Return Value

interface UseVideoPlayerReturn {
  containerRef: RefObject<HTMLDivElement>;  // Attach to container element
  videoRef: RefObject<HTMLVideoElement>;    // Attach to <video> element
  state: PlayerState;                       // Reactive player state
  controls: PlayerControls;                 // Control methods
}

Player State

| Field | Type | Description | |-------|------|-------------| | status | PlayerStatus | "idle" "loading" "ready" "playing" "paused" "buffering" "seeking" "ended" "error" | | currentTime | number | Current position in seconds | | duration | number | Total duration in seconds | | buffered | number | Buffered ahead in seconds | | volume | number | Current volume (0–1) | | muted | boolean | Muted state | | playbackRate | number | Current playback speed | | isFullscreen | boolean | Fullscreen state | | qualityLevels | QualityLevel[] | Available quality levels | | currentQuality | number | Active quality index (-1 = auto) | | autoQuality | boolean | Whether ABR is active | | subtitleTracks | SubtitleTrack[] | Available subtitle tracks | | activeSubtitle | number | Active subtitle index (-1 = off) | | error | PlayerError \| null | Current error, if any | | hasStarted | boolean | Whether playback has started at least once |

Player Controls

| Method | Description | |--------|-------------| | play() | Start playback | | pause() | Pause playback | | togglePlay() | Toggle play/pause | | seek(time) | Seek to absolute time in seconds | | seekRelative(offset) | Seek relative to current position | | setVolume(volume) | Set volume (0–1) | | toggleMute() | Toggle mute | | setPlaybackRate(rate) | Set playback speed | | setQuality(levelIndex) | Set quality (-1 for auto) | | setSubtitle(trackIndex) | Set subtitle (-1 for off) | | toggleSubtitles() | Toggle subtitles (remembers last track) | | toggleFullscreen() | Toggle fullscreen |

Subtitles

Pass an array of SubtitleTrack objects. Both VTT and SRT formats are supported (SRT is auto-converted to VTT):

<VideoPlayer
  src="https://example.com/stream.m3u8"
  subtitles={[
    { src: "/subs/en.vtt", label: "English", srclang: "en", default: true },
    { src: "/subs/es.srt", label: "Spanish", srclang: "es" },
    { src: "/subs/fr.vtt", label: "French", srclang: "fr" },
  ]}
/>

SubtitleTrack

| Field | Type | Description | |-------|------|-------------| | src | string | URL to .vtt or .srt file | | label | string | Display label (e.g. "English") | | srclang | string | BCP 47 language tag (e.g. "en") | | default | boolean | Active by default |

Next Video

Show a "next video" end screen with an optional auto-play countdown:

<VideoPlayer
  src="https://example.com/lesson-1.m3u8"
  nextVideo={{
    title: "Lesson 2: Components",
    thumbnail: "https://example.com/lesson-2-thumb.jpg",
    countdown: 10, // seconds, set 0 to disable auto-play
  }}
  onNextVideo={() => router.push("/lessons/2")}
/>

Keyboard Shortcuts

When the player container has focus:

| Key | Action | |-----|--------| | Space / K | Play / Pause | | Arrow Left / J | Rewind 10s | | Arrow Right / L | Forward 10s | | Arrow Up | Volume +10% | | Arrow Down | Volume -10% | | M | Toggle mute | | F | Toggle fullscreen | | C | Toggle captions |

Streaming Architecture

The player selects a playback mode based on the source type and browser support:

  1. MP4 — set directly on the <video> element
  2. HLS on Safari/iOS — native <video> HLS with quality levels parsed from the master playlist
  3. HLS/DASH elsewhere — Shaka Player (lazy-loaded) with ABR and quality switching

Error Recovery

Shaka mode uses a 5-stage stall recovery escalation:

| Stage | Action | |-------|--------| | 0 | Nudge playhead +0.1s | | 1 | Drop to lowest quality | | 2 | Detach and reattach player | | 3 | Full Shaka restart | | 4 | Fall back to fallbackSrc MP4 |

If player.load() fails (e.g. 403/404), the player skips the restart cycle and falls back to MP4 immediately.

Fatal Shaka errors during playback trigger restart attempts. After 3 fatal errors, the player falls back to MP4.

If no fallbackSrc is provided, the error overlay with a retry button is shown instead.

Shaka Configuration

Override the default Shaka config with the shakaConfig prop. The config is deep-merged with the defaults:

<VideoPlayer
  src="https://example.com/stream.m3u8"
  shakaConfig={{
    streaming: {
      bufferingGoal: 120,
      rebufferingGoal: 5,
    },
    abr: {
      enabled: true,
      defaultBandwidthEstimate: 10_000_000,
    },
  }}
/>
{
  streaming: {
    bufferingGoal: 60,
    rebufferingGoal: 2,
    bufferBehind: 30,
    smallGapLimit: 0.5,
    retryParameters: {
      maxAttempts: 5,
      baseDelay: 1000,
      backoffFactor: 2,
      timeout: 30_000,
    },
  },
  abr: {
    enabled: true,
    defaultBandwidthEstimate: 5_000_000,
    switchInterval: 3,
    bandwidthUpgradeTarget: 0.85,
    bandwidthDowngradeTarget: 0.7,
  },
  manifest: {
    retryParameters: {
      maxAttempts: 3,
      baseDelay: 1000,
      backoffFactor: 2,
      timeout: 30_000,
    },
  },
}

CSS Theming

The player UI is styled with CSS custom properties. Override them on the container or globally:

.my-player {
  --ihub-player-primary: #6200ea;
  --ihub-player-accent: #b388ff;
  --ihub-player-bg: #1a1a2e;
  --ihub-player-controls-bg: rgba(26, 26, 46, 0.85);
  --ihub-player-text: #ffffff;
  --ihub-player-progress: #6200ea;
  --ihub-player-progress-hover: #b388ff;
  --ihub-player-buffer: rgba(255, 255, 255, 0.3);
  --ihub-player-error: #cf6679;
  --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 className="my-player" src="..." />

CSS Custom Properties

| Variable | Default | Description | |----------|---------|-------------| | --ihub-player-primary | #00838f | Primary brand color (focus rings, thumb) | | --ihub-player-accent | #0fabbc | Accent color (hover states) | | --ihub-player-bg | #000000 | Player background | | --ihub-player-controls-bg | rgba(0,0,0,0.7) | Control bar background | | --ihub-player-text | #ffffff | Text and icon color | | --ihub-player-progress | #00838f | Progress bar fill | | --ihub-player-progress-hover | #0fabbc | Progress bar fill on hover | | --ihub-player-buffer | rgba(255,255,255,0.4) | Buffer indicator | | --ihub-player-error | #ea5f5e | Error state color | | --ihub-player-border-radius | 8px | Container border radius | | --ihub-player-font-family | "Nunito", sans-serif | Body font | | --ihub-player-heading-font | "Montserrat", sans-serif | Heading font | | --ihub-player-transition | 0.2s ease | Transition timing |

Types

All types are exported from the package:

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

Key Types

interface PlayerError {
  type: "network" | "media" | "shaka" | "unknown";
  fatal: boolean;
  message: string;
  details?: string;
  code?: number;      // Shaka error code
  category?: number;  // Shaka error category
}

interface FallbackReason {
  from: "shaka" | "hls";
  to: "mp4";
  cause: "unsupported" | "manifest_error" | "media_error" | "network_error";
  errorCount: number;
}

interface QualityLevel {
  index: number;
  width: number;
  height: number;
  bitrate: number;
  label: string;  // e.g. "720p"
}

type PlayerStatus =
  | "idle"
  | "loading"
  | "ready"
  | "playing"
  | "paused"
  | "buffering"
  | "seeking"
  | "ended"
  | "error";

type PlaybackMode = "shaka" | "native" | "none";

Common Patterns

Course Video Player

function CoursePlayer({ lesson, onComplete }) {
  const playerRef = useRef<VideoPlayerRef>(null);

  return (
    <VideoPlayer
      ref={playerRef}
      src={lesson.hlsUrl}
      fallbackSrc={lesson.mp4Url}
      poster={lesson.thumbnail}
      startTime={lesson.lastPosition}
      subtitles={lesson.subtitles}
      idleTimeout={15}
      nextVideo={
        lesson.next
          ? { title: lesson.next.title, thumbnail: lesson.next.thumbnail, countdown: 10 }
          : undefined
      }
      onNextVideo={() => router.push(`/lessons/${lesson.next.id}`)}
      onTimeUpdate={(time) => saveProgress(lesson.id, time)}
      onEnded={() => onComplete(lesson.id)}
      onError={(err) => console.error("Playback error:", err)}
      onFallback={(reason) => {
        analytics.track("fallback", { cause: reason.cause, from: reason.from });
      }}
    />
  );
}

Watch Progress Tracking

<VideoPlayer
  src="https://example.com/stream.m3u8"
  startTime={savedProgress}
  onTimeUpdate={(time) => {
    // Throttle saves to every 5 seconds
    if (Math.floor(time) % 5 === 0) {
      saveProgress(videoId, time);
    }
  }}
  onEnded={() => markAsCompleted(videoId)}
/>

Error Monitoring

<VideoPlayer
  src="https://example.com/stream.m3u8"
  fallbackSrc="https://example.com/video.mp4"
  onError={(error) => {
    Sentry.captureMessage("Video playback error", {
      extra: { type: error.type, code: error.code, fatal: error.fatal },
    });
  }}
  onFallback={(reason) => {
    analytics.track("video_fallback", {
      from: reason.from,
      cause: reason.cause,
      errorCount: reason.errorCount,
    });
  }}
/>

Browser Support

| Browser | Mode | |---------|------| | Chrome / Edge / Firefox | Shaka Player (MSE) | | Safari / iOS Safari | Native HLS | | Older / unsupported | MP4 fallback |

Development

# Install dependencies
npm install

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

# Type-check
npm run typecheck

# Run tests
npm run test

# Lint & format
npm run lint:fix
npm run format

# Build
npm run build

# Check bundle size
npm run size

License

MIT