@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.
@@ -1,909 +0,0 @@
1
- /*!
2
- * @frameset/plex-player v1.0.5
3
- * Professional video player with VAST ads, Chromecast, PiP, subtitles, playlists and more. Built by FRAMESET Studio.
4
- * (c) 2026 FRAMESET Studio
5
- * Released under the MIT License
6
- * https://frameset.dev/plex-player
7
- */
8
- (function (global, factory) {
9
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
10
- typeof define === 'function' && define.amd ? define(['exports'], factory) :
11
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.PlexPlayer = {}));
12
- })(this, (function (exports) { 'use strict';
13
-
14
- /**
15
- * @frameset/plex-player - Core Entry Point
16
- * Professional Video Player by FRAMESET Studio
17
- * https://frameset.dev
18
- */
19
-
20
- /**
21
- * Utility Functions
22
- */
23
- const Utils = {
24
- formatTime(seconds) {
25
- if (isNaN(seconds) || !isFinite(seconds)) return '0:00';
26
- seconds = Math.max(0, Math.floor(seconds));
27
- const hours = Math.floor(seconds / 3600);
28
- const minutes = Math.floor(seconds % 3600 / 60);
29
- const secs = seconds % 60;
30
- if (hours > 0) {
31
- return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
32
- }
33
- return `${minutes}:${secs.toString().padStart(2, '0')}`;
34
- },
35
- clamp(num, min, max) {
36
- return Math.min(Math.max(num, min), max);
37
- },
38
- throttle(fn, delay) {
39
- let lastCall = 0;
40
- return function (...args) {
41
- const now = Date.now();
42
- if (now - lastCall >= delay) {
43
- lastCall = now;
44
- return fn.apply(this, args);
45
- }
46
- };
47
- },
48
- debounce(fn, delay) {
49
- let timeoutId;
50
- return function (...args) {
51
- clearTimeout(timeoutId);
52
- timeoutId = setTimeout(() => fn.apply(this, args), delay);
53
- };
54
- }
55
- };
56
-
57
- /**
58
- * PlexPlayer - Main Player Class
59
- * A complete, standalone video player
60
- */
61
- class PlexPlayer {
62
- constructor(options = {}) {
63
- // Validate container
64
- if (!options.container) {
65
- throw new Error('PlexPlayer: container option is required');
66
- }
67
-
68
- // Get container element
69
- this.container = typeof options.container === 'string' ? document.querySelector(options.container) : options.container;
70
- if (!this.container) {
71
- throw new Error('PlexPlayer: container element not found');
72
- }
73
-
74
- // Store options with defaults
75
- this.options = {
76
- autoplay: false,
77
- muted: false,
78
- loop: false,
79
- volume: 1,
80
- poster: '',
81
- preload: 'metadata',
82
- keyboard: true,
83
- touch: true,
84
- pip: true,
85
- cast: true,
86
- fullscreen: true,
87
- controlsHideDelay: 3000,
88
- theme: {},
89
- subtitles: {},
90
- ads: {
91
- enabled: false
92
- },
93
- i18n: {},
94
- ...options
95
- };
96
-
97
- // Internationalization with defaults
98
- this.i18n = {
99
- play: 'Play',
100
- pause: 'Pause',
101
- mute: 'Mute',
102
- unmute: 'Unmute',
103
- fullscreen: 'Fullscreen',
104
- exitFullscreen: 'Exit Fullscreen',
105
- pip: 'Picture-in-Picture',
106
- exitPip: 'Exit Picture-in-Picture',
107
- settings: 'Settings',
108
- speed: 'Speed',
109
- quality: 'Quality',
110
- subtitles: 'Subtitles',
111
- off: 'Off',
112
- normal: 'Normal',
113
- captions: 'Captions',
114
- audio: 'Audio',
115
- volume: 'Volume',
116
- seekForward: 'Seek Forward',
117
- seekBackward: 'Seek Backward',
118
- ...this.options.i18n
119
- };
120
-
121
- // Internal state
122
- this._eventListeners = new Map();
123
- this._state = {
124
- playing: false,
125
- paused: true,
126
- muted: this.options.muted,
127
- volume: this.options.volume,
128
- currentTime: 0,
129
- duration: 0,
130
- buffered: 0,
131
- fullscreen: false,
132
- pip: false,
133
- playbackRate: 1
134
- };
135
-
136
- // Playlist state
137
- this._playlist = [];
138
- this._currentIndex = 0;
139
-
140
- // Controls timeout
141
- this._controlsTimeout = null;
142
-
143
- // Initialize player
144
- this._init();
145
- }
146
- _init() {
147
- // Add player class
148
- this.container.classList.add('plex-player');
149
- // Show controls initially
150
- this.container.classList.add('plex-controls-visible');
151
-
152
- // Apply theme
153
- this._applyTheme();
154
-
155
- // Create DOM structure
156
- this._createDOM();
157
-
158
- // Bind events
159
- this._bindEvents();
160
-
161
- // Initialize features
162
- if (this.options.keyboard) this._initKeyboard();
163
- if (this.options.touch) this._initTouch();
164
- if (this.options.pip) this._initPiP();
165
- if (this.options.cast) this._initCast();
166
-
167
- // Emit ready event
168
- setTimeout(() => this._emit('ready'), 0);
169
- }
170
- _applyTheme() {
171
- const {
172
- theme
173
- } = this.options;
174
- if (theme.primary) this.container.style.setProperty('--plex-primary', theme.primary);
175
- if (theme.background) this.container.style.setProperty('--plex-bg', theme.background);
176
- if (theme.text) this.container.style.setProperty('--plex-text', theme.text);
177
- if (theme.borderRadius) this.container.style.setProperty('--plex-border-radius', theme.borderRadius);
178
- }
179
- _createDOM() {
180
- // Video element
181
- this.video = document.createElement('video');
182
- this.video.className = 'plex-video';
183
- this.video.setAttribute('playsinline', '');
184
- this.video.setAttribute('webkit-playsinline', '');
185
- this.video.preload = this.options.preload;
186
- this.video.muted = this.options.muted;
187
- this.video.volume = this.options.volume;
188
- this.video.loop = this.options.loop;
189
- if (this.options.poster) this.video.poster = this.options.poster;
190
- this.container.appendChild(this.video);
191
-
192
- // Controls overlay
193
- this.controls = document.createElement('div');
194
- this.controls.className = 'plex-controls';
195
- this.controls.innerHTML = this._getControlsHTML();
196
- this.container.appendChild(this.controls);
197
-
198
- // Cache control elements
199
- this._cacheElements();
200
-
201
- // Big play button
202
- this.bigPlayBtn = document.createElement('div');
203
- this.bigPlayBtn.className = 'plex-big-play';
204
- this.bigPlayBtn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>`;
205
- this.container.appendChild(this.bigPlayBtn);
206
-
207
- // Loading spinner
208
- this.loader = document.createElement('div');
209
- this.loader.className = 'plex-loader';
210
- this.loader.innerHTML = '<div class="plex-spinner"></div>';
211
- this.container.appendChild(this.loader);
212
- }
213
- _getControlsHTML() {
214
- const i18n = this.i18n;
215
- return `
216
- <div class="plex-progress-container">
217
- <div class="plex-progress-bar">
218
- <div class="plex-progress-buffered"></div>
219
- <div class="plex-progress-played"></div>
220
- <div class="plex-progress-handle"></div>
221
- </div>
222
- <div class="plex-progress-preview">
223
- <div class="plex-preview-time">0:00</div>
224
- </div>
225
- </div>
226
- <div class="plex-controls-bar">
227
- <div class="plex-controls-left">
228
- <button class="plex-btn plex-play-btn" title="${i18n.play || 'Play'}">
229
- <svg class="plex-icon-play" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
230
- <svg class="plex-icon-pause" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
231
- </button>
232
- <button class="plex-btn plex-prev-btn" title="${i18n.previous || 'Previous'}">
233
- <svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
234
- </button>
235
- <button class="plex-btn plex-next-btn" title="${i18n.next || 'Next'}">
236
- <svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
237
- </button>
238
- <div class="plex-volume-container">
239
- <button class="plex-btn plex-volume-btn" title="${i18n.mute || 'Mute'}">
240
- <svg class="plex-icon-volume-high" viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>
241
- <svg class="plex-icon-volume-low" viewBox="0 0 24 24"><path d="M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z"/></svg>
242
- <svg class="plex-icon-volume-mute" viewBox="0 0 24 24"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>
243
- </button>
244
- <div class="plex-volume-slider">
245
- <div class="plex-volume-track">
246
- <div class="plex-volume-level"></div>
247
- <div class="plex-volume-handle"></div>
248
- </div>
249
- </div>
250
- </div>
251
- <div class="plex-time">
252
- <span class="plex-time-current">0:00</span>
253
- <span class="plex-time-separator">/</span>
254
- <span class="plex-time-duration">0:00</span>
255
- </div>
256
- </div>
257
- <div class="plex-controls-right">
258
- <button class="plex-btn plex-settings-btn" title="${i18n.settings || 'Settings'}">
259
- <svg viewBox="0 0 24 24"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>
260
- </button>
261
- ${this.options.pip ? `
262
- <button class="plex-btn plex-pip-btn" title="${i18n.pip || 'Picture-in-Picture'}">
263
- <svg viewBox="0 0 24 24"><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z"/></svg>
264
- </button>
265
- ` : ''}
266
- ${this.options.cast ? `
267
- <button class="plex-btn plex-cast-btn" title="${i18n.cast || 'Cast'}">
268
- <svg viewBox="0 0 24 24"><path d="M21 3H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm0-4v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11z"/></svg>
269
- </button>
270
- ` : ''}
271
- ${this.options.fullscreen ? `
272
- <button class="plex-btn plex-fullscreen-btn" title="${i18n.fullscreen || 'Fullscreen'}">
273
- <svg class="plex-icon-fullscreen" viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>
274
- <svg class="plex-icon-fullscreen-exit" viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>
275
- </button>
276
- ` : ''}
277
- </div>
278
- </div>
279
- `;
280
- }
281
- _cacheElements() {
282
- const $ = sel => this.controls.querySelector(sel);
283
- this.els = {
284
- progressContainer: $('.plex-progress-container'),
285
- progressBar: $('.plex-progress-bar'),
286
- progressBuffered: $('.plex-progress-buffered'),
287
- progressPlayed: $('.plex-progress-played'),
288
- progressHandle: $('.plex-progress-handle'),
289
- progressPreview: $('.plex-progress-preview'),
290
- previewTime: $('.plex-preview-time'),
291
- playBtn: $('.plex-play-btn'),
292
- prevBtn: $('.plex-prev-btn'),
293
- nextBtn: $('.plex-next-btn'),
294
- volumeBtn: $('.plex-volume-btn'),
295
- volumeSlider: $('.plex-volume-slider'),
296
- volumeTrack: $('.plex-volume-track'),
297
- volumeLevel: $('.plex-volume-level'),
298
- volumeHandle: $('.plex-volume-handle'),
299
- timeCurrent: $('.plex-time-current'),
300
- timeDuration: $('.plex-time-duration'),
301
- settingsBtn: $('.plex-settings-btn'),
302
- pipBtn: $('.plex-pip-btn'),
303
- castBtn: $('.plex-cast-btn'),
304
- fullscreenBtn: $('.plex-fullscreen-btn')
305
- };
306
- }
307
- _bindEvents() {
308
- // Video events
309
- this.video.addEventListener('play', () => this._onPlay());
310
- this.video.addEventListener('pause', () => this._onPause());
311
- this.video.addEventListener('ended', () => this._onEnded());
312
- this.video.addEventListener('timeupdate', () => this._onTimeUpdate());
313
- this.video.addEventListener('progress', () => this._onProgress());
314
- this.video.addEventListener('loadedmetadata', () => this._onLoadedMetadata());
315
- this.video.addEventListener('volumechange', () => this._onVolumeChange());
316
- this.video.addEventListener('waiting', () => this._onWaiting());
317
- this.video.addEventListener('canplay', () => this._onCanPlay());
318
- this.video.addEventListener('error', e => this._onError(e));
319
-
320
- // Control events
321
- this.bigPlayBtn.addEventListener('click', () => this.togglePlay());
322
- this.video.addEventListener('click', () => this.togglePlay());
323
- this.els.playBtn?.addEventListener('click', () => this.togglePlay());
324
- this.els.prevBtn?.addEventListener('click', () => this.previous());
325
- this.els.nextBtn?.addEventListener('click', () => this.next());
326
- this.els.volumeBtn?.addEventListener('click', () => this.toggleMute());
327
- this.els.pipBtn?.addEventListener('click', () => this.togglePiP());
328
- this.els.fullscreenBtn?.addEventListener('click', () => this.toggleFullscreen());
329
-
330
- // Progress bar
331
- this._bindProgressEvents();
332
-
333
- // Volume slider
334
- this._bindVolumeEvents();
335
-
336
- // Controls visibility
337
- this._bindControlsVisibility();
338
-
339
- // Fullscreen change
340
- document.addEventListener('fullscreenchange', () => this._onFullscreenChange());
341
- document.addEventListener('webkitfullscreenchange', () => this._onFullscreenChange());
342
- }
343
- _bindProgressEvents() {
344
- const progress = this.els.progressContainer;
345
- if (!progress) return;
346
- let isDragging = false;
347
- const seek = e => {
348
- const rect = this.els.progressBar.getBoundingClientRect();
349
- const percent = Utils.clamp((e.clientX - rect.left) / rect.width, 0, 1);
350
- this.seekPercent(percent * 100);
351
- };
352
- const updatePreview = e => {
353
- const rect = this.els.progressBar.getBoundingClientRect();
354
- const percent = Utils.clamp((e.clientX - rect.left) / rect.width, 0, 1);
355
- const time = percent * this.video.duration;
356
- if (this.els.previewTime) {
357
- this.els.previewTime.textContent = Utils.formatTime(time);
358
- }
359
- if (this.els.progressPreview) {
360
- const left = Utils.clamp(e.clientX - rect.left, 30, rect.width - 30);
361
- this.els.progressPreview.style.left = `${left}px`;
362
- }
363
- };
364
- progress.addEventListener('mousedown', e => {
365
- isDragging = true;
366
- seek(e);
367
- });
368
- progress.addEventListener('mousemove', e => {
369
- updatePreview(e);
370
- if (isDragging) seek(e);
371
- });
372
- document.addEventListener('mouseup', () => {
373
- isDragging = false;
374
- });
375
- document.addEventListener('mousemove', e => {
376
- if (isDragging) seek(e);
377
- });
378
- }
379
- _bindVolumeEvents() {
380
- const volumeTrack = this.els.volumeTrack;
381
- if (!volumeTrack) return;
382
- let isDragging = false;
383
- const setVolume = e => {
384
- const rect = volumeTrack.getBoundingClientRect();
385
- const percent = Utils.clamp((e.clientX - rect.left) / rect.width, 0, 1);
386
- this.setVolume(percent);
387
- };
388
- volumeTrack.addEventListener('mousedown', e => {
389
- isDragging = true;
390
- setVolume(e);
391
- });
392
- document.addEventListener('mousemove', e => {
393
- if (isDragging) setVolume(e);
394
- });
395
- document.addEventListener('mouseup', () => {
396
- isDragging = false;
397
- });
398
- }
399
- _bindControlsVisibility() {
400
- const showControls = () => {
401
- this.container.classList.add('plex-controls-visible');
402
- clearTimeout(this._controlsTimeout);
403
- if (this._state.playing) {
404
- this._controlsTimeout = setTimeout(() => {
405
- this.container.classList.remove('plex-controls-visible');
406
- }, this.options.controlsHideDelay);
407
- }
408
- };
409
- this.container.addEventListener('mousemove', showControls);
410
- this.container.addEventListener('mouseenter', showControls);
411
- this.container.addEventListener('mouseleave', () => {
412
- if (this._state.playing) {
413
- this.container.classList.remove('plex-controls-visible');
414
- }
415
- });
416
-
417
- // Always show when paused
418
- this.video.addEventListener('pause', showControls);
419
- }
420
-
421
- // Event handlers
422
- _onPlay() {
423
- this._state.playing = true;
424
- this._state.paused = false;
425
- this.container.classList.add('plex-playing');
426
- this.container.classList.remove('plex-paused');
427
- this.bigPlayBtn.style.display = 'none';
428
- this._emit('play');
429
- }
430
- _onPause() {
431
- this._state.playing = false;
432
- this._state.paused = true;
433
- this.container.classList.remove('plex-playing');
434
- this.container.classList.add('plex-paused');
435
- this.bigPlayBtn.style.display = '';
436
- this._emit('pause');
437
- }
438
- _onEnded() {
439
- this._state.playing = false;
440
- this.container.classList.remove('plex-playing');
441
-
442
- // Auto-play next if playlist
443
- if (this._playlist.length > 0 && this._currentIndex < this._playlist.length - 1) {
444
- this.next();
445
- } else {
446
- this._emit('ended');
447
- }
448
- }
449
- _onTimeUpdate() {
450
- this._state.currentTime = this.video.currentTime;
451
- const percent = this.video.currentTime / this.video.duration * 100;
452
- if (this.els.progressPlayed) {
453
- this.els.progressPlayed.style.width = `${percent}%`;
454
- }
455
- if (this.els.progressHandle) {
456
- this.els.progressHandle.style.left = `${percent}%`;
457
- }
458
- if (this.els.timeCurrent) {
459
- this.els.timeCurrent.textContent = Utils.formatTime(this.video.currentTime);
460
- }
461
- this._emit('timeupdate', {
462
- currentTime: this.video.currentTime,
463
- duration: this.video.duration
464
- });
465
- }
466
- _onProgress() {
467
- const buffered = this.video.buffered;
468
- if (buffered.length > 0) {
469
- const percent = buffered.end(buffered.length - 1) / this.video.duration * 100;
470
- this._state.buffered = percent;
471
- if (this.els.progressBuffered) {
472
- this.els.progressBuffered.style.width = `${percent}%`;
473
- }
474
- this._emit('progress', {
475
- buffered: percent
476
- });
477
- }
478
- }
479
- _onLoadedMetadata() {
480
- this._state.duration = this.video.duration;
481
- if (this.els.timeDuration) {
482
- this.els.timeDuration.textContent = Utils.formatTime(this.video.duration);
483
- }
484
- this._emit('loadedmetadata', {
485
- duration: this.video.duration
486
- });
487
- }
488
- _onVolumeChange() {
489
- this._state.volume = this.video.volume;
490
- this._state.muted = this.video.muted;
491
- const volume = this.video.muted ? 0 : this.video.volume;
492
-
493
- // Update volume UI
494
- if (this.els.volumeLevel) {
495
- this.els.volumeLevel.style.width = `${volume * 100}%`;
496
- }
497
- if (this.els.volumeHandle) {
498
- this.els.volumeHandle.style.left = `${volume * 100}%`;
499
- }
500
-
501
- // Update icon
502
- this.container.classList.remove('plex-muted', 'plex-volume-low');
503
- if (this.video.muted || volume === 0) {
504
- this.container.classList.add('plex-muted');
505
- } else if (volume < 0.5) {
506
- this.container.classList.add('plex-volume-low');
507
- }
508
- this._emit('volumechange', {
509
- volume: this.video.volume,
510
- muted: this.video.muted
511
- });
512
- }
513
- _onWaiting() {
514
- this.container.classList.add('plex-loading');
515
- this._emit('waiting');
516
- }
517
- _onCanPlay() {
518
- this.container.classList.remove('plex-loading');
519
- this._emit('canplay');
520
- }
521
- _onError(e) {
522
- this._emit('error', {
523
- code: this.video.error?.code,
524
- message: this.video.error?.message
525
- });
526
- }
527
- _onFullscreenChange() {
528
- const isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
529
- this._state.fullscreen = isFullscreen;
530
- if (isFullscreen) {
531
- this.container.classList.add('plex-fullscreen');
532
- } else {
533
- this.container.classList.remove('plex-fullscreen');
534
- }
535
- this._emit('fullscreenchange', {
536
- isFullscreen
537
- });
538
- }
539
-
540
- // Keyboard support
541
- _initKeyboard() {
542
- this._keyHandler = e => {
543
- if (!this.container.contains(document.activeElement) && document.activeElement !== document.body) return;
544
- switch (e.key) {
545
- case ' ':
546
- case 'k':
547
- e.preventDefault();
548
- this.togglePlay();
549
- break;
550
- case 'ArrowLeft':
551
- e.preventDefault();
552
- this.seek(this.video.currentTime - 10);
553
- break;
554
- case 'ArrowRight':
555
- e.preventDefault();
556
- this.seek(this.video.currentTime + 10);
557
- break;
558
- case 'ArrowUp':
559
- e.preventDefault();
560
- this.setVolume(Math.min(1, this.video.volume + 0.1));
561
- break;
562
- case 'ArrowDown':
563
- e.preventDefault();
564
- this.setVolume(Math.max(0, this.video.volume - 0.1));
565
- break;
566
- case 'm':
567
- this.toggleMute();
568
- break;
569
- case 'f':
570
- this.toggleFullscreen();
571
- break;
572
- case 'p':
573
- this.togglePiP();
574
- break;
575
- case 'n':
576
- if (e.shiftKey) {
577
- this.previous();
578
- } else {
579
- this.next();
580
- }
581
- break;
582
- }
583
- };
584
- document.addEventListener('keydown', this._keyHandler);
585
- }
586
-
587
- // Touch support
588
- _initTouch() {
589
- let touchStartX = 0;
590
- let touchStartY = 0;
591
- let touchStartTime = 0;
592
- this.video.addEventListener('touchstart', e => {
593
- touchStartX = e.touches[0].clientX;
594
- touchStartY = e.touches[0].clientY;
595
- touchStartTime = Date.now();
596
- });
597
- this.video.addEventListener('touchend', e => {
598
- const touchEndX = e.changedTouches[0].clientX;
599
- const touchEndY = e.changedTouches[0].clientY;
600
- const touchDuration = Date.now() - touchStartTime;
601
- const deltaX = touchEndX - touchStartX;
602
- const deltaY = touchEndY - touchStartY;
603
-
604
- // Tap to toggle play (if quick tap)
605
- if (Math.abs(deltaX) < 30 && Math.abs(deltaY) < 30 && touchDuration < 300) {
606
- this.togglePlay();
607
- }
608
- // Swipe to seek
609
- else if (Math.abs(deltaX) > 50 && Math.abs(deltaY) < 30) {
610
- if (deltaX > 0) {
611
- this.seek(this.video.currentTime + 10);
612
- } else {
613
- this.seek(this.video.currentTime - 10);
614
- }
615
- }
616
- });
617
- }
618
-
619
- // PiP support
620
- _initPiP() {
621
- if (!document.pictureInPictureEnabled) {
622
- if (this.els.pipBtn) {
623
- this.els.pipBtn.style.display = 'none';
624
- }
625
- return;
626
- }
627
- this.video.addEventListener('enterpictureinpicture', () => {
628
- this._state.pip = true;
629
- this.container.classList.add('plex-pip');
630
- this._emit('enterpip');
631
- });
632
- this.video.addEventListener('leavepictureinpicture', () => {
633
- this._state.pip = false;
634
- this.container.classList.remove('plex-pip');
635
- this._emit('leavepip');
636
- });
637
- }
638
-
639
- // Cast support
640
- _initCast() {
641
- // Chromecast requires HTTPS and specific browser
642
- const isChrome = /Chrome/.test(navigator.userAgent) && !/Edge/.test(navigator.userAgent);
643
- const isHTTPS = location.protocol === 'https:' || location.hostname === 'localhost';
644
- if (!isChrome || !isHTTPS) {
645
- if (this.els.castBtn) {
646
- this.els.castBtn.style.display = 'none';
647
- }
648
- return;
649
- }
650
-
651
- // Load Cast SDK if available
652
- if (window.chrome && window.chrome.cast) {
653
- this._initCastApi();
654
- } else {
655
- window['__onGCastApiAvailable'] = isAvailable => {
656
- if (isAvailable) this._initCastApi();
657
- };
658
- }
659
- }
660
- _initCastApi() {
661
- if (this.els.castBtn) {
662
- this.els.castBtn.addEventListener('click', () => {
663
- if (window.cast && window.cast.framework) {
664
- const context = cast.framework.CastContext.getInstance();
665
- context.requestSession();
666
- }
667
- });
668
- }
669
- }
670
-
671
- // Event system
672
- on(event, callback) {
673
- if (!this._eventListeners.has(event)) {
674
- this._eventListeners.set(event, new Set());
675
- }
676
- this._eventListeners.get(event).add(callback);
677
- return this;
678
- }
679
- off(event, callback) {
680
- if (this._eventListeners.has(event)) {
681
- this._eventListeners.get(event).delete(callback);
682
- }
683
- return this;
684
- }
685
- _emit(event, data = {}) {
686
- if (this._eventListeners.has(event)) {
687
- this._eventListeners.get(event).forEach(callback => {
688
- try {
689
- callback(data);
690
- } catch (e) {
691
- console.error(`PlexPlayer: Error in ${event} handler:`, e);
692
- }
693
- });
694
- }
695
- }
696
-
697
- // Public API
698
- load(src, poster) {
699
- this.video.src = src;
700
- if (poster) this.video.poster = poster;
701
- this.video.load();
702
- if (this.options.autoplay) {
703
- this.play();
704
- }
705
- return this;
706
- }
707
- loadPlaylist(items) {
708
- this._playlist = items.map((item, index) => ({
709
- ...item,
710
- index
711
- }));
712
- this._currentIndex = 0;
713
- if (this._playlist.length > 0) {
714
- const first = this._playlist[0];
715
- this.load(first.src, first.poster);
716
- this._emit('playlistload', {
717
- playlist: this._playlist
718
- });
719
- }
720
- return this;
721
- }
722
- play() {
723
- return this.video.play();
724
- }
725
- pause() {
726
- this.video.pause();
727
- return this;
728
- }
729
- togglePlay() {
730
- if (this.video.paused) {
731
- this.play();
732
- } else {
733
- this.pause();
734
- }
735
- return this;
736
- }
737
- seek(time) {
738
- this.video.currentTime = Utils.clamp(time, 0, this.video.duration || 0);
739
- return this;
740
- }
741
- seekPercent(percent) {
742
- const time = percent / 100 * this.video.duration;
743
- return this.seek(time);
744
- }
745
- setVolume(level) {
746
- this.video.volume = Utils.clamp(level, 0, 1);
747
- if (this.video.muted && level > 0) {
748
- this.video.muted = false;
749
- }
750
- return this;
751
- }
752
- getVolume() {
753
- return this.video.volume;
754
- }
755
- mute() {
756
- this.video.muted = true;
757
- return this;
758
- }
759
- unmute() {
760
- this.video.muted = false;
761
- return this;
762
- }
763
- toggleMute() {
764
- this.video.muted = !this.video.muted;
765
- return this;
766
- }
767
- setPlaybackRate(rate) {
768
- this.video.playbackRate = rate;
769
- this._state.playbackRate = rate;
770
- this._emit('ratechange', {
771
- rate
772
- });
773
- return this;
774
- }
775
- getPlaybackRate() {
776
- return this.video.playbackRate;
777
- }
778
- enterFullscreen() {
779
- const el = this.container;
780
- if (el.requestFullscreen) {
781
- return el.requestFullscreen();
782
- } else if (el.webkitRequestFullscreen) {
783
- return el.webkitRequestFullscreen();
784
- }
785
- }
786
- exitFullscreen() {
787
- if (document.exitFullscreen) {
788
- return document.exitFullscreen();
789
- } else if (document.webkitExitFullscreen) {
790
- return document.webkitExitFullscreen();
791
- }
792
- }
793
- toggleFullscreen() {
794
- if (this._state.fullscreen) {
795
- this.exitFullscreen();
796
- } else {
797
- this.enterFullscreen();
798
- }
799
- return this;
800
- }
801
- enterPiP() {
802
- if (document.pictureInPictureEnabled && this.video !== document.pictureInPictureElement) {
803
- return this.video.requestPictureInPicture();
804
- }
805
- }
806
- exitPiP() {
807
- if (document.pictureInPictureElement) {
808
- return document.exitPictureInPicture();
809
- }
810
- }
811
- togglePiP() {
812
- if (this._state.pip) {
813
- this.exitPiP();
814
- } else {
815
- this.enterPiP();
816
- }
817
- return this;
818
- }
819
- next() {
820
- if (this._playlist.length > 0 && this._currentIndex < this._playlist.length - 1) {
821
- this._currentIndex++;
822
- const item = this._playlist[this._currentIndex];
823
- this.load(item.src, item.poster);
824
- this.play();
825
- this._emit('trackchange', {
826
- index: this._currentIndex,
827
- item
828
- });
829
- }
830
- return this;
831
- }
832
- previous() {
833
- if (this._playlist.length > 0 && this._currentIndex > 0) {
834
- this._currentIndex--;
835
- const item = this._playlist[this._currentIndex];
836
- this.load(item.src, item.poster);
837
- this.play();
838
- this._emit('trackchange', {
839
- index: this._currentIndex,
840
- item
841
- });
842
- }
843
- return this;
844
- }
845
- playAt(index) {
846
- if (this._playlist.length > 0 && index >= 0 && index < this._playlist.length) {
847
- this._currentIndex = index;
848
- const item = this._playlist[this._currentIndex];
849
- this.load(item.src, item.poster);
850
- this.play();
851
- this._emit('trackchange', {
852
- index: this._currentIndex,
853
- item
854
- });
855
- }
856
- return this;
857
- }
858
- getState() {
859
- return {
860
- currentTime: this.video.currentTime,
861
- duration: this.video.duration || 0,
862
- volume: this.video.volume,
863
- muted: this.video.muted,
864
- isPlaying: !this.video.paused && !this.video.ended,
865
- isPaused: this.video.paused,
866
- isFullscreen: this._state.fullscreen,
867
- isPiP: this._state.pip,
868
- playbackRate: this.video.playbackRate,
869
- currentTrack: this._currentIndex,
870
- totalTracks: this._playlist.length
871
- };
872
- }
873
- getVideo() {
874
- return this.video;
875
- }
876
- destroy() {
877
- // Clear timeouts
878
- clearTimeout(this._controlsTimeout);
879
-
880
- // Remove keyboard listener
881
- if (this._keyHandler) {
882
- document.removeEventListener('keydown', this._keyHandler);
883
- }
884
-
885
- // Remove event listeners
886
- this._eventListeners.clear();
887
-
888
- // Clear container
889
- this.container.innerHTML = '';
890
- this.container.classList.remove('plex-player');
891
-
892
- // Emit destroy event
893
- this._emit('destroy');
894
- }
895
- }
896
-
897
- // Attach to window for UMD builds
898
- if (typeof window !== 'undefined') {
899
- window.PlexPlayer = PlexPlayer;
900
- }
901
-
902
- exports.PlexPlayer = PlexPlayer;
903
- exports.Utils = Utils;
904
- exports.default = PlexPlayer;
905
-
906
- Object.defineProperty(exports, '__esModule', { value: true });
907
-
908
- }));
909
- //# sourceMappingURL=plex-player.js.map