myetv-player 1.0.8 → 1.0.10

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.
@@ -17,6 +17,13 @@
17
17
  quality: options.quality || 'default',
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
+
20
27
  debug: true,
21
28
  ...options
22
29
  };
@@ -39,6 +46,12 @@
39
46
  this.captionStateCheckInterval = null;
40
47
  this.qualityMonitorInterval = null;
41
48
  this.resizeListenerAdded = false;
49
+ // Channel data cache
50
+ this.channelData = null;
51
+ //live streaming
52
+ this.isLiveStream = false;
53
+ this.liveCheckInterval = null;
54
+ this.isAtLiveEdge = true; // Track if viewer is at live edge
42
55
 
43
56
  this.api = player.getPluginAPI();
44
57
  if (this.api.player.options.debug) console.log('[YT Plugin] Constructor initialized', this.options);
@@ -77,6 +90,200 @@
77
90
  if (this.api.player.options.debug) console.log('[YT Plugin] Setup completed');
78
91
  }
79
92
 
93
+ /**
94
+ * Fetch YouTube channel information using YouTube Data API v3
95
+ */
96
+ async fetchChannelInfo(videoId) {
97
+ if (!this.options.apiKey) {
98
+ if (this.api.player.options.debug) {
99
+ console.warn('[YT Plugin] API Key required to fetch channel information');
100
+ }
101
+ return null;
102
+ }
103
+
104
+ try {
105
+ const videoUrl = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId}&key=${this.options.apiKey}`;
106
+ const videoResponse = await fetch(videoUrl);
107
+ const videoData = await videoResponse.json();
108
+
109
+ if (!videoData.items || videoData.items.length === 0) {
110
+ if (this.api.player.options.debug) {
111
+ console.warn('[YT Plugin] Video not found');
112
+ }
113
+ return null;
114
+ }
115
+
116
+ const channelId = videoData.items[0].snippet.channelId;
117
+ const channelTitle = videoData.items[0].snippet.channelTitle;
118
+
119
+ const channelUrl = `https://www.googleapis.com/youtube/v3/channels?part=snippet&id=${channelId}&key=${this.options.apiKey}`;
120
+ const channelResponse = await fetch(channelUrl);
121
+ const channelData = await channelResponse.json();
122
+
123
+ if (!channelData.items || channelData.items.length === 0) {
124
+ if (this.api.player.options.debug) {
125
+ console.warn('[YT Plugin] Channel not found');
126
+ }
127
+ return null;
128
+ }
129
+
130
+ const channel = channelData.items[0].snippet;
131
+
132
+ const channelInfo = {
133
+ channelId: channelId,
134
+ channelTitle: channelTitle,
135
+ channelUrl: `https://www.youtube.com/channel/${channelId}`,
136
+ thumbnailUrl: channel.thumbnails.high?.url || channel.thumbnails.default?.url || null
137
+ };
138
+
139
+ if (this.api.player.options.debug) {
140
+ console.log('[YT Plugin] Channel info fetched', channelInfo);
141
+ }
142
+
143
+ return channelInfo;
144
+
145
+ } catch (error) {
146
+ if (this.api.player.options.debug) {
147
+ console.error('[YT Plugin] Error fetching channel info', error);
148
+ }
149
+ return null;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Update main player watermark options with channel data
155
+ */
156
+ async updatePlayerWatermark() {
157
+ if (!this.options.enableChannelWatermark || !this.videoId) {
158
+ return;
159
+ }
160
+
161
+ this.channelData = await this.fetchChannelInfo(this.videoId);
162
+
163
+ if (!this.channelData) {
164
+ return;
165
+ }
166
+
167
+ if (this.api.player.options) {
168
+ this.api.player.options.watermarkUrl = this.channelData.thumbnailUrl;
169
+ this.api.player.options.watermarkLink = this.channelData.channelUrl;
170
+ this.api.player.options.watermarkTitle = this.channelData.channelTitle;
171
+
172
+ if (this.api.player.options.debug) {
173
+ console.log('[YT Plugin] Player watermark options updated', {
174
+ watermarkUrl: this.api.player.options.watermarkUrl,
175
+ watermarkLink: this.api.player.options.watermarkLink,
176
+ watermarkTitle: this.api.player.options.watermarkTitle
177
+ });
178
+ }
179
+
180
+ if (this.api.player.initializeWatermark) {
181
+ this.api.player.initializeWatermark();
182
+
183
+ // Wait for watermark to be in DOM and apply circular style
184
+ this.applyCircularWatermark();
185
+ }
186
+ }
187
+ }
188
+
189
+ applyCircularWatermark() {
190
+ let attempts = 0;
191
+ const maxAttempts = 20;
192
+
193
+ const checkAndApply = () => {
194
+ attempts++;
195
+
196
+ // Try all possible selectors for watermark elements
197
+ const watermarkSelectors = [
198
+ '.watermark',
199
+ '.watermark-image',
200
+ '.watermark img',
201
+ '.watermark a',
202
+ '.watermark-link',
203
+ '[class*="watermark"]',
204
+ 'img[src*="' + (this.channelData?.thumbnailUrl || '') + '"]'
205
+ ];
206
+
207
+ let found = false;
208
+
209
+ watermarkSelectors.forEach(selector => {
210
+ try {
211
+ const elements = this.api.container.querySelectorAll(selector);
212
+ if (elements.length > 0) {
213
+ elements.forEach(el => {
214
+ el.style.borderRadius = '50%';
215
+ el.style.overflow = 'hidden';
216
+ found = true;
217
+
218
+ if (this.api.player.options.debug) {
219
+ console.log('[YT Plugin] Applied circular style to:', selector, el);
220
+ }
221
+ });
222
+ }
223
+ } catch (e) {
224
+ // Selector might not be valid, skip it
225
+ }
226
+ });
227
+
228
+ if (!found && attempts < maxAttempts) {
229
+ if (this.api.player.options.debug) {
230
+ console.log('[YT Plugin] Watermark not found yet, retry', attempts + '/' + maxAttempts);
231
+ }
232
+ setTimeout(checkAndApply, 200);
233
+ } else if (found) {
234
+ if (this.api.player.options.debug) {
235
+ console.log('[YT Plugin] ✅ Watermark made circular successfully');
236
+ }
237
+ } else {
238
+ if (this.api.player.options.debug) {
239
+ console.warn('[YT Plugin] Could not find watermark element after', maxAttempts, 'attempts');
240
+ }
241
+ }
242
+ };
243
+
244
+ // Start checking
245
+ setTimeout(checkAndApply, 100);
246
+ }
247
+
248
+
249
+ /**
250
+ * Set auto caption language on player initialization
251
+ */
252
+ setAutoCaptionLanguage() {
253
+ if (!this.options.autoCaptionLanguage || !this.ytPlayer) {
254
+ return;
255
+ }
256
+
257
+ try {
258
+ if (this.api.player.options.debug) {
259
+ console.log('[YT Plugin] Setting auto caption language to', this.options.autoCaptionLanguage);
260
+ }
261
+
262
+ this.ytPlayer.setOption('captions', 'reload', true);
263
+ this.ytPlayer.loadModule('captions');
264
+
265
+ this.ytPlayer.setOption('captions', 'track', {
266
+ 'translationLanguage': this.options.autoCaptionLanguage
267
+ });
268
+
269
+ this.captionsEnabled = true;
270
+
271
+ const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
272
+ if (subtitlesBtn) {
273
+ subtitlesBtn.classList.add('active');
274
+ }
275
+
276
+ if (this.api.player.options.debug) {
277
+ console.log('[YT Plugin] Auto caption language set successfully');
278
+ }
279
+
280
+ } catch (error) {
281
+ if (this.api.player.options.debug) {
282
+ console.error('[YT Plugin] Error setting auto caption language', error);
283
+ }
284
+ }
285
+ }
286
+
80
287
  handleResponsiveLayout() {
81
288
 
82
289
  const containerWidth = this.api.container.offsetWidth;
@@ -458,10 +665,13 @@ width: fit-content;
458
665
  const playerVars = {
459
666
  autoplay: this.options.autoplay ? 1 : 0,
460
667
  controls: this.options.showYouTubeUI ? 1 : 0,
668
+ fs: this.options.showYouTubeUI ? 1 : 0,
669
+ disablekb: 1,
461
670
  modestbranding: 1,
462
671
  rel: 0,
463
672
  cc_load_policy: 1,
464
- cc_lang_pref: 'en',
673
+ cc_lang_pref: this.options.autoCaptionLanguage || 'en',
674
+ hl: this.options.autoCaptionLanguage || 'en',
465
675
  iv_load_policy: 3,
466
676
  ...options.playerVars
467
677
  };
@@ -645,6 +855,10 @@ width: fit-content;
645
855
  setTimeout(() => this.hidePipFromSettingsMenuOnly(), 500);
646
856
  setTimeout(() => this.hidePipFromSettingsMenuOnly(), 1500);
647
857
  setTimeout(() => this.hidePipFromSettingsMenuOnly(), 3000);
858
+ // Check if this is a live stream
859
+ setTimeout(() => this.checkIfLiveStream(), 2000);
860
+ setTimeout(() => this.checkIfLiveStream(), 5000);
861
+
648
862
 
649
863
  // Listen for window resize
650
864
  if (!this.resizeListenerAdded) {
@@ -667,7 +881,481 @@ width: fit-content;
667
881
  setTimeout(() => this.setQuality(this.options.quality), 1000);
668
882
  }
669
883
 
884
+ // NEW: Update player watermark with channel data
885
+ if (this.options.enableChannelWatermark) {
886
+ this.updatePlayerWatermark();
887
+ }
888
+
889
+ // NEW: Set auto caption language
890
+ if (this.options.autoCaptionLanguage) {
891
+ setTimeout(() => this.setAutoCaptionLanguage(), 1500);
892
+ }
893
+
670
894
  this.api.triggerEvent('youtubeplugin:playerready', {});
895
+
896
+ }
897
+
898
+ checkIfLiveStream() {
899
+ if (this.api.player.options.debug) {
900
+ console.log('[YT Plugin] 🔍 Starting live stream check...');
901
+ }
902
+
903
+ if (!this.ytPlayer) {
904
+ if (this.api.player.options.debug) {
905
+ console.log('[YT Plugin] ❌ ytPlayer not available');
906
+ }
907
+ return false;
908
+ }
909
+
910
+ try {
911
+ // Method 1: Check video data for isLive property
912
+ if (this.ytPlayer.getVideoData) {
913
+ const videoData = this.ytPlayer.getVideoData();
914
+ if (this.api.player.options.debug) {
915
+ console.log('[YT Plugin] 📹 Video Data:', videoData);
916
+ }
917
+
918
+ // Check if video data indicates it's live
919
+ if (videoData.isLive || videoData.isLiveBroadcast) {
920
+ if (this.api.player.options.debug) {
921
+ console.log('[YT Plugin] ✅ LIVE detected via videoData.isLive');
922
+ }
923
+ this.isLiveStream = true;
924
+ this.handleLiveStreamUI();
925
+ return true;
926
+ }
927
+ }
928
+
929
+ // Method 2: Check duration (live streams have special duration values)
930
+ if (this.ytPlayer.getDuration) {
931
+ const duration = this.ytPlayer.getDuration();
932
+ if (this.api.player.options.debug) {
933
+ console.log('[YT Plugin] ⏱️ Initial duration:', duration);
934
+ }
935
+
936
+ // For live streams, duration changes over time
937
+ // Wait 3 seconds and check again
938
+ setTimeout(() => {
939
+ if (!this.ytPlayer || !this.ytPlayer.getDuration) {
940
+ if (this.api.player.options.debug) {
941
+ console.log('[YT Plugin] ❌ ytPlayer lost during duration check');
942
+ }
943
+ return;
944
+ }
945
+
946
+ const newDuration = this.ytPlayer.getDuration();
947
+ const difference = Math.abs(newDuration - duration);
948
+
949
+ if (this.api.player.options.debug) {
950
+ console.log('[YT Plugin] ⏱️ Duration after 3s:', newDuration);
951
+ console.log('[YT Plugin] 📊 Duration difference:', difference);
952
+ }
953
+
954
+ // If duration increased by more than 0.5 seconds, it's live
955
+ if (difference > 0.5) {
956
+ if (this.api.player.options.debug) {
957
+ console.log('[YT Plugin] ✅ LIVE STREAM DETECTED (duration changing)');
958
+ }
959
+ this.isLiveStream = true;
960
+ this.handleLiveStreamUI();
961
+ } else {
962
+ if (this.api.player.options.debug) {
963
+ console.log('[YT Plugin] ℹ️ Regular video (duration stable)');
964
+ }
965
+ this.isLiveStream = false;
966
+ }
967
+ }, 3000);
968
+ }
969
+
970
+ // Method 3: Check player state
971
+ if (this.ytPlayer.getPlayerState) {
972
+ const state = this.ytPlayer.getPlayerState();
973
+ if (this.api.player.options.debug) {
974
+ console.log('[YT Plugin] 🎮 Player state:', state);
975
+ }
976
+ }
977
+
978
+ } catch (error) {
979
+ if (this.api.player.options.debug) {
980
+ console.error('[YT Plugin] ❌ Error checking live stream:', error);
981
+ }
982
+ }
983
+
984
+ return this.isLiveStream;
985
+ }
986
+
987
+ handleLiveStreamUI() {
988
+ if (this.api.player.options.debug) {
989
+ console.log('[YT Plugin] 🎬 Applying live stream UI changes');
990
+ console.log('[YT Plugin] 📦 Container:', this.api.container);
991
+ }
992
+
993
+ // Stop time update for live streams
994
+ if (this.timeUpdateInterval) {
995
+ clearInterval(this.timeUpdateInterval);
996
+ this.timeUpdateInterval = null;
997
+ if (this.api.player.options.debug) {
998
+ console.log('[YT Plugin] ✅ Time update interval stopped');
999
+ }
1000
+ }
1001
+
1002
+ // Apply UI changes
1003
+ this.hideTimeDisplay();
1004
+ this.createLiveBadge();
1005
+
1006
+ // Check if DVR is available before disabling progress bar
1007
+ this.checkDVRAvailability();
1008
+
1009
+ this.startLiveMonitoring();
1010
+
1011
+ if (this.api.player.options.debug) {
1012
+ console.log('[YT Plugin] ✅ Live UI setup complete');
1013
+ }
1014
+ }
1015
+
1016
+ checkDVRAvailability() {
1017
+ // Disable progress bar immediately while testing
1018
+ const progressContainer = this.api.container.querySelector('.progress-container');
1019
+ if (progressContainer) {
1020
+ progressContainer.style.opacity = '0.3';
1021
+ progressContainer.style.pointerEvents = 'none';
1022
+ }
1023
+
1024
+ // Wait a bit for YouTube to fully initialize
1025
+ setTimeout(() => {
1026
+ if (!this.ytPlayer) return;
1027
+
1028
+ try {
1029
+ const currentTime = this.ytPlayer.getCurrentTime();
1030
+ const duration = this.ytPlayer.getDuration();
1031
+ const testSeekPosition = Math.max(0, currentTime - 5);
1032
+
1033
+ if (this.api.player.options.debug) {
1034
+ console.log('[YT Plugin] 🔍 Testing DVR availability...');
1035
+ }
1036
+
1037
+ this.ytPlayer.seekTo(testSeekPosition, true);
1038
+
1039
+ setTimeout(() => {
1040
+ if (!this.ytPlayer) return;
1041
+
1042
+ const newCurrentTime = this.ytPlayer.getCurrentTime();
1043
+ const seekDifference = Math.abs(newCurrentTime - testSeekPosition);
1044
+
1045
+ const progressContainer = this.api.container.querySelector('.progress-container');
1046
+
1047
+ if (seekDifference < 2) {
1048
+ // DVR enabled - restore progress bar
1049
+ if (progressContainer) {
1050
+ progressContainer.style.opacity = '';
1051
+ progressContainer.style.pointerEvents = '';
1052
+ }
1053
+
1054
+ if (this.api.player.options.debug) {
1055
+ console.log('[YT Plugin] ✅ DVR ENABLED - progress bar active');
1056
+ }
1057
+
1058
+ this.ytPlayer.seekTo(duration, true);
1059
+ } else {
1060
+ // No DVR - keep progress bar disabled
1061
+ this.modifyProgressBarForLive();
1062
+
1063
+ if (this.api.player.options.debug) {
1064
+ console.log('[YT Plugin] ❌ DVR DISABLED - progress bar locked');
1065
+ }
1066
+ }
1067
+ }, 500);
1068
+
1069
+ } catch (error) {
1070
+ if (this.api.player.options.debug) {
1071
+ console.error('[YT Plugin] Error checking DVR:', error);
1072
+ }
1073
+ this.modifyProgressBarForLive();
1074
+ }
1075
+ }, 1000);
1076
+ }
1077
+
1078
+ createLiveBadge() {
1079
+ // Remove existing badge if present
1080
+ let existingBadge = this.api.container.querySelector('.live-badge');
1081
+ if (existingBadge) {
1082
+ existingBadge.remove();
1083
+ }
1084
+
1085
+ // Create LIVE badge
1086
+ const liveBadge = document.createElement('div');
1087
+ liveBadge.className = 'live-badge';
1088
+ liveBadge.innerHTML = 'LIVE';
1089
+ liveBadge.style.cssText = `
1090
+ display: inline-flex;
1091
+ align-items: center;
1092
+ gap: 6px;
1093
+ background: #ff0000;
1094
+ color: white;
1095
+ padding: 2px 8px;
1096
+ border-radius: 3px;
1097
+ font-size: 12px;
1098
+ font-weight: bold;
1099
+ cursor: pointer;
1100
+ user-select: none;
1101
+ margin-left: 8px;
1102
+ `;
1103
+
1104
+ // Add pulsing indicator style
1105
+ if (!document.getElementById('live-badge-style')) {
1106
+ const style = document.createElement('style');
1107
+ style.id = 'live-badge-style';
1108
+ style.textContent = `
1109
+ .live-indicator {
1110
+ width: 8px;
1111
+ height: 8px;
1112
+ background: white;
1113
+ border-radius: 50%;
1114
+ animation: live-pulse 1.5s ease-in-out infinite;
1115
+ }
1116
+ @keyframes live-pulse {
1117
+ 0%, 100% { opacity: 1; }
1118
+ 50% { opacity: 0.3; }
1119
+ }
1120
+ `;
1121
+ document.head.appendChild(style);
1122
+ }
1123
+
1124
+ // Click to return to live
1125
+ liveBadge.addEventListener('click', (e) => {
1126
+ e.stopPropagation();
1127
+ this.seekToLive();
1128
+ });
1129
+
1130
+ // Insert badge in control bar, next to time display area
1131
+ const controlsLeft = this.api.container.querySelector('.controls-left');
1132
+ if (controlsLeft) {
1133
+ controlsLeft.appendChild(liveBadge);
1134
+ if (this.api.player.options.debug) {
1135
+ console.log('[YT Plugin] Live badge added to controls-left');
1136
+ }
1137
+ } else {
1138
+ // Fallback: add to container
1139
+ this.api.container.appendChild(liveBadge);
1140
+ liveBadge.style.position = 'absolute';
1141
+ liveBadge.style.left = '10px';
1142
+ liveBadge.style.bottom = '50px';
1143
+ liveBadge.style.zIndex = '11';
1144
+ }
1145
+ }
1146
+
1147
+ seekToLive() {
1148
+ if (!this.ytPlayer || !this.isLiveStream) return;
1149
+
1150
+ try {
1151
+ // For live streams, seek to the current live position
1152
+ const duration = this.ytPlayer.getDuration();
1153
+ this.ytPlayer.seekTo(duration, true);
1154
+
1155
+ if (this.api.player.options.debug) {
1156
+ console.log('[YT Plugin] ⏩ Seeking to live edge:', duration);
1157
+ }
1158
+
1159
+ // Immediately update badge to red (will be confirmed by monitoring)
1160
+ const badge = this.api.container.querySelector('.live-badge');
1161
+ if (badge) {
1162
+ badge.style.background = '#ff0000';
1163
+ badge.textContent = 'LIVE';
1164
+ badge.title = '';
1165
+ }
1166
+
1167
+ this.isAtLiveEdge = true;
1168
+
1169
+ } catch (error) {
1170
+ if (this.api.player.options.debug) {
1171
+ console.error('[YT Plugin] Error seeking to live:', error);
1172
+ }
1173
+ }
1174
+ }
1175
+
1176
+ hideTimeDisplay() {
1177
+ // Hide both current time and duration elements
1178
+ const currentTimeEl = this.api.container.querySelector('.current-time');
1179
+ const durationEl = this.api.container.querySelector('.duration');
1180
+
1181
+ if (currentTimeEl) {
1182
+ currentTimeEl.style.display = 'none';
1183
+ if (this.api.player.options.debug) {
1184
+ console.log('[YT Plugin] Current time hidden');
1185
+ }
1186
+ }
1187
+
1188
+ if (durationEl) {
1189
+ durationEl.style.display = 'none';
1190
+ if (this.api.player.options.debug) {
1191
+ console.log('[YT Plugin] Duration hidden');
1192
+ }
1193
+ }
1194
+ }
1195
+
1196
+ showTimeDisplay() {
1197
+ const currentTimeEl = this.api.container.querySelector('.current-time');
1198
+ const durationEl = this.api.container.querySelector('.duration');
1199
+
1200
+ if (currentTimeEl) {
1201
+ currentTimeEl.style.display = '';
1202
+ }
1203
+
1204
+ if (durationEl) {
1205
+ durationEl.style.display = '';
1206
+ }
1207
+ }
1208
+
1209
+ modifyProgressBarForLive() {
1210
+ const progressContainer = this.api.container.querySelector('.progress-container');
1211
+ const progressHandle = this.api.container.querySelector('.progress-handle');
1212
+
1213
+ if (progressContainer) {
1214
+ // Disable all pointer events on progress bar
1215
+ progressContainer.style.pointerEvents = 'none';
1216
+ progressContainer.style.cursor = 'default';
1217
+ progressContainer.style.opacity = '0.6';
1218
+
1219
+ if (this.api.player.options.debug) {
1220
+ console.log('[YT Plugin] Progress bar disabled for live stream');
1221
+ }
1222
+ }
1223
+
1224
+ if (progressHandle) {
1225
+ progressHandle.style.display = 'none';
1226
+ }
1227
+ }
1228
+
1229
+ restoreProgressBarNormal() {
1230
+ const progressContainer = this.api.container.querySelector('.progress-container');
1231
+ const progressHandle = this.api.container.querySelector('.progress-handle');
1232
+
1233
+ if (progressContainer) {
1234
+ progressContainer.style.pointerEvents = '';
1235
+ progressContainer.style.cursor = '';
1236
+ progressContainer.style.opacity = '';
1237
+ }
1238
+
1239
+ if (progressHandle) {
1240
+ progressHandle.style.display = '';
1241
+ }
1242
+ }
1243
+
1244
+ startLiveMonitoring() {
1245
+ if (this.liveCheckInterval) {
1246
+ clearInterval(this.liveCheckInterval);
1247
+ }
1248
+
1249
+ this.liveCheckInterval = setInterval(() => {
1250
+ if (!this.ytPlayer || !this.isLiveStream) return;
1251
+
1252
+ try {
1253
+ const currentTime = this.ytPlayer.getCurrentTime();
1254
+ const duration = this.ytPlayer.getDuration();
1255
+ const latency = duration - currentTime;
1256
+ const playerState = this.ytPlayer.getPlayerState();
1257
+
1258
+ const badge = this.api.container.querySelector('.live-badge');
1259
+ if (badge) {
1260
+ // Check player state first
1261
+ if (playerState === YT.PlayerState.PAUSED) {
1262
+ // Keep orange when paused - don't override
1263
+ badge.style.background = '#ff8800';
1264
+ badge.textContent = '⏸ LIVE';
1265
+ badge.title = 'Livestreaming in Pause';
1266
+
1267
+ if (this.api.player.options.debug) {
1268
+ console.log('[YT Plugin] 🟠 Live paused (monitoring)');
1269
+ }
1270
+ } else if (playerState === YT.PlayerState.PLAYING) {
1271
+ // Only update color if playing
1272
+ // Check latency only if duration is reasonable
1273
+ if (latency > 60 && duration < 7200) {
1274
+ // DE-SYNCED - Black background
1275
+ badge.style.background = '#1a1a1a';
1276
+ badge.textContent = 'LIVE';
1277
+ badge.title = `${Math.floor(latency)} seconds back from the live`;
1278
+ this.isAtLiveEdge = false;
1279
+
1280
+ if (this.api.player.options.debug) {
1281
+ console.log('[YT Plugin] ⚫ De-synced, latency:', latency.toFixed(1), 's');
1282
+ }
1283
+ } else {
1284
+ // AT LIVE EDGE - Red background
1285
+ badge.style.background = '#ff0000';
1286
+ badge.textContent = 'LIVE';
1287
+ badge.title = 'Livestreaming';
1288
+ this.isAtLiveEdge = true;
1289
+
1290
+ if (this.api.player.options.debug) {
1291
+ console.log('[YT Plugin] 🔴 At live edge');
1292
+ }
1293
+ }
1294
+ }
1295
+ }
1296
+
1297
+ } catch (error) {
1298
+ if (this.api.player.options.debug) {
1299
+ console.error('[YT Plugin] Error monitoring live:', error);
1300
+ }
1301
+ }
1302
+ }, 2000); // Check every 2 seconds
1303
+ }
1304
+
1305
+ handleLiveStreamEnded() {
1306
+ if (this.api.player.options.debug) {
1307
+ console.log('[YT Plugin] 📹 Handling live stream end transition');
1308
+ }
1309
+
1310
+ // Stop live monitoring
1311
+ if (this.liveCheckInterval) {
1312
+ clearInterval(this.liveCheckInterval);
1313
+ this.liveCheckInterval = null;
1314
+ }
1315
+
1316
+ // Update badge to show "REPLAY" or remove it
1317
+ const badge = this.api.container.querySelector('.live-badge');
1318
+ if (badge) {
1319
+ // Option 1: Change to REPLAY badge
1320
+ badge.textContent = 'REPLAY';
1321
+ badge.style.background = '#555555';
1322
+ badge.style.cursor = 'default';
1323
+ badge.title = 'Registrazione del live stream';
1324
+
1325
+ // Remove click handler since there's no live to seek to
1326
+ const newBadge = badge.cloneNode(true);
1327
+ badge.parentNode.replaceChild(newBadge, badge);
1328
+
1329
+ // Option 2: Remove badge entirely after 5 seconds
1330
+ setTimeout(() => {
1331
+ if (newBadge && newBadge.parentNode) {
1332
+ newBadge.remove();
1333
+ }
1334
+ }, 5000);
1335
+
1336
+ if (this.api.player.options.debug) {
1337
+ console.log('[YT Plugin] ✅ Badge updated to REPLAY mode');
1338
+ }
1339
+ }
1340
+
1341
+ // Restore normal player behavior
1342
+ this.isLiveStream = false;
1343
+ this.isAtLiveEdge = false;
1344
+
1345
+ // Re-enable progress bar
1346
+ this.restoreProgressBarNormal();
1347
+
1348
+ // Show time display again
1349
+ this.showTimeDisplay();
1350
+
1351
+ // Restart normal time updates
1352
+ if (!this.timeUpdateInterval) {
1353
+ this.setupYouTubeSync();
1354
+ }
1355
+
1356
+ if (this.api.player.options.debug) {
1357
+ console.log('[YT Plugin] ✅ Transitioned from LIVE to REPLAY mode');
1358
+ }
671
1359
  }
672
1360
 
673
1361
  onApiChange(event) {
@@ -676,21 +1364,35 @@ width: fit-content;
676
1364
  }
677
1365
 
678
1366
  injectYouTubeCSSOverride() {
679
- if (document.getElementById('youtube-controls-override')) return;
1367
+ if (document.getElementById('youtube-controls-override')) {
1368
+ return;
1369
+ }
680
1370
 
681
1371
  const style = document.createElement('style');
682
1372
  style.id = 'youtube-controls-override';
683
1373
  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
- `;
1374
+ .video-wrapper.youtube-active .quality-control,
1375
+ .video-wrapper.youtube-active .subtitles-control {
1376
+ display: block !important;
1377
+ visibility: visible !important;
1378
+ opacity: 1 !important;
1379
+ }
1380
+
1381
+ /* Make watermark circular */
1382
+ .video-wrapper .watermark,
1383
+ .video-wrapper .watermark-image,
1384
+ .video-wrapper .watermark img {
1385
+ border-radius: 50% !important;
1386
+ overflow: hidden !important;
1387
+ }
1388
+ `;
1389
+
691
1390
  document.head.appendChild(style);
692
1391
  this.api.container.classList.add('youtube-active');
693
- if (this.api.player.options.debug) console.log('[YT Plugin] CSS override injected');
1392
+
1393
+ if (this.api.player.options.debug) {
1394
+ console.log('YT Plugin: CSS override injected');
1395
+ }
694
1396
  }
695
1397
 
696
1398
  // ===== QUALITY CONTROL METHODS =====
@@ -951,17 +1653,16 @@ width: fit-content;
951
1653
  }
