vidstream-player
v2.0.0
Published
Production-ready video player SDK — HLS, DASH, React, Vue, Angular, Web Component, config-driven theming, playlist sidebar, chapters, PiP, mini player, volume animation, touch gestures, auto-resume, progress tracking, analytics plugin, full TypeScript
Maintainers
Keywords
Readme
vidstream-player
The last video player you'll ever need.
A production-ready, framework-agnostic video player SDK with adaptive streaming, fully configurable theming, playlists, chapters, Picture-in-Picture, mini player, touch gestures, progress tracking, an analytics plugin interface, and full TypeScript support — all in one package.
Why vidstream-player?
Most video players do one thing. This one does everything.
| What you normally need | vidstream-player | |------------------------|-----------------| | HLS player + DASH player + quality selector | ✅ Built-in, lazy-loaded | | Full custom theme — your colors, fonts, radius | ✅ Config object, CSS variables | | Playlist with sidebar panel | ✅ Renders to the right of player | | Chapter markers + VTT chapters | ✅ Built-in | | Picture-in-Picture + floating mini player | ✅ Built-in | | Mobile touch gestures | ✅ Built-in | | Volume animation feedback | ✅ Icon + bar + percentage | | Analytics plugin slot | ✅ Built-in | | Progress tracking + milestones | ✅ Built-in | | React wrapper + hooks | ✅ Built-in | | Web Component for Vue / Angular | ✅ Built-in | | Full TypeScript types | ✅ Built-in |
No extra dependencies. No stitching together five libraries. Import, configure, ship.
Features at a glance
- Adaptive streaming — HLS (
.m3u8) via hls.js and DASH (.mpd) via dash.js, both lazy-loaded as separate chunks so they never bloat your initial bundle - Config-driven theming — pass a
themeobject with your brand colors, radii, fonts, and logo. Every detail is a CSS custom property you can also override directly - Watermark & anti-piracy — image or text watermarks with full color, opacity, size, and position control. Randomized anti-piracy text overlay with configurable intervals
- Playlist sidebar — multi-item queue with auto-next, looping, and a built-in panel that renders to the right of the player in a flex row. Drop one element; both appear side-by-side
- Chapters — inline config or WebVTT file; chapter-change events on the event bus
- Quality selector — ABR levels from HLS/DASH manifests surfaced automatically; manual override supported
- Subtitles & captions — WebVTT tracks with an in-player language switcher
- Picture-in-Picture — browser-native PiP
- Mini / floating player — scroll past the video and the entire player snaps to a fixed corner; click to scroll back; click × to dismiss. Works by repositioning the whole container — no video-element cloning or sync issues
- Touch gestures — double-tap to seek, swipe right-half up/down for volume, long-press for speed boost
- Volume animation — any volume change (swipe or
↑/↓key) shows a volume icon + percentage + fill-bar overlay that fades out after 1.5 s - Error recovery — exponential-backoff retry with configurable max attempts
- Progress tracking — milestone events (25 / 50 / 75 / 100 %), completion callback, periodic save with localStorage and API helpers
- Analytics plugin — drop in any provider via a simple
init(player)interface; exposes a full event bus (on/off/once) - Session ID — every player instance gets a UUID
sessionIdfor correlation across events - React wrapper —
forwardRefcomponent withusePlayerStateandusePlayerControlshooks - Web Component — works in Vue, Angular, Svelte, or plain HTML with zero framework glue
- TypeScript-first — every option, event, and method is fully typed
Install
npm install vidstream-player
# or
yarn add vidstream-player
# or
pnpm add vidstream-playerPeer dependencies
hls.js and dashjs are optional peer dependencies — install whichever streaming formats you need:
# For HLS (.m3u8) streams
npm install hls.js
# For DASH (.mpd) streams
npm install dashjs
# Both
npm install hls.js dashjsIf you only serve MP4 or WebM, you don't need either. The player lazy-loads them only when a matching source is played.
Quick start
Vanilla JS
import 'vidstream-player/styles';
import { createVideoPlayer } from 'vidstream-player';
const player = createVideoPlayer(document.getElementById('player'), {
source: { src: 'https://example.com/video.m3u8', type: 'hls' },
theme: { accentColor: '#e50914', progressColor: '#e50914' },
controls: { quality: true, pip: true, subtitles: true },
});React
import 'vidstream-player/styles';
import VidstreamPlayer from 'vidstream-player/react';
export default function App() {
return (
<VidstreamPlayer
source={{ src: 'https://example.com/video.m3u8', type: 'hls' }}
theme={{ accentColor: '#e50914', progressColor: '#e50914' }}
controls={{ quality: true, pip: true }}
onPlay={() => console.log('playing')}
/>
);
}Vue 3
// main.ts — tell Vue this is a custom element
app.config.compilerOptions.isCustomElement = (tag) => tag === 'vidstream-player';import 'vidstream-player/styles';
import 'vidstream-player'; // registers <vidstream-player><template>
<vidstream-player ref="playerEl" style="width:100%;aspect-ratio:16/9" />
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
const playerEl = ref<any>(null);
onMounted(() => {
playerEl.value.initialize({
source: { src: 'https://example.com/video.m3u8', type: 'hls' },
theme: { accentColor: '#42b883' },
});
});
</script>Angular
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import 'vidstream-player/styles';
import 'vidstream-player';
@Component({
selector: 'app-root',
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `<vidstream-player #player style="width:100%;aspect-ratio:16/9"></vidstream-player>`,
})
export class AppComponent {
// call player.initialize(config) in ngAfterViewInit
}Configuration
Every option is optional except source.
import type { PlayerConfig } from 'vidstream-player';Core options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| source | VideoSource | — | Required. { src, type, title?, poster?, chapters? } |
| mode | 'vod' \| 'live' | 'vod' | VOD shows full controls; live hides seek bar and speed |
| theme | PlayerTheme | defaults | Brand colors, radius, font, logo — see Theming |
| autoplay | boolean | false | Autoplay on load (subject to browser policy) |
| muted | boolean | false | Start muted |
| loop | boolean | false | Loop playback |
| startTime | number | — | Seek to this time (seconds) on load |
| preload | 'none' \| 'metadata' \| 'auto' | 'metadata' | HTML video preload hint |
| keyboardShortcuts | boolean | true | Space / arrows / F / M keyboard control |
| debug | boolean | false | Log lifecycle events to console |
Source types
type VideoSourceType = 'hls' | 'dash' | 'mp4' | 'webm' | 'auto';
// 'auto' detects from the extension (.m3u8 → hls, .mpd → dash)
{ src: 'https://example.com/video.m3u8', type: 'auto' }Controls
Pass only the controls you want. Omitted controls are hidden.
controls: {
quality: true, // quality selector
pip: true, // picture-in-picture button
subtitles: true, // subtitle track switcher
playbackSpeed: true, // speed menu
chapters: true, // chapter markers
playlist: true, // prev/next buttons
forward: true, // skip-forward button
rewind: true, // skip-back button
seekDuration: 10, // seconds per skip
}Theming
There are no preset theme names. You pass a plain object with exactly the colors you want. Every field is optional — omit it and the built-in default applies.
{
theme: {
primaryColor: '#ffffff', // icon and text color
accentColor: '#e50914', // progress bar, active highlights
textColor: '#ffffff',
controlBg: 'rgba(0,0,0,0.8)', // control bar background
progressColor: '#e50914',
bufferColor: 'rgba(255,255,255,0.3)',
hoverColor: '#ff1a1a',
borderRadius: '4px',
fontFamily: 'Inter, sans-serif',
iconSize: '24px',
tooltipBg: 'rgba(0,0,0,0.9)',
logoUrl: 'https://cdn.example.com/logo.png',
logoPosition: 'top-left', // 'top-left' | 'top-right'
tenantName: 'My Platform',
}
}Change theme at runtime
player.updateConfig({
theme: { accentColor: '#6366f1', progressColor: '#6366f1' },
});CSS custom properties
Every theme value maps to a CSS variable on .video-player-container. Override directly for global or per-instance changes:
.video-player-container {
--vp-accent-color: #e50914;
--vp-primary-color: #ffffff;
--vp-control-bg: rgba(0,0,0,0.85);
--vp-progress-color: #e50914;
--vp-border-radius: 4px;
--vp-font-family: 'Inter', sans-serif;
--vp-text-color: #ffffff;
--vp-hover-color: #ff1a1a;
--vp-tooltip-bg: rgba(0,0,0,0.9);
--vp-icon-size: 24px;
}Watermark & Anti-Piracy
Watermark
Full control over color, size, opacity, and position:
{
watermark: {
enabled: true,
text: 'MyPlatform', // text watermark
// imageUrl: 'https://.../logo.svg', // OR image watermark
opacity: 0.45,
position: 'top-right', // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center' | 'random'
imageWidth: '80px',
className: 'my-watermark', // add your own CSS for full control
},
}Anti-Piracy Text
Randomized floating text to deter screen recording:
{
antiPiracyText: {
enabled: true,
text: '[email protected]',
color: 'rgba(255,255,255,0.15)',
fontSize: '14px',
opacity: 0.15,
minInterval: 5000,
maxInterval: 12000,
className: 'my-anti-piracy',
},
}Adaptive Streaming — HLS & DASH
HLS.js and dash.js are loaded on demand as separate chunks — they add nothing to your initial bundle if the viewer only watches MP4.
// HLS
{ source: { src: 'https://cdn.example.com/hls/master.m3u8', type: 'hls' } }
// DASH
{ source: { src: 'https://cdn.example.com/dash/manifest.mpd', type: 'dash' } }
// Quality control
quality: {
showAuto: true,
defaultQuality: 1080,
allowedQualities: [1080, 720, 480],
}Playlist
When showPanel: true, a 280 px sidebar is automatically rendered to the right of the player in a flex row. You pass a single container element; both the player and the sidebar appear side-by-side without any extra layout work.
{
playlist: [
{ src: 'https://cdn.example.com/ep1.m3u8', title: 'Episode 1', poster: '...', duration: 1440, chapters: [{ id: '1', title: 'Intro', startTime: 0, endTime: 120 }] },
{ src: 'https://cdn.example.com/ep2.m3u8', title: 'Episode 2', poster: '...' },
],
playlistOptions: {
autoNext: true,
loop: false,
showPanel: true, // sidebar renders to the right automatically
},
}player.next(); // advance
player.prev(); // go back
player.goTo(2); // jump to indexChapters
Inline
{
chapters: [
{ id: '1', title: 'Introduction', startTime: 0, endTime: 120 },
{ id: '2', title: 'Main Content', startTime: 120, endTime: 600 },
{ id: '3', title: 'Conclusion', startTime: 600, endTime: 720 },
],
controls: { chapters: true },
}From a WebVTT file
{ chaptersVtt: 'https://cdn.example.com/chapters.vtt' }Picture-in-Picture & Mini Player
{
pip: { enabled: true },
miniPlayer: {
enabled: true,
position: 'bottom-right', // 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
width: 320, // px; height is computed 16:9
},
}The mini player activates automatically via IntersectionObserver when the player scrolls out of view. The entire player container (video + controls) is repositioned to a fixed corner of the screen — no element cloning or sync issues. Click the mini player body to scroll back to the original position. Click × to dismiss permanently.
In mini player mode, controls are automatically reduced to Play/Pause + Mute + Fullscreen so they fit in the compact overlay without crowding.
player.togglePip(); // browser-native PiPResponsive / container scaling
The player uses a ResizeObserver to watch its container width — not the viewport — so it adapts correctly whether it is full-width, embedded in a sidebar, or rendered inside a narrow column.
| Container width | CSS class | Controls hidden |
|----------------|-----------|----------------|
| ≥ 720 px | (none) | All shown |
| 560–719 px | vp-size-lg | — |
| 400–559 px | vp-size-md | Volume slider, PiP, speed/quality labels |
| 280–399 px | vp-size-sm | + Time display, rewind/forward, chapters/subtitles dropdowns |
| < 280 px | vp-size-xs | All non-essential controls hidden |
This is the same mechanism used internally for the mini player. Because it uses container width rather than @media viewport queries, it works correctly when the player is embedded in any layout — grid, flex, sidebar, modal, or full-page.
Touch Gestures
{
gestures: {
enabled: true,
doubleTapSeek: 10, // seconds to seek on double-tap (default: 10)
swipeVolume: true, // swipe right half up/down → volume
longPressSpeed: true, // hold → 2× speed (true = 2×, or pass a number)
},
}Volume feedback: any volume change — swipe or ↑/↓ keyboard — shows a centered overlay with a volume icon, a percentage readout, and a horizontal fill bar. It fades out after 1.5 s automatically.
Error Recovery
{
errorRecovery: {
maxRetries: 5,
retryDelay: 2000, // ms (doubles each attempt)
},
}Progress Tracking
Track milestone completion, save progress, and restore on return. Works with both vod and live modes:
{
lmsProgress: {
enabled: true,
videoId: 'course-101-lesson-1',
userId: 'user-abc-123',
milestones: [25, 50, 75, 100],
onMilestoneReached: (pct, data) => {
api.recordMilestone({ videoId: data.videoId, milestone: pct });
},
onComplete: (data) => {
api.markComplete(data.videoId);
},
onProgressSave: (data) => {
// called every 5 s automatically
localStorage.setItem('progress_' + data.videoId, JSON.stringify(data));
},
initialProgress: {
currentTime: 240, // resume from 4:00
watchedPercentage: 33,
},
},
}Storage helpers
import {
saveProgressToStorage,
loadProgressFromStorage,
clearProgressFromStorage,
createProgressAPICallbacks, // ready-made fetch callbacks for a REST API
} from 'vidstream-player';
const saved = loadProgressFromStorage('course-101-lesson-1');Live Streaming
{
mode: 'live',
source: { src: 'https://live.example.com/stream.m3u8', type: 'hls' },
}In live mode the seek bar and duration are hidden, a LIVE badge is shown, speed selector and chapter controls are disabled.
Analytics Plugin
import type { VidstreamAnalyticsPlugin } from 'vidstream-player';
const myPlugin: VidstreamAnalyticsPlugin = {
name: 'my-analytics',
init(player) {
console.log('Session:', player.sessionId);
player.on('play', () => track('video_play'));
player.on('pause', () => track('video_pause'));
player.on('seek', (e) => track('seek', { from: e.fromTime, to: e.toTime }));
player.on('qualityChange', (e) => track('quality', { level: e.toQuality }));
player.on('ended', () => track('video_complete'));
player.on('timeupdate', (e) => {
if (Math.floor(e.currentTime) % 10 === 0) track('heartbeat', e);
});
},
destroy() {},
};
createVideoPlayer(el, {
source: { src: '...', type: 'hls' },
plugins: { analytics: myPlugin },
});Event bus
player.on('play', handler); // subscribe
player.off('play', handler); // unsubscribe
player.once('ended', handler); // fire once then removeAvailable events
| Event | Payload |
|-------|---------|
| play | { currentTime, src, sessionId } |
| pause | { currentTime, src, sessionId } |
| ended | { src, sessionId } |
| seek | { fromTime, toTime, src, sessionId } |
| timeupdate | { currentTime, duration, percent, sessionId } |
| qualityChange | { fromQuality, toQuality, auto, sessionId } |
| chapterChange | { chapter, index, sessionId } |
| playlistChange | { index, item, sessionId } |
| buffering | { currentTime, duration, sessionId } |
| error | { code, message } |
React — Hooks & Ref
usePlayerState
import VidstreamPlayer, { usePlayerState } from 'vidstream-player/react';
import type { VidstreamPlayerRef } from 'vidstream-player/react';
import { useRef } from 'react';
function VideoPage() {
const ref = useRef<VidstreamPlayerRef>(null);
const state = usePlayerState(ref); // { playing, currentTime, duration, volume, ... }
return (
<>
<VidstreamPlayer
ref={ref}
source={{ src: 'https://example.com/video.m3u8', type: 'hls' }}
theme={{ accentColor: '#e50914', progressColor: '#e50914' }}
controls={{ quality: true, pip: true }}
/>
<p>{state.currentTime.toFixed(0)}s / {state.duration.toFixed(0)}s</p>
</>
);
}usePlayerControls
const controls = usePlayerControls(ref);
<button onClick={controls.togglePlay}>Play / Pause</button>
<button onClick={() => controls.seek(60)}>Jump to 1 min</button>
<button onClick={controls.togglePip}>PiP</button>Full ref API
ref.current?.play()
ref.current?.pause()
ref.current?.seek(seconds)
ref.current?.setVolume(0.8)
ref.current?.setMuted(true)
ref.current?.setSpeed(1.5)
ref.current?.toggleFullscreen()
ref.current?.togglePip()
ref.current?.next()
ref.current?.prev()
ref.current?.goTo(index)
ref.current?.on('play', handler)
ref.current?.once('ended', handler)
ref.current?.getState() // full PlayerState snapshot
ref.current?.sessionId // UUID stringTypeScript
import type {
PlayerConfig,
VideoSource,
PlayerMode, // 'vod' | 'live'
PlayerTheme, // theme config object
PlayerState,
GesturesConfig,
ErrorRecoveryConfig,
PipConfig,
MiniPlayerConfig,
PlaylistItem,
PlaylistOptions,
Chapter,
QualityLevel,
SubtitleTrack,
WatermarkConfig,
AntiPiracyTextConfig,
LMSProgressConfig,
LMSProgressData,
ProgressMilestone,
VidstreamPlayerInstance,
VidstreamAnalyticsPlugin,
ChapterChangeEvent,
PlaylistChangeEvent,
PluginsConfig,
} from 'vidstream-player';Player API reference
// Playback
player.play()
player.pause()
player.seek(seconds)
player.setVolume(0–1)
player.setMuted(boolean)
player.setSpeed(rate) // 0.25 – 4
player.toggleFullscreen()
player.togglePip()
player.loadSource(VideoSource) // swap source without re-init
// Playlist
player.next()
player.prev()
player.goTo(index)
// State
player.getState() // PlayerState snapshot
player.getCurrentTime()
player.getDuration()
player.getVolume()
player.isPaused()
player.getQuality()
// Config
player.updateConfig(Partial<PlayerConfig>)
// Event bus
player.on(event, handler)
player.off(event, handler)
player.once(event, handler)
// Meta
player.sessionId // UUID string
player.isReady() // boolean
// Cleanup
player.destroy()Web Component API (Vue / Angular / plain HTML)
<vidstream-player id="p"></vidstream-player>
<script type="module">
import 'vidstream-player/styles';
import 'vidstream-player'; // registers the custom element
const el = document.getElementById('p');
el.initialize({
source: { src: 'https://example.com/video.m3u8', type: 'hls' },
theme: { accentColor: '#e50914' },
});
el.play();
el.seek(30);
el.setMuted(true);
el.addEventListener('player-play', () => console.log('play'));
el.addEventListener('player-pause', () => console.log('pause'));
el.addEventListener('player-quality-change', (e) => console.log(e.detail.data));
</script>DOM events
| DOM event | Fires when |
|-----------|-----------|
| player-ready | Player fully initialized |
| player-play | Playback started |
| player-pause | Playback paused |
| player-ended | Playback finished |
| player-timeupdate | Every animation frame during playback |
| player-quality-change | ABR level changed |
| player-chapter-change | Chapter boundary crossed |
| player-playlist-change | Playlist item changed |
| player-error | Playback error |
| player-pip | PiP entered or exited |
| player-buffering | Buffering state changed |
Framework compatibility
| Framework | Import path | Notes |
|-----------|-------------|-------|
| Vanilla JS | vidstream-player | createVideoPlayer(el, config) |
| React 17+ | vidstream-player/react | Full wrapper with hooks |
| Vue 3 | vidstream-player | Web component; add isCustomElement |
| Angular 14+ | vidstream-player | Web component; add CUSTOM_ELEMENTS_SCHEMA |
| Svelte | vidstream-player | Web component; no extra config |
| Next.js | vidstream-player/react | Use dynamic(..., { ssr: false }) |
| Nuxt 3 | vidstream-player | Wrap in <ClientOnly> |
| Astro | vidstream-player | Add client:only="vanilla" directive |
Bundle impact
| Chunk | Size (gzip) | Loaded when | |-------|------------|-------------| | Core player (ESM) | ~38 kB | Always | | React wrapper | ~2.4 kB | React only | | UMD bundle | ~34 kB | UMD consumers | | hls.js (peer dep) | ~195 kB | HLS source played | | dashjs (peer dep) | ~282 kB | DASH source played |
hls.js and dashjs are peer dependencies — they are NOT bundled into vidstream-player. Install them separately only if you use HLS or DASH. They are lazy-loaded at runtime and add nothing to your initial bundle.
A pure MP4 deployment ships ~38 kB gzipped with zero streaming overhead.
Previous behavior (< 1.0): Both were bundled into the package, inflating the install size to ~11 MB with source maps. The current approach reduces the published package to ~400 KB.
Full example
import 'vidstream-player/styles';
import { createVideoPlayer } from 'vidstream-player';
import type { VidstreamAnalyticsPlugin, PlayerConfig } from 'vidstream-player';
const analyticsPlugin: VidstreamAnalyticsPlugin = {
name: 'my-tracker',
init(player) {
player.on('play', () => track('play', { session: player.sessionId }));
player.on('ended', () => track('complete', { session: player.sessionId }));
player.on('timeupdate', (e) => {
if (Math.floor(e.currentTime) % 30 === 0) track('heartbeat', e);
});
},
};
const config: PlayerConfig = {
source: {
src: 'https://cdn.example.com/course/intro.m3u8',
type: 'hls',
title: 'Lesson 1 — Introduction',
poster: '/thumb.jpg',
},
mode: 'vod',
// Brand colors — full control, no preset strings
theme: {
accentColor: '#10b981',
progressColor: '#10b981',
hoverColor: '#34d399',
controlBg: 'rgba(0,0,0,0.75)',
borderRadius: '6px',
},
// Playlist — sidebar renders automatically to the right
playlist: [
{ src: 'https://cdn.example.com/ep1.m3u8', title: 'Episode 1' },
{ src: 'https://cdn.example.com/ep2.m3u8', title: 'Episode 2' },
{ src: 'https://cdn.example.com/ep3.m3u8', title: 'Episode 3' },
],
playlistOptions: { autoNext: true, showPanel: true },
// Chapters
chapters: [
{ id: '1', title: 'Intro', startTime: 0, endTime: 60 },
{ id: '2', title: 'Concepts', startTime: 60, endTime: 300 },
{ id: '3', title: 'Exercise', startTime: 300, endTime: 540 },
],
// Controls
quality: { showAuto: true },
controls: { quality: true, subtitles: true, chapters: true, playlist: true, pip: true },
// Features
pip: { enabled: true },
miniPlayer: { enabled: true, position: 'bottom-right', width: 320 },
gestures: { enabled: true, doubleTapSeek: 10, swipeVolume: true, longPressSpeed: true },
errorRecovery: { maxRetries: 5, retryDelay: 2000 },
// Branding
watermark: {
enabled: true,
text: 'MyPlatform',
position: 'top-right',
opacity: 0.4,
},
antiPiracyText: {
enabled: true,
text: 'Licensed to [email protected]',
color: 'rgba(255,255,255,0.12)',
fontSize: '13px',
minInterval: 8000,
maxInterval: 15000,
},
// Progress tracking
lmsProgress: {
enabled: true,
videoId: 'course-101-lesson-1',
userId: 'user-abc-123',
milestones: [25, 50, 75, 100],
onMilestoneReached: (pct) => api.recordMilestone(pct),
onComplete: () => api.markComplete('course-101-lesson-1'),
},
// Analytics
plugins: { analytics: analyticsPlugin },
};
const player = createVideoPlayer(document.getElementById('player')!, config);Documentation
Full API reference, advanced recipes, and a live playground are available at:
[docs-url]
License
MIT — free for personal and commercial use.
A Pro tier with advanced features (DRM / Widevine, server-side analytics, white-label builds, priority support) is coming. Star the repo to be notified.
Contributing
Found a bug or want a feature? Open an issue or PR at github.com/antor-arif/vidstream-player.
