myetv-player 1.0.8 → 1.1.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,9 +1,9 @@
1
1
  /**
2
- * MYETV Player - Twitch Plugin
3
- * File: myetv-player-twitch-plugin.js
4
- * Integrates Twitch live streams and VODs with full API control
5
- * Created by https://www.myetv.tv https://oskarcosimo.com
6
- */
2
+ * MYETV Player - Twitch Plugin
3
+ * File: myetv-player-twitch-plugin.js
4
+ * Integrates Twitch live streams and VODs with full API control
5
+ * Created by https://www.myetv.tv https://oskarcosimo.com
6
+ */
7
7
 
8
8
  (function () {
9
9
  'use strict';
@@ -12,40 +12,31 @@
12
12
  constructor(player, options = {}) {
13
13
  this.player = player;
14
14
  this.options = {
15
- // Video source (channel for live, video for VOD, collection for collections)
16
15
  channel: options.channel || null,
17
16
  video: options.video || null,
18
17
  collection: options.collection || null,
19
-
20
- // Parent domains (REQUIRED for embedding)
21
18
  parent: options.parent || [window.location.hostname],
22
-
23
- // Player dimensions
24
19
  width: options.width || '100%',
25
20
  height: options.height || '100%',
26
-
27
- // Playback options
28
21
  autoplay: options.autoplay !== false,
29
22
  muted: options.muted || false,
30
- time: options.time || '0h0m0s', // Start time for VODs (e.g., '1h30m45s')
31
-
32
- // UI options
23
+ time: options.time || '0h0m0s',
33
24
  allowfullscreen: options.allowfullscreen !== false,
34
-
35
- // Plugin options
36
25
  debug: options.debug || false,
37
26
  replaceNativePlayer: options.replaceNativePlayer !== false,
38
27
  autoLoadFromData: options.autoLoadFromData !== false,
39
-
40
28
  ...options
41
29
  };
42
30
 
43
31
  this.twitchPlayer = null;
44
32
  this.twitchContainer = null;
33
+ this.twitchIframe = null;
45
34
  this.isPlayerReady = false;
46
35
  this.isLive = false;
36
+ this.isPausedState = true; // Track state manually
37
+ this.extractedBrandLogo = null; // Store reference to extracted brand logo
38
+ this.brandLogoHideTimer = null; // Timer for auto-hide
47
39
 
48
- // Get plugin API
49
40
  this.api = player.getPluginAPI ? player.getPluginAPI() : {
50
41
  player: player,
51
42
  video: player.video,
@@ -58,18 +49,12 @@
58
49
  };
59
50
  }
60
51
 
61
- /**
62
- * Setup plugin
63
- */
64
52
  setup() {
65
- this.api.debug('Setup started');
66
-
67
- // Auto-detect from data attributes
53
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Setup started');
68
54
  if (this.options.autoLoadFromData) {
69
55
  this.autoDetectSource();
70
56
  }
71
57
 
72
- // Load Twitch Player SDK
73
58
  this.loadTwitchSDK().then(() => {
74
59
  if (this.options.channel || this.options.video || this.options.collection) {
75
60
  this.createTwitchPlayer();
@@ -78,46 +63,81 @@
78
63
  console.error('🎮 Twitch Plugin: Failed to load SDK', error);
79
64
  });
80
65
 
81
- // Add custom methods to player
82
66
  this.addCustomMethods();
67
+ this.syncControls();
68
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Setup completed');
69
+ }
70
+
71
+ syncControls() {
72
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Syncing controls');
73
+
74
+ // Override play method
75
+ const originalPlay = this.player.play;
76
+ this.player.play = () => {
77
+ if (this.twitchPlayer && this.isPlayerReady) {
78
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Play called');
79
+ try {
80
+ this.twitchPlayer.play();
81
+ } catch (error) {
82
+ if (this.api.player.options.debug) console.error('🎮 Twitch Plugin: Play error:', error);
83
+ }
84
+ } else {
85
+ if (originalPlay) originalPlay.call(this.player);
86
+ }
87
+ };
88
+
89
+ // Override pause method
90
+ const originalPause = this.player.pause;
91
+ this.player.pause = () => {
92
+ if (this.twitchPlayer && this.isPlayerReady) {
93
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Pause called');
94
+ try {
95
+ this.twitchPlayer.pause();
96
+ } catch (error) {
97
+ if (this.api.player.options.debug) console.error('🎮 Twitch Plugin: Pause error:', error);
98
+ }
99
+ } else {
100
+ if (originalPause) originalPause.call(this.player);
101
+ }
102
+ };
83
103
 
84
- this.api.debug('Setup completed');
104
+ // DON'T override paused getter - let the UI handle it via icon changes
105
+
106
+ // Hide PiP and Speed buttons
107
+ const pipButton = this.api.container.querySelector('.pip-btn');
108
+ if (pipButton) pipButton.style.display = 'none';
109
+
110
+ const speedButton = this.api.container.querySelector('.speed-btn');
111
+ if (speedButton) speedButton.style.display = 'none';
85
112
  }
86
113
 
87
- /**
88
- * Auto-detect source from data attributes
89
- */
90
114
  autoDetectSource() {
91
- // Check data attributes
92
115
  const dataChannel = this.api.video.getAttribute('data-twitch-channel');
93
116
  const dataVideo = this.api.video.getAttribute('data-twitch-video');
94
117
  const dataVideoType = this.api.video.getAttribute('data-video-type');
95
118
 
96
119
  if (dataChannel && dataVideoType === 'twitch') {
97
120
  this.options.channel = dataChannel;
98
- this.api.debug('Channel detected: ' + dataChannel);
121
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Channel detected:', dataChannel);
99
122
  return;
100
123
  }
101
124
 
102
125
  if (dataVideo && dataVideoType === 'twitch') {
103
126
  this.options.video = dataVideo;
104
- this.api.debug('Video detected: ' + dataVideo);
127
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Video detected:', dataVideo);
105
128
  return;
106
129
  }
107
130
 
108
- // Check video src
109
131
  const src = this.api.video.src || this.api.video.currentSrc;
110
132
  if (src && this.isTwitchUrl(src)) {
111
133
  this.extractFromUrl(src);
112
134
  return;
113
135
  }
114
136
 
115
- // Check source elements
116
137
  const sources = this.api.video.querySelectorAll('source');
117
138
  for (const source of sources) {
118
139
  const sourceSrc = source.getAttribute('src');
119
140
  const sourceType = source.getAttribute('type');
120
-
121
141
  if ((sourceType === 'video/twitch' || this.isTwitchUrl(sourceSrc)) && sourceSrc) {
122
142
  this.extractFromUrl(sourceSrc);
123
143
  return;
@@ -125,53 +145,40 @@
125
145
  }
126
146
  }
127
147
 
128
- /**
129
- * Check if URL is a Twitch URL
130
- */
131
148
  isTwitchUrl(url) {
132
149
  if (!url) return false;
133
150
  return /twitch\.tv\/(videos\/|[^/]+\/?$)/.test(url);
134
151
  }
135
152
 
136
- /**
137
- * Extract channel or video from URL
138
- */
139
153
  extractFromUrl(url) {
140
- // Extract video ID: https://www.twitch.tv/videos/123456789
141
154
  const videoMatch = url.match(/twitch\.tv\/videos\/(\d+)/);
142
155
  if (videoMatch) {
143
156
  this.options.video = videoMatch[1];
144
- this.api.debug('Video ID extracted: ' + this.options.video);
157
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Video ID extracted:', this.options.video);
145
158
  return;
146
159
  }
147
160
 
148
- // Extract channel: https://www.twitch.tv/channelname
149
161
  const channelMatch = url.match(/twitch\.tv\/([^/]+)\/?$/);
150
162
  if (channelMatch) {
151
163
  this.options.channel = channelMatch[1];
152
- this.api.debug('Channel extracted: ' + this.options.channel);
164
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Channel extracted:', this.options.channel);
153
165
  return;
154
166
  }
155
167
  }
156
168
 
157
- /**
158
- * Load Twitch Player SDK
159
- */
160
169
  loadTwitchSDK() {
161
170
  return new Promise((resolve, reject) => {
162
- // Check if already loaded
163
171
  if (window.Twitch && window.Twitch.Player) {
164
- this.api.debug('Twitch SDK already loaded');
172
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: SDK already loaded');
165
173
  resolve();
166
174
  return;
167
175
  }
168
176
 
169
- // Inject script
170
177
  const script = document.createElement('script');
171
178
  script.src = 'https://player.twitch.tv/js/embed/v1.js';
172
179
  script.async = true;
173
180
  script.onload = () => {
174
- this.api.debug('Twitch SDK loaded');
181
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: SDK loaded');
175
182
  resolve();
176
183
  };
177
184
  script.onerror = () => reject(new Error('Failed to load Twitch SDK'));
@@ -179,45 +186,38 @@
179
186
  });
180
187
  }
181
188
 
182
- /**
183
- * Create Twitch player
184
- */
185
189
  createTwitchPlayer() {
186
190
  if (!this.options.channel && !this.options.video && !this.options.collection) {
187
- this.api.debug('No channel, video, or collection specified');
191
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: No channel, video, or collection specified');
188
192
  return;
189
193
  }
190
194
 
191
- // Hide native player
192
195
  if (this.options.replaceNativePlayer) {
193
196
  this.api.video.style.display = 'none';
194
197
  }
195
198
 
196
- // Create container
197
199
  this.twitchContainer = document.createElement('div');
198
200
  this.twitchContainer.id = 'twitch-player-' + Date.now();
199
201
  this.twitchContainer.style.cssText = `
200
- position: absolute;
201
- top: 0;
202
- left: 0;
203
- width: 100%;
204
- height: 100%;
205
- z-index: 100;
206
- `;
202
+ position: fixed;
203
+ top: 0;
204
+ left: 0;
205
+ width: 100vw;
206
+ height: 100vh;
207
+ z-index: 1;
208
+ `;
207
209
 
208
210
  this.api.container.appendChild(this.twitchContainer);
209
211
 
210
- // Configure options
211
212
  const playerOptions = {
212
- width: this.options.width,
213
- height: this.options.height,
213
+ width: '100%',
214
+ height: '100%',
214
215
  parent: this.options.parent,
215
- autoplay: this.options.autoplay,
216
+ autoplay: false,
216
217
  muted: this.options.muted,
217
218
  allowfullscreen: this.options.allowfullscreen
218
219
  };
219
220
 
220
- // Add source
221
221
  if (this.options.channel) {
222
222
  playerOptions.channel = this.options.channel;
223
223
  this.isLive = true;
@@ -230,13 +230,11 @@
230
230
  this.isLive = false;
231
231
  }
232
232
 
233
- // Create player
234
233
  this.twitchPlayer = new Twitch.Player(this.twitchContainer.id, playerOptions);
235
-
236
- // Setup event listeners
237
234
  this.setupEventListeners();
238
235
 
239
- this.api.debug('Twitch player created');
236
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Player created');
237
+
240
238
  this.api.triggerEvent('twitchplugin:playerready', {
241
239
  channel: this.options.channel,
242
240
  video: this.options.video,
@@ -244,322 +242,585 @@
244
242
  });
245
243
  }
246
244
 
247
- /**
248
- * Setup event listeners
249
- */
250
245
  setupEventListeners() {
251
246
  if (!this.twitchPlayer) return;
252
247
 
253
- // Player ready
254
248
  this.twitchPlayer.addEventListener(Twitch.Player.READY, () => {
255
249
  this.isPlayerReady = true;
256
- this.api.debug('Player ready');
250
+ this.isPausedState = true; // Start paused
251
+
252
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Player ready - paused state:', this.isPausedState);
253
+
254
+ this.twitchIframe = this.twitchContainer.querySelector('iframe');
255
+ if (this.twitchIframe) {
256
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Iframe ready');
257
+ }
258
+
259
+ const loadingOverlay = this.api.container.querySelector('.loading-overlay');
260
+ if (loadingOverlay) loadingOverlay.style.display = 'none';
261
+
262
+ const posterOverlay = this.api.container.querySelector('.video-poster-overlay');
263
+ if (posterOverlay) posterOverlay.style.display = 'none';
264
+
265
+ if (this.api.video) {
266
+ this.api.video.style.display = 'none';
267
+ }
268
+
269
+ this.api.container.style.height = '100%';
270
+
271
+ // **CRITICAL: Extract brand logo BEFORE hiding controlbar**
272
+ this.extractAndRepositionBrandLogo();
273
+
274
+ // **Hide controlbar completely AFTER extracting brand logo**
275
+ if (this.api.controls) {
276
+ this.api.controls.style.display = 'none';
277
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Controlbar hidden');
278
+ }
279
+
280
+ // **Hide title overlay (Twitch already has it)**
281
+ const titleOverlay = this.api.container.querySelector('.title-overlay');
282
+ if (titleOverlay) {
283
+ titleOverlay.style.display = 'none';
284
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Title overlay hidden');
285
+ }
286
+
287
+ this.setupResizeAndFullscreenListeners();
257
288
  this.api.triggerEvent('twitchplugin:ready', {});
258
289
  });
259
290
 
260
- // Playing
261
291
  this.twitchPlayer.addEventListener(Twitch.Player.PLAYING, () => {
262
- this.api.debug('Playing');
263
- this.api.triggerEvent('play', {});
264
- this.api.triggerEvent('playing', {});
292
+ if (this.api.player.options.debug) {
293
+ console.log('🟢 Twitch Plugin: PLAYING event');
294
+ }
295
+
296
+ this.isPausedState = false;
297
+
298
+ // Update UI icons
299
+ const loadingOverlay = this.api.container.querySelector('.loading-overlay');
300
+ if (loadingOverlay) loadingOverlay.style.display = 'none';
301
+
302
+ const playIcon = this.api.container.querySelector('.play-icon');
303
+ const pauseIcon = this.api.container.querySelector('.pause-icon');
304
+ if (playIcon && pauseIcon) {
305
+ playIcon.classList.add('hidden');
306
+ pauseIcon.classList.remove('hidden');
307
+ }
308
+
309
+ // Dispatch native video events
310
+ const playEvent = new Event('play');
311
+ const playingEvent = new Event('playing');
312
+ this.api.video.dispatchEvent(playEvent);
313
+ this.api.video.dispatchEvent(playingEvent);
314
+
315
+ // **Manually trigger the 'played' callbacks**
316
+ const targetPlayer = this.api.player || this.player;
317
+ if (targetPlayer.eventCallbacks && targetPlayer.eventCallbacks['played']) {
318
+ const eventData = {
319
+ type: 'played',
320
+ timestamp: Date.now(),
321
+ player: targetPlayer,
322
+ currentTime: this.getCurrentTime(),
323
+ duration: this.getDuration()
324
+ };
325
+
326
+ targetPlayer.eventCallbacks['played'].forEach((callback, index) => {
327
+ try {
328
+ callback(eventData);
329
+ if (this.api.player.options.debug) console.log(`✅ Played callback #${index} executed`);
330
+ } catch (error) {
331
+ console.error(`❌ Error in played callback #${index}:`, error);
332
+ }
333
+ });
334
+ }
265
335
  });
