@frameset/plex-player 1.0.5 → 2.0.0

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 CHANGED
@@ -1,601 +1,406 @@
1
- <p align="center">
2
- <img src="https://frameset.dev/favicon.svg" alt="FRAMESET" width="180" />
3
- </p>
4
-
5
- <h1 align="center">@frameset/plex-player</h1>
1
+ # Plex Player
6
2
 
7
3
  <p align="center">
8
- <strong>🎬 Professional HTML5 Video Player for Modern Web Applications</strong>
4
+ <img src="https://img.shields.io/npm/v/@frameset/plex-player" alt="npm version" />
5
+ <img src="https://img.shields.io/npm/dm/@frameset/plex-player" alt="npm downloads" />
6
+ <img src="https://img.shields.io/bundlephobia/minzip/@frameset/plex-player" alt="bundle size" />
7
+ <img src="https://img.shields.io/npm/l/@frameset/plex-player" alt="license" />
9
8
  </p>
10
9
 
11
10
  <p align="center">
12
- <a href="https://www.npmjs.com/package/@frameset/plex-player">
13
- <img src="https://img.shields.io/npm/v/@frameset/plex-player.svg?style=flat-square" alt="npm version" />
14
- </a>
15
- <a href="https://www.npmjs.com/package/@frameset/plex-player">
16
- <img src="https://img.shields.io/npm/dm/@frameset/plex-player.svg?style=flat-square" alt="npm downloads" />
17
- </a>
18
- <a href="https://github.com/deadseti/plex-player/blob/main/LICENSE">
19
- <img src="https://img.shields.io/npm/l/@frameset/plex-player.svg?style=flat-square" alt="license" />
20
- </a>
21
- <a href="https://bundlephobia.com/package/@frameset/plex-player">
22
- <img src="https://img.shields.io/bundlephobia/minzip/@frameset/plex-player?style=flat-square" alt="bundle size" />
23
- </a>
11
+ <strong>Ultra-performant React video player with VAST ads support, Picture-in-Picture, and advanced controls.</strong>
24
12
  </p>
25
13
 
26
14
  <p align="center">
27
- <a href="#features">Features</a>
28
- <a href="#installation">Installation</a> •
29
- <a href="#quick-start">Quick Start</a> •
30
- <a href="#api">API</a> •
31
- <a href="#frameworks">Frameworks</a> •
32
- <a href="#ads">Ads</a> •
33
- <a href="#chromecast">Chromecast</a> •
34
- <a href="#license">License</a>
15
+ Built with ❤️ by <a href="https://frameset.dev">FRAMESET STUDIO</a>
35
16
  </p>
36
17
 
37
18
  ---
38
19
 
39
20
  ## ✨ Features
40
21
 
41
- - 🎥 **Full-Featured Player** Play, pause, seek, volume, playback rate, and more
42
- - 📺 **Chromecast Support** Cast videos to any Chromecast-enabled device
43
- - 📋 **Playlist Management** Queue management with thumbnail preview
44
- - 🎨 **Customizable UI** Fully themeable with CSS variables
45
- - 📱 **Responsive Design** Works perfectly on desktop, tablet, and mobile
46
- - ⌨️ **Keyboard Shortcuts** Full keyboard navigation support
47
- - 👆 **Touch Gestures** Swipe to seek, double-tap to skip
48
- - 🖼️ **Picture-in-Picture** Native PiP support
49
- - 📺 **Fullscreen Mode** True fullscreen with controls
50
- - 📝 **Subtitles/Captions** — Multi-track subtitle support
51
- - 💰 **VAST Ads** VAST 2.0/3.0/4.0 video advertising
52
- - 🔌 **Framework Support** React, Vue 3, and Vanilla JS
53
- - 📦 **TypeScript** — Full type definitions included
54
- - 🌍 **i18n Ready** — Easy localization support
55
-
56
- ---
22
+ - 🚀 **Ultra Performant** - Optimized rendering with minimal re-renders
23
+ - 🎬 **VAST Ads Support** - Pre-roll, mid-roll, and post-roll video ads
24
+ - 📺 **Picture-in-Picture** - Native PiP support for multitasking
25
+ - 🎛️ **Advanced Controls** - Customizable playback speed, quality selector, and more
26
+ - ⌨️ **Keyboard Shortcuts** - Full keyboard navigation support
27
+ - 📱 **Mobile Friendly** - Touch-optimized controls and gestures
28
+ - 🎨 **Themeable** - Dark/Light themes with custom accent colors
29
+ - **Accessible** - ARIA labels and keyboard navigation
30
+ - 📝 **Subtitles/Captions** - Multiple text track support
31
+ - 🖼️ **Thumbnail Preview** - Hover preview on progress bar
32
+ - 💪 **TypeScript** - Full TypeScript support with type definitions
33
+ - 📦 **Tree Shakeable** - Import only what you need
57
34
 