952
1654
  }
953
1655
 
954
- // ===== SUBTITLES CONTROL METHODS =====
1656
+ // ===== RECONSTRUCTED SUBTITLE METHODS =====
955
1657
 
956
1658
  /**
957
- * Load available captions and create menu
1659
+ * Load available captions and create subtitle control
958
1660
  */
959
1661
  loadAvailableCaptions() {
960
1662
  if (!this.ytPlayer || !this.options.enableCaptions) return;
961
1663
 
962
- // Prevent creating menu multiple times
963
1664
  if (this.subtitlesMenuCreated) {
964
- if (this.api.player.options.debug) console.log('[YT Plugin] Subtitles menu already created, skipping');
1665
+ if (this.api.player.options.debug) console.log('[YT Plugin] Subtitles menu already created');
965
1666
  return;
966
1667
  }
967
1668
 
@@ -974,7 +1675,6 @@ width: fit-content;
974
1675
  if (this.api.player.options.debug) console.log('[YT Plugin] Captions module error:', e.message);
975
1676
  }
976
1677
 
977
- // FIXED: If tracklist is available and populated, use it
978
1678
  if (captionModule && Array.isArray(captionModule) && captionModule.length > 0) {
979
1679
  this.availableCaptions = captionModule.map((track, index) => {
980
1680
  const isAutomatic = track.kind === 'asr' || track.isautomatic || track.kind === 'auto';
@@ -989,39 +1689,36 @@ width: fit-content;
989
1689
  });
990
1690
 
991
1691
  if (this.api.player.options.debug) console.log('[YT Plugin] ✅ Captions loaded:', this.availableCaptions);
992
- this.createSubtitlesControl(true); // true = has tracklist
1692
+ this.createSubtitlesControl();
993
1693
  this.subtitlesMenuCreated = true;
994
1694
 
995
1695
  } else if (this.captionCheckAttempts < 5) {
996
- // Retry if tracklist not yet available
997
1696
  this.captionCheckAttempts++;
998
1697
  if (this.api.player.options.debug) console.log(`[YT Plugin] Retry caption load (${this.captionCheckAttempts}/5)`);
999
1698
  setTimeout(() => this.loadAvailableCaptions(), 1000);
1000
1699
 
1001
1700
  } 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