266
336
 
267
- // Pause
268
337
  this.twitchPlayer.addEventListener(Twitch.Player.PAUSE, () => {
269
- this.api.debug('Paused');
270
- this.api.triggerEvent('pause', {});
338
+ if (this.api.player.options.debug) {
339
+ console.log('🔴 Twitch Plugin: PAUSE event');
340
+ }
341
+
342
+ this.isPausedState = true;
343
+
344
+ const playIcon = this.api.container.querySelector('.play-icon');
345
+ const pauseIcon = this.api.container.querySelector('.pause-icon');
346
+ if (playIcon && pauseIcon) {
347
+ playIcon.classList.remove('hidden');
348
+ pauseIcon.classList.add('hidden');
349
+ }
350
+
351
+ // Dispatch native pause event
352
+ const pauseEvent = new Event('pause');
353
+ this.api.video.dispatchEvent(pauseEvent);
354
+
355
+ // **Manually trigger the 'paused' callbacks**
356
+ const targetPlayer = this.api.player || this.player;
357
+ if (targetPlayer.eventCallbacks && targetPlayer.eventCallbacks['paused']) {
358
+ const eventData = {
359
+ type: 'paused',
360
+ timestamp: Date.now(),
361
+ player: targetPlayer,
362
+ currentTime: this.getCurrentTime(),
363
+ duration: this.getDuration()
364
+ };
365
+
366
+ targetPlayer.eventCallbacks['paused'].forEach((callback, index) => {
367
+ try {
368
+ callback(eventData);
369
+ if (this.api.player.options.debug) console.log(`✅ Paused callback #${index} executed`);
370
+ } catch (error) {
371
+ console.error(`❌ Error in paused callback #${index}:`, error);
372
+ }
373
+ });
374
+ }
271
375
  });