58
35
  ## 📦 Installation
59
36
 
60
- ### NPM / Yarn / PNPM
61
-
62
37
  ```bash
63
- # npm
64
38
  npm install @frameset/plex-player
39
+ ```
65
40
 
66
- # yarn
41
+ ```bash
67
42
  yarn add @frameset/plex-player
68
-
69
- # pnpm
70
- pnpm add @frameset/plex-player
71
43
  ```
72
44
 
73
- ### CDN
74
-
75
- ```html
76
- <!-- CSS -->
77
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@frameset/plex-player/dist/plex-player.min.css">
78
-
79
- <!-- JavaScript -->
80
- <script src="https://cdn.jsdelivr.net/npm/@frameset/plex-player/dist/plex-player.min.js"></script>
45
+ ```bash
46
+ pnpm add @frameset/plex-player
81
47
  ```
82
48
 
83
- ---
84
-
85
49
  ## 🚀 Quick Start
86
50
 
87
- ### Vanilla JavaScript
88
-
89
- ```html
90
- <!DOCTYPE html>
91
- <html>
92
- <head>
93
- <link rel="stylesheet" href="@frameset/plex-player/dist/plex-player.css">
94
- </head>
95
- <body>
96
- <div id="player"></div>
97
-
98
- <script src="@frameset/plex-player/dist/plex-player.min.js"></script>
99
- <script>
100
- const player = new PlexPlayer({
101
- container: '#player',
102
- autoplay: false,
103
- muted: false,
104
- });
105
-
106
- player.load('https://example.com/video.mp4');
107
- </script>
108
- </body>
109
- </html>
110
- ```
111
-
112
- ### ES Modules
113
-
114
- ```javascript
115
- import PlexPlayer from '@frameset/plex-player';
116
- import '@frameset/plex-player/css';
117
-
118
- const player = new PlexPlayer({
119
- container: '#player',
120
- autoplay: true,
121
- });
122
-
123
- player.load('https://example.com/video.mp4');
124
- ```
125
-
126
- ---
127
-
128
- ## ⚛️ React
129
-
130
- ```bash
131
- npm install @frameset/plex-player react
132
- ```
133
-
134
- ```jsx
135
- import { PlexPlayerReact, usePlexPlayer } from '@frameset/plex-player/react';
136
- import '@frameset/plex-player/css';
51
+ ```tsx
52
+ import { PlexVideoPlayer } from '@frameset/plex-player';
53
+ import '@frameset/plex-player/styles.css';
137
54
 
138
55
  function App() {
139
- const playerRef = useRef(null);
140
-
141
- const handlePlay = () => {
142
- console.log('Video started playing');
143
- };
144
-
145
- const handleTimeUpdate = (currentTime) => {
146
- console.log('Current time:', currentTime);
147
- };
148
-
149
56
  return (
150
- <PlexPlayerReact
151
- ref={playerRef}
57
+ <PlexVideoPlayer
152
58
  src="https://example.com/video.mp4"
153
- autoplay={false}
154
- onPlay={handlePlay}
155
- onTimeUpdate={handleTimeUpdate}
59
+ poster="https://example.com/poster.jpg"
60
+ width="100%"
61
+ height="auto"
156
62
  />
157
63
  );
158
64
  }
159
65
  ```
160
66
 
161
- ### Hooks
67
+ ## 📖 Documentation
162
68
 
163
- ```jsx
164
- import {
165
- PlexPlayerReact,
166
- PlexPlayerProvider,
167
- usePlexPlayer,
168
- usePlexPlayerState,
169
- usePlexPlayerTime
170
- } from '@frameset/plex-player/react';
69
+ ### Basic Usage
171
70
 
172
- function Controls() {
173
- const player = usePlexPlayer();
174
- const state = usePlexPlayerState();
175
- const { current, duration } = usePlexPlayerTime();
71
+ ```tsx
72
+ import { PlexVideoPlayer } from '@frameset/plex-player';
73
+ import '@frameset/plex-player/styles.css';
176
74
 
