@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/LICENSE +1 -1
- package/README.md +310 -505
- package/dist/index.d.ts +438 -0
- package/dist/index.esm.js +2 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +1 -0
- package/package.json +66 -104
- package/dist/plex-player.cjs.js +0 -903
- package/dist/plex-player.cjs.js.map +0 -1
- package/dist/plex-player.css +0 -1902
- package/dist/plex-player.css.map +0 -1
- package/dist/plex-player.d.ts +0 -457
- package/dist/plex-player.esm.js +0 -897
- package/dist/plex-player.esm.js.map +0 -1
- package/dist/plex-player.js +0 -909
- package/dist/plex-player.js.map +0 -1
- package/dist/plex-player.min.js +0 -9
- package/dist/plex-player.min.js.map +0 -1
- package/dist/react/index.esm.js +0 -257
- package/dist/react/index.esm.js.map +0 -1
- package/dist/react/index.js +0 -265
- package/dist/react/index.js.map +0 -1
- package/dist/styles.tmp.js +0 -1
- package/dist/vue/index.esm.js +0 -283
- package/dist/vue/index.esm.js.map +0 -1
- package/dist/vue/index.js +0 -291
- package/dist/vue/index.js.map +0 -1
- package/src/core/index.js +0 -950
- package/src/core/player-core.js +0 -225
- package/src/react/index.jsx +0 -277
- package/src/styles.js +0 -8
- package/src/types/index.d.ts +0 -457
- package/src/vue/index.js +0 -304
package/README.md
CHANGED
|
@@ -1,601 +1,406 @@
|
|
|
1
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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="
|
|
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
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
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
|
-
|
|
41
|
+
```bash
|
|
67
42
|
yarn add @frameset/plex-player
|
|
68
|
-
|
|
69
|
-
# pnpm
|
|
70
|
-
pnpm add @frameset/plex-player
|
|
71
43
|
```
|
|
72
44
|
|
|
73
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
<
|
|
151
|
-
ref={playerRef}
|
|
57
|
+
<PlexVideoPlayer
|
|
152
58
|
src="https://example.com/video.mp4"
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
59
|
+
poster="https://example.com/poster.jpg"
|
|
60
|
+
width="100%"
|
|
61
|
+
height="auto"
|
|
156
62
|
/>
|
|
157
63
|
);
|
|
158
64
|
}
|
|
159
65
|
```
|
|
160
66
|
|
|
161
|
-
|
|
67
|
+
## 📖 Documentation
|
|
162
68
|
|
|
163
|
-
|
|
164
|
-
import {
|
|
165
|
-
PlexPlayerReact,
|
|
166
|
-
PlexPlayerProvider,
|
|
167
|
-
usePlexPlayer,
|
|
168
|
-
usePlexPlayerState,
|
|
169
|
-
usePlexPlayerTime
|
|
170
|
-
} from '@frameset/plex-player/react';
|
|
69
|
+
### Basic Usage
|
|
171
70
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
###
|
|
239
|
-
|
|
240
|
-
```
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
###
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
162
|
+
function VideoPlayer() {
|
|
163
|
+
const playerRef = useRef<PlexVideoPlayerRef>(null);
|
|
375
164
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
});
|
|
173
|
+
const handleSeek = () => {
|
|
174
|
+
playerRef.current?.seek(30); // Seek to 30 seconds
|
|
175
|
+
};
|
|
391
176
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
177
|
+
const handleFullscreen = () => {
|
|
178
|
+
playerRef.current?.toggleFullscreen();
|
|
179
|
+
};
|
|
395
180
|
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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`
|
|
447
|
-
|
|
|
448
|
-
|
|
|
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
|
-
##
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
333
|
+
.my-custom-player .plex-video-player__btn:hover {
|
|
334
|
+
background-color: rgba(0, 188, 212, 0.2);
|
|
335
|
+
}
|
|
336
|
+
```
|
|
562
337
|
|
|
563
|
-
|
|
564
|
-
npm install
|
|
338
|
+
## 📋 Types
|
|
565
339
|
|
|
566
|
-
|
|
567
|
-
|
|
340
|
+
```typescript
|
|
341
|
+
interface VideoSource {
|
|
342
|
+
src: string;
|
|
343
|
+
type?: string;
|
|
344
|
+
quality?: string;
|
|
345
|
+
label?: string;
|
|
346
|
+
}
|
|
568
347
|
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
573
|
-
|
|
356
|
+
interface VastConfig {
|
|
357
|
+
url: string;
|
|
358
|
+
skipDelay?: number;
|
|
359
|
+
position?: 'preroll' | 'midroll' | 'postroll';
|
|
360
|
+
midrollTime?: number;
|
|
361
|
+
}
|
|
574
362
|
|
|
575
|
-
|
|
576
|
-
|
|
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
|
|
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>
|