1701
+ if (this.api.player.options.debug) console.log('[YT Plugin] No tracklist - creating basic control');
1702
+ this.availableCaptions = [];
1703
+ this.createSubtitlesControl();
1006
1704
  this.subtitlesMenuCreated = true;
1007
1705
  }
1008
1706
 
1009
1707
  } catch (error) {
1010
1708
  if (this.api.player.options.debug) console.error('[YT Plugin] Error loading captions:', error);
1011
- this.createSubtitlesControl(false); // Fallback to On/Off buttons
1709
+ this.createSubtitlesControl();
1012
1710
  this.subtitlesMenuCreated = true;
1013
1711
  }
1014
1712
  }
1015
1713
 
1016
1714
  /**
1017
- * Create subtitles control button and menu
1018
- * @param {boolean} hasTracklist - true if YouTube provides caption tracks, false for auto captions only
1715
+ * Create subtitle control in the control bar
1019
1716
  */
1020
- createSubtitlesControl(hasTracklist) {
1717
+ createSubtitlesControl() {
1021
1718
  let subtitlesControl = this.api.container.querySelector('.subtitles-control');
1022
1719
  if (subtitlesControl) {
1023
1720
  if (this.api.player.options.debug) console.log('[YT Plugin] Subtitles control exists - updating menu');
1024
- this.populateSubtitlesMenu(hasTracklist);
1721
+ this.buildSubtitlesMenu();
1025
1722
  return;
1026
1723
  }
1027
1724
 
@@ -1029,13 +1726,13 @@ width: fit-content;
1029
1726
  if (!controlsRight) return;
1030
1727
 
1031
1728
  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
- `;
1729
+ <div class="subtitles-control">
1730
+ <button class="control-btn subtitles-btn" data-tooltip="Subtitles">
1731
+ <span class="icon">CC</span>
1732
+ </button>
1733
+ <div class="subtitles-menu"></div>
1734
+ </div>
1735
+ `;
1039
1736
 
1040
1737
  const qualityControl = controlsRight.querySelector('.quality-control');
1041
1738
  if (qualityControl) {
@@ -1050,89 +1747,376 @@ width: fit-content;
1050
1747
  }
1051
1748
 
1052
1749
  if (this.api.player.options.debug) console.log('[YT Plugin] Subtitles control created');
1053
- this.populateSubtitlesMenu(hasTracklist);
1750
+ this.buildSubtitlesMenu();
1054
1751
  this.bindSubtitlesButton();
1055
1752
  this.checkInitialCaptionState();
1056
1753
  this.startCaptionStateMonitoring();
1057
1754
  }
1058
1755
 
1059
1756
  /**
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
1757
+ * Build the subtitles menu
1063
1758
  */
1064
- populateSubtitlesMenu(hasTracklist) {
1759
+ buildSubtitlesMenu() {
1065
1760
  const subtitlesMenu = this.api.container.querySelector('.subtitles-menu');
1066
1761
  if (!subtitlesMenu) return;
1762
+
1067
1763
  subtitlesMenu.innerHTML = '';
1068
1764
 
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) => {
1765
+ // Off option
1766
+ const offOption = document.createElement('div');
1767
+ offOption.className = 'subtitles-option';
1768
+ offOption.textContent = 'Off';
1769
+ offOption.dataset.id = 'off';
1770
+ offOption.addEventListener('click', (e) => {
1075
1771
  e.stopPropagation();
1076
1772
  this.disableCaptions();
1773
+ this.updateMenuSelection('off');
1774
+ subtitlesMenu.classList.remove('show');
1077
1775
  });
1078
- subtitlesMenu.appendChild(offItem);
1776
+ subtitlesMenu.appendChild(offOption);
1079
1777
 
1080
- // Show available caption tracks if any
1778
+ // If captions are available
1081
1779
  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);
1780
+ // Add original languages
1781
+ this.availableCaptions.forEach((caption, index) => {
1782
+ const option = document.createElement('div');
1783
+ option.className = 'subtitles-option';
1784
+ option.textContent = caption.label;
1785
+ option.dataset.id = `caption-${index}`;
1786
+ option.addEventListener('click', (e) => {
1787
+ e.stopPropagation();
1788
+ this.setCaptionTrack(caption.languageCode);
1789
+ this.updateMenuSelection(`caption-${index}`);
1790
+ subtitlesMenu.classList.remove('show');
1791
+ });
1792
+ subtitlesMenu.appendChild(option);
1091
1793
  });
1092
1794
  } 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) => {
1795
+ // Auto-caption only (without tracklist)
1796
+ const autoOption = document.createElement('div');
1797
+ autoOption.className = 'subtitles-option';
1798
+ autoOption.textContent = 'Auto-generated';
1799
+ autoOption.dataset.id = 'auto';
1800
+ autoOption.addEventListener('click', (e) => {
1099
1801
  e.stopPropagation();
1100
1802
  this.enableAutoCaptions();
1803
+ this.updateMenuSelection('auto');
1804
+ subtitlesMenu.classList.remove('show');
1805
+ });
1806
+ subtitlesMenu.appendChild(autoOption);
1807
+ }
1808
+
1809
+ // Always add "Auto-translate" (both with and without tracklist)
1810
+ const translateOption = document.createElement('div');
1811
+ translateOption.className = 'subtitles-option translate-option';
1812
+ translateOption.textContent = 'Auto-translate';
1813
+ translateOption.dataset.id = 'translate';
1814
+ translateOption.addEventListener('click', (e) => {
1815
+ e.stopPropagation();
1816
+ this.showTranslationMenu();
1817
+ });
1818
+ subtitlesMenu.appendChild(translateOption);
1819
+ }
1820
+
1821
+ /**
1822
+ * Show translation menu (submenu)
1823
+ */
1824
+ showTranslationMenu() {
1825
+ if (this.api.player.options.debug) console.log('[YT Plugin] showTranslationMenu called');
1826
+
1827
+ const subtitlesMenu = this.api.container.querySelector('.subtitles-menu');
1828
+ if (!subtitlesMenu) return;
1829
+
1830
+ // Clear and rebuild with translation languages
1831
+ subtitlesMenu.innerHTML = '';
1832
+
1833
+ // Back option
1834
+ const backOption = document.createElement('div');
1835
+ backOption.className = 'subtitles-option back-option';
1836
+ backOption.innerHTML = '← Back';
1837
+ backOption.addEventListener('click', (e) => {
1838
+ e.stopPropagation();
1839
+ if (this.api.player.options.debug) console.log('[YT Plugin] Back clicked');
1840
+ this.buildSubtitlesMenu();
1841
+ });
1842
+ subtitlesMenu.appendChild(backOption);
1843
+
1844
+ // Add translation languages
1845
+ const translationLanguages = this.getTopTranslationLanguages();
1846
+ translationLanguages.forEach(lang => {
1847
+ const option = document.createElement('div');
1848
+ option.className = 'subtitles-option';
1849
+ option.textContent = lang.name;
1850
+ option.dataset.id = `translate-${lang.code}`;
1851
+ option.dataset.langcode = lang.code;
1852
+
1853
+ if (this.api.player.options.debug) console.log('[YT Plugin] Creating option for:', lang.name, lang.code);
1854
+
1855
+ option.addEventListener('click', (e) => {
1856
+ e.stopPropagation();
1857
+ if (this.api.player.options.debug) console.log('[YT Plugin] Language clicked:', lang.code, lang.name);
1858
+ if (this.api.player.options.debug) console.log('[YT Plugin] this:', this);
1859
+ if (this.api.player.options.debug) console.log('[YT Plugin] About to call setTranslatedCaptions with:', lang.code);
1860
+
1861
+ const result = this.setTranslatedCaptions(lang.code);
1862
+
1863
+ if (this.api.player.options.debug) console.log('[YT Plugin] setTranslatedCaptions returned:', result);
1864
+
1865
+ this.updateMenuSelection(`translate-${lang.code}`);
1866
+ subtitlesMenu.classList.remove('show');
1867
+
1868
+ // Return to main menu
1869
+ setTimeout(() => this.buildSubtitlesMenu(), 300);
1101
1870
  });
1102
- subtitlesMenu.appendChild(onItem);
1871
+
1872
+ subtitlesMenu.appendChild(option);
1873
+ });
1874
+
1875
+ if (this.api.player.options.debug) console.log('[YT Plugin] Translation menu built with', translationLanguages.length, 'languages');
1876
+ }
1877
+
1878
+ /**
1879
+ * Update selection in the menu
1880
+ */
1881
+ updateMenuSelection(selectedId) {
1882
+ const subtitlesMenu = this.api.container.querySelector('.subtitles-menu');
1883
+ if (!subtitlesMenu) return;
1884
+
1885
+ // Remove all selections
1886
+ subtitlesMenu.querySelectorAll('.subtitles-option').forEach(option => {
1887
+ option.classList.remove('selected');
1888
+ });
1889
+
1890
+ // Add selection to current option
1891
+ const selectedOption = subtitlesMenu.querySelector(`[data-id="${selectedId}"]`);
1892
+ if (selectedOption) {
1893
+ selectedOption.classList.add('selected');
1894
+ }
1895
+
1896
+ // Update button state
1897
+ const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
1898
+ if (subtitlesBtn) {
1899
+ if (selectedId === 'off') {
1900
+ subtitlesBtn.classList.remove('active');
1901
+ } else {
1902
+ subtitlesBtn.classList.add('active');
1903
+ }
1103
1904
  }
1104
1905
  }