272
376
 
273
- // Ended
274
377
  this.twitchPlayer.addEventListener(Twitch.Player.ENDED, () => {
275
- this.api.debug('Ended');
378
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Ended');
379
+ this.isPausedState = true;
380
+
381
+ const playIcon = this.api.container.querySelector('.play-icon');
382
+ const pauseIcon = this.api.container.querySelector('.pause-icon');
383
+ if (playIcon && pauseIcon) {
384
+ playIcon.classList.remove('hidden');
385
+ pauseIcon.classList.add('hidden');
386
+ }
387
+
276
388
  this.api.triggerEvent('ended', {});
277
389
  });
278
390
 
279
- // Online (stream went live)
280
391
  this.twitchPlayer.addEventListener(Twitch.Player.ONLINE, () => {
281
- this.api.debug('Stream online');
392
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Stream online');
282
393
  this.api.triggerEvent('twitchplugin:online', {});
283
394
  });
284
395
 
285
- // Offline (stream went offline)
286
396
  this.twitchPlayer.addEventListener(Twitch.Player.OFFLINE, () => {
287
- this.api.debug('Stream offline');
397
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Stream offline');
398
+ this.isPausedState = true;
288
399
  this.api.triggerEvent('twitchplugin:offline', {});
289
400
  });
290
401
 
