myetv-player 1.0.0 → 1.0.8

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.
Files changed (38) hide show
  1. package/.github/workflows/codeql.yml +100 -0
  2. package/README.md +49 -58
  3. package/SECURITY.md +50 -0
  4. package/css/myetv-player.css +424 -219
  5. package/css/myetv-player.min.css +1 -1
  6. package/dist/myetv-player.js +1759 -1502
  7. package/dist/myetv-player.min.js +1705 -1469
  8. package/package.json +7 -1
  9. package/plugins/README.md +1016 -0
  10. package/plugins/cloudflare/README.md +1068 -0
  11. package/plugins/cloudflare/myetv-player-cloudflare-stream-plugin.js +556 -0
  12. package/plugins/facebook/README.md +1024 -0
  13. package/plugins/facebook/myetv-player-facebook-plugin.js +437 -0
  14. package/plugins/gamepad-remote-controller/README.md +816 -0
  15. package/plugins/gamepad-remote-controller/myetv-player-gamepad-remote-plugin.js +678 -0
  16. package/plugins/google-adsense-ads/README.md +1 -0
  17. package/plugins/google-adsense-ads/g-adsense-ads-plugin.js +158 -0
  18. package/plugins/google-ima-ads/README.md +1 -0
  19. package/plugins/google-ima-ads/g-ima-ads-plugin.js +355 -0
  20. package/plugins/twitch/README.md +1185 -0
  21. package/plugins/twitch/myetv-player-twitch-plugin.js +569 -0
  22. package/plugins/vast-vpaid-ads/README.md +1 -0
  23. package/plugins/vast-vpaid-ads/vast-vpaid-ads-plugin.js +346 -0
  24. package/plugins/vimeo/README.md +1416 -0
  25. package/plugins/vimeo/myetv-player-vimeo.js +640 -0
  26. package/plugins/youtube/README.md +851 -0
  27. package/plugins/youtube/myetv-player-youtube-plugin.js +1714 -210
  28. package/scss/README.md +160 -0
  29. package/scss/_controls.scss +184 -30
  30. package/scss/_menus.scss +840 -672
  31. package/scss/_responsive.scss +67 -105
  32. package/scss/_volume.scss +67 -105
  33. package/src/README.md +559 -0
  34. package/src/controls.js +17 -5
  35. package/src/core.js +1237 -1060
  36. package/src/i18n.js +27 -1
  37. package/src/quality.js +478 -436
  38. package/src/subtitles.js +2 -2
@@ -1,8 +1,7 @@
1
1
  /**
2
- * MYETV Player - YouTube Plugin (Enhanced)
2
+ * MYETV Player - YouTube Plugin
3
3
  * File: myetv-player-youtube-plugin.js
4
4
  */