1105
1906
 
1907
+ /**
1908
+ * Bind subtitle button (toggle)
1909
+ */
1106
1910
  bindSubtitlesButton() {
1107
1911
  const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
1108
1912
  if (!subtitlesBtn) return;
1109
1913
 
1110
- // Remove existing event listeners by cloning
1111
1914
  const newBtn = subtitlesBtn.cloneNode(true);
1112
1915
  subtitlesBtn.parentNode.replaceChild(newBtn, subtitlesBtn);
1113
1916
 
1114
1917
  newBtn.addEventListener('click', (e) => {
1115
1918
  e.stopPropagation();
1116
- this.toggleCaptions();
1919
+
1920
+ // Toggle: if active disable, otherwise enable first available
1921
+ if (this.captionsEnabled) {
1922
+ this.disableCaptions();
1923
+ this.updateMenuSelection('off');
1924
+ } else {
1925
+ if (this.availableCaptions.length > 0) {
1926
+ const firstCaption = this.availableCaptions[0];
1927
+ this.setCaptionTrack(firstCaption.languageCode);
1928
+ this.updateMenuSelection('caption-0');
1929
+ } else {
1930
+ this.enableAutoCaptions();
1931
+ this.updateMenuSelection('auto');
1932
+ }
1933
+ }
1117
1934
  });
1118
1935
  }