291
- // Playback blocked (autoplay restrictions)
292
402
  this.twitchPlayer.addEventListener(Twitch.Player.PLAYBACK_BLOCKED, () => {
293
- this.api.debug('Playback blocked - user interaction required');
403
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Playback blocked');
404
+ this.isPausedState = true;
294
405
  this.api.triggerEvent('twitchplugin:playbackblocked', {});
406
+ console.warn('🎮 Twitch: Click on the video to start playback');
295
407
  });
296
408
  }
297
409
 
298
410
  /**
299
- * Add custom methods to player instance
300
- */
411
+ * Extract brandlogo from controlbar and reposition it in BOTTOM-RIGHT corner
412
+ * With auto-hide behavior like the controlbar
413
+ * MUST be called BEFORE hiding the controlbar
414
+ */
415
+ extractAndRepositionBrandLogo() {
416
+ // Find brandlogo inside controlbar (correct selector: .brand-logo with hyphen)
417
+ const brandLogo = this.api.container.querySelector('.brand-logo');
418
+
419
+ if (brandLogo) {
420
+ // Store reference
421
+ this.extractedBrandLogo = brandLogo;
422
+
423
+ // Store original parent for restoration
424
+ if (!brandLogo._originalParent) {
425
+ brandLogo._originalParent = brandLogo.parentElement;
426
+ }
427
+
428
+ // Extract from controlbar and append directly to container
429
+ // This way it won't be hidden when controlbar is hidden
430
+ this.api.container.appendChild(brandLogo);
431
+
432
+ // Apply new positioning styles - BOTTOM-RIGHT corner, closer to Twitch controls
433
+ brandLogo.style.cssText = `
434
+ position: absolute !important;
435
+ bottom: 35px !important;
436
+ right: 20px !important;
437
+ z-index: 2000 !important;
438
+ pointer-events: auto !important;
439
+ display: block !important;
440
+ opacity: 0 !important;
441
+ transition: opacity 0.3s ease !important;
442
+ `;
443
+
444
+ // Setup auto-hide behavior (like controlbar)
445
+ this.setupBrandLogoAutoHide(brandLogo);
446
+
447
+ if (this.api.player.options.debug) {
448
+ console.log('🎮 Twitch Plugin: Brand logo extracted with auto-hide behavior');
449
+ }
450
+ } else {
451
+ if (this.api.player.options.debug) {
452
+ console.log('🎮 Twitch Plugin: No brand logo found (.brand-logo)');
453
+ }
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Setup auto-hide behavior for brand logo (shows on mousemove, hides after 3 seconds)
459
+ */
460
+ setupBrandLogoAutoHide(brandLogo) {
461
+ // Show logo initially for 3 seconds, then hide
462
+ this.showBrandLogo();
463
+ this.scheduleBrandLogoHide();
464
+
465
+ // Mouse move handler - show logo when mouse moves
466
+ const mouseMoveHandler = () => {
467
+ this.showBrandLogo();
468
+ this.scheduleBrandLogoHide();
469
+ };
470
+
471
+ // Add mouse move listener to container
472
+ this.api.container.addEventListener('mousemove', mouseMoveHandler);
473
+ this.api.container.addEventListener('mouseenter', mouseMoveHandler);
474
+
475
+ // Store handler for cleanup
476
+ if (!this.api.container._brandLogoMouseHandler) {
477
+ this.api.container._brandLogoMouseHandler = mouseMoveHandler;
478
+ }
479
+
480
+ // Mouse leave handler - hide immediately when mouse leaves
481
+ const mouseLeaveHandler = () => {
482
+ this.hideBrandLogo();
483
+ };
484
+
485
+ this.api.container.addEventListener('mouseleave', mouseLeaveHandler);
486
+
487
+ // Store handler for cleanup
488
+ if (!this.api.container._brandLogoLeaveHandler) {
489
+ this.api.container._brandLogoLeaveHandler = mouseLeaveHandler;
490
+ }
491
+ }
492
+
493
+ /**
494
+ * Show brand logo (fade in)
495
+ */
496
+ showBrandLogo() {
497
+ if (this.extractedBrandLogo) {
498
+ this.extractedBrandLogo.style.opacity = '0.8';
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Hide brand logo (fade out)
504
+ */
505
+ hideBrandLogo() {
506
+ if (this.extractedBrandLogo) {
507
+ this.extractedBrandLogo.style.opacity = '0';
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Schedule brand logo to hide after 3 seconds of inactivity
513
+ */
514
+ scheduleBrandLogoHide() {
515
+ // Clear existing timer
516
+ if (this.brandLogoHideTimer) {
517
+ clearTimeout(this.brandLogoHideTimer);
518
+ }
519
+
520
+ // Set new timer to hide after 3 seconds
521
+ this.brandLogoHideTimer = setTimeout(() => {
522
+ this.hideBrandLogo();
523
+ }, 3000);
524
+ }
525
+
526
+ setupResizeAndFullscreenListeners() {
527
+ this.resizeHandler = () => {
528
+ this.api.container.style.height = '100%';
529
+ // Logo is already positioned with fixed coordinates, no need to reposition
530
+ };
531
+
532
+ window.addEventListener('resize', this.resizeHandler);
533
+
534
+ this.fullscreenChangeHandler = () => {
535
+ setTimeout(() => {
536
+ this.api.container.style.height = '100%';
537
+ }, 100);
538
+ };
539
+
540
+ document.addEventListener('fullscreenchange', this.fullscreenChangeHandler);
541
+ document.addEventListener('webkitfullscreenchange', this.fullscreenChangeHandler);
542
+ document.addEventListener('mozfullscreenchange', this.fullscreenChangeHandler);
543
+ document.addEventListener('MSFullscreenChange', this.fullscreenChangeHandler);
544
+ }
545
+
301
546
  addCustomMethods() {
302
- // Load channel
303
547
  this.api.player.loadTwitchChannel = (channel) => {
304
548
  return this.loadChannel(channel);
305
549
  };
306
550
 
307
- // Load video
308
551
  this.api.player.loadTwitchVideo = (videoId, timestamp) => {
309
552
  return this.loadVideo(videoId, timestamp);
310
553
  };
311
554
 
312
- // Get Twitch player instance
313
555
  this.api.player.getTwitchPlayer = () => {
314
556
  return this.twitchPlayer;
315
557
  };
316
558
 
317
- // Check if live
318
559
  this.api.player.isTwitchLive = () => {
319
560
  return this.isLive;
320
561
  };
321
562
  }
322
563
 
323
- /**
324
- * Play
325
- */
326
564
  play() {
327
565
  if (!this.twitchPlayer) {
328
566
  return Promise.reject('Player not initialized');
329
567
  }
568
+
330
569
  this.twitchPlayer.play();
331
570
  return Promise.resolve();
332
571
  }
333
572
 
334
- /**
335
- * Pause
336
- */
337
573
  pause() {
338
574
  if (!this.twitchPlayer) {
339
575
  return Promise.reject('Player not initialized');
340
576
  }
577
+
341
578
  this.twitchPlayer.pause();
342
579
  return Promise.resolve();
343
580
  }
344
581
 
345
- /**
346
- * Seek (VODs only)
347
- */
348
582
  seek(seconds) {
349
583
  if (!this.twitchPlayer) {
350
584
  return Promise.reject('Player not initialized');
351
585
  }
586
+
352
587
  if (this.isLive) {
353
588
  console.warn('🎮 Twitch Plugin: Cannot seek in live streams');
354
589
  return Promise.reject('Cannot seek in live streams');
355
590
  }
591
+
356
592
  this.twitchPlayer.seek(seconds);
357
593
  return Promise.resolve(seconds);
358
594
  }
359
595
 
360
- /**
361
- * Get current time
362
- */
363
596
  getCurrentTime() {
364
597
  if (!this.twitchPlayer) {
365
598
  return Promise.reject('Player not initialized');
366
599
  }
600
+
367
601
  return Promise.resolve(this.twitchPlayer.getCurrentTime());
368
602
  }
369
603
 
370
- /**
371
- * Get duration (VODs only)
372
- */
373
604
  getDuration() {
374
605
  if (!this.twitchPlayer) {
375
606
  return Promise.reject('Player not initialized');
376
607
  }
608
+
377
609
  if (this.isLive) {
378
610
  return Promise.resolve(0);
379
611
  }
612
+
380
613
  return Promise.resolve(this.twitchPlayer.getDuration());
381
614
  }
382
615
 
383
- /**
384
- * Set volume
385
- */
386
616
  setVolume(volume) {
387
617
  if (!this.twitchPlayer) {
388
618
  return Promise.reject('Player not initialized');
389
619
  }
620
+
390
621
  this.twitchPlayer.setVolume(volume);
391
622
  return Promise.resolve(volume);
392
623
  }
393
624
 
394
- /**
395
- * Get volume
396
- */
397
625
  getVolume() {
398
626
  if (!this.twitchPlayer) {
399
627
  return Promise.reject('Player not initialized');
400
628
  }
629
+
401
630
  return Promise.resolve(this.twitchPlayer.getVolume());
402
631
  }
403
632
 
404
- /**
405
- * Mute
406
- */
407
633
  setMuted(muted) {
408
634
  if (!this.twitchPlayer) {
409
635
  return Promise.reject('Player not initialized');
410
636
  }
637
+
411
638
  this.twitchPlayer.setMuted(muted);
412
639
  return Promise.resolve(muted);
413
640
  }
414
641
 
415
- /**
416
- * Get muted state
417
- */
418
642
  getMuted() {
419
643
  if (!this.twitchPlayer) {
420
644
  return Promise.reject('Player not initialized');
421
645
  }
646
+
422
647
  return Promise.resolve(this.twitchPlayer.getMuted());
423
648
  }
424
649
 
425
- /**
426
- * Get quality
427
- */
428
650
  getQuality() {
429
651
  if (!this.twitchPlayer) {
430
652
  return Promise.reject('Player not initialized');
431
653
  }
654
+
432
655
  return Promise.resolve(this.twitchPlayer.getQuality());
433
656
  }
434
657
 
435
- /**
436
- * Set quality
437
- */
438
658
  setQuality(quality) {
439
659
  if (!this.twitchPlayer) {
440
660
  return Promise.reject('Player not initialized');
441
661
  }
662
+
442
663
  this.twitchPlayer.setQuality(quality);
443
664
  return Promise.resolve(quality);
444
665
  }
445
666
 
446
- /**
447
- * Get available qualities
448
- */
449
667
  getQualities() {
450
668
  if (!this.twitchPlayer) {
451
669
  return Promise.reject('Player not initialized');
452
670
  }
671
+
453
672
  return Promise.resolve(this.twitchPlayer.getPlaybackStats().qualitiesAvailable);
454
673
  }
455
674
 
456
- /**
457
- * Get channel name
458
- */
459
675
  getChannel() {
460
676
  if (!this.twitchPlayer) {
461
677
  return Promise.reject('Player not initialized');
462
678
  }
679
+
463
680
  return Promise.resolve(this.twitchPlayer.getChannel());
464
681
  }
465
682
 
466
- /**
467
- * Get video ID
468
- */
469
683
  getVideo() {
470
684
  if (!this.twitchPlayer) {
471
685
  return Promise.reject('Player not initialized');
472
686
  }
687
+
473
688
  return Promise.resolve(this.twitchPlayer.getVideo());
474
689
  }
475
690
 
476
- /**
477
- * Get playback stats
478
- */
479
691
  getPlaybackStats() {
480
692
  if (!this.twitchPlayer) {
481
693
  return Promise.reject('Player not initialized');
482
694
  }
695
+
483
696
  return Promise.resolve(this.twitchPlayer.getPlaybackStats());
484
697
  }
485
698
 
486
- /**
487
- * Check if paused
488
- */
489
699
  isPaused() {
490
700
  if (!this.twitchPlayer) {
491
701
  return Promise.reject('Player not initialized');
492
702
  }
493
- return Promise.resolve(this.twitchPlayer.isPaused());
703
+
704
+ return Promise.resolve(this.isPausedState);
494
705
  }
495
706
 
496
- /**
497
- * Load channel (live stream)
498
- */
499
707
  loadChannel(channel) {
500
708
  if (!this.twitchPlayer) {
501
709
  return Promise.reject('Player not initialized');
502
710
  }
711
+
503
712
  this.options.channel = channel;
504
713
  this.options.video = null;
505
714
  this.isLive = true;
715
+ this.isPausedState = true;
716
+
506
717
  this.twitchPlayer.setChannel(channel);
507
718
  this.api.triggerEvent('twitchplugin:channelloaded', { channel });
719
+
508
720
  return Promise.resolve(channel);
509
721
  }
510
722
 
511
- /**
512
- * Load video (VOD)
513
- */
514
723
  loadVideo(videoId, timestamp = '0h0m0s') {
515
724
  if (!this.twitchPlayer) {
516
725
  return Promise.reject('Player not initialized');
517
726
  }
727
+
518
728
  this.options.video = videoId;
519
729
  this.options.channel = null;
520
730
  this.isLive = false;
731
+ this.isPausedState = true;
732
+
521
733
  this.twitchPlayer.setVideo(videoId, timestamp);
522
734
  this.api.triggerEvent('twitchplugin:videoloaded', { video: videoId, timestamp });
735
+
523
736
  return Promise.resolve(videoId);
524
737
  }
525
738
 
526
- /**
527
- * Load collection
528
- */
529
739
  loadCollection(collectionId, videoId) {
530
740
  if (!this.twitchPlayer) {
531
741
  return Promise.reject('Player not initialized');
532
742
  }
743
+
533
744
  this.options.collection = collectionId;
534
745
  this.isLive = false;
746
+ this.isPausedState = true;
747
+
535
748
  this.twitchPlayer.setCollection(collectionId, videoId);
536
749
  this.api.triggerEvent('twitchplugin:collectionloaded', { collection: collectionId, video: videoId });
750
+
537
751
  return Promise.resolve(collectionId);
538
752
  }
539
753
 
540
- /**
541
- * Dispose plugin
542
- */
543
754
  dispose() {
544
- this.api.debug('Disposing plugin');
755
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Disposing');
756
+
757
+ // Clear brand logo hide timer
758
+ if (this.brandLogoHideTimer) {
759
+ clearTimeout(this.brandLogoHideTimer);
760
+ this.brandLogoHideTimer = null;
761
+ }
762
+
763
+ // Remove mouse event listeners for brand logo auto-hide
764
+ if (this.api.container._brandLogoMouseHandler) {
765
+ this.api.container.removeEventListener('mousemove', this.api.container._brandLogoMouseHandler);
766
+ this.api.container.removeEventListener('mouseenter', this.api.container._brandLogoMouseHandler);
767
+ delete this.api.container._brandLogoMouseHandler;
768
+ }
769
+
770
+ if (this.api.container._brandLogoLeaveHandler) {
771
+ this.api.container.removeEventListener('mouseleave', this.api.container._brandLogoLeaveHandler);
772
+ delete this.api.container._brandLogoLeaveHandler;
773
+ }
774
+
775
+ // Restore brand logo to original position in controlbar
776
+ if (this.extractedBrandLogo) {
777
+ // Remove inline styles
778
+ this.extractedBrandLogo.removeAttribute('style');
779
+
780
+ // Restore to original parent
781
+ if (this.extractedBrandLogo._originalParent) {
782
+ this.extractedBrandLogo._originalParent.appendChild(this.extractedBrandLogo);
783
+ }
784
+
785
+ this.extractedBrandLogo = null;
786
+
787
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Brand logo restored');
788
+ }
545
789
 
546
790
  if (this.twitchContainer) {
547
791
  this.twitchContainer.remove();
548
792
  this.twitchContainer = null;
549
793
  }
550
794
 
795
+ if (this.resizeHandler) {
796
+ window.removeEventListener('resize', this.resizeHandler);
797
+ this.resizeHandler = null;
798
+ }
799
+
800
+ if (this.fullscreenChangeHandler) {
801
+ document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler);
802
+ document.removeEventListener('webkitfullscreenchange', this.fullscreenChangeHandler);
803
+ document.removeEventListener('mozfullscreenchange', this.fullscreenChangeHandler);
804
+ document.removeEventListener('MSFullscreenChange', this.fullscreenChangeHandler);
805
+ this.fullscreenChangeHandler = null;
806
+ }
807
+
551
808
  this.twitchPlayer = null;
809
+ this.twitchIframe = null;
810
+
811
+ // Show controls again
812
+ if (this.api.controls) {
813
+ this.api.controls.style.display = '';
814
+ }
552
815
 
553
- // Restore native player
554
816
  if (this.api.video && this.options.replaceNativePlayer) {
555
817
  this.api.video.style.display = '';
556
818
  }
557
819
 
558
- this.api.debug('Plugin disposed');
820
+ if (this.api.player.options.debug) console.log('🎮 Twitch Plugin: Disposed');
559
821
  }
560
822
  }
561
823
 
562
- // Register plugin globally
563
824
  if (typeof window.registerMYETVPlugin === 'function') {
564
825
  window.registerMYETVPlugin('twitch', TwitchPlugin);
565
826
  } else {