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.
- package/README.md +76 -2
- package/css/myetv-player.css +321 -208
- package/css/myetv-player.min.css +1 -1
- package/dist/myetv-player.js +219 -37
- package/dist/myetv-player.min.js +204 -26
- package/package.json +3 -1
- package/plugins/cloudflare/README.md +26 -4
- package/plugins/cloudflare/myetv-player-cloudflare-stream-plugin.js +1273 -217
- package/plugins/facebook/myetv-player-facebook-plugin.js +1340 -164
- package/plugins/twitch/myetv-player-twitch-plugin.js +428 -167
- package/plugins/vimeo/README.md +1 -1
- package/plugins/vimeo/myetv-player-vimeo.js +560 -247
- package/plugins/youtube/README.md +18 -7
- package/plugins/youtube/myetv-player-youtube-plugin.js +1485 -190
- package/scss/_base.scss +0 -15
- package/scss/_controls.scss +182 -2
- package/scss/_menus.scss +51 -0
- package/scss/_responsive.scss +187 -321
- package/scss/_title-overlay.scss +27 -0
- package/scss/_video.scss +0 -75
- package/scss/_watermark.scss +120 -0
- package/scss/myetv-player.scss +7 -7
- package/src/controls.js +72 -21
- package/src/core.js +43 -5
- package/src/events.js +33 -5
- package/src/utils.js +20 -6
- package/src/watermark.js +51 -0
|
@@ -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 || '
|
|
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
|
-
//
|
|
507
|
-
this.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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.
|
|
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
|
-
|
|
519
|
-
|
|
758
|
+
// Setup mouse detection
|
|
759
|
+
this.setupMouseMoveDetection();
|
|
520
760
|
|
|
521
|
-
|
|
522
|
-
|
|
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 (
|
|
526
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
542
|
-
|
|
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
|
-
|
|
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
|
-
|
|
548
|
-
this.
|
|
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'))
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
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
|
-
|
|
922
|
-
|
|
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
|
-
//
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// =====
|
|
1946
|
+
// ===== SUBTITLE METHODS =====
|
|
955
1947
|
|
|
956
1948
|
/**
|
|
957
|
-
* Load available captions and create
|
|
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
|
|
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(
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
this.
|
|
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(
|
|
1999
|
+
this.createSubtitlesControl();
|
|
1012
2000
|
this.subtitlesMenuCreated = true;
|
|
1013
2001
|
}
|
|
1014
2002
|
}
|
|
1015
2003
|
|
|
1016
2004
|
/**
|
|
1017
|
-
* Create
|
|
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(
|
|
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.
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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.
|
|
2040
|
+
this.buildSubtitlesMenu();
|
|
1054
2041
|
this.bindSubtitlesButton();
|
|
1055
2042
|
this.checkInitialCaptionState();
|
|
1056
2043
|
this.startCaptionStateMonitoring();
|
|
1057
2044
|
}
|
|
1058
2045
|
|
|
1059
2046
|
/**
|
|
1060
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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(
|
|
2066
|
+
subtitlesMenu.appendChild(offOption);
|
|
1079
2067
|
|
|
1080
|
-
//
|
|
2068
|
+
// If captions are available
|
|
1081
2069
|
if (this.availableCaptions.length > 0) {
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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
|
-
//
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
this.
|
|
2397
|
+
this.updateMenuSelection('caption-0');
|
|
2398
|
+
} else {
|
|
2399
|
+
this.updateMenuSelection('off');
|
|
1129
2400
|
}
|
|
1130
2401
|
} catch (e) {
|
|
1131
|
-
|
|
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
|
-
|
|
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
|
|