75
+ function VideoPlayer() {
177
76
  return (
178
- <div>
179
- <button onClick={() => player?.togglePlay()}>
180
- {state?.isPlaying ? 'Pause' : 'Play'}
181
- </button>
182
- <span>{current}s / {duration}s</span>
183
- </div>
184
- );
185
- }
186
-
187
- function App() {
188
- return (
189
- <PlexPlayerProvider>
190
- <PlexPlayerReact src="video.mp4" />
191
- <Controls />
192
- </PlexPlayerProvider>
77
+ <PlexVideoPlayer
78
+ src="https://example.com/video.mp4"
79
+ poster="https://example.com/poster.jpg"
80
+ autoPlay={false}
81
+ muted={false}
82
+ controls={true}
83
+ />
193
84
  );
194
85
  }
195
86
  ```
196
87
 
197
- ---
198
-
199
- ## 💚 Vue 3
200
-
201
- ```bash
202
- npm install @frameset/plex-player vue
203
- ```
204
-
205
- ```vue
206
- <template>
207
- <PlexPlayerVue
208
- ref="player"
209
- src="https://example.com/video.mp4"
210
- :autoplay="false"
211
- @play="onPlay"
212
- @timeupdate="onTimeUpdate"
213
- />
214
- </template>
215
-
216
- <script setup>
217
- import { ref } from 'vue';
218
- import { PlexPlayerVue } from '@frameset/plex-player/vue';
219
- import '@frameset/plex-player/css';
220
-
221
- const player = ref(null);
222
-
223
- const onPlay = () => {
224
- console.log('Video started playing');
225
- };
226
-
227
- const onTimeUpdate = (currentTime) => {
228
- console.log('Current time:', currentTime);
229
- };
230
-
231
- // Control player
232
- const togglePlay = () => {
233
- player.value?.togglePlay();
234
- };
235
- </script>
88
+ ### Multiple Quality Sources
89
+
90
+ ```tsx
91
+ <PlexVideoPlayer
92
+ src={[
93
+ { src: 'video-1080p.mp4', quality: '1080p', label: '1080p HD' },
94
+ { src: 'video-720p.mp4', quality: '720p', label: '720p' },
95
+ { src: 'video-480p.mp4', quality: '480p', label: '480p' },
96
+ { src: 'video-360p.mp4', quality: '360p', label: '360p' },
97
+ ]}
98
+ qualitySelector={true}
99
+ />
236
100
  ```
237
101
 
238
- ### Composables
239
-
240
- ```vue
241
- <script setup>
242
- import { PlexPlayerVue, usePlexPlayer, usePlexPlayerTime } from '@frameset/plex-player/vue';
243
-
244
- const player = usePlexPlayer();
245
- const time = usePlexPlayerTime();
246
- </script>
247
-
248
- <template>
249
- <PlexPlayerVue src="video.mp4" />
250
- <div>{{ time.current }} / {{ time.duration }}</div>
251
- </template>
102
+ ### With VAST Ads
103
+
104
+ ```tsx
105
+ <PlexVideoPlayer
106
+ src="https://example.com/video.mp4"
107
+ vast={{
108
+ url: 'https://example.com/vast.xml',
109
+ skipDelay: 5,
110
+ position: 'preroll',
111
+ }}
112
+ onAdStart={(ad) => console.log('Ad started:', ad)}
113
+ onAdEnd={() => console.log('Ad ended')}
114
+ onAdSkip={() => console.log('Ad skipped')}
115
+ />
252
116
  ```
253
117
 
254
- ---
255
-
256
- ## 📖 API Reference
257
-
258
- ### Constructor Options
259
-
260
- ```typescript
261
- interface PlexPlayerOptions {
262
- container: string | HTMLElement; // Required: container selector or element
263
- autoplay?: boolean; // Auto-play video (default: false)
264
- muted?: boolean; // Start muted (default: false)
265
- loop?: boolean; // Loop video (default: false)
266
- volume?: number; // Initial volume 0-1 (default: 1)
267
- poster?: string; // Poster image URL
268
- preload?: 'none' | 'metadata' | 'auto'; // Preload behavior (default: 'metadata')
269
- keyboard?: boolean; // Enable keyboard shortcuts (default: true)
270
- touch?: boolean; // Enable touch gestures (default: true)
271
- pip?: boolean; // Enable PiP button (default: true)
272
- cast?: boolean; // Enable Chromecast (default: true)
273
- fullscreen?: boolean; // Enable fullscreen (default: true)
274
- controlsHideDelay?: number; // Hide controls after ms (default: 3000)
275
- theme?: ThemeOptions; // Custom theme colors
276
- subtitles?: SubtitleOptions; // Subtitle configuration
277
- ads?: AdsOptions; // VAST ads configuration
278
- i18n?: I18nOptions; // Localization strings
279
- }
118
+ ### Multiple Ads (Pre-roll, Mid-roll, Post-roll)
119
+
120
+ ```tsx
121
+ <PlexVideoPlayer
122
+ src="https://example.com/video.mp4"
123
+ vast={[
124
+ { url: 'https://example.com/preroll.xml', position: 'preroll' },
125
+ { url: 'https://example.com/midroll.xml', position: 'midroll', midrollTime: 60 },
126
+ { url: 'https://example.com/postroll.xml', position: 'postroll' },
127
+ ]}
128
+ />
280
129
  ```
