playron
v1.0.4
Published
Modern OTT video player for React, Next.js, and vanilla JS
Downloads
500
Maintainers
Readme
Playron
Professional OTT video player library for React, Next.js, and Vanilla JS. HLS · DASH · DRM · VAST/VMAP · Live Stream · Subtitles · TypeScript-first
Overview
Playron is a modular, tree-shakeable video player library designed for OTT platforms. It wraps hls.js and dash.js as streaming engines and adds a full React UI layer with adaptive controls, DRM passthrough, ad integration, live stream support, and more.
import { Player } from 'playron'
import 'playron/style.css'
<Player src="https://example.com/stream.m3u8" />Installation
npm install playron
# or
yarn add playron
pnpm add playronLocal Development (yalc)
# In the playron repo
npm run build
npx yalc push
# In your project
npx yalc add playronQuick Start
React (Vite / CRA)
import { Player } from 'playron'
import 'playron/style.css'
export default function VideoPage() {
return (
<Player
src="https://example.com/stream.m3u8"
poster="https://example.com/poster.jpg"
config={{
player: { defaultVolume: 0.8, skipSeconds: 10 },
features: {
pip: true,
fullscreen: true,
theaterMode: true,
subtitles: true,
qualitySelector: true,
playbackSpeed: true,
keyboardShortcuts: true,
},
branding: { primaryColor: '#e50914' },
}}
/>
)
}Next.js — App Router
Important: Playron uses browser-only APIs (MediaSource, EME, hls.js, dash.js). You must use
dynamic()withssr: false. Using only'use client'is not enough — Next.js still pre-renders client components on the server for the initial HTML.
Step 1 — Create a client wrapper with dynamic:
// components/VideoPlayer.tsx
'use client'
import dynamic from 'next/dynamic'
import 'playron/style.css'
const Player = dynamic(
() => import('playron').then(m => m.Player),
{
ssr: false,
loading: () => (
<div style={{ width: '100%', aspectRatio: '16/9', background: '#000' }} />
),
}
)
export default function VideoPlayer({ src }: { src: string }) {
return <Player src={src} />
}Step 2 — Use it in your page (Server Component is fine here):
// app/page.tsx
import VideoPlayer from '@/components/VideoPlayer'
export default function Page() {
return <VideoPlayer src="https://example.com/stream.m3u8" />
}Next.js — Pages Router
// pages/video.tsx
import dynamic from 'next/dynamic'
import 'playron/style.css'
const Player = dynamic(
() => import('playron').then(m => m.Player),
{ ssr: false }
)
export default function VideoPage() {
return <Player src="https://example.com/stream.m3u8" />
}Vanilla JavaScript
<link rel="stylesheet" href="node_modules/playron/dist/playron.css" />
<div id="player"></div>
<script type="module">
import { PlayerCore } from 'playron/dist/playron.es.js'
const player = new PlayerCore(document.querySelector('#player video'))
await player.setSource('https://example.com/stream.m3u8')
player.eventEmitter.on('play', () => console.log('playing'))
player.eventEmitter.on('ended', () => console.log('ended'))
</script>Features
Streaming
| Feature | Status | |---------|--------| | HLS (HTTP Live Streaming) | ✅ via hls.js | | DASH (Dynamic Adaptive Streaming) | ✅ via dash.js | | Progressive MP4 | ✅ native | | Low Latency HLS (LL-HLS) | ✅ | | Low Latency DASH (LL-DASH) | ✅ | | VOD | ✅ | | Live Stream | ✅ | | DVR Window | ✅ | | Adaptive Bitrate (ABR) | ✅ engine-native |
DRM
| Key System | HLS | DASH | Platform | |-----------|-----|------|----------| | Widevine | ✅ | ✅ | Chrome, Firefox, Edge, Android | | PlayReady | ❌ | ✅ | Edge, Windows | | FairPlay | ❌ | ❌ | Not yet implemented | | AES-128 | ✅ native | — | All browsers |
Note: FairPlay (Safari/iOS) is planned for a future release. When Widevine content is loaded in Safari, Playron automatically shows a DRM error overlay with browser recommendations.
UI Controls
| Control | Description | |---------|-------------| | Play / Pause | Center overlay + control bar | | Seek Bar | Scrubbing, buffered range visualization, thumbnail preview on hover | | Volume | Slider + mute toggle, localStorage persistence | | Playback Speed | 0.25× – 2× | | Quality Selector | Manual ABR override | | Audio Track | Multi-language audio | | Subtitles | WebVTT + CEA-608/708 closed captions | | Fullscreen | Native API | | Picture-in-Picture | Native API | | Theater Mode | Layout toggle | | AirPlay | Safari WebKit API | | Skip Forward / Backward | Configurable seconds | | Skip Intro / Outro | Time-range based | | Chapters | Navigation + auto-detection | | Jump to Live | Appears when behind live edge | | Live Latency Display | "Xs behind live" indicator | | Timeline Markers | Sports events, chapters, ads | | Settings Panel | YouTube-style unified panel (Quality · Speed · Audio · Subtitles) | | Keyboard Shortcuts | Space, M, F, arrows, I, O, and more | | Error Overlay | DRM-aware messages, network errors, retry | | End Card | Next episode countdown | | Context Menu | Loop, copy URL, copy speed |
Ad System
| Format | Support | |--------|---------| | VAST 2.0 / 3.0 / 4.x | ✅ | | VMAP 1.0 | ✅ | | Pre-roll | ✅ | | Mid-roll | ✅ | | Post-roll | ✅ | | Skip button | ✅ configurable delay | | VPAID 2.0 | ✅ iframe sandbox | | Companion banner | ✅ | | Ad pod | ✅ | | Impression / tracking events | ✅ |
Accessibility
- WCAG 2.1 AA compliant
- ARIA roles on all interactive elements
- Full keyboard navigation
- Screen reader live region for status announcements
Configuration
<Player
src="..."
config={{
player: {
defaultVolume: 1, // 0–1
defaultPlaybackRate: 1,
skipSeconds: 10,
autoResume: true,
loop: false,
preload: 'auto', // 'none' | 'metadata' | 'auto'
},
ui: {
autoHideDelay: 3000, // ms
showTitle: true,
showTimeTooltip: true,
showVolumeSlider: true,
controlBarPosition: 'bottom',
},
features: {
pip: true,
fullscreen: true,
theaterMode: true,
chapters: true,
skipIntro: true,
subtitles: true,
qualitySelector: true,
audioTrackSelector: true,
playbackSpeed: true,
keyboardShortcuts: true,
},
branding: {
primaryColor: '#e50914', // hex
accentColor: '#ffffff',
logo: 'https://example.com/logo.svg',
logoLink: 'https://example.com',
},
metadata: {
title: 'My Video',
thumbnail: 'https://example.com/thumb.jpg',
rating: 'TV-MA',
},
thumbnails: {
vttUrl: 'https://example.com/thumbnails.vtt', // storyboard preview
},
drm: {
enabled: true,
widevine: {
licenseUrl: 'https://license.example.com/widevine',
headers: { 'Authorization': 'Bearer TOKEN' },
withCredentials: false,
},
playready: {
licenseUrl: 'https://license.example.com/playready',
},
// fairplay: not yet implemented
preferredSystem: 'widevine',
},
ads: {
enabled: true,
vmapUrl: 'https://ads.example.com/vmap.xml', // takes priority over vastUrl
vastUrl: 'https://ads.example.com/vast.xml',
skipAfter: 5, // seconds before skip button appears
timeout: 10000, // fetch timeout ms
maxWrapperDepth: 5, // VAST wrapper chain limit
},
live: {
atLiveEdgeTolerance: 3, // seconds
showJumpToLiveButton: true,
showLatency: true,
dvr: { enabled: true, maxDvrWindow: 1800 },
},
advanced: {
bufferAhead: 30,
bufferBehind: 10,
maxBufferLength: 60,
debug: false,
},
}}
/>DRM Usage
Widevine (Chrome / Firefox / Edge)
<Player
src="https://example.com/encrypted.mpd"
config={{
drm: {
enabled: true,
widevine: {
licenseUrl: 'https://license.example.com/widevine',
headers: { 'Authorization': 'Bearer YOUR_TOKEN' },
},
},
}}
/>Safari / iOS
FairPlay is not yet implemented. When a user on Safari attempts to play Widevine-protected content, Playron shows a DRM error overlay recommending Chrome or Firefox. This behaviour is automatic — no extra configuration needed.
Event System
import { PlayerCore } from 'playron'
const player = new PlayerCore(videoElement)
player.eventEmitter.on('play', ({ timestamp }) => { })
player.eventEmitter.on('pause', ({ timestamp }) => { })
player.eventEmitter.on('ended', ({ timestamp }) => { })
player.eventEmitter.on('timeupdate', ({ currentTime, duration }) => { })
player.eventEmitter.on('volumechange',({ volume, isMuted }) => { })
player.eventEmitter.on('qualitychange',({ from, to }) => { })
player.eventEmitter.on('error', ({ code, message, details }) => { })
player.eventEmitter.on('ready', ({ player }) => { })
player.eventEmitter.on('destroy', ({ timestamp }) => { })
// One-time listener
player.eventEmitter.once('ready', ({ player }) => player.play())
// Remove listener
player.eventEmitter.off('play', handler)React Hooks
import { usePlayerState, usePlayerMethods, usePlayer } from 'playron'
function MyControls() {
const { isPlaying, volume, currentTime, duration, isLive, isBuffering } = usePlayerState()
const { play, pause, seekTo, setVolume, seekToLiveEdge } = usePlayerMethods()
const player = usePlayer() // raw PlayerCore instance
}PlayerCore API
class PlayerCore {
// Playback
play(): Promise<void>
pause(): void
seekTo(time: number): void
setPlaybackRate(rate: number): void
// Volume
setVolume(volume: number): void // 0–1
toggleMute(): void
// Display
toggleFullscreen(): Promise<void>
togglePip(): Promise<void>
toggleTheaterMode(): void
// Stream
setSource(src: string): Promise<void>
getStreamType(): 'hls' | 'dash' | 'mp4' | 'unknown'
isLive(): boolean
// Live
seekToLiveEdge(): void
isAtLiveEdge(): boolean
getCurrentLatency(): number
getDVRRange(): { start: number; end: number } | null
// Quality / Audio / Subtitles
setQuality(id: string): void
getAvailableQualities(): QualityLevel[]
setAudioTrack(id: string): void
getAvailableAudioTracks(): AudioTrack[]
// Skip
skipForward(seconds?: number): void
skipBackward(seconds?: number): void
// DRM
setupDrm(config: DrmConfig): void
// Buffer
getBufferedRanges(): Array<{ start: number; end: number }>
// Cleanup
destroy(): void
}Bundle Size
| File | Raw | Gzip |
|------|-----|------|
| playron.es.js | 2.1 MB | 559 KB |
| playron.cjs.js | 1.6 MB | 490 KB |
| playron.css | 5.9 KB | 2 KB |
Bundle includes hls.js + dash.js streaming engines, DRM passthrough layer, VAST/VMAP ad engine, WebVTT + CEA-608 subtitle decoders, and full React UI. In production, ESM tree-shaking eliminates unused modules.
Framework Support
| Framework | Support | Notes |
|-----------|---------|-------|
| React 18 / 19 | ✅ Full | Hooks + Context API |
| Next.js App Router | ✅ Full | Add 'use client' |
| Next.js Pages Router | ✅ Full | Use dynamic(..., { ssr: false }) |
| Vite + React | ✅ Full | Reference dev environment |
| Remix | ✅ Full | Use ClientOnly wrapper |
| Astro | 🟡 Partial | client:only="react" |
| Vanilla JS | 🟡 Partial | Use PlayerCore directly |
| Vue / Nuxt | 🟡 Partial | Mount PlayerCore in onMounted |
Browser Support
| Browser | HLS | DASH | Widevine | PlayReady | FairPlay | |---------|-----|------|----------|-----------|---------| | Chrome 90+ | ✅ | ✅ | ✅ | — | — | | Firefox 88+ | ✅ | ✅ | ✅ | — | — | | Edge 90+ | ✅ | ✅ | ✅ | ✅ | — | | Safari 14+ | ✅ | ✅ | ❌ | — | ⏳ planned | | iOS Safari 14+ | ✅ | ✅ | ❌ | — | ⏳ planned | | Chrome Android | ✅ | ✅ | ✅ | — | — |
Development
npm install
npm run dev # HTTPS dev server (port 5173)
npm run build # TypeScript check + library build
npm run build:watch # Watch mode
npm run push # Build + yalc push to linked projects
npm run lint # ESLintHTTPS is required. See
SSL-SETUP.mdfor local certificate setup.
Roadmap
- [x] HLS engine (hls.js wrapper)
- [x] DASH engine (dash.js wrapper)
- [x] Widevine DRM — HLS + DASH
- [x] PlayReady DRM — DASH
- [x] AES-128 HLS decryption
- [x] VAST / VMAP ad system
- [x] WebVTT subtitles
- [x] CEA-608 / 708 closed captions
- [x] Live stream + DVR
- [x] LL-HLS / LL-DASH
- [x] Thumbnail storyboard preview
- [x] AirPlay
- [x] Analytics plugin
- [x] Stall detection + auto-recovery
- [x] DRM-aware error overlay
- [ ] FairPlay — Safari / iOS (in progress)
- [ ] Chromecast
- [ ] SSAI (Google DAI / AWS Elemental)
- [ ] TTML / IMSC subtitles
- [ ] Offline playback (Service Worker)
- [ ] 360° video (WebGL)
Troubleshooting
window is not defined (Next.js)
Cause: Playron bundles hls.js and dash.js which access browser APIs at module evaluation time. Next.js pre-renders even 'use client' components on the server for the initial HTML — 'use client' alone is not sufficient.
Fix: Always wrap Playron in dynamic() with ssr: false:
// ❌ Wrong — causes "window is not defined" on the server
'use client'
import { Player } from 'playron'
// ✅ Correct
'use client'
import dynamic from 'next/dynamic'
const Player = dynamic(
() => import('playron').then(m => m.Player),
{ ssr: false }
)Module not found: Can't resolve 'playron/dist/playron.css'
Cause: The correct CSS export path is playron/style.css, not playron/dist/playron.css.
// ❌ Wrong
import 'playron/dist/playron.css'
// ✅ Correct
import 'playron/style.css'Both paths now work as of v1.0.3, but playron/style.css is the canonical import.
Player renders but has no styles
Make sure you import the CSS once at the top level of your app (e.g. layout.tsx, _app.tsx, or globals.css):
// app/layout.tsx
import 'playron/style.css'Video plays but DRM content fails on Safari
FairPlay (Safari/iOS) is not yet implemented. Playron will automatically show a DRM error overlay with browser recommendations when Widevine content is loaded in Safari. See the DRM section for the current support matrix.
License
MIT © Suat Erkilic
