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.
@@ -14,9 +14,19 @@
14
14
  autoplay: options.autoplay !== undefined ? options.autoplay : false,
15
15
  showYouTubeUI: options.showYouTubeUI !== undefined ? options.showYouTubeUI : false,
16
16
  autoLoadFromData: options.autoLoadFromData !== undefined ? options.autoLoadFromData : true,
17
- quality: options.quality || 'default',
17
+ quality: options.quality || 'auto',
18
18
  enableQualityControl: options.enableQualityControl !== undefined ? options.enableQualityControl : true,
19
19
  enableCaptions: options.enableCaptions !== undefined ? options.enableCaptions : true,
20
+
21
+ // Channel watermark option (default false - requires API key)
22
+ enableChannelWatermark: options.enableChannelWatermark !== undefined ? options.enableChannelWatermark : false,
23
+
24
+ // Auto caption language option
25
+ autoCaptionLanguage: options.autoCaptionLanguage || null, // e.g., 'it', 'en', 'es', 'de', 'fr'
26
+
27
+ // Enable or disable click over youtube player
28
+ mouseClick: options.mouseClick !== undefined ? options.mouseClick : false,
29
+
20
30
  debug: true,
21
31
  ...options
22
32
  };
@@ -39,6 +49,12 @@
39
49
  this.captionStateCheckInterval = null;
40
50
  this.qualityMonitorInterval = null;
41
51
  this.resizeListenerAdded = false;
52
+ // Channel data cache
53
+ this.channelData = null;
54
+ //live streaming
55
+ this.isLiveStream = false;
56
+ this.liveCheckInterval = null;
57
+ this.isAtLiveEdge = true; // Track if viewer is at live edge
42
58
 
43
59
  this.api = player.getPluginAPI();
44
60
  if (this.api.player.options.debug) console.log('[YT Plugin] Constructor initialized', this.options);
@@ -77,6 +93,208 @@
77
93
  if (this.api.player.options.debug) console.log('[YT Plugin] Setup completed');
78
94
  }
79
95
 