281
130
 
282
- ### Methods
283
-
284
- | Method | Description | Returns |
285
- |--------|-------------|---------|
286
- | `load(src)` | Load a video URL | `void` |
287
- | `loadPlaylist(items)` | Load a playlist | `void` |
288
- | `play()` | Start playback | `Promise<void>` |
289
- | `pause()` | Pause playback | `void` |
290
- | `togglePlay()` | Toggle play/pause | `void` |
291
- | `seek(time)` | Seek to time in seconds | `void` |
292
- | `seekPercent(percent)` | Seek to percentage (0-100) | `void` |
293
- | `setVolume(level)` | Set volume (0-1) | `void` |
294
- | `getVolume()` | Get current volume | `number` |
295
- | `mute()` | Mute audio | `void` |
296
- | `unmute()` | Unmute audio | `void` |
297
- | `toggleMute()` | Toggle mute | `void` |
298
- | `setPlaybackRate(rate)` | Set playback speed | `void` |
299
- | `enterFullscreen()` | Enter fullscreen | `Promise<void>` |
300
- | `exitFullscreen()` | Exit fullscreen | `Promise<void>` |
301
- | `toggleFullscreen()` | Toggle fullscreen | `void` |
302
- | `enterPiP()` | Enter Picture-in-Picture | `Promise<void>` |
303
- | `exitPiP()` | Exit Picture-in-Picture | `Promise<void>` |
304
- | `togglePiP()` | Toggle PiP | `void` |
305
- | `cast()` | Start Chromecast | `void` |
306
- | `next()` | Play next in playlist | `void` |
307
- | `previous()` | Play previous in playlist | `void` |
308
- | `playAt(index)` | Play specific playlist item | `void` |
309
- | `getState()` | Get current player state | `PlayerState` |
310
- | `destroy()` | Destroy player instance | `void` |
311
-
312
- ### Events
313
-
314
- ```javascript
315
- player.on('play', () => { /* Video started */ });
316
- player.on('pause', () => { /* Video paused */ });
317
- player.on('ended', () => { /* Video ended */ });
318
- player.on('timeupdate', ({ currentTime, duration }) => { /* Time changed */ });
319
- player.on('progress', ({ buffered }) => { /* Buffer progress */ });
320
- player.on('volumechange', ({ volume, muted }) => { /* Volume changed */ });
321
- player.on('fullscreenchange', ({ isFullscreen }) => { /* Fullscreen toggled */ });
322
- player.on('error', (error) => { /* Error occurred */ });
323
- player.on('ready', () => { /* Player ready */ });
324
- player.on('trackchange', ({ index, item }) => { /* Playlist track changed */ });
131
+ ### With Subtitles
132
+
133
+ ```tsx
134
+ <PlexVideoPlayer
135
+ src="https://example.com/video.mp4"
136
+ textTracks={[
137
+ { src: 'subtitles-en.vtt', kind: 'subtitles', srclang: 'en', label: 'English' },
138
+ { src: 'subtitles-es.vtt', kind: 'subtitles', srclang: 'es', label: 'Spanish' },
139
+ { src: 'subtitles-fr.vtt', kind: 'subtitles', srclang: 'fr', label: 'French' },
140
+ ]}
141
+ />
325
142
  ```
326
143
 
327
- ---
144
+ ### Custom Styling
328
145
 
329
- ## 💰 VAST Ads
330
-
331
- Plex Player supports VAST 2.0, 3.0, and 4.0 video advertising standards.
332
-
333
- ```javascript
334
- const player = new PlexPlayer({
335
- container: '#player',
336
- ads: {
337
- enabled: true,
338
- tagUrl: 'https://your-ad-server.com/vast.xml',
339
- skipOffset: 5, // Allow skip after 5 seconds
340
- timeout: 10000, // Ad request timeout in ms
341
- },
342
- });
343
-
344
- // Listen to ad events
345
- player.on('adstart', (ad) => {
346
- console.log('Ad started:', ad);
347
- });
348
-
349
- player.on('adend', () => {
350
- console.log('Ad finished');
351
- });
352
-
353
- player.on('adskip', () => {
354
- console.log('Ad skipped');
355
- });
356
-
357
- player.on('aderror', (error) => {
358
- console.log('Ad error:', error);
359
- });
146
+ ```tsx
147
+ <PlexVideoPlayer
148
+ src="https://example.com/video.mp4"
149
+ accentColor="#ff5722"
150
+ theme="dark"
151
+ className="my-custom-player"
152
+ style={{ borderRadius: '12px', overflow: 'hidden' }}
153
+ />
360
154
  ```
