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