myetv-player 1.0.6 → 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.
- package/README.md +13 -0
- package/css/myetv-player.css +374 -208
- package/css/myetv-player.min.css +1 -1
- package/dist/myetv-player.js +213 -31
- package/dist/myetv-player.min.js +188 -20
- package/package.json +3 -1
- package/plugins/youtube/README.md +13 -5
- package/plugins/youtube/myetv-player-youtube-plugin.js +1150 -141
- package/scss/_base.scss +0 -15
- package/scss/_controls.scss +311 -30
- package/scss/_menus.scss +51 -0
- package/scss/_responsive.scss +187 -321
- package/scss/_video.scss +0 -75
- package/scss/_watermark.scss +120 -0
- package/scss/myetv-player.scss +7 -7
- package/src/controls.js +73 -22
- package/src/core.js +56 -4
- package/src/events.js +33 -5
- package/src/watermark.js +51 -0
|
@@ -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'))
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
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
|
-
// =====
|
|
1656
|
+
// ===== RECONSTRUCTED SUBTITLE METHODS =====
|
|
955
1657
|
|
|
956
1658
|
/**
|
|
957
|
-
* Load available captions and create
|
|
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
|
|
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(
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
this.
|
|
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(
|
|
1709
|
+
this.createSubtitlesControl();
|
|
1012
1710
|
this.subtitlesMenuCreated = true;
|
|
1013
1711
|
}
|
|
1014
1712
|
}
|
|
1015
1713
|
|
|
1016
1714
|
/**
|
|
1017
|
-
* Create
|
|
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(
|
|
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.
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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.
|
|
1750
|
+
this.buildSubtitlesMenu();
|
|
1054
1751
|
this.bindSubtitlesButton();
|
|
1055
1752
|
this.checkInitialCaptionState();
|
|
1056
1753
|
this.startCaptionStateMonitoring();
|
|
1057
1754
|
}
|
|
1058
1755
|
|
|
1059
1756
|
/**
|
|
1060
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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(
|
|
1776
|
+
subtitlesMenu.appendChild(offOption);
|
|
1079
1777
|
|
|
1080
|
-
//
|
|
1778
|
+
// If captions are available
|
|
1081
1779
|
if (this.availableCaptions.length > 0) {
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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
|
-
//
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
this.
|
|
2107
|
+
this.updateMenuSelection('caption-0');
|
|
2108
|
+
} else {
|
|
2109
|
+
this.updateMenuSelection('off');
|
|
1129
2110
|
}
|
|
1130
2111
|
} catch (e) {
|
|
1131
|
-
|
|
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
|
|