1119
1936
 
1937
+ /**
1938
+ * Set a specific caption track
1939
+ */
1940
+ setCaptionTrack(languageCode) {
1941
+ if (!this.ytPlayer) return false;
1942
+
1943
+ try {
1944
+ this.ytPlayer.setOption('captions', 'track', { languageCode: languageCode });
1945
+ this.ytPlayer.loadModule('captions');
1946
+
1947
+ this.captionsEnabled = true;
1948
+ this.currentCaption = languageCode;
1949
+ this.currentTranslation = null;
1950
+
1951
+ if (this.api.player.options.debug) {
1952
+ console.log('[YT Plugin] Caption track set:', languageCode);
1953
+ }
1954
+
1955
+ return true;
1956
+ } catch (error) {
1957
+ if (this.api.player.options.debug) {
1958
+ console.error('[YT Plugin] Error setting caption track:', error);
1959
+ }
1960
+ return false;
1961
+ }
1962
+ }
1963
+
1964
+ /**
1965
+ * Set automatic translation
1966
+ */
1967
+ setTranslatedCaptions(translationLanguageCode) {
1968
+ if (this.api.player.options.debug) console.log('[YT Plugin] setTranslatedCaptions called with:', translationLanguageCode);
1969
+
1970
+ if (!this.ytPlayer) {
1971
+ if (this.api.player.options.debug) console.error('[YT Plugin] ytPlayer not available');
1972
+ return false;
1973
+ }
1974
+
1975
+ try {
1976
+ if (this.api.player.options.debug) console.log('[YT Plugin] Available captions:', this.availableCaptions);
1977
+
1978
+ if (this.availableCaptions.length > 0) {
1979
+ // WITH TRACKLIST: Use first available caption as base
1980
+ const baseLanguageCode = this.availableCaptions[0].languageCode;
1981
+ if (this.api.player.options.debug) console.log('[YT Plugin] Using base language:', baseLanguageCode);
1982
+
1983
+ this.ytPlayer.setOption('captions', 'track', {
1984
+ 'languageCode': baseLanguageCode,
1985
+ 'translationLanguage': {
1986
+ 'languageCode': translationLanguageCode
1987
+ }
1988
+ });
1989
+ } else {
1990
+ // WITHOUT TRACKLIST: Get current auto-generated track
1991
+ if (this.api.player.options.debug) console.log('[YT Plugin] No tracklist - getting auto-generated track');
1992
+
1993
+ let currentTrack = null;
1994
+ try {
1995
+ currentTrack = this.ytPlayer.getOption('captions', 'track');
1996
+ if (this.api.player.options.debug) console.log('[YT Plugin] Current track:', currentTrack);
1997
+ } catch (e) {
1998
+ if (this.api.player.options.debug) console.log('[YT Plugin] Could not get current track:', e.message);
1999
+ }
2000
+
2001
+ if (currentTrack && currentTrack.languageCode) {
2002
+ // Use auto-generated language as base
2003
+ if (this.api.player.options.debug) console.log('[YT Plugin] Using auto-generated language:', currentTrack.languageCode);
2004
+
2005
+ this.ytPlayer.setOption('captions', 'track', {
2006
+ 'languageCode': currentTrack.languageCode,
2007
+ 'translationLanguage': {
2008
+ 'languageCode': translationLanguageCode
2009
+ }
2010
+ });
2011
+ } else {
2012
+ // Fallback: try with 'en' as base
2013
+ if (this.api.player.options.debug) console.log('[YT Plugin] Fallback: using English as base');
2014
+
2015
+ this.ytPlayer.setOption('captions', 'track', {
2016
+ 'languageCode': 'en',
2017
+ 'translationLanguage': {
2018
+ 'languageCode': translationLanguageCode
2019
+ }
2020
+ });
2021
+ }
2022
+ }
2023
+
2024
+ this.captionsEnabled = true;
2025
+ this.currentTranslation = translationLanguageCode;
2026
+
2027
+ if (this.api.player.options.debug) console.log('[YT Plugin] ✅ Translation applied');
2028
+
2029
+ return true;
2030
+ } catch (error) {
2031
+ if (this.api.player.options.debug) console.error('[YT Plugin] Error setting translation:', error);
2032
+ return false;
2033
+ }
2034
+ }
2035
+
2036
+ /**
2037
+ * Get language name from code
2038
+ */
2039
+ getLanguageName(languageCode) {
2040
+ const languages = this.getTopTranslationLanguages();
2041
+ const lang = languages.find(l => l.code === languageCode);
2042
+ return lang ? lang.name : languageCode;
2043
+ }
2044
+
2045
+ /**
2046
+ * Enable automatic captions
2047
+ */
2048
+ enableAutoCaptions() {
2049
+ if (!this.ytPlayer) return false;
2050
+
2051
+ try {
2052
+ this.ytPlayer.setOption('captions', 'reload', true);
2053
+ this.ytPlayer.loadModule('captions');
2054
+
2055
+ this.captionsEnabled = true;
2056
+ this.currentCaption = null;
2057
+ this.currentTranslation = null;
2058
+
2059
+ if (this.api.player.options.debug) {
2060
+ console.log('[YT Plugin] Auto captions enabled');
2061
+ }
2062
+
2063
+ return true;
2064
+ } catch (error) {
2065
+ if (this.api.player.options.debug) {
2066
+ console.error('[YT Plugin] Error enabling auto captions:', error);
2067
+ }
2068
+ return false;
2069
+ }
2070
+ }
2071
+
2072
+ /**
2073
+ * Disable captions
2074
+ */
2075
+ disableCaptions() {
2076
+ if (!this.ytPlayer) return false;
2077
+
2078
+ try {
2079
+ this.ytPlayer.unloadModule('captions');
2080
+
2081
+ this.captionsEnabled = false;
2082
+ this.currentCaption = null;
2083
+ this.currentTranslation = null;
2084
+
2085
+ if (this.api.player.options.debug) {
2086
+ console.log('[YT Plugin] Captions disabled');
2087
+ }
2088
+
2089
+ return true;
2090
+ } catch (error) {
2091
+ if (this.api.player.options.debug) {
2092
+ console.error('[YT Plugin] Error disabling captions:', error);
2093
+ }
2094
+ return false;
2095
+ }
2096
+ }
2097
+
2098
+ /**
2099
+ * Check initial caption state
2100
+ */
1120
2101
  checkInitialCaptionState() {
1121
2102
  setTimeout(() => {
1122
2103
  try {
1123
2104
  const currentTrack = this.ytPlayer.getOption('captions', 'track');
1124
2105
  if (currentTrack && currentTrack.languageCode) {
1125
2106
  this.captionsEnabled = true;
1126
- const subtitlesBtn = this.api.container.querySelector('.subtitles-btn');
1127
- if (subtitlesBtn) subtitlesBtn.classList.add('active');
1128
- this.updateSubtitlesMenuActiveState();
2107
+ this.updateMenuSelection('caption-0');
2108
+ } else {
2109
+ this.updateMenuSelection('off');
1129
2110
  }
1130
2111
  } catch (e) {
1131
- // Ignore errors
2112
+ this.updateMenuSelection('off');
1132
2113
  }
1133
2114
  }, 1500);
1134
2115
  }
