@instincthub/video-player
v0.1.0
Published
HLS-first video player with MP4 fallback for React
Downloads
10
Maintainers
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-playerPeer 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 mode —
useVideoPlayerhook 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:
- MP4 source — plays directly via
<video> - M3U8 + Safari/iOS — plays natively via
<video>(no hls.js loaded) - M3U8 + Chrome/Firefox/Edge — lazy-loads hls.js for ABR streaming
- HLS failure — falls back to
fallbackSrcMP4, 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")}
/>idleTimeoutis 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:
- Network errors — retries with exponential backoff (3 attempts), then falls back to MP4
- Media errors — calls
recoverMediaError(), thenswapAudioCodec(), then falls back to MP4 - 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 lintLicense
MIT