361
155
 
362
- ### Ad Options
363
-
364
- | Option | Type | Default | Description |
365
- |--------|------|---------|-------------|
366
- | `enabled` | `boolean` | `false` | Enable ads |
367
- | `tagUrl` | `string` | — | VAST tag URL |
368
- | `skipOffset` | `number` | `5` | Seconds before skip allowed |
369
- | `timeout` | `number` | `10000` | Request timeout in ms |
370
- | `maxRedirects` | `number` | `5` | Maximum VAST redirects |
156
+ ### Using Ref for Programmatic Control
371
157
 
372
- ---
158
+ ```tsx
159
+ import { useRef } from 'react';
160
+ import { PlexVideoPlayer, PlexVideoPlayerRef } from '@frameset/plex-player';
373
161
 
374
- ## 📺 Chromecast
162
+ function VideoPlayer() {
163
+ const playerRef = useRef<PlexVideoPlayerRef>(null);
375
164
 
376
- Chromecast is enabled by default. The cast button appears automatically when a Chromecast device is available on the network.
377
-
378
- ```javascript
379
- const player = new PlexPlayer({
380
- container: '#player',
381
- cast: true, // Enable Chromecast (default)
382
- });
383
-
384
- // Manual cast
385
- player.cast();
165
+ const handlePlayPause = () => {
166
+ if (playerRef.current?.isPlaying()) {
167
+ playerRef.current.pause();
168
+ } else {
169
+ playerRef.current?.play();
170
+ }
171
+ };
386
172
 
387
- // Listen to cast events
388
- player.on('castconnected', ({ deviceName }) => {
389
- console.log('Connected to:', deviceName);
390
- });
173
+ const handleSeek = () => {
174
+ playerRef.current?.seek(30); // Seek to 30 seconds
175
+ };
391
176
 
392
- player.on('castdisconnected', () => {
393
- console.log('Disconnected from Chromecast');
394
- });
177
+ const handleFullscreen = () => {
178
+ playerRef.current?.toggleFullscreen();
179
+ };
395
180
 