1135
2116
 
2117
+ /**
2118
+ * Monitor caption state
2119
+ */
1136
2120
  startCaptionStateMonitoring() {
1137
2121
  if (this.captionStateCheckInterval) {
1138
2122
  clearInterval(this.captionStateCheckInterval);
@@ -1154,7 +2138,6 @@ width: fit-content;
1154
2138
  subtitlesBtn.classList.remove('active');
1155
2139
  }
1156
2140
  }
1157
- this.updateSubtitlesMenuActiveState();
1158
2141
  }
1159
2142
  } catch (e) {
1160
2143
  // Ignore errors
@@ -1210,78 +2193,6 @@ width: fit-content;
1210
2193
  }
1211
2194
  }
1212
2195
 
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
2196
  /**
1286
2197
  * Enable automatic captions (when no tracklist available)
1287
2198
  */
@@ -1365,6 +2276,66 @@ width: fit-content;
1365
2276
  const playIcon = this.api.container.querySelector('.play-icon');
1366
2277
  const pauseIcon = this.api.container.querySelector('.pause-icon');
1367
2278
 
2279
+ // Get live badge
2280
+ const badge = this.api.container.querySelector('.live-badge');
2281
+
2282
+ // Handle live stream ended
2283
+ if (this.isLiveStream && event.data === YT.PlayerState.ENDED) {
2284
+ if (this.api.player.options.debug) {
2285
+ console.log('[YT Plugin] 🔴➡️📹 Live stream ended (player state: ENDED)');
2286
+ }
2287
+ this.handleLiveStreamEnded();
2288
+ return;
2289
+ }
2290
+
2291
+ // Update live badge based on state
2292
+ if (this.isLiveStream && badge) {
2293
+ if (event.data === YT.PlayerState.PAUSED) {
2294
+ // Orange when paused during live
2295
+ badge.style.background = '#ff8800';
2296
+ badge.textContent = '⏸ LIVE';
2297
+ badge.title = 'Livestreaming in Pause';
2298
+
2299
+ if (this.api.player.options.debug) {
2300
+ console.log('[YT Plugin] 🟠 Live paused');
2301
+ }
2302
+ } else if (event.data === YT.PlayerState.PLAYING) {
2303
+ // Red when playing (will be checked for de-sync below)
2304
+ badge.style.background = '#ff0000';
2305
+ badge.textContent = 'LIVE';
2306
+ badge.title = 'Livestreaming';
2307
+
2308
+ if (this.api.player.options.debug) {
2309
+ console.log('[YT Plugin] 🔴 Live playing');
2310
+ }
2311
+ }
2312
+ }
2313
+
2314
+ // Check for de-sync when user seeks during live
2315
+ if (this.isLiveStream && event.data === YT.PlayerState.PLAYING) {
2316
+ setTimeout(() => {
2317
+ if (!this.ytPlayer) return;
2318
+
2319
+ const currentTime = this.ytPlayer.getCurrentTime();
2320
+ const duration = this.ytPlayer.getDuration();
2321
+ const latency = duration - currentTime;
2322
+
2323
+ // If latency > 60s and duration is reasonable, user has seeked back
2324
+ if (latency > 60 && duration < 7200) {
2325
+ const badge = this.api.container.querySelector('.live-badge');
2326
+ if (badge) {
2327
+ badge.style.background = '#1a1a1a';
2328
+ badge.title = `${Math.floor(latency)} seconds back from the live`;
2329
+ this.isAtLiveEdge = false;
2330
+
2331
+ if (this.api.player.options.debug) {
2332
+ console.log('[YT Plugin] ⚫ User seeked back, de-synced from live');
2333
+ }
2334
+ }
2335
+ }
2336
+ }, 500);
2337
+ }
2338
+
1368
2339
  switch (event.data) {
1369
2340
  case YT.PlayerState.PLAYING:
1370
2341
  this.api.triggerEvent('played', {});
@@ -1913,6 +2884,44 @@ width: fit-content;
1913
2884
  }
