playron 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +504 -209
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,287 +1,582 @@
|
|
|
1
1
|
# Playron
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Professional OTT video player library for React, Next.js, and Vanilla JS.
|
|
4
|
+
> HLS · DASH · DRM · VAST/VMAP · Live Stream · Subtitles · TypeScript-first
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- 🎬 **Adaptive Streaming**: HLS and DASH support
|
|
8
|
-
- 🔒 **DRM**: Widevine, PlayReady, FairPlay support
|
|
9
|
-
- 📱 **Responsive**: Mobile and desktop compatible
|
|
10
|
-
- 🎨 **Customizable**: All UI components can be customized
|
|
11
|
-
- 📊 **Analytics**: Integrated analytics support
|
|
12
|
-
- 📺 **Ads**: VAST/VMAP ad support
|
|
13
|
-
- ⚡ **Performant**: Optimized with React Compiler
|
|
14
|
-
- 🎯 **TypeScript**: Full type safety
|
|
6
|
+
---
|
|
15
7
|
|
|
16
|
-
##
|
|
8
|
+
## Overview
|
|
17
9
|
|
|
18
|
-
|
|
10
|
+
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.
|
|
19
11
|
|
|
20
|
-
|
|
12
|
+
```tsx
|
|
13
|
+
import { Player } from 'playron'
|
|
14
|
+
import 'playron/style.css'
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
# In the Playron directory
|
|
24
|
-
npm install
|
|
25
|
-
npm run build
|
|
26
|
-
npm link
|
|
16
|
+
<Player src="https://example.com/stream.m3u8" />
|
|
27
17
|
```
|
|
28
18
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
npm link playron
|
|
33
|
-
```
|
|
19
|
+
---
|
|
34
20
|
|
|
35
|
-
|
|
21
|
+
## Installation
|
|
36
22
|
|
|
37
23
|
```bash
|
|
38
24
|
npm install playron
|
|
39
25
|
# or
|
|
40
26
|
yarn add playron
|
|
41
|
-
# or
|
|
42
27
|
pnpm add playron
|
|
43
28
|
```
|
|
44
29
|
|
|
45
|
-
|
|
30
|
+
### Local Development (yalc)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# In the playron repo
|
|
34
|
+
npm run build
|
|
35
|
+
npx yalc push
|
|
36
|
+
|
|
37
|
+
# In your project
|
|
38
|
+
npx yalc add playron
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
46
42
|
|
|
47
|
-
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### React (Vite / CRA)
|
|
48
46
|
|
|
49
47
|
```tsx
|
|
50
|
-
import { Player } from 'playron'
|
|
51
|
-
import 'playron/style.css'
|
|
48
|
+
import { Player } from 'playron'
|
|
49
|
+
import 'playron/style.css'
|
|
52
50
|
|
|
53
|
-
function
|
|
51
|
+
export default function VideoPage() {
|
|
54
52
|
return (
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
<Player
|
|
54
|
+
src="https://example.com/stream.m3u8"
|
|
55
|
+
poster="https://example.com/poster.jpg"
|
|
56
|
+
config={{
|
|
57
|
+
player: { defaultVolume: 0.8, skipSeconds: 10 },
|
|
58
|
+
features: {
|
|
59
|
+
pip: true,
|
|
60
|
+
fullscreen: true,
|
|
61
|
+
theaterMode: true,
|
|
62
|
+
subtitles: true,
|
|
63
|
+
qualitySelector: true,
|
|
64
|
+
playbackSpeed: true,
|
|
65
|
+
keyboardShortcuts: true,
|
|
66
|
+
},
|
|
67
|
+
branding: { primaryColor: '#e50914' },
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
64
71
|
}
|
|
65
72
|
```
|
|
66
73
|
|
|
67
|
-
###
|
|
74
|
+
### Next.js — App Router
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<head>
|
|
73
|
-
<link rel="stylesheet" href="node_modules/playron/dist/playron.css">
|
|
74
|
-
</head>
|
|
75
|
-
<body>
|
|
76
|
-
<div id="player-container"></div>
|
|
77
|
-
|
|
78
|
-
<script type="module">
|
|
79
|
-
import { Playron } from './node_modules/playron/dist/playron.es.js';
|
|
80
|
-
|
|
81
|
-
const player = new Playron({
|
|
82
|
-
containerId: 'player-container',
|
|
83
|
-
src: 'https://example.com/video.m3u8',
|
|
84
|
-
poster: 'https://example.com/poster.jpg',
|
|
85
|
-
autoPlay: false,
|
|
86
|
-
width: '100%',
|
|
87
|
-
height: 'auto'
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Event listeners
|
|
91
|
-
player.on('play', () => console.log('Video playing'));
|
|
92
|
-
player.on('pause', () => console.log('Video paused'));
|
|
93
|
-
|
|
94
|
-
// Player controls
|
|
95
|
-
player.play();
|
|
96
|
-
player.pause();
|
|
97
|
-
player.setVolume(0.5);
|
|
98
|
-
player.seekTo(30);
|
|
99
|
-
</script>
|
|
100
|
-
</body>
|
|
101
|
-
</html>
|
|
102
|
-
```
|
|
76
|
+
> **Important:** Playron uses browser-only APIs (MediaSource, EME, hls.js, dash.js).
|
|
77
|
+
> You **must** use `dynamic()` with `ssr: false`. Using only `'use client'` is not enough —
|
|
78
|
+
> Next.js still pre-renders client components on the server for the initial HTML.
|
|
103
79
|
|
|
104
|
-
|
|
80
|
+
**Step 1** — Create a client wrapper with `dynamic`:
|
|
105
81
|
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
82
|
+
```tsx
|
|
83
|
+
// components/VideoPlayer.tsx
|
|
84
|
+
'use client'
|
|
85
|
+
import dynamic from 'next/dynamic'
|
|
86
|
+
import 'playron/style.css'
|
|
87
|
+
|
|
88
|
+
const Player = dynamic(
|
|
89
|
+
() => import('playron').then(m => m.Player),
|
|
90
|
+
{
|
|
91
|
+
ssr: false,
|
|
92
|
+
loading: () => (
|
|
93
|
+
<div style={{ width: '100%', aspectRatio: '16/9', background: '#000' }} />
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
export default function VideoPlayer({ src }: { src: string }) {
|
|
99
|
+
return <Player src={src} />
|
|
100
|
+
}
|
|
124
101
|
```
|
|
125
102
|
|
|
126
|
-
|
|
103
|
+
**Step 2** — Use it in your page (Server Component is fine here):
|
|
127
104
|
|
|
128
|
-
|
|
105
|
+
```tsx
|
|
106
|
+
// app/page.tsx
|
|
107
|
+
import VideoPlayer from '@/components/VideoPlayer'
|
|
129
108
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
src: string; // Video source URL
|
|
133
|
-
poster?: string; // Poster image URL
|
|
134
|
-
autoPlay?: boolean; // Auto-play video
|
|
135
|
-
muted?: boolean; // Start muted
|
|
136
|
-
controls?: boolean; // Show controls
|
|
137
|
-
loop?: boolean; // Loop video
|
|
138
|
-
width?: string | number; // Player width
|
|
139
|
-
height?: string | number; // Player height
|
|
140
|
-
className?: string; // Custom CSS class
|
|
141
|
-
style?: React.CSSProperties; // Inline styles
|
|
142
|
-
|
|
143
|
-
// DRM Config
|
|
144
|
-
drm?: {
|
|
145
|
-
widevine?: { licenseUrl: string };
|
|
146
|
-
playready?: { licenseUrl: string };
|
|
147
|
-
fairplay?: { licenseUrl: string; certificateUrl: string };
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// Events
|
|
151
|
-
onPlay?: () => void;
|
|
152
|
-
onPause?: () => void;
|
|
153
|
-
onEnded?: () => void;
|
|
154
|
-
onError?: (error: Error) => void;
|
|
155
|
-
onTimeUpdate?: (time: number) => void;
|
|
109
|
+
export default function Page() {
|
|
110
|
+
return <VideoPlayer src="https://example.com/stream.m3u8" />
|
|
156
111
|
}
|
|
157
112
|
```
|
|
158
113
|
|
|
159
|
-
###
|
|
114
|
+
### Next.js — Pages Router
|
|
160
115
|
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// Source
|
|
174
|
-
setSource(src: string): Promise<void>;
|
|
175
|
-
setPoster(poster: string): void;
|
|
176
|
-
|
|
177
|
-
// Events
|
|
178
|
-
on<T>(eventType: string, callback: (event: T) => void): void;
|
|
179
|
-
off<T>(eventType: string, callback: (event: T) => void): void;
|
|
180
|
-
|
|
181
|
-
// State
|
|
182
|
-
getState(): PlayerState;
|
|
183
|
-
|
|
184
|
-
// Cleanup
|
|
185
|
-
destroy(): void;
|
|
116
|
+
```tsx
|
|
117
|
+
// pages/video.tsx
|
|
118
|
+
import dynamic from 'next/dynamic'
|
|
119
|
+
import 'playron/style.css'
|
|
120
|
+
|
|
121
|
+
const Player = dynamic(
|
|
122
|
+
() => import('playron').then(m => m.Player),
|
|
123
|
+
{ ssr: false }
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
export default function VideoPage() {
|
|
127
|
+
return <Player src="https://example.com/stream.m3u8" />
|
|
186
128
|
}
|
|
187
129
|
```
|
|
188
130
|
|
|
189
|
-
###
|
|
131
|
+
### Vanilla JavaScript
|
|
190
132
|
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
'play' | 'pause' | 'ended' | 'error' |
|
|
194
|
-
'timeupdate' | 'volumechange' | 'mute' | 'unmute' |
|
|
195
|
-
'seeked' | 'seeking' | 'loadstart' | 'loadeddata' |
|
|
196
|
-
'canplay' | 'waiting' | 'stalled'
|
|
197
|
-
```
|
|
133
|
+
```html
|
|
134
|
+
<link rel="stylesheet" href="node_modules/playron/dist/playron.css" />
|
|
198
135
|
|
|
199
|
-
|
|
136
|
+
<div id="player"></div>
|
|
200
137
|
|
|
201
|
-
|
|
138
|
+
<script type="module">
|
|
139
|
+
import { PlayerCore } from 'playron/dist/playron.es.js'
|
|
202
140
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
import { ControlBar, PlayButton, MuteButton } from 'playron';
|
|
141
|
+
const player = new PlayerCore(document.querySelector('#player video'))
|
|
142
|
+
await player.setSource('https://example.com/stream.m3u8')
|
|
206
143
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
<video id="my-video" />
|
|
211
|
-
<ControlBar>
|
|
212
|
-
<PlayButton />
|
|
213
|
-
<MuteButton />
|
|
214
|
-
{/* Add your own custom controls */}
|
|
215
|
-
</ControlBar>
|
|
216
|
-
</div>
|
|
217
|
-
);
|
|
218
|
-
}
|
|
144
|
+
player.eventEmitter.on('play', () => console.log('playing'))
|
|
145
|
+
player.eventEmitter.on('ended', () => console.log('ended'))
|
|
146
|
+
</script>
|
|
219
147
|
```
|
|
220
148
|
|
|
221
|
-
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Features
|
|
152
|
+
|
|
153
|
+
### Streaming
|
|
154
|
+
|
|
155
|
+
| Feature | Status |
|
|
156
|
+
|---------|--------|
|
|
157
|
+
| HLS (HTTP Live Streaming) | ✅ via **hls.js** |
|
|
158
|
+
| DASH (Dynamic Adaptive Streaming) | ✅ via **dash.js** |
|
|
159
|
+
| Progressive MP4 | ✅ native |
|
|
160
|
+
| Low Latency HLS (LL-HLS) | ✅ |
|
|
161
|
+
| Low Latency DASH (LL-DASH) | ✅ |
|
|
162
|
+
| VOD | ✅ |
|
|
163
|
+
| Live Stream | ✅ |
|
|
164
|
+
| DVR Window | ✅ |
|
|
165
|
+
| Adaptive Bitrate (ABR) | ✅ engine-native |
|
|
166
|
+
|
|
167
|
+
### DRM
|
|
168
|
+
|
|
169
|
+
| Key System | HLS | DASH | Platform |
|
|
170
|
+
|-----------|-----|------|----------|
|
|
171
|
+
| **Widevine** | ✅ | ✅ | Chrome, Firefox, Edge, Android |
|
|
172
|
+
| **PlayReady** | ❌ | ✅ | Edge, Windows |
|
|
173
|
+
| **FairPlay** | ❌ | ❌ | *Not yet implemented* |
|
|
174
|
+
| **AES-128** | ✅ native | — | All browsers |
|
|
175
|
+
|
|
176
|
+
> **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.
|
|
177
|
+
|
|
178
|
+
### UI Controls
|
|
179
|
+
|
|
180
|
+
| Control | Description |
|
|
181
|
+
|---------|-------------|
|
|
182
|
+
| Play / Pause | Center overlay + control bar |
|
|
183
|
+
| Seek Bar | Scrubbing, buffered range visualization, thumbnail preview on hover |
|
|
184
|
+
| Volume | Slider + mute toggle, localStorage persistence |
|
|
185
|
+
| Playback Speed | 0.25× – 2× |
|
|
186
|
+
| Quality Selector | Manual ABR override |
|
|
187
|
+
| Audio Track | Multi-language audio |
|
|
188
|
+
| Subtitles | WebVTT + CEA-608/708 closed captions |
|
|
189
|
+
| Fullscreen | Native API |
|
|
190
|
+
| Picture-in-Picture | Native API |
|
|
191
|
+
| Theater Mode | Layout toggle |
|
|
192
|
+
| AirPlay | Safari WebKit API |
|
|
193
|
+
| Skip Forward / Backward | Configurable seconds |
|
|
194
|
+
| Skip Intro / Outro | Time-range based |
|
|
195
|
+
| Chapters | Navigation + auto-detection |
|
|
196
|
+
| Jump to Live | Appears when behind live edge |
|
|
197
|
+
| Live Latency Display | "Xs behind live" indicator |
|
|
198
|
+
| Timeline Markers | Sports events, chapters, ads |
|
|
199
|
+
| Settings Panel | YouTube-style unified panel (Quality · Speed · Audio · Subtitles) |
|
|
200
|
+
| Keyboard Shortcuts | Space, M, F, arrows, I, O, and more |
|
|
201
|
+
| Error Overlay | DRM-aware messages, network errors, retry |
|
|
202
|
+
| End Card | Next episode countdown |
|
|
203
|
+
| Context Menu | Loop, copy URL, copy speed |
|
|
204
|
+
|
|
205
|
+
### Ad System
|
|
206
|
+
|
|
207
|
+
| Format | Support |
|
|
208
|
+
|--------|---------|
|
|
209
|
+
| VAST 2.0 / 3.0 / 4.x | ✅ |
|
|
210
|
+
| VMAP 1.0 | ✅ |
|
|
211
|
+
| Pre-roll | ✅ |
|
|
212
|
+
| Mid-roll | ✅ |
|
|
213
|
+
| Post-roll | ✅ |
|
|
214
|
+
| Skip button | ✅ configurable delay |
|
|
215
|
+
| VPAID 2.0 | ✅ iframe sandbox |
|
|
216
|
+
| Companion banner | ✅ |
|
|
217
|
+
| Ad pod | ✅ |
|
|
218
|
+
| Impression / tracking events | ✅ |
|
|
219
|
+
|
|
220
|
+
### Accessibility
|
|
221
|
+
|
|
222
|
+
- WCAG 2.1 AA compliant
|
|
223
|
+
- ARIA roles on all interactive elements
|
|
224
|
+
- Full keyboard navigation
|
|
225
|
+
- Screen reader live region for status announcements
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Configuration
|
|
222
230
|
|
|
223
231
|
```tsx
|
|
224
232
|
<Player
|
|
225
|
-
src="
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
233
|
+
src="..."
|
|
234
|
+
config={{
|
|
235
|
+
player: {
|
|
236
|
+
defaultVolume: 1, // 0–1
|
|
237
|
+
defaultPlaybackRate: 1,
|
|
238
|
+
skipSeconds: 10,
|
|
239
|
+
autoResume: true,
|
|
240
|
+
loop: false,
|
|
241
|
+
preload: 'auto', // 'none' | 'metadata' | 'auto'
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
ui: {
|
|
245
|
+
autoHideDelay: 3000, // ms
|
|
246
|
+
showTitle: true,
|
|
247
|
+
showTimeTooltip: true,
|
|
248
|
+
showVolumeSlider: true,
|
|
249
|
+
controlBarPosition: 'bottom',
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
features: {
|
|
253
|
+
pip: true,
|
|
254
|
+
fullscreen: true,
|
|
255
|
+
theaterMode: true,
|
|
256
|
+
chapters: true,
|
|
257
|
+
skipIntro: true,
|
|
258
|
+
subtitles: true,
|
|
259
|
+
qualitySelector: true,
|
|
260
|
+
audioTrackSelector: true,
|
|
261
|
+
playbackSpeed: true,
|
|
262
|
+
keyboardShortcuts: true,
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
branding: {
|
|
266
|
+
primaryColor: '#e50914', // hex
|
|
267
|
+
accentColor: '#ffffff',
|
|
268
|
+
logo: 'https://example.com/logo.svg',
|
|
269
|
+
logoLink: 'https://example.com',
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
metadata: {
|
|
273
|
+
title: 'My Video',
|
|
274
|
+
thumbnail: 'https://example.com/thumb.jpg',
|
|
275
|
+
rating: 'TV-MA',
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
thumbnails: {
|
|
279
|
+
vttUrl: 'https://example.com/thumbnails.vtt', // storyboard preview
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
drm: {
|
|
283
|
+
enabled: true,
|
|
284
|
+
widevine: {
|
|
285
|
+
licenseUrl: 'https://license.example.com/widevine',
|
|
286
|
+
headers: { 'Authorization': 'Bearer TOKEN' },
|
|
287
|
+
withCredentials: false,
|
|
288
|
+
},
|
|
289
|
+
playready: {
|
|
290
|
+
licenseUrl: 'https://license.example.com/playready',
|
|
291
|
+
},
|
|
292
|
+
// fairplay: not yet implemented
|
|
293
|
+
preferredSystem: 'widevine',
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
ads: {
|
|
297
|
+
enabled: true,
|
|
298
|
+
vmapUrl: 'https://ads.example.com/vmap.xml', // takes priority over vastUrl
|
|
299
|
+
vastUrl: 'https://ads.example.com/vast.xml',
|
|
300
|
+
skipAfter: 5, // seconds before skip button appears
|
|
301
|
+
timeout: 10000, // fetch timeout ms
|
|
302
|
+
maxWrapperDepth: 5, // VAST wrapper chain limit
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
live: {
|
|
306
|
+
atLiveEdgeTolerance: 3, // seconds
|
|
307
|
+
showJumpToLiveButton: true,
|
|
308
|
+
showLatency: true,
|
|
309
|
+
dvr: { enabled: true, maxDvrWindow: 1800 },
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
advanced: {
|
|
313
|
+
bufferAhead: 30,
|
|
314
|
+
bufferBehind: 10,
|
|
315
|
+
maxBufferLength: 60,
|
|
316
|
+
debug: false,
|
|
229
317
|
},
|
|
230
|
-
playready: {
|
|
231
|
-
licenseUrl: 'https://example.com/playready-license'
|
|
232
|
-
}
|
|
233
318
|
}}
|
|
234
319
|
/>
|
|
235
320
|
```
|
|
236
321
|
|
|
237
|
-
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## DRM Usage
|
|
325
|
+
|
|
326
|
+
### Widevine (Chrome / Firefox / Edge)
|
|
238
327
|
|
|
239
328
|
```tsx
|
|
240
329
|
<Player
|
|
241
|
-
src="https://example.com/
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
330
|
+
src="https://example.com/encrypted.mpd"
|
|
331
|
+
config={{
|
|
332
|
+
drm: {
|
|
333
|
+
enabled: true,
|
|
334
|
+
widevine: {
|
|
335
|
+
licenseUrl: 'https://license.example.com/widevine',
|
|
336
|
+
headers: { 'Authorization': 'Bearer YOUR_TOKEN' },
|
|
337
|
+
},
|
|
338
|
+
},
|
|
246
339
|
}}
|
|
247
340
|
/>
|
|
248
341
|
```
|
|
249
342
|
|
|
343
|
+
### Safari / iOS
|
|
344
|
+
|
|
345
|
+
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.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Event System
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
import { PlayerCore } from 'playron'
|
|
353
|
+
|
|
354
|
+
const player = new PlayerCore(videoElement)
|
|
355
|
+
|
|
356
|
+
player.eventEmitter.on('play', ({ timestamp }) => { })
|
|
357
|
+
player.eventEmitter.on('pause', ({ timestamp }) => { })
|
|
358
|
+
player.eventEmitter.on('ended', ({ timestamp }) => { })
|
|
359
|
+
player.eventEmitter.on('timeupdate', ({ currentTime, duration }) => { })
|
|
360
|
+
player.eventEmitter.on('volumechange',({ volume, isMuted }) => { })
|
|
361
|
+
player.eventEmitter.on('qualitychange',({ from, to }) => { })
|
|
362
|
+
player.eventEmitter.on('error', ({ code, message, details }) => { })
|
|
363
|
+
player.eventEmitter.on('ready', ({ player }) => { })
|
|
364
|
+
player.eventEmitter.on('destroy', ({ timestamp }) => { })
|
|
365
|
+
|
|
366
|
+
// One-time listener
|
|
367
|
+
player.eventEmitter.once('ready', ({ player }) => player.play())
|
|
368
|
+
|
|
369
|
+
// Remove listener
|
|
370
|
+
player.eventEmitter.off('play', handler)
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### React Hooks
|
|
374
|
+
|
|
375
|
+
```tsx
|
|
376
|
+
import { usePlayerState, usePlayerMethods, usePlayer } from 'playron'
|
|
377
|
+
|
|
378
|
+
function MyControls() {
|
|
379
|
+
const { isPlaying, volume, currentTime, duration, isLive, isBuffering } = usePlayerState()
|
|
380
|
+
const { play, pause, seekTo, setVolume, seekToLiveEdge } = usePlayerMethods()
|
|
381
|
+
const player = usePlayer() // raw PlayerCore instance
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## PlayerCore API
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
class PlayerCore {
|
|
391
|
+
// Playback
|
|
392
|
+
play(): Promise<void>
|
|
393
|
+
pause(): void
|
|
394
|
+
seekTo(time: number): void
|
|
395
|
+
setPlaybackRate(rate: number): void
|
|
396
|
+
|
|
397
|
+
// Volume
|
|
398
|
+
setVolume(volume: number): void // 0–1
|
|
399
|
+
toggleMute(): void
|
|
400
|
+
|
|
401
|
+
// Display
|
|
402
|
+
toggleFullscreen(): Promise<void>
|
|
403
|
+
togglePip(): Promise<void>
|
|
404
|
+
toggleTheaterMode(): void
|
|
405
|
+
|
|
406
|
+
// Stream
|
|
407
|
+
setSource(src: string): Promise<void>
|
|
408
|
+
getStreamType(): 'hls' | 'dash' | 'mp4' | 'unknown'
|
|
409
|
+
isLive(): boolean
|
|
410
|
+
|
|
411
|
+
// Live
|
|
412
|
+
seekToLiveEdge(): void
|
|
413
|
+
isAtLiveEdge(): boolean
|
|
414
|
+
getCurrentLatency(): number
|
|
415
|
+
getDVRRange(): { start: number; end: number } | null
|
|
416
|
+
|
|
417
|
+
// Quality / Audio / Subtitles
|
|
418
|
+
setQuality(id: string): void
|
|
419
|
+
getAvailableQualities(): QualityLevel[]
|
|
420
|
+
setAudioTrack(id: string): void
|
|
421
|
+
getAvailableAudioTracks(): AudioTrack[]
|
|
422
|
+
|
|
423
|
+
// Skip
|
|
424
|
+
skipForward(seconds?: number): void
|
|
425
|
+
skipBackward(seconds?: number): void
|
|
426
|
+
|
|
427
|
+
// DRM
|
|
428
|
+
setupDrm(config: DrmConfig): void
|
|
429
|
+
|
|
430
|
+
// Buffer
|
|
431
|
+
getBufferedRanges(): Array<{ start: number; end: number }>
|
|
432
|
+
|
|
433
|
+
// Cleanup
|
|
434
|
+
destroy(): void
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## Bundle Size
|
|
441
|
+
|
|
442
|
+
| File | Raw | Gzip |
|
|
443
|
+
|------|-----|------|
|
|
444
|
+
| `playron.es.js` | 2.1 MB | **559 KB** |
|
|
445
|
+
| `playron.cjs.js` | 1.6 MB | **490 KB** |
|
|
446
|
+
| `playron.css` | 5.9 KB | **2 KB** |
|
|
447
|
+
|
|
448
|
+
> 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.
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Framework Support
|
|
453
|
+
|
|
454
|
+
| Framework | Support | Notes |
|
|
455
|
+
|-----------|---------|-------|
|
|
456
|
+
| React 18 / 19 | ✅ Full | Hooks + Context API |
|
|
457
|
+
| Next.js App Router | ✅ Full | Add `'use client'` |
|
|
458
|
+
| Next.js Pages Router | ✅ Full | Use `dynamic(..., { ssr: false })` |
|
|
459
|
+
| Vite + React | ✅ Full | Reference dev environment |
|
|
460
|
+
| Remix | ✅ Full | Use `ClientOnly` wrapper |
|
|
461
|
+
| Astro | 🟡 Partial | `client:only="react"` |
|
|
462
|
+
| Vanilla JS | 🟡 Partial | Use `PlayerCore` directly |
|
|
463
|
+
| Vue / Nuxt | 🟡 Partial | Mount `PlayerCore` in `onMounted` |
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Browser Support
|
|
468
|
+
|
|
469
|
+
| Browser | HLS | DASH | Widevine | PlayReady | FairPlay |
|
|
470
|
+
|---------|-----|------|----------|-----------|---------|
|
|
471
|
+
| Chrome 90+ | ✅ | ✅ | ✅ | — | — |
|
|
472
|
+
| Firefox 88+ | ✅ | ✅ | ✅ | — | — |
|
|
473
|
+
| Edge 90+ | ✅ | ✅ | ✅ | ✅ | — |
|
|
474
|
+
| Safari 14+ | ✅ | ✅ | ❌ | — | ⏳ planned |
|
|
475
|
+
| iOS Safari 14+ | ✅ | ✅ | ❌ | — | ⏳ planned |
|
|
476
|
+
| Chrome Android | ✅ | ✅ | ✅ | — | — |
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
250
480
|
## Development
|
|
251
481
|
|
|
252
482
|
```bash
|
|
253
|
-
# Install dependencies
|
|
254
483
|
npm install
|
|
255
484
|
|
|
256
|
-
#
|
|
257
|
-
npm run
|
|
485
|
+
npm run dev # HTTPS dev server (port 5173)
|
|
486
|
+
npm run build # TypeScript check + library build
|
|
487
|
+
npm run build:watch # Watch mode
|
|
488
|
+
npm run push # Build + yalc push to linked projects
|
|
489
|
+
npm run lint # ESLint
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
> **HTTPS is required.** See `SSL-SETUP.md` for local certificate setup.
|
|
258
493
|
|
|
259
|
-
|
|
260
|
-
npm run build
|
|
494
|
+
---
|
|
261
495
|
|
|
262
|
-
|
|
263
|
-
|
|
496
|
+
## Roadmap
|
|
497
|
+
|
|
498
|
+
- [x] HLS engine (hls.js wrapper)
|
|
499
|
+
- [x] DASH engine (dash.js wrapper)
|
|
500
|
+
- [x] Widevine DRM — HLS + DASH
|
|
501
|
+
- [x] PlayReady DRM — DASH
|
|
502
|
+
- [x] AES-128 HLS decryption
|
|
503
|
+
- [x] VAST / VMAP ad system
|
|
504
|
+
- [x] WebVTT subtitles
|
|
505
|
+
- [x] CEA-608 / 708 closed captions
|
|
506
|
+
- [x] Live stream + DVR
|
|
507
|
+
- [x] LL-HLS / LL-DASH
|
|
508
|
+
- [x] Thumbnail storyboard preview
|
|
509
|
+
- [x] AirPlay
|
|
510
|
+
- [x] Analytics plugin
|
|
511
|
+
- [x] Stall detection + auto-recovery
|
|
512
|
+
- [x] DRM-aware error overlay
|
|
513
|
+
- [ ] **FairPlay** — Safari / iOS *(in progress)*
|
|
514
|
+
- [ ] Chromecast
|
|
515
|
+
- [ ] SSAI (Google DAI / AWS Elemental)
|
|
516
|
+
- [ ] TTML / IMSC subtitles
|
|
517
|
+
- [ ] Offline playback (Service Worker)
|
|
518
|
+
- [ ] 360° video (WebGL)
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## Troubleshooting
|
|
523
|
+
|
|
524
|
+
### `window is not defined` (Next.js)
|
|
525
|
+
|
|
526
|
+
**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.
|
|
527
|
+
|
|
528
|
+
**Fix:** Always wrap Playron in `dynamic()` with `ssr: false`:
|
|
529
|
+
|
|
530
|
+
```tsx
|
|
531
|
+
// ❌ Wrong — causes "window is not defined" on the server
|
|
532
|
+
'use client'
|
|
533
|
+
import { Player } from 'playron'
|
|
534
|
+
|
|
535
|
+
// ✅ Correct
|
|
536
|
+
'use client'
|
|
537
|
+
import dynamic from 'next/dynamic'
|
|
538
|
+
|
|
539
|
+
const Player = dynamic(
|
|
540
|
+
() => import('playron').then(m => m.Player),
|
|
541
|
+
{ ssr: false }
|
|
542
|
+
)
|
|
264
543
|
```
|
|
265
544
|
|
|
266
|
-
|
|
545
|
+
---
|
|
267
546
|
|
|
268
|
-
|
|
269
|
-
- `playron.es.js` - ES Module (for modern projects)
|
|
270
|
-
- `playron.umd.js` - UMD format (direct use in browser)
|
|
271
|
-
- `playron.css` - Styles
|
|
272
|
-
- `index.d.ts` - TypeScript definitions
|
|
547
|
+
### `Module not found: Can't resolve 'playron/dist/playron.css'`
|
|
273
548
|
|
|
274
|
-
|
|
549
|
+
**Cause:** The correct CSS export path is `playron/style.css`, not `playron/dist/playron.css`.
|
|
275
550
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
- Mobile browsers (iOS Safari 14+, Chrome Android 90+)
|
|
551
|
+
```tsx
|
|
552
|
+
// ❌ Wrong
|
|
553
|
+
import 'playron/dist/playron.css'
|
|
280
554
|
|
|
281
|
-
|
|
555
|
+
// ✅ Correct
|
|
556
|
+
import 'playron/style.css'
|
|
557
|
+
```
|
|
282
558
|
|
|
283
|
-
|
|
559
|
+
Both paths now work as of v1.0.3, but `playron/style.css` is the canonical import.
|
|
284
560
|
|
|
285
|
-
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
### Player renders but has no styles
|
|
564
|
+
|
|
565
|
+
Make sure you import the CSS **once** at the top level of your app (e.g. `layout.tsx`, `_app.tsx`, or `globals.css`):
|
|
566
|
+
|
|
567
|
+
```tsx
|
|
568
|
+
// app/layout.tsx
|
|
569
|
+
import 'playron/style.css'
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
### Video plays but DRM content fails on Safari
|
|
575
|
+
|
|
576
|
+
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](#drm-usage) for the current support matrix.
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## License
|
|
286
581
|
|
|
287
|
-
Suat Erkilic
|
|
582
|
+
MIT © Suat Erkilic
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playron",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Modern OTT video player for React, Next.js, and vanilla JS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/playron.cjs.js",
|
|
@@ -12,8 +12,14 @@
|
|
|
12
12
|
"import": "./dist/playron.es.js",
|
|
13
13
|
"require": "./dist/playron.cjs.js"
|
|
14
14
|
},
|
|
15
|
-
"./style.css": "./dist/playron.css"
|
|
15
|
+
"./style.css": "./dist/playron.css",
|
|
16
|
+
"./dist/playron.css": "./dist/playron.css",
|
|
17
|
+
"./dist/*": "./dist/*"
|
|
16
18
|
},
|
|
19
|
+
"sideEffects": [
|
|
20
|
+
"*.css",
|
|
21
|
+
"./dist/playron.css"
|
|
22
|
+
],
|
|
17
23
|
"files": [
|
|
18
24
|
"dist"
|
|
19
25
|
],
|