96
+ /**
97
+ * Fetch YouTube channel information using YouTube Data API v3
98
+ */
99
+ async fetchChannelInfo(videoId) {
100
+ if (!this.options.apiKey) {
101
+ if (this.api.player.options.debug) {
102
+ console.warn('[YT Plugin] API Key required to fetch channel information');
103
+ }
104
+ return null;
105
+ }
106
+
107
+ try {
108
+ const videoUrl = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId}&key=${this.options.apiKey}`;
109
+ const videoResponse = await fetch(videoUrl);
110
+ const videoData = await videoResponse.json();
111
+
112
+ if (!videoData.items || videoData.items.length === 0) {
113
+ if (this.api.player.options.debug) {
114
+ console.warn('[YT Plugin] Video not found');
115
+ }
116
+ return null;
117
+ }
118
+
119
+ const channelId = videoData.items[0].snippet.channelId;
120
+ const channelTitle = videoData.items[0].snippet.channelTitle;
121
+
122
+ const channelUrl = `https://www.googleapis.com/youtube/v3/channels?part=snippet&id=${channelId}&key=${this.options.apiKey}`;
123
+ const channelResponse = await fetch(channelUrl);
124
+ const channelData = await channelResponse.json();
125
+
126
+ if (!channelData.items || channelData.items.length === 0) {
127
+ if (this.api.player.options.debug) {
128
+ console.warn('[YT Plugin] Channel not found');
129
+ }
130
+ return null;
131
+ }
132
+
133
+ const channel = channelData.items[0].snippet;
134
+
135
+ const channelInfo = {
136
+ channelId: channelId,
137
+ channelTitle: channelTitle,
138
+ channelUrl: `https://www.youtube.com/channel/${channelId}`,
139
+ thumbnailUrl: channel.thumbnails.high?.url || channel.thumbnails.default?.url || null
140
+ };
141
+
142
+ if (this.api.player.options.debug) {
143
+ console.log('[YT Plugin] Channel info fetched', channelInfo);
144
+ }
145
+
146
+ return channelInfo;
147
+
148
+ } catch (error) {
149
+ if (this.api.player.options.debug) {
150
+ console.error('[YT Plugin] Error fetching channel info', error);
151
+ }
152
+ return null;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Update main player watermark options with channel data
158
+ */
159
+ async updatePlayerWatermark() {
160
+ // Don't create watermark when YouTube native UI is active
161
+ if (this.options.showYouTubeUI) {
162
+ if (this.api.player.options.debug) {
163
+ console.log('[YT Plugin] Skipping watermark - YouTube UI active');
164
+ }
165
+ return;
166
+ }
167
+
168
+ if (!this.options.enableChannelWatermark || !this.videoId) {
169
+ return;
170
+ }
171
+
172
+ this.channelData = await this.fetchChannelInfo(this.videoId);
173
+
174
+ if (!this.channelData) {
175
+ return;
176
+ }
177
+
178
+ if (this.api.player.options) {
179
+ this.api.player.options.watermarkUrl = this.channelData.thumbnailUrl;
180
+ this.api.player.options.watermarkLink = this.channelData.channelUrl;
181
+ this.api.player.options.watermarkTitle = this.channelData.channelTitle;
182
+
183
+ if (this.api.player.options.debug) {
184
+ console.log('[YT Plugin] Player watermark options updated', {
185
+ watermarkUrl: this.api.player.options.watermarkUrl,
186
+ watermarkLink: this.api.player.options.watermarkLink,
187
+ watermarkTitle: this.api.player.options.watermarkTitle
188
+ });
189
+ }
190
+
191
+ if (this.api.player.initializeWatermark) {
192
+ this.api.player.initializeWatermark();
193
+
194
+ // Wait for watermark to be in DOM and apply circular style
195
+ this.applyCircularWatermark();
196
+ }
197
+ }
198
+ }
199
+
200
+ applyCircularWatermark() {
201
+ let attempts = 0;
202
+ const maxAttempts = 20;
203
+
204
+ const checkAndApply = () => {
205
+ attempts++;
206
+
207
+ // Try all possible selectors for watermark elements
208
+ const watermarkSelectors = [
209
+ '.watermark',
210
+ '.watermark-image',
211
+ '.watermark img',
212
+ '.watermark a',
213
+ '.watermark-link',
214
+ '[class*="watermark"]',
215
+ 'img[src*="' + (this.channelData?.thumbnailUrl || '') + '"]'
216
+ ];
217
+
218
+ let found = false;
219
+
220
+ watermarkSelectors.forEach(selector => {
221
+ try {
222
+ const elements = this.api.container.querySelectorAll(selector);
223
+ if (elements.length > 0) {
224
+ elements.forEach(el => {
225
+ el.style.borderRadius = '50%';
226
+ el.style.overflow = 'hidden';
227
+ found = true;
228
+
229
+ if (this.api.player.options.debug) {
230
+ console.log('[YT Plugin] Applied circular style to:', selector, el);
231
+ }
232
+ });
233
+ }
234
+ } catch (e) {
235
+ // Selector might not be valid, skip it
236
+ }
237
+ });
238
+
239
+ if (!found && attempts < maxAttempts) {
240
+ if (this.api.player.options.debug) {
241
+ console.log('[YT Plugin] Watermark not found yet, retry', attempts + '/' + maxAttempts);
242
+ }
243
+ setTimeout(checkAndApply, 200);
244
+ } else if (found) {
245
+ if (this.api.player.options.debug) {
246
+ console.log('[YT Plugin] ✅ Watermark made circular successfully');
247
+ }
248
+ } else {
249
+ if (this.api.player.options.debug) {
250
+ console.warn('[YT Plugin] Could not find watermark element after', maxAttempts, 'attempts');
251
+ }
252
+ }
253
+ };
254
+
255
+ // Start checking
256
+ setTimeout(checkAndApply, 100);
257
+ }
258
+
259
+
260
+ /**
261
+ * Set auto caption language on player initialization
262
+ */
263
+ setAutoCaptionLanguage() {
264
+ if (!this.options.autoCaptionLanguage || !this.ytPlayer) {
265
+ return;
266
+ }
267
+
268
+ try {
269
+ if (this.api.player.options.debug) {
270
+ console.log('[YT Plugin] Setting auto caption language to', this.options.autoCaptionLanguage);
271
+ }
272
+
273
+ this.ytPlayer.setOption('captions', 'reload', true);
274
+ this.ytPlayer.loadModule('captions');
275
+
276
+ this.ytPlayer.setOption('captions', 'track', {
277
+ 'translationLanguage': this.options.autoCaptionLanguage
278
+ });
279
+
280
+ this.captionsEnabled = true;
281
+
282
+ const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
283
+ if (subtitlesBtn) {
284
+ subtitlesBtn.classList.add('active');
285
+ }
286
+
287
+ if (this.api.player.options.debug) {
288
+ console.log('[YT Plugin] Auto caption language set successfully');
289
+ }
290
+
291
+ } catch (error) {
292
+ if (this.api.player.options.debug) {
293
+ console.error('[YT Plugin] Error setting auto caption language', error);
294
+ }
295
+ }
296
+ }
297
+
80
298
  handleResponsiveLayout() {
81
299
 
82
300
  const containerWidth = this.api.container.offsetWidth;
@@ -458,11 +676,15 @@ width: fit-content;
458
676
  const playerVars = {
459
677
  autoplay: this.options.autoplay ? 1 : 0,
460
678
  controls: this.options.showYouTubeUI ? 1 : 0,
679
+ fs: this.options.showYouTubeUI ? 1 : 0,
680
+ disablekb: 1,
461
681
  modestbranding: 1,
462
682
  rel: 0,
463
683
  cc_load_policy: 1,
464
- cc_lang_pref: 'en',
684
+ cc_lang_pref: this.options.autoCaptionLanguage || 'en',
685
+ hl: this.options.autoCaptionLanguage || 'en',
465
686
  iv_load_policy: 3,
687
+ showinfo: 0,
466
688
  ...options.playerVars
467
689
  };
468
690
 
@@ -497,62 +719,206 @@ width: fit-content;
497
719
  createMouseMoveOverlay() {
498
720
  if (this.mouseMoveOverlay) return;
499
721
 
722
+ // Do NOT create overlay if YouTube native UI is enabled (ToS compliant)
723
+ if (this.options.showYouTubeUI) {
724
+ if (this.api.player.options.debug) {
725
+ console.log('[YT Plugin] Skipping overlay - YouTube native UI enabled (ToS compliant)');
726
+ }
727
+
728
+ // Enable clicks on YouTube player
729
+ if (this.options.mouseClick !== false) {
730
+ this.enableYouTubeClicks();
731
+ }
732
+
733
+ // Setup mouse detection for custom controls visibility
734
+ this.setupMouseMoveDetection();
735
+ return;
736
+ }
737
+
500
738
  this.mouseMoveOverlay = document.createElement('div');
501
739
  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
740
 
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
- });
741
+ // Apply pointer-events based on mouseClick option
742
+ const pointerEvents = this.options.mouseClick ? 'none' : 'auto';
743
+
744
+ this.mouseMoveOverlay.style.cssText = `
745
+ position: absolute;
746
+ top: 0;
747
+ left: 0;
748
+ width: 100%;
749
+ height: 100%;
750
+ z-index: 2;
751
+ background: transparent;
752
+ pointer-events: ${pointerEvents};
753
+ cursor: default;
754
+ `;
513
755
 
514
- this.mouseMoveOverlay.addEventListener('click', (e) => {
515
- const doubleTap = this.api.player.options.doubleTapPause;
516
- const pauseClick = this.api.player.options.pauseClick;
756
+ this.api.container.insertBefore(this.mouseMoveOverlay, this.api.controls);
517
757
 
518
- if (doubleTap) {
519
- let controlsHidden = false;
758
+ // Setup mouse detection
759
+ this.setupMouseMoveDetection();
520
760
 
521
- if (this.api.controls) {
522
- controlsHidden = this.api.controls.classList.contains('hide');
761
+ // Only add event listeners if mouseClick is disabled
762
+ if (!this.options.mouseClick) {
763
+ this.mouseMoveOverlay.addEventListener('mousemove', (e) => {
764
+ if (this.api.player.onMouseMove) {
765
+ this.api.player.onMouseMove(e);
523
766
  }
767
+ });
768
+
769
+ this.mouseMoveOverlay.addEventListener('click', (e) => {
770
+ const doubleTap = this.api.player.options.doubleTapPause;
771
+ const pauseClick = this.api.player.options.pauseClick;
524
772
 
525
- if (!controlsHidden) {
526
- const controls = this.player.container.querySelector('.controls');
527
- if (controls) {
528
- controlsHidden = controls.classList.contains('hide');
773
+ if (doubleTap) {
774
+ let controlsHidden = false;
775
+ if (this.api.controls) {
776
+ controlsHidden = this.api.controls.classList.contains('hide');
529
777
  }
530
- }
531
778
 
532
- if (!controlsHidden && this.api.controls) {
533
- const style = window.getComputedStyle(this.api.controls);
534
- controlsHidden = style.opacity === '0' || style.visibility === 'hidden';
535
- }
779
+ if (!controlsHidden) {
780
+ const controls = this.player.container.querySelector('.controls');
781
+ if (controls) {
782
+ controlsHidden = controls.classList.contains('hide');
783
+ }
784
+ }
536
785
 
537
- if (controlsHidden) {
538
- if (this.api.player.showControlsNow) {
539
- this.api.player.showControlsNow();
786
+ if (!controlsHidden && this.api.controls) {
787
+ const style = window.getComputedStyle(this.api.controls);
788
+ controlsHidden = style.opacity === '0' || style.visibility === 'hidden';
540
789
  }
541
- if (this.api.player.resetAutoHideTimer) {
542
- this.api.player.resetAutoHideTimer();
790
+
791
+ if (controlsHidden) {
792
+ if (this.api.player.showControlsNow) {
793
+ this.api.player.showControlsNow();
794
+ }
795
+ if (this.api.player.resetAutoHideTimer) {
796
+ this.api.player.resetAutoHideTimer();
797
+ }
798
+ return;
543
799
  }
544
- return;
800
+
801
+ this.togglePlayPauseYT();
802
+ } else if (pauseClick) {
803
+ this.togglePlayPauseYT();
545
804
  }
805
+ });
806
+ }
807
+ }
808
+
809
+ // monitor mouse movement over container
810
+ setupMouseMoveDetection() {
811
+ // track last mouse position
812
+ this.lastMouseX = null;
813
+ this.lastMouseY = null;
814
+ this.mouseCheckInterval = null;
815
+
816
+ // Listener on container
817
+ this.api.container.addEventListener('mouseenter', () => {
818
+ if (this.api.player.options.debug) {
819
+ console.log('[YT Plugin] Mouse entered player container');
820
+ }
821
+
822
+ // show controls immediately
823
+ if (this.api.player.showControlsNow) {
824
+ this.api.player.showControlsNow();
825
+ }
826
+ if (this.api.player.resetAutoHideTimer) {
827
+ this.api.player.resetAutoHideTimer();
828
+ }
829
+
830
+ // start monitoring
831
+ this.startMousePositionTracking();
832
+ });
833
+
834
+ this.api.container.addEventListener('mouseleave', () => {
835
+ if (this.api.player.options.debug) {
836
+ console.log('[YT Plugin] Mouse left player container');
837
+ }
838
+
839
+ // stop monitoring
840
+ this.stopMousePositionTracking();
841
+ });
842
+
843
+ // capture mouse move on container
844
+ this.api.container.addEventListener('mousemove', (e) => {
845
+ this.lastMouseX = e.clientX;
846
+ this.lastMouseY = e.clientY;
847
+
848
+ if (this.api.player.onMouseMove) {
849
+ this.api.player.onMouseMove(e);
850
+ }
546
851
 
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();
852
+ if (this.api.player.resetAutoHideTimer) {
853
+ this.api.player.resetAutoHideTimer();
552
854
  }
553
855
  });
554
856
  }
555
857
 
858
+ // check mouse position on iframe
859
+ startMousePositionTracking() {
860
+ if (this.mouseCheckInterval) return;
861
+
862
+ this.mouseCheckInterval = setInterval(() => {
863
+ // Listener to capture mouse position on iframe
864
+ const handleGlobalMove = (e) => {
865
+ const newX = e.clientX;
866
+ const newY = e.clientY;
867
+
868
+ // if mouse is moving
869
+ if (this.lastMouseX !== newX || this.lastMouseY !== newY) {
870
+ this.lastMouseX = newX;
871
+ this.lastMouseY = newY;
872
+
873
+ // verify if mouse is enter the container
874
+ const rect = this.api.container.getBoundingClientRect();
875
+ const isInside = (
876
+ newX >= rect.left &&
877
+ newX <= rect.right &&
878
+ newY >= rect.top &&
879
+ newY <= rect.bottom
880
+ );
881
+
882
+ if (isInside) {
883
+ if (this.api.player.showControlsNow) {
884
+ this.api.player.showControlsNow();
885
+ }
886
+ if (this.api.player.resetAutoHideTimer) {
887
+ this.api.player.resetAutoHideTimer();
888
+ }
889
+ }
890
+ }
891
+ };
892
+
893
+ // Listener temp
894
+ document.addEventListener('mousemove', handleGlobalMove, { once: true, passive: true });
895
+ }, 100); // Check ogni 100ms
896
+ }
897
+
898
+ stopMousePositionTracking() {
899
+ if (this.mouseCheckInterval) {
900
+ clearInterval(this.mouseCheckInterval);
901
+ this.mouseCheckInterval = null;
902
+ }
903
+ }
904
+
905
+
906
+ // enable or disable clicks over youtube player
907
+ enableYouTubeClicks() {
908
+ if (this.ytPlayerContainer) {
909
+ this.ytPlayerContainer.style.pointerEvents = 'auto';
910
+
911
+ const iframe = this.ytPlayerContainer.querySelector('iframe');
912
+ if (iframe) {
913
+ iframe.style.pointerEvents = 'auto';
914
+ }
915
+
916
+ if (this.api.player.options.debug) {
917
+ console.log('[YT Plugin] YouTube clicks enabled - overlay transparent');
918
+ }
919
+ }
920
+ }
921
+
556
922
  togglePlayPauseYT() {
557
923
  if (!this.ytPlayer) return;
558
924
 
@@ -578,6 +944,9 @@ width: fit-content;
578
944
  this.mouseMoveOverlay.remove();
579
945
  this.mouseMoveOverlay = null;
580
946
  }
947
+
948
+ // stop tracking mouse
949
+ this.stopMousePositionTracking();
581
950
  }
582
951
 
583
952
  hidePosterOverlay() {
@@ -632,12 +1001,46 @@ width: fit-content;
632
1001
  document.head.appendChild(forceVisibilityCSS);
633
1002
  if (this.api.player.options.debug) console.log('[YT Plugin] 🎨 CSS force visibility injected');
634
1003
 
1004
+ // Enable YouTube clicks if option is set
1005
+ if (this.options.mouseClick) {
1006
+ this.enableYouTubeClicks();
1007
+ }
635
1008
  this.hideLoadingOverlay();
636
1009
  this.hideInitialLoading();
637
1010
  this.injectYouTubeCSSOverride();
638
1011
 
639
1012
  this.syncControls();
640
1013
 
1014
+ // Hide custom controls when YouTube native UI is enabled
1015
+ if (this.options.showYouTubeUI) {
1016
+ // Hide controls
1017
+ if (this.api.controls) {
1018
+ this.api.controls.style.display = 'none';
1019
+ this.api.controls.style.opacity = '0';
1020
+ this.api.controls.style.visibility = 'hidden';
1021
+ this.api.controls.style.pointerEvents = 'none';
1022
+ }
1023
+
1024
+ // Hide overlay title
1025
+ const overlayTitle = this.api.container.querySelector('.title-overlay');
1026
+ if (overlayTitle) {
1027
+ overlayTitle.style.display = 'none';
1028
+ overlayTitle.style.opacity = '0';
1029
+ overlayTitle.style.visibility = 'hidden';
1030
+ }
1031
+
1032
+ // Hide watermark
1033
+ const watermark = this.api.container.querySelector('.watermark');
1034
+ if (watermark) {
1035
+ watermark.style.display = 'none';
1036
+ watermark.style.opacity = '0';
1037
+ watermark.style.visibility = 'hidden';
1038
+ }
1039
+
1040
+ // Force hide via CSS
1041
+ this.forceHideCustomControls();
1042
+ }
1043
+
641
1044
  // Handle responsive layout for PiP and subtitles buttons
642
1045
  this.handleResponsiveLayout();
643
1046
 
@@ -645,6 +1048,10 @@ width: fit-content;
645
1048
  setTimeout(() => this.hidePipFromSettingsMenuOnly(), 500);
646
1049
  setTimeout(() => this.hidePipFromSettingsMenuOnly(), 1500);
647
1050
  setTimeout(() => this.hidePipFromSettingsMenuOnly(), 3000);
1051
+ // Check if this is a live stream
1052
+ setTimeout(() => this.checkIfLiveStream(), 2000);
1053
+ setTimeout(() => this.checkIfLiveStream(), 5000);
1054
+
648
1055
 
649
1056
  // Listen for window resize
650
1057
  if (!this.resizeListenerAdded) {
@@ -667,7 +1074,549 @@ width: fit-content;
667
1074
  setTimeout(() => this.setQuality(this.options.quality), 1000);
668
1075
  }
669
1076
 
1077
+ // NEW: Update player watermark with channel data
1078
+ if (this.options.enableChannelWatermark) {
1079
+ this.updatePlayerWatermark();
1080
+ }
1081
+
1082
+ // NEW: Set auto caption language
1083
+ if (this.options.autoCaptionLanguage) {
1084
+ setTimeout(() => this.setAutoCaptionLanguage(), 1500);
1085
+ }
1086
+
670
1087
  this.api.triggerEvent('youtubeplugin:playerready', {});
1088
+
1089
+ }
1090
+
1091
+ forceHideCustomControls() {
1092
+ const existingStyle = document.getElementById('yt-force-hide-controls');
1093
+ if (existingStyle) {
1094
+ return;
1095
+ }
1096
+
1097
+ const style = document.createElement('style');
1098
+ style.id = 'yt-force-hide-controls';
1099
+ style.textContent = `
1100
+ .video-wrapper.youtube-native-ui .controls,
1101
+ .video-wrapper.youtube-native-ui .title-overlay,
1102
+ .video-wrapper.youtube-native-ui .watermark {
1103
+ display: none !important;
1104
+ opacity: 0 !important;
1105
+ visibility: hidden !important;
1106
+ pointer-events: none !important;
1107
+ }
1108
+ `;
1109
+ document.head.appendChild(style);
1110
+
1111
+ this.api.container.classList.add('youtube-native-ui');
1112
+
1113
+ if (this.api.player.options.debug) {
1114
+ console.log('[YT Plugin] CSS injected - custom elements hidden (simple method)');
1115
+ }
1116
+ }
1117
+
1118
+ checkIfLiveStream() {
1119
+ if (this.api.player.options.debug) {
1120
+ console.log('[YT Plugin] Starting live stream check...');
1121
+ }
1122
+
1123
+ if (!this.ytPlayer) {
1124
+ if (this.api.player.options.debug) {
1125
+ console.log('[YT Plugin] ytPlayer not available');
1126
+ }
1127
+ return false;
1128
+ }
1129
+
1130
+ try {
1131
+ // Method 1: Check video data for isLive property
1132
+ if (this.ytPlayer.getVideoData) {
1133
+ const videoData = this.ytPlayer.getVideoData();
1134
+
1135
+ if (this.api.player.options.debug) {
1136
+ console.log('[YT Plugin] Video Data:', videoData);
1137
+ }
1138
+
1139
+ // Check if video data indicates it's live
1140
+ if (videoData.isLive || videoData.isLiveBroadcast) {
1141
+ if (this.api.player.options.debug) {
1142
+ console.log('[YT Plugin] LIVE detected via videoData.isLive');
1143
+ }
1144
+ this.isLiveStream = true;
1145
+ this.handleLiveStreamUI();
1146
+ return true;
1147
+ }
1148
+ }
1149
+
1150
+ // Method 2: Check duration - live streams have special duration values
1151
+ if (this.ytPlayer.getDuration) {
1152
+ const duration = this.ytPlayer.getDuration();
1153
+
1154
+ if (this.api.player.options.debug) {
1155
+ console.log('[YT Plugin] Initial duration:', duration);
1156
+ }
1157
+
1158
+ setTimeout(() => {
1159
+ if (!this.ytPlayer || !this.ytPlayer.getDuration) {
1160
+ if (this.api.player.options.debug) {
1161
+ console.log('[YT Plugin] ytPlayer lost during duration check');
1162
+ }
1163
+ return;
1164
+ }
1165
+
1166
+ const newDuration = this.ytPlayer.getDuration();
1167
+ const difference = Math.abs(newDuration - duration);
1168
+
1169
+ if (this.api.player.options.debug) {
1170
+ console.log('[YT Plugin] Duration after 5s:', newDuration);
1171
+ console.log('[YT Plugin] Duration difference:', difference);
1172
+ }
1173
+
1174
+ if (difference > 10) {
1175
+ if (this.api.player.options.debug) {
1176
+ console.log('[YT Plugin] LIVE STREAM DETECTED - duration changing significantly');
1177
+ }
1178
+ this.isLiveStream = true;
1179
+ this.handleLiveStreamUI();
1180
+ } else {
1181
+ if (this.api.player.options.debug) {
1182
+ console.log('[YT Plugin] Regular video - duration stable');
1183
+ }
1184
+ this.isLiveStream = false;
1185
+ }
1186
+ }, 5000);
1187
+ }
1188
+
1189
+ // Method 3: Check player state
1190
+ if (this.ytPlayer.getPlayerState) {
1191
+ const state = this.ytPlayer.getPlayerState();
1192
+ if (this.api.player.options.debug) {
1193
+ console.log('[YT Plugin] Player state:', state);
1194
+ }
1195
+ }
1196
+
1197
+ } catch (error) {
1198
+ if (this.api.player.options.debug) {
1199
+ console.error('[YT Plugin] Error checking live stream:', error);
1200
+ }
1201
+ }
1202
+
1203
+ return this.isLiveStream;
1204
+ }
1205
+
1206
+ handleLiveStreamUI() {
1207
+ if (this.api.player.options.debug) {
1208
+ console.log('[YT Plugin] 🎬 Applying live stream UI changes');
1209
+ console.log('[YT Plugin] 📦 Container:', this.api.container);
1210
+ }
1211
+
1212
+ // Stop time update for live streams
1213
+ if (this.timeUpdateInterval) {
1214
+ clearInterval(this.timeUpdateInterval);
1215
+ this.timeUpdateInterval = null;
1216
+ if (this.api.player.options.debug) {
1217
+ console.log('[YT Plugin] ✅ Time update interval stopped');
1218
+ }
1219
+ }
1220
+
1221
+ // Apply UI changes
1222
+ this.hideTimeDisplay();
1223
+ this.createLiveBadge();
1224
+
1225
+ // Check if DVR is available before disabling progress bar
1226
+ this.checkDVRAvailability();
1227
+
1228
+ this.startLiveMonitoring();
1229
+
1230
+ // Force progress bar to 100% for live streams
1231
+ this.liveProgressInterval = setInterval(() => {
1232
+ if (this.isLiveStream && this.api.player.progressFilled) {
1233
+ this.api.player.progressFilled.style.width = '100%';
1234
+
1235
+ if (this.api.player.progressHandle) {
1236
+ this.api.player.progressHandle.style.left = '100%';
1237
+ }
1238
+ }
1239
+ }, 100); // Every 100ms to override any other updates
1240
+
1241
+ if (this.api.player.options.debug) {
1242
+ console.log('[YT Plugin] ✅ Live UI setup complete');
1243
+ }
1244
+ }
1245
+
1246
+ checkDVRAvailability() {
1247
+ const progressContainer = this.api.container.querySelector('.progress-container');
1248
+ const progressFill = this.api.container.querySelector('.progress-fill');
1249
+
1250
+ if (progressContainer) {
1251
+ progressContainer.style.opacity = '0.3';
1252
+ progressContainer.style.pointerEvents = 'none';
1253
+ }
1254
+
1255
+ // Set darkgoldenrod during test
1256
+ if (progressFill) {
1257
+ progressFill.style.backgroundColor = 'darkgoldenrod';
1258
+ }
1259
+
1260
+ setTimeout(() => {
1261
+ if (!this.ytPlayer) return;
1262
+
1263
+ try {
1264
+ const currentTime = this.ytPlayer.getCurrentTime();
1265
+ const duration = this.ytPlayer.getDuration();
1266
+ const testSeekPosition = Math.max(0, currentTime - 5);
1267
+
1268
+ if (this.api.player.options.debug) {
1269
+ console.log('[YT Plugin] 🔍 Testing DVR availability...');
1270
+ }
1271
+
1272
+ this.ytPlayer.seekTo(testSeekPosition, true);
1273
+
1274
+ setTimeout(() => {
1275
+ if (!this.ytPlayer) return;
1276
+
1277
+ const newCurrentTime = this.ytPlayer.getCurrentTime();
1278
+ const seekDifference = Math.abs(newCurrentTime - testSeekPosition);
1279
+
1280
+ const progressContainer = this.api.container.querySelector('.progress-container');
1281
+ const progressFill = this.api.container.querySelector('.progress-fill');
1282
+
1283
+ if (seekDifference < 2) {
1284
+ // DVR enabled - restore with theme color
1285
+ if (progressContainer) {
1286
+ progressContainer.style.opacity = '';
1287
+ progressContainer.style.pointerEvents = '';
1288
+ }
1289
+
1290
+ // Remove inline style to use theme color
1291
+ if (progressFill) {
1292
+ progressFill.style.backgroundColor = ''; // Let theme CSS handle color
1293
+ }
1294
+
1295
+ if (this.api.player.options.debug) {
1296
+ console.log('[YT Plugin] ✅ DVR ENABLED - progress bar active with theme color');
1297
+ }
1298
+
1299
+ this.ytPlayer.seekTo(duration, true);
1300
+ } else {
1301
+ // No DVR - keep darkgoldenrod
1302
+ this.modifyProgressBarForLive();
1303
+
1304
+ if (this.api.player.options.debug) {
1305
+ console.log('[YT Plugin] ❌ DVR DISABLED - progress bar locked with darkgoldenrod');
1306
+ }
1307
+ }
1308
+ }, 500);
1309
+
1310
+ } catch (error) {
1311
+ if (this.api.player.options.debug) {
1312
+ console.error('[YT Plugin] Error checking DVR:', error);
1313
+ }
1314
+ this.modifyProgressBarForLive();
1315
+ }
1316
+ }, 1000);
1317
+ }
1318
+
1319
+ createLiveBadge() {
1320
+ // Remove existing badge if present
1321
+ let existingBadge = this.api.container.querySelector('.live-badge');
1322
+ if (existingBadge) {
1323
+ existingBadge.remove();
1324
+ }
1325
+
1326
+ // Create LIVE badge
1327
+ const liveBadge = document.createElement('div');
1328
+ liveBadge.className = 'live-badge';
1329
+ liveBadge.innerHTML = 'LIVE';
1330
+ liveBadge.style.cssText = `
1331
+ display: inline-flex;
1332
+ align-items: center;
1333
+ gap: 6px;
1334
+ background: #ff0000;
1335
+ color: white;
1336
+ padding: 2px 8px;
1337
+ border-radius: 3px;
1338
+ font-size: 12px;
1339
+ font-weight: bold;
1340
+ cursor: pointer;
1341
+ user-select: none;
1342
+ margin-left: 8px;
1343
+ `;
1344
+
1345
+ // Add pulsing indicator style
1346
+ if (!document.getElementById('live-badge-style')) {
1347
+ const style = document.createElement('style');
1348
+ style.id = 'live-badge-style';
1349
+ style.textContent = `
1350
+ .live-indicator {
1351
+ width: 8px;
1352
+ height: 8px;
1353
+ background: white;
1354
+ border-radius: 50%;
1355
+ animation: live-pulse 1.5s ease-in-out infinite;
1356
+ }
1357
+ @keyframes live-pulse {
1358
+ 0%, 100% { opacity: 1; }
1359
+ 50% { opacity: 0.3; }
1360
+ }
1361
+ `;
1362
+ document.head.appendChild(style);
1363
+ }
1364
+
1365
+ // Click to return to live
1366
+ liveBadge.addEventListener('click', (e) => {
1367
+ e.stopPropagation();
1368
+ this.seekToLive();
1369
+ });
1370
+
1371
+ // Insert badge in control bar, next to time display area
1372
+ const controlsLeft = this.api.container.querySelector('.controls-left');
1373
+ if (controlsLeft) {
1374
+ controlsLeft.appendChild(liveBadge);
1375
+ if (this.api.player.options.debug) {
1376
+ console.log('[YT Plugin] Live badge added to controls-left');
1377
+ }
1378
+ } else {
1379
+ // Fallback: add to container
1380
+ this.api.container.appendChild(liveBadge);
1381
+ liveBadge.style.position = 'absolute';
1382
+ liveBadge.style.left = '10px';
1383
+ liveBadge.style.bottom = '50px';
1384
+ liveBadge.style.zIndex = '11';
1385
+ }
1386
+ }
1387
+
1388
+ seekToLive() {
1389
+ if (!this.ytPlayer || !this.isLiveStream) return;
1390
+
1391
+ try {
1392
+ // For live streams, seek to the current live position
1393
+ const duration = this.ytPlayer.getDuration();
1394
+ this.ytPlayer.seekTo(duration, true);
1395
+
1396
+ if (this.api.player.options.debug) {
1397
+ console.log('[YT Plugin] ⏩ Seeking to live edge:', duration);
1398
+ }
1399
+
1400
+ // Immediately update badge to red (will be confirmed by monitoring)
1401
+ const badge = this.api.container.querySelector('.live-badge');
1402
+ if (badge) {
1403
+ badge.style.background = '#ff0000';
1404
+ badge.textContent = 'LIVE';
1405
+ badge.title = '';
1406
+ }
1407
+
1408
+ this.isAtLiveEdge = true;
1409
+
1410
+ } catch (error) {
1411
+ if (this.api.player.options.debug) {
1412
+ console.error('[YT Plugin] Error seeking to live:', error);
1413
+ }
1414
+ }
1415
+ }
1416
+
1417
+ hideTimeDisplay() {
1418
+ // Hide both current time and duration elements
1419
+ const currentTimeEl = this.api.container.querySelector('.current-time');
1420
+ const durationEl = this.api.container.querySelector('.duration');
1421
+
1422
+ if (currentTimeEl) {
1423
+ currentTimeEl.style.display = 'none';
1424
+ if (this.api.player.options.debug) {
1425
+ console.log('[YT Plugin] Current time hidden');
1426
+ }
1427
+ }
1428
+
1429
+ if (durationEl) {
1430
+ durationEl.style.display = 'none';
1431
+ if (this.api.player.options.debug) {
1432
+ console.log('[YT Plugin] Duration hidden');
1433
+ }
1434
+ }
1435
+ }
1436
+
1437
+ showTimeDisplay() {
1438
+ const currentTimeEl = this.api.container.querySelector('.current-time');
1439
+ const durationEl = this.api.container.querySelector('.duration');
1440
+
1441
+ if (currentTimeEl) {
1442
+ currentTimeEl.style.display = '';
1443
+ }
1444
+
1445
+ if (durationEl) {
1446
+ durationEl.style.display = '';
1447
+ }
1448
+ }
1449
+
1450
+ modifyProgressBarForLive() {
1451
+ const progressContainer = this.api.container.querySelector('.progress-container');
1452
+ const progressHandle = this.api.container.querySelector('.progress-handle');
1453
+ const progressFill = this.api.container.querySelector('.progress-fill');
1454
+
1455
+ if (progressContainer) {
1456
+ // Disable all pointer events on progress bar
1457
+ progressContainer.style.pointerEvents = 'none';
1458
+ progressContainer.style.cursor = 'default';
1459
+ progressContainer.style.opacity = '0.6';
1460
+
1461
+ if (this.api.player.options.debug) {
1462
+ console.log('[YT Plugin] Progress bar disabled for live stream');
1463
+ }
1464
+ }
1465
+
1466
+ if (progressHandle) {
1467
+ progressHandle.style.display = 'none';
1468
+ }
1469
+
1470
+ // Change color to darkgoldenrod when disabled
1471
+ if (progressFill) {
1472
+ progressFill.style.backgroundColor = 'darkgoldenrod';
1473
+
1474
+ if (this.api.player.options.debug) {
1475
+ console.log('[YT Plugin] Progress fill color changed to darkgoldenrod');
1476
+ }
1477
+ }
1478
+ }
1479
+
1480
+ restoreProgressBarNormal() {
1481
+ const progressContainer = this.api.container.querySelector('.progress-container');
1482
+ const progressHandle = this.api.container.querySelector('.progress-handle');
1483
+ const progressFill = this.api.container.querySelector('.progress-fill');
1484
+
1485
+ if (progressContainer) {
1486
+ progressContainer.style.pointerEvents = '';
1487
+ progressContainer.style.cursor = '';
1488
+ progressContainer.style.opacity = '';
1489
+ }
1490
+
1491
+ if (progressHandle) {
1492
+ progressHandle.style.display = '';
1493
+ }
1494
+
1495
+ // Remove inline backgroundColor to let CSS theme take over
1496
+ if (progressFill) {
1497
+ progressFill.style.backgroundColor = ''; // Reset to theme color
1498
+
1499
+ if (this.api.player.options.debug) {
1500
+ console.log('[YT Plugin] Progress fill color restored to theme default');
1501
+ }
1502
+ }
1503
+ }
1504
+
1505
+ startLiveMonitoring() {
1506
+ if (this.liveCheckInterval) {
1507
+ clearInterval(this.liveCheckInterval);
1508
+ }
1509
+
1510
+ this.liveCheckInterval = setInterval(() => {
1511
+ if (!this.ytPlayer || !this.isLiveStream) return;
1512
+
1513
+ try {
1514
+ const currentTime = this.ytPlayer.getCurrentTime();
1515
+ const duration = this.ytPlayer.getDuration();
1516
+ const latency = duration - currentTime;
1517
+ const playerState = this.ytPlayer.getPlayerState();
1518
+
1519
+ const badge = this.api.container.querySelector('.live-badge');
1520
+ if (badge) {
1521
+ // Check player state first
1522
+ if (playerState === YT.PlayerState.PAUSED) {
1523
+ // Keep orange when paused - don't override
1524
+ badge.style.background = '#ff8800';
1525
+ badge.textContent = '⏸ LIVE';
1526
+ badge.title = 'Livestreaming in Pause';
1527
+
1528
+ if (this.api.player.options.debug) {
1529
+ console.log('[YT Plugin] 🟠 Live paused (monitoring)');
1530
+ }
1531
+ } else if (playerState === YT.PlayerState.PLAYING) {
1532
+ // Only update color if playing
1533
+ // Check latency only if duration is reasonable
1534
+ if (latency > 60) {
1535
+ // DE-SYNCED - Black background
1536
+ badge.style.background = '#1a1a1a';
1537
+ badge.textContent = 'LIVE';
1538
+ badge.title = `${Math.floor(latency)} seconds back from the live`;
1539
+ this.isAtLiveEdge = false;
1540
+
1541
+ if (this.api.player.options.debug) {
1542
+ console.log('[YT Plugin] ⚫ De-synced, latency:', latency.toFixed(1), 's');
1543
+ }
1544
+ } else {
1545
+ // AT LIVE EDGE - Red background
1546
+ badge.style.background = '#ff0000';
1547
+ badge.textContent = 'LIVE';
1548
+ badge.title = 'Livestreaming';
1549
+ this.isAtLiveEdge = true;
1550
+
1551
+ if (this.api.player.options.debug) {
1552
+ console.log('[YT Plugin] 🔴 At live edge');
1553
+ }
1554
+ }
1555
+ }
1556
+ }
1557
+
1558
+ } catch (error) {
1559
+ if (this.api.player.options.debug) {
1560
+ console.error('[YT Plugin] Error monitoring live:', error);
1561
+ }
1562
+ }
1563
+ }, 2000); // Check every 2 seconds
1564
+ }
1565
+
1566
+ handleLiveStreamEnded() {
1567
+ if (this.api.player.options.debug) {
1568
+ console.log('[YT Plugin] 📹 Handling live stream end transition');
1569
+ }
1570
+
1571
+ // Stop live monitoring
1572
+ if (this.liveCheckInterval) {
1573
+ clearInterval(this.liveCheckInterval);
1574
+ this.liveCheckInterval = null;
1575
+ }
1576
+
1577
+ // Update badge to show "REPLAY" or remove it
1578
+ const badge = this.api.container.querySelector('.live-badge');
1579
+ if (badge) {
1580
+ // Option 1: Change to REPLAY badge
1581
+ badge.textContent = 'REPLAY';
1582
+ badge.style.background = '#555555';
1583
+ badge.style.cursor = 'default';
1584
+ badge.title = 'Registrazione del live stream';
1585
+
1586
+ // Remove click handler since there's no live to seek to
1587
+ const newBadge = badge.cloneNode(true);
1588
+ badge.parentNode.replaceChild(newBadge, badge);
1589
+
1590
+ // Option 2: Remove badge entirely after 5 seconds
1591
+ setTimeout(() => {
1592
+ if (newBadge && newBadge.parentNode) {
1593
+ newBadge.remove();
1594
+ }
1595
+ }, 5000);
1596
+
1597
+ if (this.api.player.options.debug) {
1598
+ console.log('[YT Plugin] ✅ Badge updated to REPLAY mode');
1599
+ }
1600
+ }
1601
+
1602
+ // Restore normal player behavior
1603
+ this.isLiveStream = false;
1604
+ this.isAtLiveEdge = false;
1605
+
1606
+ // Re-enable progress bar
1607
+ this.restoreProgressBarNormal();
1608
+
1609
+ // Show time display again
1610
+ this.showTimeDisplay();
1611
+
1612
+ if (this.liveProgressInterval) {
1613
+ clearInterval(this.liveProgressInterval);
1614
+ this.liveProgressInterval = null;
1615
+ }
1616
+
1617
+ if (this.api.player.options.debug) {
1618
+ console.log('[YT Plugin] ✅ Transitioned from LIVE to REPLAY mode');
1619
+ }
671
1620
  }
672
1621
 
673
1622
  onApiChange(event) {
@@ -676,21 +1625,34 @@ width: fit-content;
676
1625
  }
677
1626
 
678
1627
  injectYouTubeCSSOverride() {
679
- if (document.getElementById('youtube-controls-override')) return;
1628
+ if (document.getElementById('youtube-controls-override')) {
1629
+ return;
1630
+ }
680
1631
 
681
1632
  const style = document.createElement('style');
682
1633
  style.id = 'youtube-controls-override';
683
1634
  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
- `;
1635
+ .video-wrapper.youtube-active .quality-control,
1636
+ .video-wrapper.youtube-active .subtitles-control {
1637
+ display: block !important;
1638
+ visibility: visible !important;
1639
+ opacity: 1 !important;
1640
+ }
1641
+
1642
+ /* Make watermark circular */
1643
+ .video-wrapper .watermark,
1644
+ .video-wrapper .watermark-image,
1645
+ .video-wrapper .watermark img {
1646
+ border-radius: 50% !important;
1647
+ overflow: hidden !important;
1648
+ }
1649
+ `;
691
1650
  document.head.appendChild(style);
692
1651
  this.api.container.classList.add('youtube-active');
693
- if (this.api.player.options.debug) console.log('[YT Plugin] CSS override injected');
1652
+
1653
+ if (this.api.player.options.debug) {
1654
+ console.log('[YT Plugin] CSS override injected (ToS compliant)');
1655
+ }
694
1656
  }
695
1657
 
696
1658
  // ===== QUALITY CONTROL METHODS =====
@@ -918,27 +1880,57 @@ width: fit-content;
918
1880
  if (!this.ytPlayer || !this.ytPlayer.setPlaybackQuality) return false;
919
1881
 
920
1882
  try {
921
- // Try multiple methods to force quality change
922
- this.ytPlayer.setPlaybackQuality(quality);
1883
+ if (this.api.player.options.debug) {
1884
+ console.log('[YT Plugin] Setting quality to:', quality);
1885
+ console.log('[YT Plugin] Current quality:', this.ytPlayer.getPlaybackQuality());
1886
+ console.log('[YT Plugin] Available qualities:', this.ytPlayer.getAvailableQualityLevels());
1887
+ }
923
1888
 
924
- // Also try setPlaybackQualityRange if available
925
- if (this.ytPlayer.setPlaybackQualityRange) {
926
- this.ytPlayer.setPlaybackQualityRange(quality, quality);
1889
+ // Check if requested quality is actually available
1890
+ const availableLevels = this.ytPlayer.getAvailableQualityLevels();
1891
+ if (quality !== 'default' && quality !== 'auto' && !availableLevels.includes(quality)) {
1892
+ if (this.api.player.options.debug) {
1893
+ console.warn('[YT Plugin] Requested quality not available:', quality);
1894
+ }
927
1895
  }
928
1896
 
929
1897
  // Update state
930
1898
  this.currentQuality = quality;
931
- this.currentPlayingQuality = quality; // Force UI update
1899
+
1900
+ // Set the quality
1901
+ this.ytPlayer.setPlaybackQuality(quality);
1902
+
1903
+ // Also try setPlaybackQualityRange for better enforcement
1904
+ if (this.ytPlayer.setPlaybackQualityRange) {
1905
+ this.ytPlayer.setPlaybackQualityRange(quality, quality);
1906
+ }
932
1907
 
933
1908
  // Force UI update immediately
934
1909
  this.updateQualityMenuPlayingState(quality);
935
-
936
- // Update button display
937
1910
  const qualityLabel = this.getQualityLabel(quality);
938
- this.updateQualityButtonDisplay(qualityLabel, '');
1911
+
1912
+ // For manual quality selection, show only the selected quality
1913
+ if (quality !== 'default' && quality !== 'auto') {
1914
+ this.updateQualityButtonDisplay(qualityLabel, '');
1915
+ } else {
1916
+ // For auto mode, show "Auto" and let monitoring update the actual quality
1917
+ this.updateQualityButtonDisplay('Auto', '');
1918
+ }
939
1919
 
940
1920
  if (this.api.player.options.debug) {
941
- console.log('[YT Plugin] Quality set to:', quality);
1921
+ // Check actual quality after a moment
1922
+ setTimeout(() => {
1923
+ if (this.ytPlayer && this.ytPlayer.getPlaybackQuality) {
1924
+ const actualQuality = this.ytPlayer.getPlaybackQuality();
1925
+ console.log('[YT Plugin] Actual quality after 1s:', actualQuality);
1926
+ if (actualQuality !== quality && quality !== 'default' && quality !== 'auto') {
1927
+ console.warn('[YT Plugin] YouTube did not apply requested quality. This may mean:');
1928
+ console.warn(' - The quality is not available for this video');
1929
+ console.warn(' - Embedding restrictions apply');
1930
+ console.warn(' - Network/bandwidth limitations');
1931
+ }
1932
+ }
1933
+ }, 1000); // Check every 1 second for faster updates
942
1934
  }
943
1935
 
944
1936
  this.api.triggerEvent('youtubeplugin:qualitychanged', { quality });
@@ -951,17 +1943,16 @@ width: fit-content;
951
1943
  }
952
1944
  }
953
1945
 
954
- // ===== SUBTITLES CONTROL METHODS =====
1946
+ // ===== SUBTITLE METHODS =====
955
1947
 
956
1948
  /**
957
- * Load available captions and create menu
1949
+ * Load available captions and create subtitle control
958
1950
  */
959
1951
  loadAvailableCaptions() {
960
1952
  if (!this.ytPlayer || !this.options.enableCaptions) return;
961
1953
 
962
- // Prevent creating menu multiple times
963
1954
  if (this.subtitlesMenuCreated) {
964
- if (this.api.player.options.debug) console.log('[YT Plugin] Subtitles menu already created, skipping');
1955
+ if (this.api.player.options.debug) console.log('[YT Plugin] Subtitles menu already created');
965
1956
  return;
966
1957
  }
967
1958
 
@@ -974,7 +1965,6 @@ width: fit-content;
974
1965
  if (this.api.player.options.debug) console.log('[YT Plugin] Captions module error:', e.message);
975
1966
  }
976
1967
 
977
- // FIXED: If tracklist is available and populated, use it
978
1968
  if (captionModule && Array.isArray(captionModule) && captionModule.length > 0) {
979
1969
  this.availableCaptions = captionModule.map((track, index) => {
980
1970
  const isAutomatic = track.kind === 'asr' || track.isautomatic || track.kind === 'auto';
@@ -989,39 +1979,36 @@ width: fit-content;
989
1979
  });
990
1980
 
991
1981
  if (this.api.player.options.debug) console.log('[YT Plugin] ✅ Captions loaded:', this.availableCaptions);
992
- this.createSubtitlesControl(true); // true = has tracklist
1982
+ this.createSubtitlesControl();
993
1983
  this.subtitlesMenuCreated = true;
994
1984
 
995
1985
  } else if (this.captionCheckAttempts < 5) {
996
- // Retry if tracklist not yet available
997
1986
  this.captionCheckAttempts++;
998
1987
  if (this.api.player.options.debug) console.log(`[YT Plugin] Retry caption load (${this.captionCheckAttempts}/5)`);
999
1988
  setTimeout(() => this.loadAvailableCaptions(), 1000);
1000
1989
 
1001
1990
  } 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
1991
+ if (this.api.player.options.debug) console.log('[YT Plugin] No tracklist - creating basic control');
1992
+ this.availableCaptions = [];
1993
+ this.createSubtitlesControl();
1006
1994
  this.subtitlesMenuCreated = true;
1007
1995
  }
1008
1996
 
1009
1997
  } catch (error) {
1010
1998
  if (this.api.player.options.debug) console.error('[YT Plugin] Error loading captions:', error);
1011
- this.createSubtitlesControl(false); // Fallback to On/Off buttons
1999
+ this.createSubtitlesControl();
1012
2000
  this.subtitlesMenuCreated = true;
1013
2001
  }
1014
2002
  }
1015
2003
 
1016
2004
  /**
1017
- * Create subtitles control button and menu
1018
- * @param {boolean} hasTracklist - true if YouTube provides caption tracks, false for auto captions only
2005
+ * Create subtitle control in the control bar
1019
2006
  */
1020
- createSubtitlesControl(hasTracklist) {
2007
+ createSubtitlesControl() {
1021
2008
  let subtitlesControl = this.api.container.querySelector('.subtitles-control');
1022
2009
  if (subtitlesControl) {
1023
2010
  if (this.api.player.options.debug) console.log('[YT Plugin] Subtitles control exists - updating menu');
1024
- this.populateSubtitlesMenu(hasTracklist);
2011
+ this.buildSubtitlesMenu();
1025
2012
  return;
1026
2013
  }
1027
2014
 
@@ -1029,13 +2016,13 @@ width: fit-content;
1029
2016
  if (!controlsRight) return;
1030
2017
 
1031
2018
  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
- `;
2019
+ <div class="subtitles-control">
2020
+ <button class="control-btn subtitles-btn" data-tooltip="Subtitles">
2021
+ <span class="icon">CC</span>
2022
+ </button>
2023
+ <div class="subtitles-menu"></div>
2024
+ </div>
2025
+ `;
1039
2026
 
1040
2027
  const qualityControl = controlsRight.querySelector('.quality-control');
1041
2028
  if (qualityControl) {
@@ -1050,89 +2037,376 @@ width: fit-content;
1050
2037
  }
1051
2038
 
1052
2039
  if (this.api.player.options.debug) console.log('[YT Plugin] Subtitles control created');
1053
- this.populateSubtitlesMenu(hasTracklist);
2040
+ this.buildSubtitlesMenu();
1054
2041
  this.bindSubtitlesButton();
1055
2042
  this.checkInitialCaptionState();
1056
2043
  this.startCaptionStateMonitoring();
1057
2044
  }
1058
2045
 
1059
2046
  /**
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
2047
+ * Build the subtitles menu
1063
2048
  */
1064
- populateSubtitlesMenu(hasTracklist) {
2049
+ buildSubtitlesMenu() {
1065
2050
  const subtitlesMenu = this.api.container.querySelector('.subtitles-menu');
1066
2051
  if (!subtitlesMenu) return;
2052
+
1067
2053
  subtitlesMenu.innerHTML = '';
1068
2054
 
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) => {
2055
+ // Off option
2056
+ const offOption = document.createElement('div');
2057
+ offOption.className = 'subtitles-option';
2058
+ offOption.textContent = 'Off';
2059
+ offOption.dataset.id = 'off';
2060
+ offOption.addEventListener('click', (e) => {
1075
2061
  e.stopPropagation();
1076
2062
  this.disableCaptions();
2063
+ this.updateMenuSelection('off');
2064
+ subtitlesMenu.classList.remove('show');
1077
2065
  });
1078
- subtitlesMenu.appendChild(offItem);
2066
+ subtitlesMenu.appendChild(offOption);
1079
2067
 
1080
- // Show available caption tracks if any
2068
+ // If captions are available
1081
2069
  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);
2070
+ // Add original languages
2071
+ this.availableCaptions.forEach((caption, index) => {
2072
+ const option = document.createElement('div');
2073
+ option.className = 'subtitles-option';
2074
+ option.textContent = caption.label;
2075
+ option.dataset.id = `caption-${index}`;
2076
+ option.addEventListener('click', (e) => {
2077
+ e.stopPropagation();
2078
+ this.setCaptionTrack(caption.languageCode);
2079
+ this.updateMenuSelection(`caption-${index}`);
2080
+ subtitlesMenu.classList.remove('show');
2081
+ });
2082
+ subtitlesMenu.appendChild(option);
1091
2083
  });
1092
2084
  } 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) => {
2085
+ // Auto-caption only (without tracklist)
2086
+ const autoOption = document.createElement('div');
2087
+ autoOption.className = 'subtitles-option';
2088
+ autoOption.textContent = 'Auto-generated';
2089
+ autoOption.dataset.id = 'auto';
2090
+ autoOption.addEventListener('click', (e) => {
1099
2091
  e.stopPropagation();
1100
2092
  this.enableAutoCaptions();
2093
+ this.updateMenuSelection('auto');
2094
+ subtitlesMenu.classList.remove('show');
2095
+ });
2096
+ subtitlesMenu.appendChild(autoOption);
2097
+ }
2098
+
2099
+ // Always add "Auto-translate" (both with and without tracklist)
2100
+ const translateOption = document.createElement('div');
2101
+ translateOption.className = 'subtitles-option translate-option';
2102
+ translateOption.textContent = 'Auto-translate';
2103
+ translateOption.dataset.id = 'translate';
2104
+ translateOption.addEventListener('click', (e) => {
2105
+ e.stopPropagation();
2106
+ this.showTranslationMenu();
2107
+ });
2108
+ subtitlesMenu.appendChild(translateOption);
2109
+ }
2110
+
2111
+ /**
2112
+ * Show translation menu (submenu)
2113
+ */
2114
+ showTranslationMenu() {
2115
+ if (this.api.player.options.debug) console.log('[YT Plugin] showTranslationMenu called');
2116
+
2117
+ const subtitlesMenu = this.api.container.querySelector('.subtitles-menu');
2118
+ if (!subtitlesMenu) return;
2119
+
2120
+ // Clear and rebuild with translation languages
2121
+ subtitlesMenu.innerHTML = '';
2122
+
2123
+ // Back option
2124
+ const backOption = document.createElement('div');
2125
+ backOption.className = 'subtitles-option back-option';
2126
+ backOption.innerHTML = '← Back';
2127
+ backOption.addEventListener('click', (e) => {
2128
+ e.stopPropagation();
2129
+ if (this.api.player.options.debug) console.log('[YT Plugin] Back clicked');
2130
+ this.buildSubtitlesMenu();
2131
+ });
2132
+ subtitlesMenu.appendChild(backOption);
2133
+
2134
+ // Add translation languages
2135
+ const translationLanguages = this.getTopTranslationLanguages();
2136
+ translationLanguages.forEach(lang => {
2137
+ const option = document.createElement('div');
2138
+ option.className = 'subtitles-option';
2139
+ option.textContent = lang.name;
2140
+ option.dataset.id = `translate-${lang.code}`;
2141
+ option.dataset.langcode = lang.code;
2142
+
2143
+ if (this.api.player.options.debug) console.log('[YT Plugin] Creating option for:', lang.name, lang.code);
2144
+
2145
+ option.addEventListener('click', (e) => {
2146
+ e.stopPropagation();
2147
+ if (this.api.player.options.debug) console.log('[YT Plugin] Language clicked:', lang.code, lang.name);
2148
+ if (this.api.player.options.debug) console.log('[YT Plugin] this:', this);
2149
+ if (this.api.player.options.debug) console.log('[YT Plugin] About to call setTranslatedCaptions with:', lang.code);
2150
+
2151
+ const result = this.setTranslatedCaptions(lang.code);
2152
+
2153
+ if (this.api.player.options.debug) console.log('[YT Plugin] setTranslatedCaptions returned:', result);
2154
+
2155
+ this.updateMenuSelection(`translate-${lang.code}`);
2156
+ subtitlesMenu.classList.remove('show');
2157
+
2158
+ // Return to main menu
2159
+ setTimeout(() => this.buildSubtitlesMenu(), 300);
1101
2160
  });
1102
- subtitlesMenu.appendChild(onItem);
2161
+
2162
+ subtitlesMenu.appendChild(option);
2163
+ });
2164
+
2165
+ if (this.api.player.options.debug) console.log('[YT Plugin] Translation menu built with', translationLanguages.length, 'languages');
2166
+ }
2167
+
2168
+ /**
2169
+ * Update selection in the menu
2170
+ */
2171
+ updateMenuSelection(selectedId) {
2172
+ const subtitlesMenu = this.api.container.querySelector('.subtitles-menu');
2173
+ if (!subtitlesMenu) return;
2174
+
2175
+ // Remove all selections
2176
+ subtitlesMenu.querySelectorAll('.subtitles-option').forEach(option => {
2177
+ option.classList.remove('selected');
2178
+ });
2179
+
2180
+ // Add selection to current option
2181
+ const selectedOption = subtitlesMenu.querySelector(`[data-id="${selectedId}"]`);
2182
+ if (selectedOption) {
2183
+ selectedOption.classList.add('selected');
2184
+ }
2185
+
2186
+ // Update button state
2187
+ const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
2188
+ if (subtitlesBtn) {
2189
+ if (selectedId === 'off') {
2190
+ subtitlesBtn.classList.remove('active');
2191
+ } else {
2192
+ subtitlesBtn.classList.add('active');
2193
+ }
1103
2194
  }
1104
2195
  }
1105
2196
 
2197
+ /**
2198
+ * Bind subtitle button (toggle)
2199
+ */
1106
2200
  bindSubtitlesButton() {
1107
2201
  const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
1108
2202
  if (!subtitlesBtn) return;
1109
2203
 
1110
- // Remove existing event listeners by cloning
1111
2204
  const newBtn = subtitlesBtn.cloneNode(true);
1112
2205
  subtitlesBtn.parentNode.replaceChild(newBtn, subtitlesBtn);
1113
2206
 
1114
2207
  newBtn.addEventListener('click', (e) => {
1115
2208
  e.stopPropagation();
1116
- this.toggleCaptions();
2209
+
2210
+ // Toggle: if active disable, otherwise enable first available
2211
+ if (this.captionsEnabled) {
2212
+ this.disableCaptions();
2213
+ this.updateMenuSelection('off');
2214
+ } else {
2215
+ if (this.availableCaptions.length > 0) {
2216
+ const firstCaption = this.availableCaptions[0];
2217
+ this.setCaptionTrack(firstCaption.languageCode);
2218
+ this.updateMenuSelection('caption-0');
2219
+ } else {
2220
+ this.enableAutoCaptions();
2221
+ this.updateMenuSelection('auto');
2222
+ }
2223
+ }
1117
2224
  });
1118
2225
  }
1119
2226
 
2227
+ /**
2228
+ * Set a specific caption track
2229
+ */
2230
+ setCaptionTrack(languageCode) {
2231
+ if (!this.ytPlayer) return false;
2232
+
2233
+ try {
2234
+ this.ytPlayer.setOption('captions', 'track', { languageCode: languageCode });
2235
+ this.ytPlayer.loadModule('captions');
2236
+
2237
+ this.captionsEnabled = true;
2238
+ this.currentCaption = languageCode;
2239
+ this.currentTranslation = null;
2240
+
2241
+ if (this.api.player.options.debug) {
2242
+ console.log('[YT Plugin] Caption track set:', languageCode);
2243
+ }
2244
+
2245
+ return true;
2246
+ } catch (error) {
2247
+ if (this.api.player.options.debug) {
2248
+ console.error('[YT Plugin] Error setting caption track:', error);
2249
+ }
2250
+ return false;
2251
+ }
2252
+ }
2253
+
2254
+ /**
2255
+ * Set automatic translation
2256
+ */
2257
+ setTranslatedCaptions(translationLanguageCode) {
2258
+ if (this.api.player.options.debug) console.log('[YT Plugin] setTranslatedCaptions called with:', translationLanguageCode);
2259
+
2260
+ if (!this.ytPlayer) {
2261
+ if (this.api.player.options.debug) console.error('[YT Plugin] ytPlayer not available');
2262
+ return false;
2263
+ }
2264
+
2265
+ try {
2266
+ if (this.api.player.options.debug) console.log('[YT Plugin] Available captions:', this.availableCaptions);
2267
+
2268
+ if (this.availableCaptions.length > 0) {
2269
+ // WITH TRACKLIST: Use first available caption as base
2270
+ const baseLanguageCode = this.availableCaptions[0].languageCode;
2271
+ if (this.api.player.options.debug) console.log('[YT Plugin] Using base language:', baseLanguageCode);
2272
+
2273
+ this.ytPlayer.setOption('captions', 'track', {
2274
+ 'languageCode': baseLanguageCode,
2275
+ 'translationLanguage': {
2276
+ 'languageCode': translationLanguageCode
2277
+ }
2278
+ });
2279
+ } else {
2280
+ // WITHOUT TRACKLIST: Get current auto-generated track
2281
+ if (this.api.player.options.debug) console.log('[YT Plugin] No tracklist - getting auto-generated track');
2282
+
2283
+ let currentTrack = null;
2284
+ try {
2285
+ currentTrack = this.ytPlayer.getOption('captions', 'track');
2286
+ if (this.api.player.options.debug) console.log('[YT Plugin] Current track:', currentTrack);
2287
+ } catch (e) {
2288
+ if (this.api.player.options.debug) console.log('[YT Plugin] Could not get current track:', e.message);
2289
+ }
2290
+
2291
+ if (currentTrack && currentTrack.languageCode) {
2292
+ // Use auto-generated language as base
2293
+ if (this.api.player.options.debug) console.log('[YT Plugin] Using auto-generated language:', currentTrack.languageCode);
2294
+
2295
+ this.ytPlayer.setOption('captions', 'track', {
2296
+ 'languageCode': currentTrack.languageCode,
2297
+ 'translationLanguage': {
2298
+ 'languageCode': translationLanguageCode
2299
+ }
2300
+ });
2301
+ } else {
2302
+ // Fallback: try with 'en' as base
2303
+ if (this.api.player.options.debug) console.log('[YT Plugin] Fallback: using English as base');
2304
+
2305
+ this.ytPlayer.setOption('captions', 'track', {
2306
+ 'languageCode': 'en',
2307
+ 'translationLanguage': {
2308
+ 'languageCode': translationLanguageCode
2309
+ }
2310
+ });
2311
+ }
2312
+ }
2313
+
2314
+ this.captionsEnabled = true;
2315
+ this.currentTranslation = translationLanguageCode;
2316
+
2317
+ if (this.api.player.options.debug) console.log('[YT Plugin] ✅ Translation applied');
2318
+
2319
+ return true;
2320
+ } catch (error) {
2321
+ if (this.api.player.options.debug) console.error('[YT Plugin] Error setting translation:', error);
2322
+ return false;
2323
+ }
2324
+ }
2325
+
2326
+ /**
2327
+ * Get language name from code
2328
+ */
2329
+ getLanguageName(languageCode) {
2330
+ const languages = this.getTopTranslationLanguages();
2331
+ const lang = languages.find(l => l.code === languageCode);
2332
+ return lang ? lang.name : languageCode;
2333
+ }
2334
+
2335
+ /**
2336
+ * Enable automatic captions
2337
+ */
2338
+ enableAutoCaptions() {
2339
+ if (!this.ytPlayer) return false;
2340
+
2341
+ try {
2342
+ this.ytPlayer.setOption('captions', 'reload', true);
2343
+ this.ytPlayer.loadModule('captions');
2344
+
2345
+ this.captionsEnabled = true;
2346
+ this.currentCaption = null;
2347
+ this.currentTranslation = null;
2348
+
2349
+ if (this.api.player.options.debug) {
2350
+ console.log('[YT Plugin] Auto captions enabled');
2351
+ }
2352
+
2353
+ return true;
2354
+ } catch (error) {
2355
+ if (this.api.player.options.debug) {
2356
+ console.error('[YT Plugin] Error enabling auto captions:', error);
2357
+ }
2358
+ return false;
2359
+ }
2360
+ }
2361
+
2362
+ /**
2363
+ * Disable captions
2364
+ */
2365
+ disableCaptions() {
2366
+ if (!this.ytPlayer) return false;
2367
+
2368
+ try {
2369
+ this.ytPlayer.unloadModule('captions');
2370
+
2371
+ this.captionsEnabled = false;
2372
+ this.currentCaption = null;
2373
+ this.currentTranslation = null;
2374
+
2375
+ if (this.api.player.options.debug) {
2376
+ console.log('[YT Plugin] Captions disabled');
2377
+ }
2378
+
2379
+ return true;
2380
+ } catch (error) {
2381
+ if (this.api.player.options.debug) {
2382
+ console.error('[YT Plugin] Error disabling captions:', error);
2383
+ }
2384
+ return false;
2385
+ }
2386
+ }
2387
+
2388
+ /**
2389
+ * Check initial caption state
2390
+ */
1120
2391
  checkInitialCaptionState() {
1121
2392
  setTimeout(() => {
1122
2393
  try {
1123
2394
  const currentTrack = this.ytPlayer.getOption('captions', 'track');
1124
2395
  if (currentTrack && currentTrack.languageCode) {
1125
2396
  this.captionsEnabled = true;
1126
- const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
1127
- if (subtitlesBtn) subtitlesBtn.classList.add('active');
1128
- this.updateSubtitlesMenuActiveState();
2397
+ this.updateMenuSelection('caption-0');
2398
+ } else {
2399
+ this.updateMenuSelection('off');
1129
2400
  }
1130
2401
  } catch (e) {
1131
- // Ignore errors
2402
+ this.updateMenuSelection('off');
1132
2403
  }
1133
2404
  }, 1500);
1134
2405
  }
1135
2406
 
2407
+ /**
2408
+ * Monitor caption state
2409
+ */
1136
2410
  startCaptionStateMonitoring() {
1137
2411
  if (this.captionStateCheckInterval) {
1138
2412
  clearInterval(this.captionStateCheckInterval);
@@ -1154,7 +2428,6 @@ width: fit-content;
1154
2428
  subtitlesBtn.classList.remove('active');
1155
2429
  }
1156
2430
  }
1157
- this.updateSubtitlesMenuActiveState();
1158
2431
  }
1159
2432
  } catch (e) {
1160
2433
  // Ignore errors
@@ -1210,78 +2483,6 @@ width: fit-content;
1210
2483
  }
1211
2484
  }