1914
2885
 
1915
2886
  this.showPosterOverlay();
2887
+
2888
+ if (this.liveCheckInterval) {
2889
+ clearInterval(this.liveCheckInterval);
2890
+ this.liveCheckInterval = null;
2891
+ }
2892
+
2893
+ // Restore normal UI when destroying
2894
+ if (this.isLiveStream) {
2895
+ this.showTimeDisplay();
2896
+ this.restoreProgressBarNormal();
2897
+
2898
+ const liveBadge = this.api.container.querySelector('.live-badge');
2899
+ if (liveBadge) {
2900
+ liveBadge.remove();
2901
+ }
2902
+ }
2903
+
2904
+ // Clear live stream intervals
2905
+ if (this.liveCheckInterval) {
2906
+ clearInterval(this.liveCheckInterval);
2907
+ this.liveCheckInterval = null;
2908
+ }
2909
+
2910
+ // Restore normal UI
2911
+ if (this.isLiveStream) {
2912
+ this.showTimeDisplay();
2913
+ this.restoreProgressBarNormal();
2914
+
2915
+ const liveBadge = this.api.container.querySelector('.live-badge');
2916
+ if (liveBadge) {
2917
+ liveBadge.remove();
2918
+ }
2919
+ }
2920
+
2921
+ // Reset live stream tracking
2922
+ this.isLiveStream = false;
2923
+ this.isAtLiveEdge = false;
2924
+
1916
2925
  }
1917
2926
  }
1918
2927