396
- player.on('caststatechange', ({ state }) => {
397
- console.log('Cast state:', state);
398
- });
181
+ return (
182
+ <>
183
+ <PlexVideoPlayer
184
+ ref={playerRef}
185
+ src="https://example.com/video.mp4"
186
+ />
187
+ <div>
188
+ <button onClick={handlePlayPause}>Play/Pause</button>
189
+ <button onClick={handleSeek}>Skip to 0:30</button>
190
+ <button onClick={handleFullscreen}>Fullscreen</button>
191
+ </div>
192
+ </>
193
+ );
194
+ }
399
195
  ```
400
196
 
401
- ### Requirements
197
+ ### Using the usePlayer Hook
198
+
199
+ ```tsx
200
+ import { usePlayer } from '@frameset/plex-player';
201
+
202
+ function CustomPlayer() {
203
+ const {
204
+ state,
205
+ videoRef,
206
+ containerRef,
207
+ play,
208
+ pause,
209
+ togglePlay,
210
+ seek,
211
+ setVolume,
212
+ toggleMute,
213
+ setPlaybackRate,
214
+ toggleFullscreen,
215
+ togglePip,
216
+ } = usePlayer({ autoPlay: false, muted: false });
402
217
 
403
- - Chromecast requires HTTPS in production
404
- - Works in Chrome and Edge browsers
405
- - Cast SDK is loaded automatically
406
-
407
- ---
408
-
409
- ## 🎨 Theming
410
-
411
- Customize the player appearance using CSS variables:
412
-
413
- ```css
414
- :root {
415
- --plex-primary: #e5a00d; /* Primary accent color */
416
- --plex-bg: rgba(0, 0, 0, 0.8); /* Background color */
417
- --plex-text: #ffffff; /* Text color */
418
- --plex-progress-bg: #3a3a3a; /* Progress bar background */
419
- --plex-progress: #e5a00d; /* Progress bar fill */
420
- --plex-buffered: #6a6a6a; /* Buffered area color */
421
- --plex-control-size: 40px; /* Control button size */
422
- --plex-border-radius: 4px; /* Border radius */
218
+ return (
219
+ <div ref={containerRef}>
220
+ <video ref={videoRef} src="https://example.com/video.mp4" />
221
+ <div>
222
+ <button onClick={togglePlay}>
223
+ {state.isPlaying ? 'Pause' : 'Play'}
224
+ </button>
225
+ <span>{state.currentTime} / {state.duration}</span>
226
+ </div>
227
+ </div>
228
+ );
423
229
  }
424
230
  ```
425
231
 
426
- Or via JavaScript:
427
-
428
- ```javascript
429
- const player = new PlexPlayer({
430
- container: '#player',
431
- theme: {
432
- primary: '#e5a00d',
433
- background: 'rgba(0, 0, 0, 0.8)',
434
- text: '#ffffff',
435
- borderRadius: '8px',
436
- },
437
- });
438
- ```
439
-
440
- ---
232
+ ## ⚙️ Props
233
+
234
+ | Prop | Type | Default | Description |
235
+ |------|------|---------|-------------|
236
+ | `src` | `string \| VideoSource[]` | required | Video source URL or array of sources |
237
+ | `poster` | `string` | - | Poster image URL |
238
+ | `autoPlay` | `boolean` | `false` | Auto-play video on load |
239
+ | `muted` | `boolean` | `false` | Mute video on load |
240
+ | `loop` | `boolean` | `false` | Loop video playback |
241
+ | `preload` | `'none' \| 'metadata' \| 'auto'` | `'metadata'` | Preload behavior |
242
+ | `width` | `number \| string` | `'100%'` | Player width |
243
+ | `height` | `number \| string` | `'auto'` | Player height |
244
+ | `controls` | `boolean` | `true` | Show controls |
245
+ | `pip` | `boolean` | `true` | Enable Picture-in-Picture |
246
+ | `fullscreen` | `boolean` | `true` | Enable fullscreen |
247
+ | `playbackSpeed` | `boolean` | `true` | Enable playback speed control |
248
+ | `playbackSpeeds` | `number[]` | `[0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]` | Available speeds |
249
+ | `volume` | `boolean` | `true` | Enable volume control |
250
+ | `initialVolume` | `number` | `1` | Initial volume (0-1) |
251
+ | `progressBar` | `boolean` | `true` | Show progress bar |
252
+ | `timeDisplay` | `boolean` | `true` | Show time display |
253
+ | `qualitySelector` | `boolean` | `true` | Enable quality selector |
254
+ | `textTracks` | `TextTrack[]` | - | Subtitle tracks |
255
+ | `vast` | `VastConfig \| VastConfig[]` | - | VAST ads config |
256
+ | `keyboard` | `boolean` | `true` | Enable keyboard shortcuts |
257
+ | `hotkeys` | `HotkeyConfig` | - | Custom hotkey mappings |
258
+ | `className` | `string` | - | Custom CSS class |
259
+ | `style` | `CSSProperties` | - | Inline styles |
260
+ | `accentColor` | `string` | - | Custom accent color |
261
+ | `theme` | `'dark' \| 'light' \| 'auto'` | `'dark'` | Color theme |
262
+ | `controlsTimeout` | `number` | `3000` | Controls hide timeout (ms) |
263
+ | `doubleClickFullscreen` | `boolean` | `true` | Double-click for fullscreen |
264
+ | `clickToPlay` | `boolean` | `true` | Click to play/pause |
265
+ | `thumbnailPreview` | `ThumbnailConfig` | - | Thumbnail preview config |
266
+
267
+ ## 🎯 Event Handlers
268
+
269
+ | Event | Parameters | Description |
270
+ |-------|------------|-------------|
271
+ | `onPlay` | - | Fired when playback starts |
272
+ | `onPause` | - | Fired when playback pauses |
273
+ | `onEnded` | - | Fired when video ends |
274
+ | `onTimeUpdate` | `time: number` | Fired on time update |
275
+ | `onProgress` | `buffered: number` | Fired on buffer progress |
276
+ | `onVolumeChange` | `volume: number, muted: boolean` | Fired on volume change |
277
+ | `onSeeking` | `time: number` | Fired when seeking |
278
+ | `onSeeked` | `time: number` | Fired after seek completes |
279
+ | `onRateChange` | `rate: number` | Fired on playback rate change |
280
+ | `onQualityChange` | `quality: string` | Fired on quality change |
281
+ | `onFullscreenChange` | `isFullscreen: boolean` | Fired on fullscreen toggle |
282
+ | `onPipChange` | `isPip: boolean` | Fired on PiP toggle |
283
+ | `onError` | `error: MediaError` | Fired on error |
284
+ | `onReady` | - | Fired when player is ready |
285
+ | `onAdStart` | `ad: VastAdInfo` | Fired when ad starts |
286
+ | `onAdEnd` | - | Fired when ad ends |
287
+ | `onAdSkip` | - | Fired when ad is skipped |
288
+ | `onAdError` | `error: Error` | Fired on ad error |
441
289
 
442
290
  ## ⌨️ Keyboard Shortcuts
443
291
 
444
292
  | Key | Action |
445
293
  |-----|--------|
446
- | `Space` / `K` | Toggle play/pause |
447
- | `←` | Rewind 10 seconds |
448
- | `→` | Forward 10 seconds |
294
+ | `Space` | Play/Pause |
295
+ | `M` | Mute/Unmute |
296
+ | `F` | Toggle Fullscreen |
297
+ | `P` | Toggle Picture-in-Picture |
298
+ | `←` | Seek backward 10s |
299
+ | `→` | Seek forward 10s |
300
+ | `Shift + ←` | Seek backward 30s |
301
+ | `Shift + →` | Seek forward 30s |
449
302
  | `↑` | Volume up |
450
303
  | `↓` | Volume down |
451
- | `M` | Toggle mute |
452
- | `F` | Toggle fullscreen |
453
- | `P` | Toggle Picture-in-Picture |
454
- | `C` | Toggle captions |
455
- | `N` | Next track |
456
- | `Shift + N` | Previous track |
457
- | `0-9` | Seek to 0%-90% |
458
- | `Home` | Go to beginning |
459
- | `End` | Go to end |
460
-
461
- ---
462
-
463
- ## 🌍 Localization (i18n)
464
-
465
- ```javascript
466
- const player = new PlexPlayer({
467
- container: '#player',
468
- i18n: {
469
- play: 'Play',
470
- pause: 'Pause',
471
- mute: 'Mute',
472
- unmute: 'Unmute',
473
- fullscreen: 'Fullscreen',
474
- exitFullscreen: 'Exit Fullscreen',
475
- pip: 'Picture-in-Picture',
476
- cast: 'Cast',
477
- settings: 'Settings',
478
- speed: 'Speed',
479
- quality: 'Quality',
480
- subtitles: 'Subtitles',
481
- off: 'Off',
482
- },
483
- });
484
- ```
485
-
486
- ---
487
304
 
488
- ## 📋 Playlist
489
-
490
- ```javascript
491
- const player = new PlexPlayer({
492
- container: '#player',
493
- });
494
-
495
- // Load playlist
496
- player.loadPlaylist([
497
- {
498
- src: 'video1.mp4',
499
- title: 'Episode 1',
500
- poster: 'thumb1.jpg',
501
- duration: 3600,
502
- },
503
- {
504
- src: 'video2.mp4',
505
- title: 'Episode 2',
506
- poster: 'thumb2.jpg',
507
- duration: 3540,
508
- },
509
- ]);
510
-
511
- // Navigation
512
- player.next();
513
- player.previous();
514
- player.playAt(2);
515
- ```
305
+ ## 🎨 Theming
516
306
 
517
- ---
307
+ ### CSS Custom Properties
518
308
 
519
- ## 📝 Subtitles
520
-
521
- ```javascript
522
- const player = new PlexPlayer({
523
- container: '#player',
524
- subtitles: {
525
- default: 'en',
526
- tracks: [
527
- { label: 'English', srclang: 'en', src: 'subs/en.vtt' },
528
- { label: 'Spanish', srclang: 'es', src: 'subs/es.vtt' },
529
- { label: 'French', srclang: 'fr', src: 'subs/fr.vtt' },
530
- ],
531
- style: {
532
- fontSize: '20px',
533
- color: '#ffffff',
534
- background: 'rgba(0, 0, 0, 0.75)',
535
- },
536
- },
537
- });
309
+ ```css
310
+ :root {
311
+ --plex-primary: #e50914;
312
+ --plex-secondary: #ffffff;
313
+ --plex-bg: rgba(0, 0, 0, 0.8);
314
+ --plex-control-bg: rgba(0, 0, 0, 0.7);
315
+ --plex-progress-bg: rgba(255, 255, 255, 0.3);
316
+ --plex-buffered-bg: rgba(255, 255, 255, 0.5);
317
+ --plex-hover: rgba(255, 255, 255, 0.1);
318
+ --plex-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
319
+ --plex-transition: all 0.2s ease;
320
+ }
538
321
  ```
539
322
 
540
- ---
541
-
542
- ## 📊 Browser Support
323
+ ### Custom Styling Example
543
324
 
544
- | Browser | Version |
545
- |---------|---------|
546
- | Chrome | 60+ |
547
- | Firefox | 55+ |
548
- | Safari | 11+ |
549
- | Edge | 79+ |
550
- | Opera | 47+ |
551
- | iOS Safari | 11+ |
552
- | Chrome Android | 60+ |
553
-
554
- ---
555
-
556
- ## 🔧 Development
325
+ ```css
326
+ .my-custom-player {
327
+ --plex-primary: #00bcd4;
328
+ border-radius: 16px;
329
+ overflow: hidden;
330
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
331
+ }
557
332
 
558
- ```bash
559
- # Clone repository
560
- git clone https://github.com/frameset-studio/plex-player.git
561
- cd plex-player
333
+ .my-custom-player .plex-video-player__btn:hover {
334
+ background-color: rgba(0, 188, 212, 0.2);
335
+ }
336
+ ```
562
337
 
563
- # Install dependencies
564
- npm install
338
+ ## 📋 Types
565
339
 
566
- # Start development server
567
- npm run dev
340
+ ```typescript
341
+ interface VideoSource {
342
+ src: string;
343
+ type?: string;
344
+ quality?: string;
345
+ label?: string;
346
+ }
568
347
 
569
- # Build for production
570
- npm run build
348
+ interface TextTrack {
349
+ src: string;
350
+ kind: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
351
+ srclang: string;
352
+ label: string;
353
+ default?: boolean;
354
+ }
571
355
 
572
- # Run tests
573
- npm test
356
+ interface VastConfig {
357
+ url: string;
358
+ skipDelay?: number;
359
+ position?: 'preroll' | 'midroll' | 'postroll';
360
+ midrollTime?: number;
361
+ }
574
362
 
575
- # Lint code
576
- npm run lint
363
+ interface PlexVideoPlayerRef {
364
+ play: () => Promise<void>;
365
+ pause: () => void;
366
+ stop: () => void;
367
+ seek: (time: number) => void;
368
+ setVolume: (volume: number) => void;
369
+ mute: () => void;
370
+ unmute: () => void;
371
+ toggleMute: () => void;
372
+ enterFullscreen: () => Promise<void>;
373
+ exitFullscreen: () => Promise<void>;
374
+ toggleFullscreen: () => Promise<void>;
375
+ enterPip: () => Promise<void>;
376
+ exitPip: () => Promise<void>;
377
+ togglePip: () => Promise<void>;
378
+ setPlaybackRate: (rate: number) => void;
379
+ setQuality: (quality: string) => void;
380
+ getCurrentTime: () => number;
381
+ getDuration: () => number;
382
+ getVolume: () => number;
383
+ isMuted: () => boolean;
384
+ isPlaying: () => boolean;
385
+ isFullscreen: () => boolean;
386
+ isPip: () => boolean;
387
+ getVideoElement: () => HTMLVideoElement | null;
388
+ }
577
389
  ```
578
390
 
579
- ---
391
+ ## 🌐 Browser Support
392
+
393
+ - Chrome 80+
394
+ - Firefox 75+
395
+ - Safari 13+
396
+ - Edge 80+
580
397
 
581
398
  ## 📄 License
582
399
 
583
- MIT © [FRAMESET Studio](https://frameset.dev)
400
+ MIT © [FRAMESET STUDIO](https://frameset.dev)
584
401
 
585
402
  ---
586
403
 
587
404
  <p align="center">
588
- <a href="https://frameset.dev">
589
- <img src="https://frameset.dev/favicon.svg" alt="FRAMESET" width="120" />
590
- </a>
591
- </p>
592
-
593
- <p align="center">
594
- Made with ❤️ by <a href="https://frameset.dev">FRAMESET Studio</a>
595
- </p>
596
-
597
- <p align="center">
598
- <a href="https://www.linkedin.com/company/framesetdev/">LinkedIn</a> •
599
- <a href="https://github.com/deadseti">GitHub</a> •
600
- <a href="https://frameset.dev">Website</a>
405
+ Made with ❤️ by <a href="https://frameset.dev">FRAMESET STUDIO</a>
601
406
  </p>