1212
2485
 
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
2486
  /**
1286
2487
  * Enable automatic captions (when no tracklist available)
1287
2488
  */
@@ -1365,6 +2566,41 @@ width: fit-content;
1365
2566
  const playIcon = this.api.container.querySelector('.play-icon');
1366
2567
  const pauseIcon = this.api.container.querySelector('.pause-icon');
1367
2568
 
2569
+ // Get live badge
2570
+ const badge = this.api.container.querySelector('.live-badge');
2571
+
2572
+ // Handle live stream ended
2573
+ if (this.isLiveStream && event.data === YT.PlayerState.ENDED) {
2574
+ if (this.api.player.options.debug) {
2575
+ console.log('[YT Plugin] 🔴➡️📹 Live stream ended (player state: ENDED)');
2576
+ }
2577
+ this.handleLiveStreamEnded();
2578
+ return;
2579
+ }
2580
+
2581
+ // Update live badge based on state
2582
+ if (this.isLiveStream && badge) {
2583
+ if (event.data === YT.PlayerState.PAUSED) {
2584
+ // Orange when paused during live
2585
+ badge.style.background = '#ff8800';
2586
+ badge.textContent = '⏸ LIVE';
2587
+ badge.title = 'Livestreaming in Pause';
2588
+
2589
+ if (this.api.player.options.debug) {
2590
+ console.log('[YT Plugin] 🟠 Live paused');
2591
+ }
2592
+ } else if (event.data === YT.PlayerState.PLAYING) {
2593
+ // Red when playing (will be checked for de-sync below)
2594
+ badge.style.background = '#ff0000';
2595
+ badge.textContent = 'LIVE';
2596
+ badge.title = 'Livestreaming';
2597
+
2598
+ if (this.api.player.options.debug) {
2599
+ console.log('[YT Plugin] 🔴 Live playing');
2600
+ }
2601
+ }
2602
+ }
2603
+
1368
2604
  switch (event.data) {
1369
2605
  case YT.PlayerState.PLAYING:
1370
2606
  this.api.triggerEvent('played', {});
@@ -1729,7 +2965,24 @@ width: fit-content;
1729
2965
  const duration = this.ytPlayer.getDuration();
1730
2966
 
1731
2967
  if (this.api.player.progressFilled && duration) {
1732
- const progress = (currentTime / duration) * 100;
2968
+ let progress;
2969
+
2970
+ // For live streams, always show progress at 100%
2971
+ if (this.isLiveStream) {
2972
+ progress = 100;
2973
+ } else {
2974
+ // For regular videos, calculate normally
2975
+ progress = (currentTime / duration) * 100;
2976
+ }
2977
+
2978
+ // Check if live badge exists = it's a live stream
2979
+ const liveBadge = this.api.container.querySelector('.live-badge');
2980
+
2981
+ if (liveBadge) {
2982
+ // Force 100% for live streams
2983
+ progress = 100;
2984
+ }
2985
+
1733
2986
  this.api.player.progressFilled.style.width = `${progress}%`;
1734
2987
  if (this.api.player.progressHandle) {
1735
2988
  this.api.player.progressHandle.style.left = `${progress}%`;
@@ -1913,6 +3166,48 @@ width: fit-content;
1913
3166
  }
1914
3167
 
1915
3168
  this.showPosterOverlay();
3169
+
3170
+ if (this.liveCheckInterval) {
3171
+ clearInterval(this.liveCheckInterval);
3172
+ this.liveCheckInterval = null;
3173
+ }
3174
+
3175
+ // Restore normal UI when destroying
3176
+ if (this.isLiveStream) {
3177
+ this.showTimeDisplay();
3178
+ this.restoreProgressBarNormal();
3179
+
3180
+ const liveBadge = this.api.container.querySelector('.live-badge');
3181
+ if (liveBadge) {
3182
+ liveBadge.remove();
3183
+ }
3184
+ }
3185
+
3186
+ // Clear live stream intervals
3187
+ if (this.liveCheckInterval) {
3188
+ clearInterval(this.liveCheckInterval);
3189
+ this.liveCheckInterval = null;
3190
+ }
3191
+
3192
+ // Restore normal UI
3193
+ if (this.isLiveStream) {
3194
+ this.showTimeDisplay();
3195
+ this.restoreProgressBarNormal();
3196
+
3197
+ const liveBadge = this.api.container.querySelector('.live-badge');
3198
+ if (liveBadge) {
3199
+ liveBadge.remove();
3200
+ }
3201
+ }
3202
+
3203
+ // Reset live stream tracking
3204
+ this.isLiveStream = false;
3205
+ this.isAtLiveEdge = false;
3206
+
3207
+ if (this.liveProgressInterval) {
3208
+ clearInterval(this.liveProgressInterval);
3209
+ this.liveProgressInterval = null;
3210
+ }
1916
3211
  }
1917
3212
  }
1918
3213