@frameset/plex-player 1.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 +21 -0
- package/README.md +601 -0
- package/dist/plex-player.cjs.js +879 -0
- package/dist/plex-player.cjs.js.map +1 -0
- package/dist/plex-player.css +1943 -0
- package/dist/plex-player.css.map +1 -0
- package/dist/plex-player.d.ts +457 -0
- package/dist/plex-player.esm.js +873 -0
- package/dist/plex-player.esm.js.map +1 -0
- package/dist/plex-player.js +885 -0
- package/dist/plex-player.js.map +1 -0
- package/dist/plex-player.min.js +9 -0
- package/dist/plex-player.min.js.map +1 -0
- package/dist/react/index.esm.js +257 -0
- package/dist/react/index.esm.js.map +1 -0
- package/dist/react/index.js +265 -0
- package/dist/react/index.js.map +1 -0
- package/dist/styles.tmp.js +1 -0
- package/dist/vue/index.esm.js +283 -0
- package/dist/vue/index.esm.js.map +1 -0
- package/dist/vue/index.js +291 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +132 -0
- package/src/core/index.js +924 -0
- package/src/core/player-core.js +225 -0
- package/src/react/index.jsx +277 -0
- package/src/styles.js +8 -0
- package/src/types/index.d.ts +457 -0
- package/src/vue/index.js +304 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 FRAMESET Studio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
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>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>🎬 Professional HTML5 Video Player for Modern Web Applications</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<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>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<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>
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## ✨ Features
|
|
40
|
+
|
|
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
|
+
---
|
|
57
|
+
|
|
58
|
+
## 📦 Installation
|
|
59
|
+
|
|
60
|
+
### NPM / Yarn / PNPM
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# npm
|
|
64
|
+
npm install @frameset/plex-player
|
|
65
|
+
|
|
66
|
+
# yarn
|
|
67
|
+
yarn add @frameset/plex-player
|
|
68
|
+
|
|
69
|
+
# pnpm
|
|
70
|
+
pnpm add @frameset/plex-player
|
|
71
|
+
```
|
|
72
|
+
|
|
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>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 🚀 Quick Start
|
|
86
|
+
|
|
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';
|
|
137
|
+
|
|
138
|
+
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
|
+
return (
|
|
150
|
+
<PlexPlayerReact
|
|
151
|
+
ref={playerRef}
|
|
152
|
+
src="https://example.com/video.mp4"
|
|
153
|
+
autoplay={false}
|
|
154
|
+
onPlay={handlePlay}
|
|
155
|
+
onTimeUpdate={handleTimeUpdate}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Hooks
|
|
162
|
+
|
|
163
|
+
```jsx
|
|
164
|
+
import {
|
|
165
|
+
PlexPlayerReact,
|
|
166
|
+
PlexPlayerProvider,
|
|
167
|
+
usePlexPlayer,
|
|
168
|
+
usePlexPlayerState,
|
|
169
|
+
usePlexPlayerTime
|
|
170
|
+
} from '@frameset/plex-player/react';
|
|
171
|
+
|
|
172
|
+
function Controls() {
|
|
173
|
+
const player = usePlexPlayer();
|
|
174
|
+
const state = usePlexPlayerState();
|
|
175
|
+
const { current, duration } = usePlexPlayerTime();
|
|
176
|
+
|
|
177
|
+
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>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
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>
|
|
236
|
+
```
|
|
237
|
+
|
|
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>
|
|
252
|
+
```
|
|
253
|
+
|
|
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
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
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 */ });
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
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
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
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 |
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## 📺 Chromecast
|
|
375
|
+
|
|
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();
|
|
386
|
+
|
|
387
|
+
// Listen to cast events
|
|
388
|
+
player.on('castconnected', ({ deviceName }) => {
|
|
389
|
+
console.log('Connected to:', deviceName);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
player.on('castdisconnected', () => {
|
|
393
|
+
console.log('Disconnected from Chromecast');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
player.on('caststatechange', ({ state }) => {
|
|
397
|
+
console.log('Cast state:', state);
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Requirements
|
|
402
|
+
|
|
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 */
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
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
|
+
---
|
|
441
|
+
|
|
442
|
+
## ⌨️ Keyboard Shortcuts
|
|
443
|
+
|
|
444
|
+
| Key | Action |
|
|
445
|
+
|-----|--------|
|
|
446
|
+
| `Space` / `K` | Toggle play/pause |
|
|
447
|
+
| `←` | Rewind 10 seconds |
|
|
448
|
+
| `→` | Forward 10 seconds |
|
|
449
|
+
| `↑` | Volume up |
|
|
450
|
+
| `↓` | 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
|
+
|
|
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
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
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
|
+
});
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
## 📊 Browser Support
|
|
543
|
+
|
|
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
|
|
557
|
+
|
|
558
|
+
```bash
|
|
559
|
+
# Clone repository
|
|
560
|
+
git clone https://github.com/frameset-studio/plex-player.git
|
|
561
|
+
cd plex-player
|
|
562
|
+
|
|
563
|
+
# Install dependencies
|
|
564
|
+
npm install
|
|
565
|
+
|
|
566
|
+
# Start development server
|
|
567
|
+
npm run dev
|
|
568
|
+
|
|
569
|
+
# Build for production
|
|
570
|
+
npm run build
|
|
571
|
+
|
|
572
|
+
# Run tests
|
|
573
|
+
npm test
|
|
574
|
+
|
|
575
|
+
# Lint code
|
|
576
|
+
npm run lint
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## 📄 License
|
|
582
|
+
|
|
583
|
+
MIT © [FRAMESET Studio](https://frameset.dev)
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
<p align="center">
|
|
588
|
+
<a href="https://frameset.dev">
|
|
589
|
+
<img src="https://frameset.dev/logo-dark.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>
|
|
601
|
+
</p>
|