5
-
6
5
  (function () {
7
6
  'use strict';
8
7
 
@@ -10,169 +9,370 @@
10
9
  constructor(player, options = {}) {
11
10
  this.player = player;
12
11
  this.options = {
13
- videoId: options.videoId || null, // Direct video ID
12
+ videoId: options.videoId || null,
14
13
  apiKey: options.apiKey || null,
15
14
  autoplay: options.autoplay !== undefined ? options.autoplay : false,
16
15
  showYouTubeUI: options.showYouTubeUI !== undefined ? options.showYouTubeUI : false,
17
16
  autoLoadFromData: options.autoLoadFromData !== undefined ? options.autoLoadFromData : true,
18
- quality: options.quality || 'default', // 'default', 'hd720', 'hd1080'
17
+ quality: options.quality || 'default',
18
+ enableQualityControl: options.enableQualityControl !== undefined ? options.enableQualityControl : true,
19
+ enableCaptions: options.enableCaptions !== undefined ? options.enableCaptions : true,
20
+ debug: true,
19
21
  ...options
20
22
  };
21
23
 
22
24
  this.ytPlayer = null;
23
25
  this.isYouTubeReady = false;
24
26
  this.videoId = this.options.videoId;
27
+ this.availableQualities = [];
28
+ this.currentQuality = 'default';
29
+ this.currentPlayingQuality = null;
30
+ this.availableCaptions = [];
31
+ this.currentCaption = null;
32
+ this.currentTranslation = null;
33
+ this.captionsEnabled = false;
34
+ this.subtitlesMenuCreated = false;
35
+ this.timeUpdateInterval = null;
36
+ this.mouseMoveOverlay = null;
37
+ this.qualityCheckAttempts = 0;
38
+ this.captionCheckAttempts = 0;
39
+ this.captionStateCheckInterval = null;
40
+ this.qualityMonitorInterval = null;
41
+ this.resizeListenerAdded = false;
25
42
 
26
- // Get plugin API
27
43
  this.api = player.getPluginAPI();
44
+ if (this.api.player.options.debug) console.log('[YT Plugin] Constructor initialized', this.options);
28
45
  }
29
46
 
30
- /**
31
- * Setup plugin (called automatically after instantiation)
32
- */
33
- setup() {
34
- this.api.debug('YouTube plugin setup started');
47
+ // Top 10 most spoken languages for auto-translation
48
+ getTopTranslationLanguages() {
49
+ return [
50
+ { code: 'en', name: 'English' },
51
+ { code: 'es', name: 'Spanish' },
52
+ { code: 'hi', name: 'Hindi' },
53
+ { code: 'ar', name: 'Arabic' },
54
+ { code: 'pt', name: 'Portuguese' },
55
+ { code: 'ru', name: 'Russian' },
56
+ { code: 'ja', name: 'Japanese' },
57
+ { code: 'de', name: 'German' },
58
+ { code: 'fr', name: 'French' },
59
+ { code: 'it', name: 'Italian' }
60
+ ];
61
+ }
35
62
 
36
- // Load YouTube IFrame API
63
+ setup() {
64
+ if (this.api.player.options.debug) console.log('[YT Plugin] Setup started');
37
65
  this.loadYouTubeAPI();
38
-
39
- // Add custom methods to player
40
66
  this.addPlayerMethods();
41
-
42
- // Register hooks
43
67
  this.registerHooks();
44
68
 
45
- // Auto-load video ID from various sources
46
69
  if (this.options.autoLoadFromData) {
47
70
  this.autoDetectVideoId();
48
71
  }
49
72
 
50
- // If video ID is provided in options, load it immediately
51
73
  if (this.videoId) {
52
74
  this.waitForAPIThenLoad();
53
75
  }
54
76
 
55
- this.api.debug('YouTube plugin setup completed');
77
+ if (this.api.player.options.debug) console.log('[YT Plugin] Setup completed');
56
78
  }
57
79
 
58
- /**
59
- * Add custom methods to player instance
60
- */
61
- addPlayerMethods() {
62
- // Load YouTube video by ID
63
- this.player.loadYouTubeVideo = (videoId, options = {}) => {
64
- return this.loadVideo(videoId, options);
65
- };
80
+ handleResponsiveLayout() {
66
81
 
67
- // Get current YouTube video ID
68
- this.player.getYouTubeVideoId = () => {
69
- return this.videoId;
70
- };
82
+ const containerWidth = this.api.container.offsetWidth;
83
+ const pipBtn = this.api.container.querySelector('.pip-btn');
84
+ const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
85
+ const settingsMenu = this.api.container.querySelector('.settings-menu');
71
86
 
72
- // Check if YouTube is active
73
- this.player.isYouTubeActive = () => {
74
- return this.ytPlayer !== null;
75
- };
87
+ // ALWAYS hide PiP for YouTube
88
+ if (pipBtn) {
89
+ pipBtn.style.display = 'none';
90
+ }
91
+
92
+ // Breakpoint at 600px
93
+ if (containerWidth < 600) {
94
+ // Hide subtitles button
95
+ if (subtitlesBtn) {
96
+ subtitlesBtn.style.display = 'none';
97
+ }
98
+
99
+ // Add subtitles option to settings menu
100
+ if (settingsMenu) {
101
+ let subtitlesWrapper = settingsMenu.querySelector('.yt-subtitles-wrapper');
102
+
103
+ if (!subtitlesWrapper) {
104
+ // Get i18n text
105
+ let subtitlesText = 'Subtitles';
106
+ if (this.api.player && this.api.player.t) {
107
+ subtitlesText = this.api.player.t('subtitles');
108
+ } else if (this.player && this.player.t) {
109
+ subtitlesText = this.player.t('subtitles');
110
+ }
111
+
112
+ // Create wrapper
113
+ subtitlesWrapper = document.createElement('div');
114
+ subtitlesWrapper.className = 'yt-subtitles-wrapper';
115
+ subtitlesWrapper.style.cssText = 'position: relative; display: block;';
116
+
117
+ // Create trigger
118
+ const trigger = document.createElement('div');
119
+ trigger.className = 'quality-option';
120
+ trigger.textContent = subtitlesText;
121
+
122
+ // Create submenu
123
+ const submenu = document.createElement('div');
124
+ submenu.className = 'yt-subtitles-submenu';
125
+ submenu.style.cssText = `
126
+ display: none;
127
+ position: absolute;
128
+ right: 100%;
129
+ top: 0;
130
+ margin-right: 5px;
131
+ background: #1c1c1c;
132
+ border: 1px solid rgba(255,255,255,0.1);
133
+ border-radius: 4px;
134
+ padding: 8px 0;
135
+ z-index: 999999;
136
+ color: white;
137
+ width: fit-content;
138
+ max-width: 180px;
139
+ max-height: 250px;
140
+ overflow-y: auto;
141
+ overflow-x: hidden;`;
142
+
143
+ // Hover state tracking
144
+ let isHoveringTrigger = false;
145
+ let isHoveringSubmenu = false;
146
+ let hideTimeout = null;
147
+
148
+ const checkAndHide = () => {
149
+ if (hideTimeout) clearTimeout(hideTimeout);
150
+ hideTimeout = setTimeout(() => {
151
+ if (!isHoveringTrigger && !isHoveringSubmenu) {
152
+ submenu.style.display = 'none';
153
+ }
154
+ }, 200);
155
+ };
156
+
157
+ // Add OFF option
158
+ const offOption = document.createElement('div');
159
+ offOption.className = 'quality-option';
160
+ offOption.textContent = 'Off';
161
+ offOption.style.cssText = 'padding: 8px 16px; cursor: pointer; color: white;';
162
+ offOption.addEventListener('click', () => {
163
+ if (this.disableCaptions) this.disableCaptions();
164
+ submenu.style.display = 'none';
165
+ settingsMenu.classList.remove('show');
166
+ });
167
+ submenu.appendChild(offOption);
168
+
169
+ // Add caption options
170
+ if (this.availableCaptions && this.availableCaptions.length > 0) {
171
+ this.availableCaptions.forEach((caption, index) => {
172
+ const option = document.createElement('div');
173
+ option.className = 'quality-option';
174
+ option.textContent = caption.label || caption.languageName;
175
+ option.style.cssText = 'padding: 8px 16px; cursor: pointer; color: white;';
176
+ option.addEventListener('click', () => {
177
+ if (this.setCaptions) this.setCaptions(index);
178
+ submenu.style.display = 'none';
179
+ settingsMenu.classList.remove('show');
180
+ });
181
+ submenu.appendChild(option);
182
+ });
183
+ } else {
184
+ // Add Auto option
185
+ const autoOption = document.createElement('div');
186
+ autoOption.className = 'quality-option';
187
+ autoOption.textContent = 'On (Auto)';
188
+ autoOption.style.cssText = 'padding: 8px 16px; cursor: pointer; color: white;';
189
+ autoOption.addEventListener('click', () => {
190
+ if (this.enableAutoCaptions) this.enableAutoCaptions();
191
+ submenu.style.display = 'none';
192
+ settingsMenu.classList.remove('show');
193
+ });
194
+ submenu.appendChild(autoOption);
195
+ }
196
+
197
+ // Trigger events
198
+ trigger.addEventListener('mouseenter', () => {
199
+ isHoveringTrigger = true;
200
+ if (hideTimeout) clearTimeout(hideTimeout);
201
+ submenu.style.display = 'block';
202
+ });
203
+
204
+ trigger.addEventListener('mouseleave', () => {
205
+ isHoveringTrigger = false;
206
+ checkAndHide();
207
+ });
208
+
209
+ // Submenu events
210
+ submenu.addEventListener('mouseenter', () => {
211
+ isHoveringSubmenu = true;
212
+ if (hideTimeout) clearTimeout(hideTimeout);
213
+ submenu.style.display = 'block';
214
+ });
215
+
216
+ submenu.addEventListener('mouseleave', () => {
217
+ isHoveringSubmenu = false;
218
+ checkAndHide();
219
+ });
220
+
221
+ // Click alternative
222
+ trigger.addEventListener('click', (e) => {
223
+ e.stopPropagation();
224
+ if (submenu.style.display === 'none' || !submenu.style.display) {
225
+ submenu.style.display = 'block';
226
+ } else {
227
+ submenu.style.display = 'none';
228
+ }
229
+ });
230
+
231
+ // Assemble
232
+ subtitlesWrapper.appendChild(trigger);
233
+ subtitlesWrapper.appendChild(submenu);
234
+ settingsMenu.insertBefore(subtitlesWrapper, settingsMenu.firstChild);
235
+ }
236
+ }
237
+ } else {
238
+ // Wide screen
239
+ if (subtitlesBtn) {
240
+ subtitlesBtn.style.display = '';
241
+ }
242
+
243
+ // Remove from settings
244
+ if (settingsMenu) {
245
+ const subtitlesWrapper = settingsMenu.querySelector('.yt-subtitles-wrapper');
246
+ if (subtitlesWrapper) {
247
+ subtitlesWrapper.remove();
248
+ }
249
+ }
250
+ }
251
+ }
252
+
253
+ hidePipFromSettingsMenuOnly() {
254
+ const settingsMenu = this.api.container.querySelector('.settings-menu');
255
+ if (!settingsMenu) return;
256
+
257
+ // Use MutationObserver to watch when options are added to settings menu
258
+ if (!this.pipObserver) {
259
+ this.pipObserver = new MutationObserver(() => {
260
+ const allOptions = settingsMenu.children;
261
+
262
+ for (let i = 0; i < allOptions.length; i++) {
263
+ const option = allOptions[i];
264
+
265
+ // Skip our subtitles wrapper
266
+ if (option.classList.contains('yt-subtitles-wrapper')) continue;
267
+
268
+ const text = option.textContent.trim().toLowerCase();
269
+
270
+ // Check if it's the PiP option
271
+ if (text.includes('picture') || text === 'pip' || text.includes('in picture')) {
272
+ option.style.display = 'none';
273
+ }
274
+ }
275
+ });
276
+
277
+ // Start observing
278
+ this.pipObserver.observe(settingsMenu, {
279
+ childList: true,
280
+ subtree: true
281
+ });
282
+ }
283
+ }
284
+
285
+ addPlayerMethods() {
286
+ this.player.loadYouTubeVideo = (videoId, options = {}) => this.loadVideo(videoId, options);
287
+ this.player.getYouTubeVideoId = () => this.videoId;
288
+ this.player.isYouTubeActive = () => this.ytPlayer !== null;
289
+ this.player.getYouTubeQualities = () => this.getAvailableQualities();
290
+ this.player.setYouTubeQuality = (quality) => this.setQuality(quality);
291
+ this.player.getYouTubeCurrentQuality = () => this.getCurrentQuality();
292
+ this.player.getYouTubeCaptions = () => this.getAvailableCaptions();
293
+ this.player.setYouTubeCaptions = (trackIndex) => this.setCaptions(trackIndex);
294
+ this.player.toggleYouTubeCaptions = () => this.toggleCaptions();
76
295
  }
77
296
 
78
- /**
79
- * Register plugin hooks
80
- */
81
297
  registerHooks() {
82
- // Check for YouTube URL before play
83
298
  this.api.registerHook('beforePlay', (data) => {
84
299
  this.checkForYouTubeUrl();
85
300
  });
86
301
  }
87
302
 
88
- /**
89
- * Auto-detect video ID from various sources
90
- */
91
303
  autoDetectVideoId() {
92
- // Priority 1: Check data-video-id attribute
93
304
  const dataVideoId = this.api.video.getAttribute('data-video-id');
94
305
  const dataVideoType = this.api.video.getAttribute('data-video-type');
95
306
 
96
307
  if (dataVideoId && dataVideoType === 'youtube') {
97
308
  this.videoId = dataVideoId;
98
- this.api.debug('YouTube video ID detected from data-video-id: ' + this.videoId);
309
+ if (this.api.player.options.debug) console.log('[YT Plugin] Video ID from data-video-id:', this.videoId);
99
310
  return;
100
311
  }
101
312
 
102
- // Priority 2: Check video source URL
103
313
  const src = this.api.video.src || this.api.video.currentSrc;
104
314
  if (src) {
105
315
  const extractedId = this.extractYouTubeVideoId(src);
106
316
  if (extractedId) {
107
317
  this.videoId = extractedId;
108
- this.api.debug('YouTube video ID detected from src: ' + this.videoId);
318
+ if (this.api.player.options.debug) console.log('[YT Plugin] Video ID from src:', this.videoId);
109
319
  return;
110
320
  }
111
321
  }
112
322
 
113
- // Priority 3: Check source elements
114
323
  const sources = this.api.video.querySelectorAll('source');
115
324
  for (const source of sources) {
116
325
  const sourceSrc = source.getAttribute('src');
117
326
  const sourceType = source.getAttribute('type');
118
-
119
327
  if (sourceType === 'video/youtube' || sourceType === 'video/x-youtube') {
120
328
  const extractedId = this.extractYouTubeVideoId(sourceSrc);
121
329
  if (extractedId) {
122
330
  this.videoId = extractedId;
123
- this.api.debug('YouTube video ID detected from source element: ' + this.videoId);
331
+ if (this.api.player.options.debug) console.log('[YT Plugin] Video ID from source:', this.videoId);
124
332
  return;
125
333
  }
126
334
  }
127
335
  }
128
336
  }
129
337
 
130
- /**
131
- * Wait for API to be ready then load video
132
- */
133
338
  waitForAPIThenLoad() {
134
- if (this.isYouTubeReady) {
135
- this.loadVideo(this.videoId);
136
- } else {
137
- // Wait for API to be ready
138
- const checkInterval = setInterval(() => {
139
- if (this.isYouTubeReady) {
140
- clearInterval(checkInterval);
141
- this.loadVideo(this.videoId);
142
- }
143
- }, 100);
144
- }
339
+ if (this.api.player.options.debug) console.log('[YT Plugin] Waiting for API...');
340
+
341
+ const checkAndLoad = () => {
342
+ if (window.YT && window.YT.Player && typeof window.YT.Player === 'function') {
343
+ // API is ready
344
+ this.isYouTubeReady = true;
345
+ if (this.api.player.options.debug) console.log('[YT Plugin] API confirmed ready, loading video');
346
+ this.loadVideo(this.videoId);
347
+ } else {
348
+ // API not ready yet, check again
349
+ if (this.api.player.options.debug) console.log('[YT Plugin] API not ready, retrying in 100ms...');
350
+ setTimeout(checkAndLoad, 100);
351
+ }
352
+ };
353
+
354
+ checkAndLoad();
145
355
  }
146
356
 
147
- /**
148
- * Load YouTube IFrame API
149
- */
150
357
  loadYouTubeAPI() {
151
358
  if (window.YT && window.YT.Player) {
152
359
  this.isYouTubeReady = true;
153
- this.api.debug('YouTube API already loaded');
360
+ if (this.api.player.options.debug) console.log('[YT Plugin] YouTube API already loaded');
154
361
  return;
155
362
  }
156
363
 
157
- // Load YouTube IFrame API script
158
364
  const tag = document.createElement('script');
159
365
  tag.src = 'https://www.youtube.com/iframe_api';
160
366
  const firstScriptTag = document.getElementsByTagName('script')[0];
161
367
  firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
162
368
 
163
- // Set callback for when API is ready
164
369
  window.onYouTubeIframeAPIReady = () => {
165
370
  this.isYouTubeReady = true;
166
- this.api.debug('YouTube API loaded and ready');
167
-
168
- // Trigger custom event
371
+ if (this.api.player.options.debug) console.log('[YT Plugin] YouTube API loaded and ready');
169
372
  this.api.triggerEvent('youtubeplugin:ready', {});
170
373
  };
171
374
  }
172
375
 
173
- /**
174
- * Check if current video source is a YouTube URL
175
- */
176
376
  checkForYouTubeUrl() {
177
377
  const src = this.api.video.src || this.api.video.currentSrc;
178
378
  const videoId = this.extractYouTubeVideoId(src);
@@ -181,238 +381,1542 @@
181
381
  this.loadVideo(videoId);
182
382
  return true;
183
383
  }
184
-
185
384
  return false;
186
385
  }
187
386
 
188
- /**
189
- * Extract YouTube video ID from URL
190
- * Supports various YouTube URL formats
191
- * @param {String} url - YouTube URL or video ID
192
- * @returns {String|null} Video ID or null
193
- */
194
387
  extractYouTubeVideoId(url) {
195
388
  if (!url) return null;
196
-
197
- // Remove whitespace
198
389
  url = url.trim();
199
390
 
200
391
  const patterns = [
201
- // Standard watch URL
202
392
  /(?:youtube\.com\/watch\?v=)([^&\n?#]+)/,
203
- // Shortened youtu.be URL
204
393
  /(?:youtu\.be\/)([^&\n?#]+)/,
205
- // Embed URL
206
394
  /(?:youtube\.com\/embed\/)([^&\n?#]+)/,
207
- // Direct video ID (11 characters)
208
395
  /^([a-zA-Z0-9_-]{11})$/
209
396
  ];
210
397
 
211
398
  for (const pattern of patterns) {
212
399
  const match = url.match(pattern);
213
- if (match && match[1]) {
214
- return match[1];
215
- }
400
+ if (match && match[1]) return match[1];
216
401
  }
217
-
218
402
  return null;
219
403
  }
220
404
 
221
- /**
222
- * Load YouTube video by ID
223
- * @param {String} videoId - YouTube video ID
224
- * @param {Object} options - Load options
225
- */
226
405
  loadVideo(videoId, options = {}) {
227
406
  if (!videoId) {
228
- this.api.debug('No video ID provided to loadVideo()');
407
+ if (this.api.player.options.debug) console.error('[YT Plugin] No video ID provided');
229
408
  return;
230
409
  }
231
410
 
232
411
  if (!this.isYouTubeReady) {
233
- this.api.debug('YouTube API not ready yet, waiting...');
412
+ if (this.api.player.options.debug) console.log('[YT Plugin] Waiting for API...');
234
413
  setTimeout(() => this.loadVideo(videoId, options), 100);
235
414
  return;
236
415
  }
237
416
 
417
+ if (this.api.player.options.debug) console.log('[YT Plugin] Loading video:', videoId);
238
418
  this.videoId = videoId;
419
+ this.qualityCheckAttempts = 0;
420
+ this.captionCheckAttempts = 0;
421
+ this.subtitlesMenuCreated = false;
239
422
 
240
- // Hide native video element
241
- this.api.video.style.display = 'none';
423
+ this.api.video.style.pointerEvents = 'none';
424
+
425
+ this.hidePosterOverlay();
426
+ this.hideInitialLoading();
427
+ this.hideLoadingOverlay();
242
428
 
243
- // Create YouTube player container if not exists
244
429
  if (!this.ytPlayerContainer) {
245
430
  this.ytPlayerContainer = document.createElement('div');
246
431
  this.ytPlayerContainer.id = 'yt-player-' + Date.now();
247
432
  this.ytPlayerContainer.className = 'yt-player-container';
433
+ this.ytPlayerContainer.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;z-index:2;';
434
+ // Ensure controls are above YouTube player
435
+ if (this.api.controls) {
436
+ this.api.controls.style.zIndex = '10';
437
+ this.api.controls.style.pointerEvents = 'auto';
438
+ }
248
439
 
249
- // Style to match video element
250
- this.ytPlayerContainer.style.position = 'absolute';
251
- this.ytPlayerContainer.style.top = '0';
252
- this.ytPlayerContainer.style.left = '0';
253
- this.ytPlayerContainer.style.width = '100%';
254
- this.ytPlayerContainer.style.height = '100%';
440
+ // Ensure YouTube iframe is visible
441
+ const ytIframe = this.ytPlayerContainer.querySelector('iframe');
442
+ if (ytIframe) {
443
+ ytIframe.style.pointerEvents = 'auto';
444
+ }
255
445
 
256
446
  this.api.container.insertBefore(this.ytPlayerContainer, this.api.controls);
447
+ } else {
448
+ this.ytPlayerContainer.style.visibility = 'visible';
449
+ this.ytPlayerContainer.style.opacity = '1';
257
450
  }
258
451
 
259
- // Destroy existing player if present
452
+ this.createMouseMoveOverlay();
453
+
260
454
  if (this.ytPlayer) {
261
455
  this.ytPlayer.destroy();
262
456
  }
263
457
 
264
- // Merge options
265
458
  const playerVars = {
266
459
  autoplay: this.options.autoplay ? 1 : 0,
267
460
  controls: this.options.showYouTubeUI ? 1 : 0,
268
461
  modestbranding: 1,
269
462
  rel: 0,
463
+ cc_load_policy: 1,
464
+ cc_lang_pref: 'en',
465
+ iv_load_policy: 3,
270
466
  ...options.playerVars
271
467
  };
272
468
 
273
- // Initialize YouTube player
469
+ if (this.api.player.options.debug) console.log('[YT Plugin] Player vars:', playerVars);
470
+
274
471
  this.ytPlayer = new YT.Player(this.ytPlayerContainer.id, {
275
472
  videoId: videoId,
276
473
  playerVars: playerVars,
277
474
  events: {
278
- 'onReady': (event) => this.onPlayerReady(event),
279
- 'onStateChange': (event) => this.onPlayerStateChange(event),
280
- 'onError': (event) => this.onPlayerError(event)
475
+ 'onReady': (e) => this.onPlayerReady(e),
476
+ 'onStateChange': (e) => this.onPlayerStateChange(e),
477
+ 'onPlaybackQualityChange': (e) => this.onPlaybackQualityChange(e),
478
+ 'onError': (e) => this.onPlayerError(e),
479
+ 'onApiChange': (e) => this.onApiChange(e)
281
480
  }
282
481
  });
283
482
 
284
- this.api.debug('YouTube video loaded: ' + videoId);
483
+ // Force iframe to fill container properly
484
+ setTimeout(() => {
485
+ const iframe = this.ytPlayerContainer.querySelector('iframe');
486
+ if (iframe) {
487
+ iframe.style.cssText = 'position:absolute!important;top:0!important;left:0!important;width:100%!important;height:100%!important;';
488
+ if (this.api.player.options.debug) console.log('[YT Plugin] Iframe forced to absolute positioning');
489
+ }
490
+ }, 500);
491
+
492
+
493
+ if (this.api.player.options.debug) console.log('[YT Plugin] YouTube player created');
285
494
  this.api.triggerEvent('youtubeplugin:videoloaded', { videoId });
286
495
  }
287
496
 
288
- /**
289
- * YouTube player ready callback
290
- */
497
+ createMouseMoveOverlay() {
498
+ if (this.mouseMoveOverlay) return;
499
+
500
+ this.mouseMoveOverlay = document.createElement('div');
501
+ this.mouseMoveOverlay.className = 'yt-mousemove-overlay';
502
+ this.mouseMoveOverlay.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;z-index:2;background:transparent;pointer-events:auto;cursor:default;';
503
+
504
+ this.api.container.insertBefore(this.mouseMoveOverlay, this.api.controls);
505
+
506
+ // Pass mousemove to core player WITHOUT constantly resetting timer
507
+ this.mouseMoveOverlay.addEventListener('mousemove', (e) => {
508
+ // Let the core handle mousemove - it has its own autoHide logic
509
+ if (this.api.player.onMouseMove) {
510
+ this.api.player.onMouseMove(e);
511
+ }
512
+ });
513
+
514
+ this.mouseMoveOverlay.addEventListener('click', (e) => {
515
+ const doubleTap = this.api.player.options.doubleTapPause;
516
+ const pauseClick = this.api.player.options.pauseClick;
517
+
518
+ if (doubleTap) {
519
+ let controlsHidden = false;
520
+
521
+ if (this.api.controls) {
522
+ controlsHidden = this.api.controls.classList.contains('hide');
523
+ }
524
+
525
+ if (!controlsHidden) {
526
+ const controls = this.player.container.querySelector('.controls');
527
+ if (controls) {
528
+ controlsHidden = controls.classList.contains('hide');
529
+ }
530
+ }
531
+
532
+ if (!controlsHidden && this.api.controls) {
533
+ const style = window.getComputedStyle(this.api.controls);
534
+ controlsHidden = style.opacity === '0' || style.visibility === 'hidden';
535
+ }
536
+
537
+ if (controlsHidden) {
538
+ if (this.api.player.showControlsNow) {
539
+ this.api.player.showControlsNow();
540
+ }
541
+ if (this.api.player.resetAutoHideTimer) {
542
+ this.api.player.resetAutoHideTimer();
543
+ }
544
+ return;
545
+ }
546
+
547
+ // Controls visible: toggle play/pause
548
+ this.togglePlayPauseYT();
549
+ } else if (pauseClick) {
550
+ // Always toggle on click when pauseClick is enabled
551
+ this.togglePlayPauseYT();
552
+ }
553
+ });
554
+ }
555
+
556
+ togglePlayPauseYT() {
557
+ if (!this.ytPlayer) return;
558
+
559
+ try {
560
+ const state = this.ytPlayer.getPlayerState();
561
+
562
+ if (state === YT.PlayerState.PLAYING) {
563
+ this.ytPlayer.pauseVideo();
564
+ } else if (state === YT.PlayerState.PAUSED ||
565
+ state === YT.PlayerState.CUED ||
566
+ state === YT.PlayerState.UNSTARTED ||
567
+ state === -1) {
568
+ // Handle all non-playing states including initial/unstarted
569
+ this.ytPlayer.playVideo();
570
+ }
571
+ } catch (error) {
572
+ if (this.api.player.options.debug) console.error('[YT Plugin] Error toggling play/pause:', error);
573
+ }
574
+ }
575
+
576
+ removeMouseMoveOverlay() {
577
+ if (this.mouseMoveOverlay) {
578
+ this.mouseMoveOverlay.remove();
579
+ this.mouseMoveOverlay = null;
580
+ }
581
+ }
582
+
583
+ hidePosterOverlay() {
584
+ const posterOverlay = this.api.container.querySelector('.video-poster-overlay');
585
+ if (posterOverlay) {
586
+ posterOverlay.classList.add('hidden');
587
+ posterOverlay.classList.remove('visible');
588
+ }
589
+ }
590
+
591
+ showPosterOverlay() {
592
+ const posterOverlay = this.api.container.querySelector('.video-poster-overlay');
593
+ if (posterOverlay && this.api.player.options.poster) {
594
+ posterOverlay.classList.remove('hidden');
595
+ posterOverlay.classList.add('visible');
596
+ }
597
+ }
598
+
599
+ hideInitialLoading() {
600
+ const initialLoading = this.api.container.querySelector('.initial-loading');
601
+ if (initialLoading) initialLoading.style.display = 'none';
602
+ }
603
+
604
+ hideLoadingOverlay() {
605
+ const loadingOverlay = this.api.container.querySelector('.loading-overlay');
606
+ if (loadingOverlay) {
607
+ loadingOverlay.classList.remove('show');
608
+ loadingOverlay.style.display = 'none';
609
+ }
610
+ }
611
+
291
612
  onPlayerReady(event) {
292
- this.api.debug('YouTube player ready');
613
+ if (this.api.player.options.debug) console.log('[YT Plugin] Player ready event fired');
614
+ // Force visibility
615
+ this.api.video.style.removeProperty('display');
616
+ this.ytPlayerContainer.style.display = 'block';
617
+ this.ytPlayerContainer.style.visibility = 'visible';
618
+ this.ytPlayerContainer.style.opacity = '1';
619
+ this.ytPlayerContainer.style.zIndex = '2';
620
+ if (this.api.player.options.debug) console.log('[YT Plugin] ✅ Forced container visibility');
621
+ // Force visibility with !important via CSS injection
622
+ const forceVisibilityCSS = document.createElement('style');
623
+ forceVisibilityCSS.id = 'yt-force-visibility-' + this.player.video.id;
624
+ forceVisibilityCSS.textContent = `
625
+ #yt-player-container-${this.player.video.id} {
626
+ display: block !important;
627
+ visibility: visible !important;
628
+ opacity: 1 !important;
629
+ z-index: 2 !important;
630
+ }
631
+ `;
632
+ document.head.appendChild(forceVisibilityCSS);
633
+ if (this.api.player.options.debug) console.log('[YT Plugin] 🎨 CSS force visibility injected');
634
+
635
+ this.hideLoadingOverlay();
636
+ this.hideInitialLoading();
637
+ this.injectYouTubeCSSOverride();
293
638
 
294
- // Sync controls with YouTube player
295
639
  this.syncControls();
296
640
 
297
- // Set quality if specified
641
+ // Handle responsive layout for PiP and subtitles buttons
642
+ this.handleResponsiveLayout();
643
+
644
+ // Hide PiP from settings menu (separate function, called after responsive layout)
645
+ setTimeout(() => this.hidePipFromSettingsMenuOnly(), 500);
646
+ setTimeout(() => this.hidePipFromSettingsMenuOnly(), 1500);
647
+ setTimeout(() => this.hidePipFromSettingsMenuOnly(), 3000);
648
+
649
+ // Listen for window resize
650
+ if (!this.resizeListenerAdded) {
651
+ window.addEventListener('resize', () => this.handleResponsiveLayout());
652
+ this.resizeListenerAdded = true;
653
+ }
654
+
655
+
656
+ // Load qualities with multiple attempts
657
+ setTimeout(() => this.loadAvailableQualities(), 500);
658
+ setTimeout(() => this.loadAvailableQualities(), 1000);
659
+ setTimeout(() => this.loadAvailableQualities(), 2000);
660
+
661
+ // Load captions with multiple attempts
662
+ setTimeout(() => this.loadAvailableCaptions(), 500);
663
+ setTimeout(() => this.loadAvailableCaptions(), 1000);
664
+ setTimeout(() => this.loadAvailableCaptions(), 2000);
665
+
298
666
  if (this.options.quality && this.options.quality !== 'default') {
299
- this.ytPlayer.setPlaybackQuality(this.options.quality);
667
+ setTimeout(() => this.setQuality(this.options.quality), 1000);
300
668
  }
301
669
 
302
670
  this.api.triggerEvent('youtubeplugin:playerready', {});
303
671
  }
304
672
 
305
- /**
306
- * YouTube player state change callback
307
- */
308
- onPlayerStateChange(event) {
309
- switch (event.data) {
310
- case YT.PlayerState.PLAYING:
311
- this.api.triggerEvent('played', {});
312
- break;
313
- case YT.PlayerState.PAUSED:
314
- this.api.triggerEvent('paused', {});
315
- break;
316
- case YT.PlayerState.ENDED:
317
- this.api.triggerEvent('ended', {});
318
- break;
319
- case YT.PlayerState.BUFFERING:
320
- this.api.debug('YouTube player buffering');
321
- break;
322
- }
673
+ onApiChange(event) {
674
+ if (this.api.player.options.debug) console.log('[YT Plugin] API changed event - loading captions');
675
+ setTimeout(() => this.loadAvailableCaptions(), 500);
323
676
  }
324
677
 
325
- /**
326
- * YouTube player error callback
327
- */
328
- onPlayerError(event) {
329
- const errorMessages = {
330
- 2: 'Invalid video ID',
331
- 5: 'HTML5 player error',
332
- 100: 'Video not found or private',
333
- 101: 'Video not allowed to be played in embedded players',
334
- 150: 'Video not allowed to be played in embedded players'
335
- };
678
+ injectYouTubeCSSOverride() {
679
+ if (document.getElementById('youtube-controls-override')) return;
336
680
 
337
- const errorMsg = errorMessages[event.data] || 'Unknown error';
338
- this.api.debug('YouTube player error: ' + errorMsg);
681
+ const style = document.createElement('style');
682
+ style.id = 'youtube-controls-override';
683
+ style.textContent = `
684
+ .video-wrapper.youtube-active .quality-control,
685
+ .video-wrapper.youtube-active .subtitles-control {
686
+ display: block !important;
687
+ visibility: visible !important;
688
+ opacity: 1 !important;
689
+ }
690
+ `;
691
+ document.head.appendChild(style);
692
+ this.api.container.classList.add('youtube-active');
693
+ if (this.api.player.options.debug) console.log('[YT Plugin] CSS override injected');
694
+ }
339
695
 
340
- this.api.triggerEvent('youtubeplugin:error', {
341
- errorCode: event.data,
342
- errorMessage: errorMsg
343
- });
696
+ // ===== QUALITY CONTROL METHODS =====
697
+
698
+ loadAvailableQualities() {
699
+ if (!this.ytPlayer || !this.ytPlayer.getAvailableQualityLevels) {
700
+ if (this.api.player.options.debug) console.log('[YT Plugin] Player not ready for quality check');
701
+ return;
702
+ }
703
+
704
+ const rawQualities = this.ytPlayer.getAvailableQualityLevels();
705
+ if (this.api.player.options.debug) console.log('[YT Plugin] Raw qualities from YouTube:', rawQualities);
706
+
707
+ if (rawQualities && rawQualities.length > 0) {
708
+ const uniqueQualities = rawQualities.filter((q, index, self) => {
709
+ return q !== 'auto' && self.indexOf(q) === index;
710
+ });
711
+
712
+ this.availableQualities = uniqueQualities.map(quality => ({
713
+ id: quality,
714
+ label: this.getQualityLabel(quality),
715
+ value: quality
716
+ }));
717
+
718
+ if (this.api.player.options.debug) console.log('[YT Plugin] ✅ Qualities loaded:', this.availableQualities);
719
+ this.api.triggerEvent('youtubeplugin:qualitiesloaded', { qualities: this.availableQualities });
720
+ this.injectFakeQualities();
721
+ this.createQualityControl();
722
+ this.startQualityMonitoring();
723
+ } else if (this.qualityCheckAttempts < 10) {
724
+ this.qualityCheckAttempts++;
725
+ if (this.api.player.options.debug) console.log(`[YT Plugin] Retry quality load (${this.qualityCheckAttempts}/10)`);
726
+ setTimeout(() => this.loadAvailableQualities(), 1000);
727
+ }
344
728
  }
345
729
 
346
- /**
347
- * Sync player controls with YouTube player
348
- */
349
- syncControls() {
350
- // Override play/pause methods
351
- const originalPlay = this.player.play;
352
- const originalPause = this.player.pause;
730
+ startQualityMonitoring() {
731
+ if (this.qualityMonitorInterval) {
732
+ clearInterval(this.qualityMonitorInterval);
733
+ }
353
734
 
354
- this.player.play = () => {
355
- if (this.ytPlayer && this.ytPlayer.playVideo) {
356
- this.ytPlayer.playVideo();
357
- } else {
358
- originalPlay.call(this.player);
359
- }
360
- };
735
+ this.qualityMonitorInterval = setInterval(() => {
736
+ if (!this.ytPlayer || !this.ytPlayer.getPlaybackQuality) return;
361
737
 
362
- this.player.pause = () => {
363
- if (this.ytPlayer && this.ytPlayer.pauseVideo) {
364
- this.ytPlayer.pauseVideo();
365
- } else {
366
- originalPause.call(this.player);
367
- }
368
- };
738
+ const actualQuality = this.ytPlayer.getPlaybackQuality();
369
739
 
370
- // Sync time updates
371
- if (this.ytPlayer) {
372
- setInterval(() => {
373
- if (this.ytPlayer && this.ytPlayer.getCurrentTime) {
374
- const currentTime = this.ytPlayer.getCurrentTime();
375
- const duration = this.ytPlayer.getDuration();
740
+ // Only update UI if in AUTO mode, otherwise respect manual selection
741
+ if (this.currentQuality === 'default' || this.currentQuality === 'auto') {
742
+ if (actualQuality !== this.currentPlayingQuality) {
743
+ this.currentPlayingQuality = actualQuality;
744
+ if (this.api.player.options.debug) {
745
+ console.log('[YT Plugin] Playing quality changed to:', actualQuality);
746
+ }
747
+ this.updateQualityMenuPlayingState(actualQuality);
748
+ }
376
749
 
377
- // Update progress bar if available
378
- if (this.api.player.progressFilled && duration) {
379
- const progress = (currentTime / duration) * 100;
380
- this.api.player.progressFilled.style.width = progress + '%';
381
- if (this.api.player.progressHandle) {
382
- this.api.player.progressHandle.style.left = progress + '%';
383
- }
750
+ const qualityLabel = this.getQualityLabel(actualQuality);
751
+ this.updateQualityButtonDisplay('Auto', qualityLabel);
752
+ }
753
+ // If manual quality selected, keep showing what user chose
754
+ else {
755
+ // Still monitor actual quality but don't change UI
756
+ if (actualQuality !== this.currentPlayingQuality) {
757
+ this.currentPlayingQuality = actualQuality;
758
+ if (this.api.player.options.debug) {
759
+ console.log('[YT Plugin] Actual playing quality:', actualQuality, '(requested:', this.currentQuality + ')');
384
760
  }
385
761
  }
386
- }, 250);
387
- }
762
+ }
763
+ }, 2000);
388
764
  }
389
765
 
390
- /**
391
- * Dispose plugin
392
- */
393
- dispose() {
394
- this.api.debug('YouTube plugin disposed');
766
+ createQualityControl() {
767
+ let qualityControl = this.api.container.querySelector('.quality-control');
768
+ if (qualityControl) {
769
+ if (this.api.player.options.debug) console.log('[YT Plugin] Quality control already exists');
770
+ this.updatePlayerQualityMenu();
771
+ return;
772
+ }
395
773
 
396
- // Destroy YouTube player
397
- if (this.ytPlayer && this.ytPlayer.destroy) {
398
- this.ytPlayer.destroy();
399
- this.ytPlayer = null;
774
+ const controlsRight = this.api.container.querySelector('.controls-right');
775
+ if (!controlsRight) {
776
+ if (this.api.player.options.debug) console.error('[YT Plugin] controls-right not found!');
777
+ return;
400
778
  }
401
779
 
402
- // Remove container
403
- if (this.ytPlayerContainer) {
404
- this.ytPlayerContainer.remove();
405
- this.ytPlayerContainer = null;
780
+ const qualityHTML = `
781
+ <div class="quality-control">
782
+ <button class="control-btn quality-btn" data-tooltip="Video Quality">
783
+ <div class="quality-btn-text">
784
+ <div class="selected-quality">Auto</div>
785
+ <div class="current-quality"></div>
786
+ </div>
787
+ </button>
788
+ <div class="quality-menu"></div>
789
+ </div>
790
+ `;
791
+
792
+ const fullscreenBtn = controlsRight.querySelector('.fullscreen-btn');
793
+ if (fullscreenBtn) {
794
+ fullscreenBtn.insertAdjacentHTML('beforebegin', qualityHTML);
795
+ } else {
796
+ controlsRight.insertAdjacentHTML('beforeend', qualityHTML);
406
797
  }
407
798
 
408
- // Show native video element again
409
- if (this.api.video) {
410
- this.api.video.style.display = '';
799
+ if (this.api.player.options.debug) console.log('[YT Plugin] Quality control created in DOM');
800
+ this.updatePlayerQualityMenu();
801
+ }
802
+
803
+ injectFakeQualities() {
804
+ if (!this.player.qualities) this.player.qualities = [];
805
+ if (this.player.qualities.length === 0) {
806
+ this.availableQualities.forEach(quality => {
807
+ this.player.qualities.push({
808
+ quality: quality.label,
809
+ src: `youtube:${this.videoId}:${quality.value}`,
810
+ label: quality.label,
811
+ type: 'video/youtube'
812
+ });
813
+ });
814
+ if (this.api.player.options.debug) console.log('[YT Plugin] Fake qualities injected:', this.player.qualities.length);
815
+ }
816
+ }
817
+
818
+ updatePlayerQualityMenu() {
819
+ const qualityMenu = this.api.container.querySelector('.quality-menu');
820
+ if (!qualityMenu) {
821
+ if (this.api.player.options.debug) console.error('[YT Plugin] Quality menu not found');
822
+ return;
411
823
  }
824
+
825
+ if (this.api.player.options.debug) console.log('[YT Plugin] Updating quality menu...');
826
+ qualityMenu.innerHTML = '';
827
+
828
+ // Add "Auto" option
829
+ const autoItem = document.createElement('div');
830
+ autoItem.className = 'quality-option selected';
831
+ autoItem.textContent = 'Auto';
832
+ autoItem.dataset.quality = 'auto';
833
+ autoItem.addEventListener('click', () => {
834
+ this.setQuality('default');
835
+ this.updateQualityMenuActiveState('auto');
836
+ this.updateQualityButtonDisplay('Auto', '');
837
+ });
838
+ qualityMenu.appendChild(autoItem);
839
+
840
+ // Add quality options
841
+ this.availableQualities.forEach(quality => {
842
+ const menuItem = document.createElement('div');
843
+ menuItem.className = 'quality-option';
844
+ menuItem.textContent = quality.label;
845
+ menuItem.dataset.quality = quality.value;
846
+ menuItem.addEventListener('click', () => {
847
+ this.setQuality(quality.value);
848
+ this.updateQualityMenuActiveState(quality.value);
849
+ this.updateQualityButtonDisplay(quality.label, '');
850
+ });
851
+ qualityMenu.appendChild(menuItem);
852
+ });
853
+
854
+ if (this.api.player.options.debug) console.log('[YT Plugin] Quality menu updated');
855
+ }
856
+
857
+ updateQualityMenuActiveState(qualityValue) {
858
+ const qualityMenu = this.api.container.querySelector('.quality-menu');
859
+ if (!qualityMenu) return;
860
+
861
+ qualityMenu.querySelectorAll('.quality-option').forEach(item => {
862
+ if (item.dataset.quality === qualityValue) {
863
+ item.classList.add('selected');
864
+ } else {
865
+ item.classList.remove('selected');
866
+ }
867
+ });
868
+ }
869
+
870
+ updateQualityMenuPlayingState(actualQuality) {
871
+ const qualityMenu = this.api.container.querySelector('.quality-menu');
872
+ if (!qualityMenu) return;
873
+
874
+ qualityMenu.querySelectorAll('.quality-option').forEach(item => {
875
+ if (item.dataset.quality === actualQuality) {
876
+ item.classList.add('playing');
877
+ } else {
878
+ item.classList.remove('playing');
879
+ }
880
+ });
881
+ }
882
+
883
+ updateQualityButtonDisplay(topText, bottomText) {
884
+ const selectedQualityEl = this.api.container.querySelector('.selected-quality');
885
+ const currentQualityEl = this.api.container.querySelector('.current-quality');
886
+
887
+ if (selectedQualityEl) selectedQualityEl.textContent = topText;
888
+ if (currentQualityEl) currentQualityEl.textContent = bottomText;
889
+ }
890
+
891
+ getQualityLabel(quality) {
892
+ const qualityLabels = {
893
+ 'highres': '4K',
894
+ 'hd2160': '4K',
895
+ 'hd1440': '2K',
896
+ 'hd1080': '1080p',
897
+ 'hd720': '720p',
898
+ 'large': '480p',
899
+ 'medium': '360p',
900
+ 'small': '240p',
901
+ 'tiny': '144p',
902
+ 'auto': 'Auto'
903
+ };
904
+ return qualityLabels[quality] || quality.toUpperCase();
905
+ }
906
+
907
+ getAvailableQualities() {
908
+ return this.availableQualities;
909
+ }
910
+
911
+ getCurrentQuality() {
912
+ if (!this.ytPlayer || !this.ytPlayer.getPlaybackQuality) return this.currentQuality;
913
+ this.currentQuality = this.ytPlayer.getPlaybackQuality();
914
+ return this.currentQuality;
915
+ }
916
+
917
+ setQuality(quality) {
918
+ if (!this.ytPlayer || !this.ytPlayer.setPlaybackQuality) return false;
919
+
920
+ try {
921
+ // Try multiple methods to force quality change
922
+ this.ytPlayer.setPlaybackQuality(quality);
923
+
924
+ // Also try setPlaybackQualityRange if available
925
+ if (this.ytPlayer.setPlaybackQualityRange) {
926
+ this.ytPlayer.setPlaybackQualityRange(quality, quality);
927
+ }
928
+
929
+ // Update state
930
+ this.currentQuality = quality;
931
+ this.currentPlayingQuality = quality; // Force UI update
932
+
933
+ // Force UI update immediately
934
+ this.updateQualityMenuPlayingState(quality);
935
+
936
+ // Update button display
937
+ const qualityLabel = this.getQualityLabel(quality);
938
+ this.updateQualityButtonDisplay(qualityLabel, '');
939
+
940
+ if (this.api.player.options.debug) {
941
+ console.log('[YT Plugin] Quality set to:', quality);
942
+ }
943
+
944
+ this.api.triggerEvent('youtubeplugin:qualitychanged', { quality });
945
+ return true;
946
+ } catch (error) {
947
+ if (this.api.player.options.debug) {
948
+ console.error('[YT Plugin] Error setting quality:', error);
949
+ }
950
+ return false;
951
+ }
952
+ }
953
+
954
+ // ===== SUBTITLES CONTROL METHODS =====
955
+
956
+ /**
957
+ * Load available captions and create menu
958
+ */
959
+ loadAvailableCaptions() {
960
+ if (!this.ytPlayer || !this.options.enableCaptions) return;
961
+
962
+ // Prevent creating menu multiple times
963
+ if (this.subtitlesMenuCreated) {
964
+ if (this.api.player.options.debug) console.log('[YT Plugin] Subtitles menu already created, skipping');
965
+ return;
966
+ }
967
+
968
+ try {
969
+ let captionModule = null;
970
+ try {
971
+ captionModule = this.ytPlayer.getOption('captions', 'tracklist');
972
+ if (this.api.player.options.debug) console.log('[YT Plugin] Captions tracklist:', captionModule);
973
+ } catch (e) {
974
+ if (this.api.player.options.debug) console.log('[YT Plugin] Captions module error:', e.message);
975
+ }
976
+
977
+ // FIXED: If tracklist is available and populated, use it
978
+ if (captionModule && Array.isArray(captionModule) && captionModule.length > 0) {
979
+ this.availableCaptions = captionModule.map((track, index) => {
980
+ const isAutomatic = track.kind === 'asr' || track.isautomatic || track.kind === 'auto';
981
+ const languageName = track.languageName || track.displayName || track.name?.simpleText || track.languageCode || 'Unknown';
982
+ return {
983
+ index: index,
984
+ languageCode: track.languageCode || track.vssid || track.id,
985
+ languageName: isAutomatic ? `${languageName} (Auto)` : languageName,
986
+ label: isAutomatic ? `${languageName} (Auto)` : languageName,
987
+ isAutomatic: isAutomatic
988
+ };
989
+ });
990
+
991
+ if (this.api.player.options.debug) console.log('[YT Plugin] ✅ Captions loaded:', this.availableCaptions);
992
+ this.createSubtitlesControl(true); // true = has tracklist
993
+ this.subtitlesMenuCreated = true;
994
+
995
+ } else if (this.captionCheckAttempts < 5) {
996
+ // Retry if tracklist not yet available
997
+ this.captionCheckAttempts++;
998
+ if (this.api.player.options.debug) console.log(`[YT Plugin] Retry caption load (${this.captionCheckAttempts}/5)`);
999
+ setTimeout(() => this.loadAvailableCaptions(), 1000);
1000
+
1001
+ } else {
1002
+ // FIXED: After 5 attempts without tracklist, use Off/On (Auto) buttons
1003
+ if (this.api.player.options.debug) console.log('[YT Plugin] No tracklist found - using Off/On (Auto)');
1004
+ this.availableCaptions = []; // Empty tracklist
1005
+ this.createSubtitlesControl(false); // false = no tracklist, use On/Off buttons
1006
+ this.subtitlesMenuCreated = true;
1007
+ }
1008
+
1009
+ } catch (error) {
1010
+ if (this.api.player.options.debug) console.error('[YT Plugin] Error loading captions:', error);
1011
+ this.createSubtitlesControl(false); // Fallback to On/Off buttons
1012
+ this.subtitlesMenuCreated = true;
1013
+ }
1014
+ }
1015
+
1016
+ /**
1017
+ * Create subtitles control button and menu
1018
+ * @param {boolean} hasTracklist - true if YouTube provides caption tracks, false for auto captions only
1019
+ */
1020
+ createSubtitlesControl(hasTracklist) {
1021
+ let subtitlesControl = this.api.container.querySelector('.subtitles-control');
1022
+ if (subtitlesControl) {
1023
+ if (this.api.player.options.debug) console.log('[YT Plugin] Subtitles control exists - updating menu');
1024
+ this.populateSubtitlesMenu(hasTracklist);
1025
+ return;
1026
+ }
1027
+
1028
+ const controlsRight = this.api.container.querySelector('.controls-right');
1029
+ if (!controlsRight) return;
1030
+
1031
+ const subtitlesHTML = `
1032
+ <div class="subtitles-control">
1033
+ <button class="control-btn subtitles-btn" data-tooltip="Subtitles">
1034
+ <span class="icon">CC</span>
1035
+ </button>
1036
+ <div class="subtitles-menu"></div>
1037
+ </div>
1038
+ `;
1039
+
1040
+ const qualityControl = controlsRight.querySelector('.quality-control');
1041
+ if (qualityControl) {
1042
+ qualityControl.insertAdjacentHTML('beforebegin', subtitlesHTML);
1043
+ } else {
1044
+ const fullscreenBtn = controlsRight.querySelector('.fullscreen-btn');
1045
+ if (fullscreenBtn) {
1046
+ fullscreenBtn.insertAdjacentHTML('beforebegin', subtitlesHTML);
1047
+ } else {
1048
+ controlsRight.insertAdjacentHTML('beforeend', subtitlesHTML);
1049
+ }
1050
+ }
1051
+
1052
+ if (this.api.player.options.debug) console.log('[YT Plugin] Subtitles control created');
1053
+ this.populateSubtitlesMenu(hasTracklist);
1054
+ this.bindSubtitlesButton();
1055
+ this.checkInitialCaptionState();
1056
+ this.startCaptionStateMonitoring();
1057
+ }
1058
+
1059
+ /**
1060
+ * Populate subtitles menu with tracks or On/Off buttons
1061
+ * FIXED: Correctly handles both scenarios
1062
+ * @param {boolean} hasTracklist - true if tracks available, false for auto captions only
1063
+ */
1064
+ populateSubtitlesMenu(hasTracklist) {
1065
+ const subtitlesMenu = this.api.container.querySelector('.subtitles-menu');
1066
+ if (!subtitlesMenu) return;
1067
+ subtitlesMenu.innerHTML = '';
1068
+
1069
+ // OFF option
1070
+ const offItem = document.createElement('div');
1071
+ offItem.className = 'subtitles-option';
1072
+ offItem.textContent = 'Off';
1073
+ offItem.dataset.track = 'off';
1074
+ offItem.addEventListener('click', (e) => {
1075
+ e.stopPropagation();
1076
+ this.disableCaptions();
1077
+ });
1078
+ subtitlesMenu.appendChild(offItem);
1079
+
1080
+ // Show available caption tracks if any
1081
+ if (this.availableCaptions.length > 0) {
1082
+ this.availableCaptions.forEach(caption => {
1083
+ const menuItem = document.createElement('div');
1084
+ menuItem.className = 'subtitles-option';
1085
+ menuItem.textContent = caption.label;
1086
+ menuItem.addEventListener('click', () => this.setCaptions(caption.index));
1087
+ menuItem.dataset.track = caption.index;
1088
+ menuItem.dataset.languageCode = caption.languageCode;
1089
+ // Display only - no click handler
1090
+ subtitlesMenu.appendChild(menuItem);
1091
+ });
1092
+ } else {
1093
+ // No tracklist - show ON (Auto) option
1094
+ const onItem = document.createElement('div');
1095
+ onItem.className = 'subtitles-option';
1096
+ onItem.textContent = 'On (Auto)';
1097
+ onItem.dataset.track = 'auto';
1098
+ onItem.addEventListener('click', (e) => {
1099
+ e.stopPropagation();
1100
+ this.enableAutoCaptions();
1101
+ });
1102
+ subtitlesMenu.appendChild(onItem);
1103
+ }
1104
+ }
1105
+
1106
+ bindSubtitlesButton() {
1107
+ const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
1108
+ if (!subtitlesBtn) return;
1109
+
1110
+ // Remove existing event listeners by cloning
1111
+ const newBtn = subtitlesBtn.cloneNode(true);
1112
+ subtitlesBtn.parentNode.replaceChild(newBtn, subtitlesBtn);
1113
+
1114
+ newBtn.addEventListener('click', (e) => {
1115
+ e.stopPropagation();
1116
+ this.toggleCaptions();
1117
+ });
1118
+ }
1119
+
1120
+ checkInitialCaptionState() {
1121
+ setTimeout(() => {
1122
+ try {
1123
+ const currentTrack = this.ytPlayer.getOption('captions', 'track');
1124
+ if (currentTrack && currentTrack.languageCode) {
1125
+ this.captionsEnabled = true;
1126
+ const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
1127
+ if (subtitlesBtn) subtitlesBtn.classList.add('active');
1128
+ this.updateSubtitlesMenuActiveState();
1129
+ }
1130
+ } catch (e) {
1131
+ // Ignore errors
1132
+ }
1133
+ }, 1500);
1134
+ }
1135
+
1136
+ startCaptionStateMonitoring() {
1137
+ if (this.captionStateCheckInterval) {
1138
+ clearInterval(this.captionStateCheckInterval);
1139
+ }
1140
+
1141
+ this.captionStateCheckInterval = setInterval(() => {
1142
+ try {
1143
+ const currentTrack = this.ytPlayer.getOption('captions', 'track');
1144
+ const wasEnabled = this.captionsEnabled;
1145
+
1146
+ this.captionsEnabled = !!(currentTrack && currentTrack.languageCode);
1147
+
1148
+ if (wasEnabled !== this.captionsEnabled) {
1149
+ const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
1150
+ if (subtitlesBtn) {
1151
+ if (this.captionsEnabled) {
1152
+ subtitlesBtn.classList.add('active');
1153
+ } else {
1154
+ subtitlesBtn.classList.remove('active');
1155
+ }
1156
+ }
1157
+ this.updateSubtitlesMenuActiveState();
1158
+ }
1159
+ } catch (e) {
1160
+ // Ignore errors
1161
+ }
1162
+ }, 1000);
1163
+ }
1164
+
1165
+ updateSubtitlesMenuActiveState() {
1166
+ const subtitlesMenu = this.api.container.querySelector('.subtitles-menu');
1167
+ if (!subtitlesMenu) return;
1168
+
1169
+ // Use 'selected' class (NOT 'active' - that causes repopulation)
1170
+ subtitlesMenu.querySelectorAll('.subtitles-option').forEach(item => {
1171
+ item.classList.remove('selected');
1172
+ });
1173
+
1174
+ subtitlesMenu.querySelectorAll('.subtitles-option').forEach(item => {
1175
+ const itemTrack = item.dataset.track;
1176
+
1177
+ if (!this.captionsEnabled && itemTrack === 'off') {
1178
+ item.classList.add('selected');
1179
+ } else if (this.captionsEnabled && itemTrack === String(this.currentCaption)) {
1180
+ item.classList.add('selected');
1181
+ } else if (this.captionsEnabled && itemTrack === 'auto' && this.currentCaption === null) {
1182
+ item.classList.add('selected');
1183
+ }
1184
+ });
1185
+ }
1186
+
1187
+ /**
1188
+ * Enable specific caption track
1189
+ * @param {number} trackIndex - Index of the caption track
1190
+ */
1191
+ setCaptions(trackIndex) {
1192
+ if (!this.ytPlayer) return false;
1193
+
1194
+ try {
1195
+ const track = this.availableCaptions[trackIndex];
1196
+ if (!track) return false;
1197
+
1198
+ this.ytPlayer.setOption('captions', 'track', { languageCode: track.languageCode });
1199
+ this.captionsEnabled = true;
1200
+
1201
+ const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
1202
+ if (subtitlesBtn) subtitlesBtn.classList.add('active');
1203
+ this.updateSubtitlesMenuActiveState();
1204
+
1205
+ if (this.api.player.options.debug) console.log('[YT Plugin] Captions enabled:', track.label);
1206
+ return true;
1207
+ } catch (error) {
1208
+ if (this.api.player.options.debug) console.error('[YT Plugin] Error enabling captions:', error);
1209
+ return false;
1210
+ }
1211
+ }
1212
+
1213
+ setTranslatedCaptions(translationLanguageCode) {
1214
+ if (!this.ytPlayer) return false;
1215
+
1216
+ try {
1217
+ // First, disable current captions if any
1218
+ if (this.captionsEnabled) {
1219
+ this.ytPlayer.unloadModule('captions');
1220
+ }
1221
+
1222
+ // If no caption tracks exist, try to enable auto-generated captions
1223
+ if (this.availableCaptions.length === 0) {
1224
+ if (this.api.player.options.debug) {
1225
+ console.log('[YT Plugin] Enabling auto-generated captions with translation to:', translationLanguageCode);
1226
+ }
1227
+
1228
+ // Enable auto-generated captions with translation
1229
+ this.ytPlayer.setOption('captions', 'track', {
1230
+ translationLanguage: translationLanguageCode
1231
+ });
1232
+ this.ytPlayer.loadModule('captions');
1233
+ this.currentCaption = null;
1234
+ } else {
1235
+ // Use the first available caption track as base for translation
1236
+ const baseCaption = this.availableCaptions[0];
1237
+
1238
+ if (this.api.player.options.debug) {
1239
+ console.log('[YT Plugin] Translating from', baseCaption.languageCode, 'to', translationLanguageCode);
1240
+ }
1241
+
1242
+ // Set caption with translation
1243
+ this.ytPlayer.setOption('captions', 'track', {
1244
+ languageCode: baseCaption.languageCode,
1245
+ translationLanguage: translationLanguageCode
1246
+ });
1247
+ this.ytPlayer.loadModule('captions');
1248
+ this.currentCaption = baseCaption.index;
1249
+ }
1250
+
1251
+ // Update state
1252
+ this.captionsEnabled = true;
1253
+ this.currentTranslation = translationLanguageCode;
1254
+
1255
+ // Update UI
1256
+ const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
1257
+ if (subtitlesBtn) subtitlesBtn.classList.add('active');
1258
+
1259
+ // Update menu state
1260
+ this.updateSubtitlesMenuActiveState();
1261
+
1262
+ // Close the menu
1263
+ const subtitlesMenu = this.api.container.querySelector('.subtitles-menu');
1264
+ if (subtitlesMenu) {
1265
+ subtitlesMenu.classList.remove('show');
1266
+ }
1267
+
1268
+ if (this.api.player.options.debug) {
1269
+ console.log('[YT Plugin] ✅ Auto-translation enabled:', translationLanguageCode);
1270
+ }
1271
+
1272
+ this.api.triggerEvent('youtubeplugin:captionchanged', {
1273
+ translationLanguage: translationLanguageCode
1274
+ });
1275
+
1276
+ return true;
1277
+ } catch (error) {
1278
+ if (this.api.player.options.debug) {
1279
+ console.error('[YT Plugin] Error setting translated captions:', error);
1280
+ }
1281
+ return false;
1282
+ }
1283
+ }
1284
+
1285
+ /**
1286
+ * Enable automatic captions (when no tracklist available)
1287
+ */
1288
+ enableAutoCaptions() {
1289
+ if (!this.ytPlayer) return false;
1290
+
1291
+ try {
1292
+ this.ytPlayer.setOption('captions', 'reload', true);
1293
+ this.ytPlayer.loadModule('captions');
1294
+ this.captionsEnabled = true;
1295
+
1296
+ const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
1297
+ if (subtitlesBtn) subtitlesBtn.classList.add('active');
1298
+ this.updateSubtitlesMenuActiveState();
1299
+
1300
+ if (this.api.player.options.debug) console.log('[YT Plugin] Auto captions enabled');
1301
+ return true;
1302
+ } catch (error) {
1303
+ if (this.api.player.options.debug) console.error('[YT Plugin] Error enabling auto captions:', error);
1304
+ return false;
1305
+ }
1306
+ }
1307
+
1308
+ /**
1309
+ * Disable captions
1310
+ */
1311
+ disableCaptions() {
1312
+ if (!this.ytPlayer) return false;
1313
+
1314
+ try {
1315
+ this.ytPlayer.unloadModule('captions');
1316
+ this.captionsEnabled = false;
1317
+ this.currentCaption = null;
1318
+
1319
+ const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
1320
+ if (subtitlesBtn) subtitlesBtn.classList.remove('active');
1321
+
1322
+ this.updateSubtitlesMenuActiveState();
1323
+
1324
+ return true;
1325
+ } catch (error) {
1326
+ if (this.options.debug) console.error('[YT Plugin] Error:', error);
1327
+ return false;
1328
+ }
1329
+ }
1330
+
1331
+ /**
1332
+ * Toggle captions on/off
1333
+ */
1334
+ toggleCaptions() {
1335
+ if (this.captionsEnabled) {
1336
+ return this.disableCaptions();
1337
+ } else {
1338
+ // If there are tracks, enable the first one, otherwise enable auto captions
1339
+ if (this.availableCaptions.length > 0) {
1340
+ return this.setCaptions(0);
1341
+ } else {
1342
+ return this.enableAutoCaptions();
1343
+ }
1344
+ }
1345
+ }
1346
+
1347
+ getAvailableCaptions() {
1348
+ return this.availableCaptions;
1349
+ }
1350
+
1351
+ // ===== PLAYER EVENT HANDLERS =====
1352
+
1353
+ onPlayerStateChange(event) {
1354
+ const states = {
1355
+ '-1': 'UNSTARTED',
1356
+ '0': 'ENDED',
1357
+ '1': 'PLAYING',
1358
+ '2': 'PAUSED',
1359
+ '3': 'BUFFERING',
1360
+ '5': 'CUED'
1361
+ };
1362
+ if (this.api.player.options.debug) console.log('[YT Plugin] State:', states[event.data], event.data);
1363
+
1364
+ // Get play/pause icons
1365
+ const playIcon = this.api.container.querySelector('.play-icon');
1366
+ const pauseIcon = this.api.container.querySelector('.pause-icon');
1367
+
1368
+ switch (event.data) {
1369
+ case YT.PlayerState.PLAYING:
1370
+ this.api.triggerEvent('played', {});
1371
+ this.hideLoadingOverlay();
1372
+
1373
+ // Show pause icon, hide play icon
1374
+ if (playIcon && pauseIcon) {
1375
+ playIcon.classList.add('hidden');
1376
+ pauseIcon.classList.remove('hidden');
1377
+ }
1378
+
1379
+ if (this.availableQualities.length === 0) {
1380
+ this.loadAvailableQualities();
1381
+ }
1382
+ break;
1383
+
1384
+ case YT.PlayerState.PAUSED:
1385
+ case YT.PlayerState.CUED:
1386
+ case YT.PlayerState.UNSTARTED:
1387
+ this.api.triggerEvent('paused', {});
1388
+
1389
+ // Show play icon, hide pause icon
1390
+ if (playIcon && pauseIcon) {
1391
+ playIcon.classList.remove('hidden');
1392
+ pauseIcon.classList.add('hidden');
1393
+ }
1394
+ break;
1395
+
1396
+ case YT.PlayerState.ENDED:
1397
+ this.api.triggerEvent('ended', {});
1398
+
1399
+ // Show play icon (for replay)
1400
+ if (playIcon && pauseIcon) {
1401
+ playIcon.classList.remove('hidden');
1402
+ pauseIcon.classList.add('hidden');
1403
+ }
1404
+ break;
1405
+ }
1406
+ }
1407
+
1408
+ onPlaybackQualityChange(event) {
1409
+ this.currentQuality = event.data;
1410
+ if (this.api.player.options.debug) console.log('[YT Plugin] Quality changed to:', event.data);
1411
+ this.api.triggerEvent('youtubeplugin:qualitychanged', {
1412
+ quality: event.data,
1413
+ label: this.getQualityLabel(event.data)
1414
+ });
1415
+ }
1416
+
1417
+ onPlayerError(event) {
1418
+ const errorMessages = {
1419
+ 2: 'Invalid video ID',
1420
+ 5: 'HTML5 player error',
1421
+ 100: 'Video not found or private',
1422
+ 101: 'Video not allowed in embedded players',
1423
+ 150: 'Video not allowed in embedded players'
1424
+ };
1425
+ const errorMsg = errorMessages[event.data] || 'Unknown error';
1426
+ if (this.api.player.options.debug) console.error('[YT Plugin] Error:', errorMsg);
1427
+ this.api.triggerEvent('youtubeplugin:error', {
1428
+ errorCode: event.data,
1429
+ errorMessage: errorMsg
1430
+ });
1431
+ }
1432
+
1433
+ // ===== PLAYER CONTROL SYNC =====
1434
+
1435
+ syncControls() {
1436
+ if (this.api.player.options.debug) console.log('[YT Plugin] Syncing controls');
1437
+
1438
+ // Override play method
1439
+ const originalPlay = this.player.play;
1440
+ this.player.play = () => {
1441
+ if (this.ytPlayer && this.ytPlayer.playVideo) {
1442
+ this.ytPlayer.playVideo();
1443
+ } else {
1444
+ originalPlay.call(this.player);
1445
+ }
1446
+ };
1447
+
1448
+ // Override pause method
1449
+ const originalPause = this.player.pause;
1450
+ this.player.pause = () => {
1451
+ if (this.ytPlayer && this.ytPlayer.pauseVideo) {
1452
+ this.ytPlayer.pauseVideo();
1453
+ } else {
1454
+ originalPause.call(this.player);
1455
+ }
1456
+ };
1457
+
1458
+ // Override seek method
1459
+ const originalSetCurrentTime = this.player.setCurrentTime;
1460
+ this.player.seek = (time) => {
1461
+ if (this.ytPlayer && this.ytPlayer.seekTo) {
1462
+ this.ytPlayer.seekTo(time, true);
1463
+ } else if (originalSetCurrentTime) {
1464
+ originalSetCurrentTime.call(this.player, time);
1465
+ }
1466
+ };
1467
+
1468
+ const originalSeek = this.player.seek;
1469
+ this.player.seek = (time) => {
1470
+ if (this.ytPlayer && this.ytPlayer.seekTo) {
1471
+ this.ytPlayer.seekTo(time, true);
1472
+ } else if (originalSeek) {
1473
+ originalSeek.call(this.player, time);
1474
+ }
1475
+ };
1476
+
1477
+ // Ensure setCurrentTime also works
1478
+ this.player.setCurrentTime = (time) => {
1479
+ if (this.ytPlayer && this.ytPlayer.seekTo) {
1480
+ this.ytPlayer.seekTo(time, true);
1481
+ } else if (originalSetCurrentTime) {
1482
+ originalSetCurrentTime.call(this.player, time);
1483
+ }
1484
+ };
1485
+
1486
+ // Override volume control
1487
+ const originalUpdateVolume = this.player.updateVolume;
1488
+ this.player.updateVolume = (value) => {
1489
+ if (this.ytPlayer && this.ytPlayer.setVolume) {
1490
+ const volume = Math.max(0, Math.min(100, value));
1491
+ this.ytPlayer.setVolume(volume);
1492
+
1493
+ if (this.api.player.volumeSlider) {
1494
+ this.api.player.volumeSlider.value = volume;
1495
+ }
1496
+
1497
+ this.api.container.style.setProperty('--player-volume-fill', `${volume}%`);
1498
+
1499
+ if (volume > 0 && this.ytPlayer.isMuted && this.ytPlayer.isMuted()) {
1500
+ this.ytPlayer.unMute();
1501
+ this.updateMuteButtonState(false);
1502
+ } else if (volume === 0 && this.ytPlayer.isMuted && !this.ytPlayer.isMuted()) {
1503
+ this.ytPlayer.mute();
1504
+ this.updateMuteButtonState(true);
1505
+ } else {
1506
+ this.updateMuteButtonState(volume === 0);
1507
+ }
1508
+
1509
+ if (this.api.player.updateVolumeTooltip) {
1510
+ this.api.player.updateVolumeTooltip();
1511
+ }
1512
+ } else {
1513
+ originalUpdateVolume.call(this.player, value);
1514
+ }
1515
+ };
1516
+
1517
+ // Override mute toggle
1518
+ const originalToggleMute = this.player.toggleMute;
1519
+ this.player.toggleMute = () => {
1520
+ if (this.ytPlayer && this.ytPlayer.isMuted && this.ytPlayer.mute && this.ytPlayer.unMute) {
1521
+ const isMuted = this.ytPlayer.isMuted();
1522
+
1523
+ if (isMuted) {
1524
+ this.ytPlayer.unMute();
1525
+ } else {
1526
+ this.ytPlayer.mute();
1527
+ }
1528
+
1529
+ this.updateMuteButtonState(!isMuted);
1530
+
1531
+ if (!isMuted) {
1532
+ this.api.container.style.setProperty('--player-volume-fill', '0%');
1533
+ } else {
1534
+ const volume = this.ytPlayer.getVolume();
1535
+ this.api.container.style.setProperty('--player-volume-fill', `${volume}%`);
1536
+ }
1537
+ } else {
1538
+ originalToggleMute.call(this.player);
1539
+ }
1540
+ };
1541
+
1542
+ // Volume tooltip events for YouTube
1543
+ if (this.api.player.volumeSlider) {
1544
+ const volumeSlider = this.api.player.volumeSlider;
1545
+ const volumeContainer = this.api.container.querySelector('.volume-container');
1546
+
1547
+ // Remove existing listeners to avoid duplicates
1548
+ const newVolumeSlider = volumeSlider.cloneNode(true);
1549
+ volumeSlider.parentNode.replaceChild(newVolumeSlider, volumeSlider);
1550
+ this.api.player.volumeSlider = newVolumeSlider;
1551
+
1552
+ // Update tooltip on input (slider drag)
1553
+ newVolumeSlider.addEventListener('input', (e) => {
1554
+ const value = parseFloat(e.target.value);
1555
+ this.player.updateVolume(value);
1556
+
1557
+ // Update tooltip position and text during drag
1558
+ if (this.api.player.updateVolumeTooltipPosition) {
1559
+ this.api.player.updateVolumeTooltipPosition(value / 100);
1560
+ }
1561
+ if (this.api.player.updateVolumeTooltip) {
1562
+ this.api.player.updateVolumeTooltip();
1563
+ }
1564
+ });
1565
+
1566
+ // Update tooltip position on mousemove over slider
1567
+ newVolumeSlider.addEventListener('mousemove', (e) => {
1568
+ const rect = newVolumeSlider.getBoundingClientRect();
1569
+ const mouseX = e.clientX - rect.left;
1570
+ const percentage = Math.max(0, Math.min(1, mouseX / rect.width));
1571
+
1572
+ // Update tooltip position as mouse moves
1573
+ if (this.api.player.updateVolumeTooltipPosition) {
1574
+ this.api.player.updateVolumeTooltipPosition(percentage);
1575
+ }
1576
+
1577
+ // Update tooltip text to show value under mouse
1578
+ const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
1579
+ if (volumeTooltip) {
1580
+ volumeTooltip.textContent = Math.round(percentage * 100) + '%';
1581
+ }
1582
+ });
1583
+
1584
+ // Show/hide tooltip on hover
1585
+ if (volumeContainer) {
1586
+ volumeContainer.addEventListener('mouseenter', () => {
1587
+ const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
1588
+ if (volumeTooltip) {
1589
+ volumeTooltip.classList.add('visible');
1590
+ }
1591
+ });
1592
+
1593
+ volumeContainer.addEventListener('mouseleave', () => {
1594
+ const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
1595
+ if (volumeTooltip) {
1596
+ volumeTooltip.classList.remove('visible');
1597
+ }
1598
+ });
1599
+ }
1600
+
1601
+ if (this.api.player.options.debug) {
1602
+ console.log('[YT Plugin] Volume tooltip events bound');
1603
+ }
1604
+ }
1605
+
1606
+ // Override playback speed
1607
+ const originalChangeSpeed = this.player.changeSpeed;
1608
+ if (originalChangeSpeed) {
1609
+ this.player.changeSpeed = (e) => {
1610
+ if (!e.target.classList.contains('speed-option')) return;
1611
+
1612
+ const speed = parseFloat(e.target.getAttribute('data-speed'));
1613
+
1614
+ if (this.ytPlayer && this.ytPlayer.setPlaybackRate && speed > 0) {
1615
+ this.ytPlayer.setPlaybackRate(speed);
1616
+
1617
+ const speedBtn = this.api.container.querySelector('.speed-btn');
1618
+ if (speedBtn) speedBtn.textContent = `${speed}x`;
1619
+
1620
+ const speedMenu = this.api.container.querySelector('.speed-menu');
1621
+ if (speedMenu) {
1622
+ speedMenu.querySelectorAll('.speed-option').forEach(option => {
1623
+ option.classList.remove('active');
1624
+ });
1625
+ e.target.classList.add('active');
1626
+ }
1627
+ } else {
1628
+ originalChangeSpeed.call(this.player, e);
1629
+ }
1630
+ };
1631
+ }
1632
+
1633
+ // Override progress bar seeking
1634
+ if (this.api.player.progressContainer) {
1635
+ const progressContainer = this.api.player.progressContainer;
1636
+ const newProgressContainer = progressContainer.cloneNode(true);
1637
+ progressContainer.parentNode.replaceChild(newProgressContainer, progressContainer);
1638
+
1639
+ this.api.player.progressContainer = newProgressContainer;
1640
+ this.api.player.progressFilled = newProgressContainer.querySelector('.progress-filled');
1641
+ this.api.player.progressHandle = newProgressContainer.querySelector('.progress-handle');
1642
+ this.api.player.progressBuffer = newProgressContainer.querySelector('.progress-buffer');
1643
+
1644
+ // Create tooltip for seek preview
1645
+ const seekTooltip = document.createElement('div');
1646
+ seekTooltip.className = 'yt-seek-tooltip';
1647
+ seekTooltip.style.cssText = 'position:absolute;bottom:calc(100% + 10px);left:0;background:rgba(28,28,28,0.95);color:#fff;padding:6px 10px;border-radius:3px;font-size:13px;font-weight:500;white-space:nowrap;pointer-events:none;visibility:hidden;z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,0.3);';
1648
+ newProgressContainer.appendChild(seekTooltip);
1649
+
1650
+ // Format time function for tooltip
1651
+ const formatTimeForTooltip = (seconds) => {
1652
+ if (!seconds || isNaN(seconds)) return '0:00';
1653
+ const hrs = Math.floor(seconds / 3600);
1654
+ const mins = Math.floor((seconds % 3600) / 60);
1655
+ const secs = Math.floor(seconds % 60);
1656
+ if (hrs > 0) {
1657
+ return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
1658
+ }
1659
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
1660
+ };
1661
+
1662
+ let isSeeking = false;
1663
+
1664
+ const handleSeek = (e) => {
1665
+ if (!this.ytPlayer || !this.ytPlayer.getDuration) return;
1666
+
1667
+ const rect = newProgressContainer.getBoundingClientRect();
1668
+ const clickX = e.clientX - rect.left;
1669
+ const percentage = Math.max(0, Math.min(1, clickX / rect.width));
1670
+ const duration = this.ytPlayer.getDuration();
1671
+ const targetTime = percentage * duration;
1672
+
1673
+ this.ytPlayer.seekTo(targetTime, true);
1674
+
1675
+ const progress = percentage * 100 + '%';
1676
+ if (this.api.player.progressFilled) {
1677
+ this.api.player.progressFilled.style.width = progress;
1678
+ }
1679
+ if (this.api.player.progressHandle) {
1680
+ this.api.player.progressHandle.style.left = progress;
1681
+ }
1682
+ };
1683
+
1684
+ newProgressContainer.addEventListener('mousedown', (e) => {
1685
+ isSeeking = true;
1686
+ handleSeek(e);
1687
+ });
1688
+
1689
+ newProgressContainer.addEventListener('mousemove', (e) => {
1690
+ if (isSeeking) {
1691
+ handleSeek(e);
1692
+ }
1693
+
1694
+ // Show tooltip with timestamp
1695
+ if (!isSeeking && this.ytPlayer && this.ytPlayer.getDuration) {
1696
+ const rect = newProgressContainer.getBoundingClientRect();
1697
+ const mouseX = e.clientX - rect.left;
1698
+ const percentage = Math.max(0, Math.min(1, mouseX / rect.width));
1699
+ const duration = this.ytPlayer.getDuration();
1700
+ const time = percentage * duration;
1701
+
1702
+ seekTooltip.textContent = formatTimeForTooltip(time);
1703
+ seekTooltip.style.left = mouseX + 'px';
1704
+ seekTooltip.style.visibility = 'visible';
1705
+ }
1706
+ });
1707
+
1708
+ newProgressContainer.addEventListener('mouseleave', () => {
1709
+ seekTooltip.style.visibility = 'hidden';
1710
+ });
1711
+
1712
+ document.addEventListener('mouseup', () => {
1713
+ isSeeking = false;
1714
+ });
1715
+
1716
+ newProgressContainer.addEventListener('click', handleSeek);
1717
+ }
1718
+
1719
+ this.bindVolumeSlider();
1720
+
1721
+ // Time update interval
1722
+ if (this.timeUpdateInterval) {
1723
+ clearInterval(this.timeUpdateInterval);
1724
+ }
1725
+
1726
+ this.timeUpdateInterval = setInterval(() => {
1727
+ if (this.ytPlayer && this.ytPlayer.getCurrentTime && this.ytPlayer.getDuration) {
1728
+ const currentTime = this.ytPlayer.getCurrentTime();
1729
+ const duration = this.ytPlayer.getDuration();
1730
+
1731
+ if (this.api.player.progressFilled && duration) {
1732
+ const progress = (currentTime / duration) * 100;
1733
+ this.api.player.progressFilled.style.width = `${progress}%`;
1734
+ if (this.api.player.progressHandle) {
1735
+ this.api.player.progressHandle.style.left = `${progress}%`;
1736
+ }
1737
+ }
1738
+
1739
+ const currentTimeEl = this.api.container.querySelector('.current-time');
1740
+ const durationEl = this.api.container.querySelector('.duration');
1741
+
1742
+ if (currentTimeEl) currentTimeEl.textContent = this.formatTime(currentTime);
1743
+ if (durationEl && duration) durationEl.textContent = this.formatTime(duration);
1744
+ }
1745
+ }, 250);
1746
+ }
1747
+
1748
+ bindVolumeSlider() {
1749
+ const volumeSlider = this.api.container.querySelector('.volume-slider');
1750
+ if (!volumeSlider) return;
1751
+
1752
+ const newVolumeSlider = volumeSlider.cloneNode(true);
1753
+ volumeSlider.parentNode.replaceChild(newVolumeSlider, volumeSlider);
1754
+ this.api.player.volumeSlider = newVolumeSlider;
1755
+
1756
+ if (this.ytPlayer && this.ytPlayer.getVolume) {
1757
+ const initialVolume = this.ytPlayer.getVolume();
1758
+ newVolumeSlider.value = initialVolume;
1759
+ this.api.container.style.setProperty('--player-volume-fill', `${initialVolume}%`);
1760
+ }
1761
+
1762
+ // Handle volume changes with proper unmute logic
1763
+ newVolumeSlider.addEventListener('input', (e) => {
1764
+ const volume = parseFloat(e.target.value);
1765
+
1766
+ if (this.ytPlayer && this.ytPlayer.setVolume) {
1767
+ this.ytPlayer.setVolume(volume);
1768
+ this.api.container.style.setProperty('--player-volume-fill', `${volume}%`);
1769
+
1770
+ // Always update mute button state correctly
1771
+ if (volume > 0 && this.ytPlayer.isMuted && this.ytPlayer.isMuted()) {
1772
+ this.ytPlayer.unMute();
1773
+ this.updateMuteButtonState(false);
1774
+ } else if (volume === 0 && this.ytPlayer.isMuted && !this.ytPlayer.isMuted()) {
1775
+ this.ytPlayer.mute();
1776
+ this.updateMuteButtonState(true);
1777
+ } else {
1778
+ // Update button state even when not changing mute status
1779
+ this.updateMuteButtonState(volume === 0 || (this.ytPlayer.isMuted && this.ytPlayer.isMuted()));
1780
+ }
1781
+
1782
+ // Update tooltip position during drag
1783
+ if (this.api.player.updateVolumeTooltipPosition) {
1784
+ this.api.player.updateVolumeTooltipPosition(volume / 100);
1785
+ }
1786
+
1787
+ // Update tooltip position during drag
1788
+ if (this.api.player.updateVolumeTooltipPosition) {
1789
+ this.api.player.updateVolumeTooltipPosition(volume / 100);
1790
+ }
1791
+
1792
+ // Update tooltip text manually instead of using updateVolumeTooltip
1793
+ const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
1794
+ if (volumeTooltip) {
1795
+ volumeTooltip.textContent = Math.round(volume) + '%';
1796
+ }
1797
+
1798
+ }
1799
+ });
1800
+
1801
+ // Update tooltip position on mousemove
1802
+ newVolumeSlider.addEventListener('mousemove', (e) => {
1803
+ const rect = newVolumeSlider.getBoundingClientRect();
1804
+ const mouseX = e.clientX - rect.left;
1805
+ const percentage = Math.max(0, Math.min(1, mouseX / rect.width));
1806
+
1807
+ // Update tooltip position
1808
+ if (this.api.player.updateVolumeTooltipPosition) {
1809
+ this.api.player.updateVolumeTooltipPosition(percentage);
1810
+ }
1811
+
1812
+ // Update tooltip text to show value under mouse
1813
+ const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
1814
+ if (volumeTooltip) {
1815
+ volumeTooltip.textContent = Math.round(percentage * 100) + '%';
1816
+ }
1817
+ });
1818
+
1819
+ // Show/hide tooltip on hover
1820
+ const volumeContainer = this.api.container.querySelector('.volume-container');
1821
+ if (volumeContainer) {
1822
+ volumeContainer.addEventListener('mouseenter', () => {
1823
+ const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
1824
+ if (volumeTooltip) {
1825
+ volumeTooltip.classList.add('visible');
1826
+ }
1827
+ });
1828
+
1829
+ volumeContainer.addEventListener('mouseleave', () => {
1830
+ const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
1831
+ if (volumeTooltip) {
1832
+ volumeTooltip.classList.remove('visible');
1833
+ }
1834
+ });
1835
+ }
1836
+
1837
+ if (this.api.player.options.debug) {
1838
+ console.log('[YT Plugin] Volume slider bound with tooltip events');
1839
+ }
1840
+ }
1841
+
1842
+ updateMuteButtonState(isMuted) {
1843
+ const volumeIcon = this.api.container.querySelector('.volume-icon');
1844
+ const muteIcon = this.api.container.querySelector('.mute-icon');
1845
+
1846
+ if (volumeIcon && muteIcon) {
1847
+ if (isMuted) {
1848
+ volumeIcon.classList.add('hidden');
1849
+ muteIcon.classList.remove('hidden');
1850
+ } else {
1851
+ volumeIcon.classList.remove('hidden');
1852
+ muteIcon.classList.add('hidden');
1853
+ }
1854
+ }
1855
+ }
1856
+
1857
+ formatTime(seconds) {
1858
+ if (!seconds || isNaN(seconds)) return '0:00';
1859
+
1860
+ const hours = Math.floor(seconds / 3600);
1861
+ const minutes = Math.floor((seconds % 3600) / 60);
1862
+ const secs = Math.floor(seconds % 60);
1863
+
1864
+ if (hours > 0) {
1865
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
1866
+ } else {
1867
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
1868
+ }
1869
+ }
1870
+
1871
+ // ===== CLEANUP =====
1872
+
1873
+ dispose() {
1874
+ if (this.api.player.options.debug) console.log('[YT Plugin] Disposing');
1875
+
1876
+ if (this.timeUpdateInterval) {
1877
+ clearInterval(this.timeUpdateInterval);
1878
+ this.timeUpdateInterval = null;
1879
+ }
1880
+
1881
+ if (this.captionStateCheckInterval) {
1882
+ clearInterval(this.captionStateCheckInterval);
1883
+ this.captionStateCheckInterval = null;
1884
+ }
1885
+
1886
+ if (this.qualityMonitorInterval) {
1887
+ clearInterval(this.qualityMonitorInterval);
1888
+ this.qualityMonitorInterval = null;
1889
+ }
1890
+
1891
+ if (this.ytPlayer && this.ytPlayer.destroy) {
1892
+ this.ytPlayer.destroy();
1893
+ this.ytPlayer = null;
1894
+ }
1895
+
1896
+ if (this.ytPlayerContainer) {
1897
+ this.ytPlayerContainer.remove();
1898
+ this.ytPlayerContainer = null;
1899
+ }
1900
+
1901
+ this.removeMouseMoveOverlay();
1902
+
1903
+ this.api.container.classList.remove('youtube-active');
1904
+ const styleEl = document.getElementById('youtube-controls-override');
1905
+ if (styleEl) styleEl.remove();
1906
+
1907
+ if (this.player.qualities && this.player.qualities.length > 0) {
1908
+ // Remove YouTube fake qualities
1909
+ }
1910
+
1911
+ if (this.api.video) {
1912
+ this.api.video.style.display = '';
1913
+ }
1914
+
1915
+ this.showPosterOverlay();
412
1916
  }
413
1917
  }
414
1918
 
415
- // Register plugin globally
1919
+ // Register plugin
416
1920
  window.registerMYETVPlugin('youtube', YouTubePlugin);
417
1921
 
418
- })();
1922
+ })();