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

playron

v1.0.4

Published

Modern OTT video player for React, Next.js, and vanilla JS

Downloads

500

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 playron

Local Development (yalc)

# In the playron repo
npm run build
npx yalc push

# In your project
npx yalc add playron

Quick 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() with ssr: 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         # ESLint

HTTPS is required. See SSL-SETUP.md for 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