myetv-player 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm-publish.yml +30 -0
- package/LICENSE +21 -0
- package/README.md +866 -0
- package/build.js +189 -0
- package/css/README.md +1 -0
- package/css/myetv-player.css +13702 -0
- package/css/myetv-player.min.css +1 -0
- package/dist/README.md +1 -0
- package/dist/myetv-player.js +6408 -0
- package/dist/myetv-player.min.js +6183 -0
- package/package.json +27 -0
- package/plugins/README.md +1 -0
- package/plugins/google-analytics/README.md +1 -0
- package/plugins/google-analytics/myetv-player-g-analytics-plugin.js +548 -0
- package/plugins/youtube/README.md +1 -0
- package/plugins/youtube/myetv-player-youtube-plugin.js +418 -0
- package/scss/README.md +1 -0
- package/scss/_audio-player.scss +21 -0
- package/scss/_base.scss +131 -0
- package/scss/_controls.scss +30 -0
- package/scss/_loading.scss +111 -0
- package/scss/_menus.scss +4070 -0
- package/scss/_mixins.scss +112 -0
- package/scss/_poster.scss +8 -0
- package/scss/_progress-bar.scss +2203 -0
- package/scss/_resolution.scss +68 -0
- package/scss/_responsive.scss +1532 -0
- package/scss/_themes.scss +30 -0
- package/scss/_title-overlay.scss +2262 -0
- package/scss/_tooltips.scss +7 -0
- package/scss/_variables.scss +49 -0
- package/scss/_video.scss +2401 -0
- package/scss/_volume.scss +1981 -0
- package/scss/_watermark.scss +8 -0
- package/scss/myetv-player.scss +51 -0
- package/scss/package.json +16 -0
- package/src/README.md +1 -0
- package/src/chapters.js +521 -0
- package/src/controls.js +1005 -0
- package/src/core.js +1650 -0
- package/src/events.js +330 -0
- package/src/fullscreen.js +82 -0
- package/src/i18n.js +348 -0
- package/src/playlist.js +177 -0
- package/src/plugins.js +384 -0
- package/src/quality.js +921 -0
- package/src/streaming.js +346 -0
- package/src/subtitles.js +426 -0
- package/src/utils.js +51 -0
- package/src/watermark.js +195 -0
package/src/core.js
ADDED
|
@@ -0,0 +1,1650 @@
|
|
|
1
|
+
// Core Module for MYETV Video Player
|
|
2
|
+
// Conservative modularization - original code preserved exactly
|
|
3
|
+
// Created by https://www.myetv.tv https://oskarcosimo.com
|
|
4
|
+
|
|
5
|
+
constructor(videoElement, options = {}) {
|
|
6
|
+
this.video = typeof videoElement === 'string'
|
|
7
|
+
? document.getElementById(videoElement)
|
|
8
|
+
: videoElement;
|
|
9
|
+
|
|
10
|
+
if (!this.video) {
|
|
11
|
+
throw new Error('Video element not found: ' + videoElement);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
this.options = {
|
|
15
|
+
showQualitySelector: true,
|
|
16
|
+
showSpeedControl: true,
|
|
17
|
+
showFullscreen: true,
|
|
18
|
+
showPictureInPicture: true,
|
|
19
|
+
showSubtitles: true,
|
|
20
|
+
subtitlesEnabled: false,
|
|
21
|
+
autoHide: true,
|
|
22
|
+
autoHideDelay: 3000,
|
|
23
|
+
poster: null, // URL of poster image
|
|
24
|
+
showPosterOnEnd: false, // Show poster again when video ends
|
|
25
|
+
keyboardControls: true,
|
|
26
|
+
showSeekTooltip: true,
|
|
27
|
+
showTitleOverlay: false,
|
|
28
|
+
videoTitle: '',
|
|
29
|
+
persistentTitle: false,
|
|
30
|
+
debug: false, // Enable/disable debug logging
|
|
31
|
+
autoplay: false, // if video should autoplay at start
|
|
32
|
+
defaultQuality: 'auto', // 'auto', '1080p', '720p', '480p', etc.
|
|
33
|
+
language: null, // language of the player (default english)
|
|
34
|
+
pauseClick: true, // the player should be paused when click over the video area
|
|
35
|
+
doubleTapPause: true, // first tap (or click) show the controlbar, second tap (or click) pause
|
|
36
|
+
brandLogoEnabled: false, // Enable/disable brand logo
|
|
37
|
+
brandLogoUrl: '', // URL for brand logo image
|
|
38
|
+
brandLogoLinkUrl: '', // Optional URL to open when clicking the logo
|
|
39
|
+
playlistEnabled: true, // Enable/disable playlist detection
|
|
40
|
+
playlistAutoPlay: true, // Auto-play next video when current ends
|
|
41
|
+
playlistLoop: false, // Loop playlist when reaching the end
|
|
42
|
+
loop: false, // Loop video when it ends (restart from beginning)
|
|
43
|
+
volumeSlider: 'horizontal', //volume slider type: 'horizontal' or 'vertical'
|
|
44
|
+
// WATERMARK OVERLAY
|
|
45
|
+
watermarkUrl: '', // URL of watermark image
|
|
46
|
+
watermarkLink: '', // Optional URL to open when clicking watermark
|
|
47
|
+
watermarkPosition: 'bottomright', // Position: topleft, topright, bottomleft, bottomright
|
|
48
|
+
watermarkTitle: '', // Optional tooltip title
|
|
49
|
+
hideWatermark: true, // Hide watermark with controls (default: true)
|
|
50
|
+
// ADAPTIVE STREAMING SUPPORT
|
|
51
|
+
adaptiveStreaming: false, // Enable DASH/HLS adaptive streaming
|
|
52
|
+
dashLibUrl: 'https://cdn.dashjs.org/latest/dash.all.min.js', // Dash.js library URL
|
|
53
|
+
hlsLibUrl: 'https://cdn.jsdelivr.net/npm/hls.js@latest', // HLS.js library URL
|
|
54
|
+
adaptiveQualityControl: true, // Show quality control for adaptive streams
|
|
55
|
+
// AUDIO PLAYER
|
|
56
|
+
audiofile: false,
|
|
57
|
+
audiowave: false,
|
|
58
|
+
// RESOLUTION CONTROL
|
|
59
|
+
resolution: "normal", // "normal", "4:3", "16:9", "stretched", "fit-to-screen", "scale-to-fit"
|
|
60
|
+
...options
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
this.isUserSeeking = false;
|
|
64
|
+
this.controlsTimeout = null;
|
|
65
|
+
this.titleTimeout = null;
|
|
66
|
+
this.currentQualityIndex = 0;
|
|
67
|
+
this.qualities = [];
|
|
68
|
+
this.originalSources = [];
|
|
69
|
+
this.isPiPSupported = this.checkPiPSupport();
|
|
70
|
+
this.seekTooltip = null;
|
|
71
|
+
this.titleOverlay = null;
|
|
72
|
+
this.isPlayerReady = false;
|
|
73
|
+
|
|
74
|
+
// Subtitle management
|
|
75
|
+
this.textTracks = [];
|
|
76
|
+
this.currentSubtitleTrack = null;
|
|
77
|
+
this.subtitlesEnabled = false;
|
|
78
|
+
this.customSubtitleRenderer = null;
|
|
79
|
+
|
|
80
|
+
// Chapter management
|
|
81
|
+
this.chapters = [];
|
|
82
|
+
this.chapterMarkersContainer = null;
|
|
83
|
+
this.chapterTooltip = null;
|
|
84
|
+
|
|
85
|
+
// Dual quality indicator management
|
|
86
|
+
this.selectedQuality = this.options.defaultQuality || 'auto';
|
|
87
|
+
this.currentPlayingQuality = null;
|
|
88
|
+
this.qualityMonitorInterval = null;
|
|
89
|
+
|
|
90
|
+
// Quality change management
|
|
91
|
+
this.qualityChangeTimeout = null;
|
|
92
|
+
this.isChangingQuality = false;
|
|
93
|
+
|
|
94
|
+
// Quality debug
|
|
95
|
+
this.debugQuality = false;
|
|
96
|
+
|
|
97
|
+
// Auto-hide system
|
|
98
|
+
this.autoHideTimer = null;
|
|
99
|
+
this.mouseOverControls = false;
|
|
100
|
+
this.autoHideDebug = false;
|
|
101
|
+
this.autoHideInitialized = false;
|
|
102
|
+
|
|
103
|
+
// Poster management
|
|
104
|
+
this.posterOverlay = null;
|
|
105
|
+
|
|
106
|
+
// Watermark overlay
|
|
107
|
+
this.watermarkElement = null;
|
|
108
|
+
|
|
109
|
+
// Custom event system
|
|
110
|
+
this.eventCallbacks = {
|
|
111
|
+
'played': [],
|
|
112
|
+
'paused': [],
|
|
113
|
+
'subtitlechange': [],
|
|
114
|
+
'chapterchange': [],
|
|
115
|
+
'pipchange': [],
|
|
116
|
+
'fullscreenchange': [],
|
|
117
|
+
'speedchange': [],
|
|
118
|
+
'timeupdate': [],
|
|
119
|
+
'volumechange': [],
|
|
120
|
+
'qualitychange': [],
|
|
121
|
+
'playlistchange': [],
|
|
122
|
+
'ended': []
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Playlist management
|
|
126
|
+
this.playlist = [];
|
|
127
|
+
this.currentPlaylistIndex = -1;
|
|
128
|
+
this.playlistId = null;
|
|
129
|
+
this.isPlaylistActive = false;
|
|
130
|
+
|
|
131
|
+
// Adaptive streaming management
|
|
132
|
+
this.dashPlayer = null;
|
|
133
|
+
this.hlsPlayer = null;
|
|
134
|
+
this.adaptiveStreamingType = null; // 'dash', 'hls', or null
|
|
135
|
+
this.isAdaptiveStream = false;
|
|
136
|
+
this.adaptiveQualities = [];
|
|
137
|
+
this.librariesLoaded = {
|
|
138
|
+
dash: false,
|
|
139
|
+
hls: false
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
this.lastTimeUpdate = 0; // For throttling timeupdate events
|
|
143
|
+
|
|
144
|
+
if (this.options.language && this.isI18nAvailable()) {
|
|
145
|
+
VideoPlayerTranslations.setLanguage(this.options.language);
|
|
146
|
+
}
|
|
147
|
+
// Apply autoplay if enabled
|
|
148
|
+
if (options.autoplay) {
|
|
149
|
+
this.video.autoplay = true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
this.interceptAutoLoading();
|
|
154
|
+
this.createPlayerStructure();
|
|
155
|
+
this.initializeElements();
|
|
156
|
+
// audio player adaptation
|
|
157
|
+
this.adaptToAudioFile = function () {
|
|
158
|
+
if (this.options.audiofile) {
|
|
159
|
+
// Nascondere video
|
|
160
|
+
if (this.video) {
|
|
161
|
+
this.video.style.display = 'none';
|
|
162
|
+
}
|
|
163
|
+
if (this.container) {
|
|
164
|
+
this.container.classList.add('audio-player');
|
|
165
|
+
}
|
|
166
|
+
if (this.options.audiowave) {
|
|
167
|
+
this.initAudioWave();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
// Audio wave with Web Audio API
|
|
172
|
+
this.initAudioWave = function () {
|
|
173
|
+
if (!this.video) return;
|
|
174
|
+
|
|
175
|
+
this.audioWaveCanvas = document.createElement('canvas');
|
|
176
|
+
this.audioWaveCanvas.className = 'audio-wave-canvas';
|
|
177
|
+
this.container.appendChild(this.audioWaveCanvas);
|
|
178
|
+
|
|
179
|
+
const canvasCtx = this.audioWaveCanvas.getContext('2d');
|
|
180
|
+
const WIDTH = this.audioWaveCanvas.width = this.container.clientWidth;
|
|
181
|
+
const HEIGHT = this.audioWaveCanvas.height = 60; // altezza onda audio
|
|
182
|
+
|
|
183
|
+
// Setup Web Audio API
|
|
184
|
+
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
185
|
+
this.audioCtx = new AudioContext();
|
|
186
|
+
this.analyser = this.audioCtx.createAnalyser();
|
|
187
|
+
this.source = this.audioCtx.createMediaElementSource(this.video);
|
|
188
|
+
this.source.connect(this.analyser);
|
|
189
|
+
this.analyser.connect(this.audioCtx.destination);
|
|
190
|
+
|
|
191
|
+
this.analyser.fftSize = 2048;
|
|
192
|
+
const bufferLength = this.analyser.fftSize;
|
|
193
|
+
const dataArray = new Uint8Array(bufferLength);
|
|
194
|
+
|
|
195
|
+
// canvas
|
|
196
|
+
const draw = () => {
|
|
197
|
+
requestAnimationFrame(draw);
|
|
198
|
+
this.analyser.getByteTimeDomainData(dataArray);
|
|
199
|
+
|
|
200
|
+
canvasCtx.fillStyle = '#222';
|
|
201
|
+
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
|
|
202
|
+
|
|
203
|
+
canvasCtx.lineWidth = 2;
|
|
204
|
+
canvasCtx.strokeStyle = '#33ccff';
|
|
205
|
+
canvasCtx.beginPath();
|
|
206
|
+
|
|
207
|
+
const sliceWidth = WIDTH / bufferLength;
|
|
208
|
+
let x = 0;
|
|
209
|
+
|
|
210
|
+
for (let i = 0; i < bufferLength; i++) {
|
|
211
|
+
const v = dataArray[i] / 128.0;
|
|
212
|
+
const y = v * HEIGHT / 2;
|
|
213
|
+
|
|
214
|
+
if (i === 0) {
|
|
215
|
+
canvasCtx.moveTo(x, y);
|
|
216
|
+
} else {
|
|
217
|
+
canvasCtx.lineTo(x, y);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
x += sliceWidth;
|
|
221
|
+
}
|
|
222
|
+
canvasCtx.lineTo(WIDTH, HEIGHT / 2);
|
|
223
|
+
canvasCtx.stroke();
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
draw();
|
|
227
|
+
};
|
|
228
|
+
this.adaptToAudioFile();
|
|
229
|
+
this.bindEvents();
|
|
230
|
+
|
|
231
|
+
if (this.options.keyboardControls) {
|
|
232
|
+
this.setupKeyboardControls();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this.updateVolumeSliderVisual();
|
|
236
|
+
this.initVolumeTooltip();
|
|
237
|
+
this.updateTooltips();
|
|
238
|
+
this.markPlayerReady();
|
|
239
|
+
this.initializePluginSystem();
|
|
240
|
+
this.restoreSourcesAsync();
|
|
241
|
+
|
|
242
|
+
this.initializeSubtitles();
|
|
243
|
+
this.initializeQualityMonitoring();
|
|
244
|
+
|
|
245
|
+
this.initializeResolution();
|
|
246
|
+
this.initializeChapters();
|
|
247
|
+
this.initializePoster();
|
|
248
|
+
this.initializeWatermark();
|
|
249
|
+
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (this.options.debug) console.error('Video player initialization error:', error);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
getPlayerState() {
|
|
256
|
+
return {
|
|
257
|
+
isPlaying: !this.isPaused(),
|
|
258
|
+
isPaused: this.isPaused(),
|
|
259
|
+
currentTime: this.getCurrentTime(),
|
|
260
|
+
duration: this.getDuration(),
|
|
261
|
+
volume: this.getVolume(),
|
|
262
|
+
isMuted: this.isMuted(),
|
|
263
|
+
playbackRate: this.getPlaybackRate(),
|
|
264
|
+
isFullscreen: this.isFullscreenActive(),
|
|
265
|
+
isPictureInPicture: this.isPictureInPictureActive(),
|
|
266
|
+
subtitlesEnabled: this.isSubtitlesEnabled(),
|
|
267
|
+
currentSubtitle: this.getCurrentSubtitleTrack(),
|
|
268
|
+
selectedQuality: this.getSelectedQuality(),
|
|
269
|
+
currentQuality: this.getCurrentPlayingQuality(),
|
|
270
|
+
isAutoQuality: this.isAutoQualityActive()
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
isI18nAvailable() {
|
|
275
|
+
return typeof VideoPlayerTranslations !== 'undefined' &&
|
|
276
|
+
VideoPlayerTranslations !== null &&
|
|
277
|
+
typeof VideoPlayerTranslations.t === 'function';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
t(key) {
|
|
281
|
+
if (this.isI18nAvailable()) {
|
|
282
|
+
try {
|
|
283
|
+
return VideoPlayerTranslations.t(key);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (this.options.debug) console.warn('Translation error:', error);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const fallback = {
|
|
290
|
+
'play_pause': 'Play/Pause (Space)',
|
|
291
|
+
'mute_unmute': 'Mute/Unmute (M)',
|
|
292
|
+
'volume': 'Volume',
|
|
293
|
+
'playback_speed': 'Playback speed',
|
|
294
|
+
'video_quality': 'Video quality',
|
|
295
|
+
'picture_in_picture': 'Picture-in-Picture (P)',
|
|
296
|
+
'fullscreen': 'Fullscreen (F)',
|
|
297
|
+
'subtitles': 'Subtitles (S)',
|
|
298
|
+
'subtitles_enable': 'Enable subtitles',
|
|
299
|
+
'subtitles_disable': 'Disable subtitles',
|
|
300
|
+
'subtitles_off': 'Off',
|
|
301
|
+
'auto': 'Auto',
|
|
302
|
+
'brand_logo': 'Brand logo',
|
|
303
|
+
'next_video': 'Next video (N)',
|
|
304
|
+
'prev_video': 'Previous video (P)',
|
|
305
|
+
'playlist_next': 'Next',
|
|
306
|
+
'playlist_prev': 'Previous'
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
return fallback[key] || key;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
interceptAutoLoading() {
|
|
313
|
+
this.saveOriginalSources();
|
|
314
|
+
this.disableSources();
|
|
315
|
+
|
|
316
|
+
this.video.preload = 'none';
|
|
317
|
+
this.video.controls = false;
|
|
318
|
+
this.video.autoplay = false;
|
|
319
|
+
|
|
320
|
+
if (this.video.src && this.video.src !== window.location.href) {
|
|
321
|
+
this.originalSrc = this.video.src;
|
|
322
|
+
this.video.removeAttribute('src');
|
|
323
|
+
this.video.src = '';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.hideNativePlayer();
|
|
327
|
+
|
|
328
|
+
if (this.options.debug) console.log('📁 Sources temporarily disabled to prevent blocking');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
saveOriginalSources() {
|
|
332
|
+
const sources = this.video.querySelectorAll('source');
|
|
333
|
+
this.originalSources = [];
|
|
334
|
+
|
|
335
|
+
sources.forEach((source, index) => {
|
|
336
|
+
if (source.src) {
|
|
337
|
+
this.originalSources.push({
|
|
338
|
+
element: source,
|
|
339
|
+
src: source.src,
|
|
340
|
+
type: source.type || 'video/mp4',
|
|
341
|
+
quality: source.getAttribute('data-quality') || `quality-${index}`,
|
|
342
|
+
index: index
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if (this.options.debug) console.log(`📁 Saved ${this.originalSources.length} sources originali:`, this.originalSources);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
disableSources() {
|
|
351
|
+
const sources = this.video.querySelectorAll('source');
|
|
352
|
+
sources.forEach(source => {
|
|
353
|
+
if (source.src) {
|
|
354
|
+
source.removeAttribute('src');
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
restoreSourcesAsync() {
|
|
360
|
+
setTimeout(() => {
|
|
361
|
+
this.restoreSources();
|
|
362
|
+
}, 200);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async restoreSources() {
|
|
366
|
+
try {
|
|
367
|
+
// Check for adaptive streaming first
|
|
368
|
+
let adaptiveSource = null;
|
|
369
|
+
|
|
370
|
+
if (this.originalSrc) {
|
|
371
|
+
adaptiveSource = this.originalSrc;
|
|
372
|
+
} else if (this.originalSources.length > 0) {
|
|
373
|
+
// Check if any source is adaptive
|
|
374
|
+
const firstSource = this.originalSources[0];
|
|
375
|
+
if (firstSource.src && this.detectStreamType(firstSource.src)) {
|
|
376
|
+
adaptiveSource = firstSource.src;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Initialize adaptive streaming if detected
|
|
381
|
+
if (adaptiveSource && this.options.adaptiveStreaming) {
|
|
382
|
+
const adaptiveInitialized = await this.initializeAdaptiveStreaming(adaptiveSource);
|
|
383
|
+
if (adaptiveInitialized) {
|
|
384
|
+
if (this.options.debug) console.log('📡 Adaptive streaming initialized');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Fallback to traditional sources
|
|
390
|
+
if (this.originalSrc) {
|
|
391
|
+
this.video.src = this.originalSrc;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
this.originalSources.forEach(sourceData => {
|
|
395
|
+
if (sourceData.element && sourceData.src) {
|
|
396
|
+
sourceData.element.src = sourceData.src;
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
this.qualities = this.originalSources.map(s => ({
|
|
401
|
+
src: s.src,
|
|
402
|
+
quality: s.quality,
|
|
403
|
+
type: s.type
|
|
404
|
+
}));
|
|
405
|
+
|
|
406
|
+
if (this.originalSrc && this.qualities.length === 0) {
|
|
407
|
+
this.qualities.push({
|
|
408
|
+
src: this.originalSrc,
|
|
409
|
+
quality: 'default',
|
|
410
|
+
type: 'video/mp4'
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (this.qualities.length > 0) {
|
|
415
|
+
this.video.load();
|
|
416
|
+
|
|
417
|
+
// CRITICAL: Re-initialize subtitles AFTER video.load() completes
|
|
418
|
+
this.video.addEventListener('loadedmetadata', () => {
|
|
419
|
+
setTimeout(() => {
|
|
420
|
+
this.reinitializeSubtitles();
|
|
421
|
+
if (this.options.debug) console.log('🔄 Subtitles re-initialized after video load');
|
|
422
|
+
}, 300);
|
|
423
|
+
}, { once: true });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (this.options.debug) console.log('✅ Sources ripristinate:', this.qualities);
|
|
427
|
+
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (this.options.debug) console.error('❌ Errore ripristino sources:', error);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
reinitializeSubtitles() {
|
|
434
|
+
if (this.options.debug) console.log('🔄 Re-initializing subtitles...');
|
|
435
|
+
|
|
436
|
+
// Clear existing subtitle data
|
|
437
|
+
this.textTracks = [];
|
|
438
|
+
this.currentSubtitleTrack = null;
|
|
439
|
+
this.subtitlesEnabled = false;
|
|
440
|
+
|
|
441
|
+
// Re-detect and initialize subtitles
|
|
442
|
+
this.detectTextTracks();
|
|
443
|
+
this.updateSubtitlesUI();
|
|
444
|
+
this.bindSubtitleEvents();
|
|
445
|
+
|
|
446
|
+
if (this.options.debug) console.log(`📝 Re-detected ${this.textTracks.length} subtitle tracks`);
|
|
447
|
+
|
|
448
|
+
// Auto-enable first subtitle track if available and default is set
|
|
449
|
+
const defaultTrack = this.getDefaultSubtitleTrack();
|
|
450
|
+
if (defaultTrack !== -1 && this.options.subtitlesEnabled === true) { // <-- AGGIUNTO!
|
|
451
|
+
setTimeout(() => {
|
|
452
|
+
this.enableSubtitleTrack(defaultTrack);
|
|
453
|
+
if (this.options.debug) console.log(`🎯 Auto-enabled default subtitle track: ${defaultTrack}`);
|
|
454
|
+
}, 100);
|
|
455
|
+
} else {
|
|
456
|
+
if (this.options.debug) {
|
|
457
|
+
console.log(`📝 Default subtitle track NOT auto-enabled:`, {
|
|
458
|
+
defaultTrack: defaultTrack,
|
|
459
|
+
subtitlesEnabled: this.options.subtitlesEnabled
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
getDefaultSubtitleTrack() {
|
|
466
|
+
if (!this.video.textTracks) return -1;
|
|
467
|
+
|
|
468
|
+
for (let i = 0; i < this.video.textTracks.length; i++) {
|
|
469
|
+
const track = this.video.textTracks[i];
|
|
470
|
+
if (track.mode === 'showing' || track.default) {
|
|
471
|
+
return i;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return -1;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
markPlayerReady() {
|
|
478
|
+
setTimeout(() => {
|
|
479
|
+
this.isPlayerReady = true;
|
|
480
|
+
if (this.container) {
|
|
481
|
+
this.container.classList.add('player-initialized');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (this.video) {
|
|
485
|
+
this.video.style.visibility = '';
|
|
486
|
+
this.video.style.opacity = '';
|
|
487
|
+
this.video.style.pointerEvents = '';
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// INITIALIZE AUTO-HIDE AFTER EVERYTHING IS READY
|
|
491
|
+
setTimeout(() => {
|
|
492
|
+
if (this.options.autoHide && !this.autoHideInitialized) {
|
|
493
|
+
this.initAutoHide();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Fix: Apply default quality (auto or specific)
|
|
497
|
+
if (this.selectedQuality && this.qualities && this.qualities.length > 0) {
|
|
498
|
+
if (this.options.debug) console.log(`🎯 Applying defaultQuality: "${this.selectedQuality}"`);
|
|
499
|
+
|
|
500
|
+
if (this.selectedQuality === 'auto') {
|
|
501
|
+
this.enableAutoQuality();
|
|
502
|
+
} else {
|
|
503
|
+
// Check if requested quality is available
|
|
504
|
+
const requestedQuality = this.qualities.find(q => q.quality === this.selectedQuality);
|
|
505
|
+
if (requestedQuality) {
|
|
506
|
+
if (this.options.debug) console.log(`✅ Quality "${this.selectedQuality}" available`);
|
|
507
|
+
this.setQuality(this.selectedQuality);
|
|
508
|
+
} else {
|
|
509
|
+
if (this.options.debug) console.warn(`⚠️ Quality "${this.selectedQuality}" not available - fallback to auto`);
|
|
510
|
+
if (this.options.debug) console.log('📋 Available qualities:', this.qualities.map(q => q.quality));
|
|
511
|
+
this.enableAutoQuality();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Autoplay
|
|
517
|
+
if (this.options.autoplay) {
|
|
518
|
+
if (this.options.debug) console.log('🎬 Autoplay enabled');
|
|
519
|
+
setTimeout(() => {
|
|
520
|
+
this.video.play().catch(error => {
|
|
521
|
+
if (this.options.debug) console.warn('⚠️ Autoplay blocked:', error);
|
|
522
|
+
});
|
|
523
|
+
}, 100);
|
|
524
|
+
}
|
|
525
|
+
}, 200);
|
|
526
|
+
|
|
527
|
+
}, 100);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
createPlayerStructure() {
|
|
531
|
+
let wrapper = this.video.closest('.video-wrapper');
|
|
532
|
+
if (!wrapper) {
|
|
533
|
+
wrapper = document.createElement('div');
|
|
534
|
+
wrapper.className = 'video-wrapper';
|
|
535
|
+
this.video.parentNode.insertBefore(wrapper, this.video);
|
|
536
|
+
wrapper.appendChild(this.video);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
this.container = wrapper;
|
|
540
|
+
|
|
541
|
+
this.createInitialLoading();
|
|
542
|
+
this.createLoadingOverlay();
|
|
543
|
+
this.collectVideoQualities();
|
|
544
|
+
this.createControls();
|
|
545
|
+
this.createBrandLogo();
|
|
546
|
+
this.detectPlaylist();
|
|
547
|
+
|
|
548
|
+
if (this.options.showTitleOverlay) {
|
|
549
|
+
this.createTitleOverlay();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
createInitialLoading() {
|
|
554
|
+
const initialLoader = document.createElement('div');
|
|
555
|
+
initialLoader.className = 'initial-loading';
|
|
556
|
+
initialLoader.innerHTML = '<div class="loading-spinner"></div>';
|
|
557
|
+
this.container.appendChild(initialLoader);
|
|
558
|
+
this.initialLoading = initialLoader;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
collectVideoQualities() {
|
|
562
|
+
if (this.options.debug) console.log('📁 Video qualities will be loaded with restored sources');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
createLoadingOverlay() {
|
|
566
|
+
const overlay = document.createElement('div');
|
|
567
|
+
overlay.className = 'loading-overlay';
|
|
568
|
+
overlay.id = 'loadingOverlay-' + this.getUniqueId();
|
|
569
|
+
overlay.innerHTML = '<div class="loading-spinner"></div>';
|
|
570
|
+
this.container.appendChild(overlay);
|
|
571
|
+
this.loadingOverlay = overlay;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
createTitleOverlay() {
|
|
575
|
+
const overlay = document.createElement('div');
|
|
576
|
+
overlay.className = 'title-overlay';
|
|
577
|
+
overlay.id = 'titleOverlay-' + this.getUniqueId();
|
|
578
|
+
|
|
579
|
+
const titleText = document.createElement('h2');
|
|
580
|
+
titleText.className = 'title-text';
|
|
581
|
+
titleText.textContent = this.options.videoTitle || '';
|
|
582
|
+
|
|
583
|
+
overlay.appendChild(titleText);
|
|
584
|
+
|
|
585
|
+
if (this.controls) {
|
|
586
|
+
this.container.insertBefore(overlay, this.controls);
|
|
587
|
+
} else {
|
|
588
|
+
this.container.appendChild(overlay);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
this.titleOverlay = overlay;
|
|
592
|
+
|
|
593
|
+
if (this.options.persistentTitle && this.options.videoTitle) {
|
|
594
|
+
this.showTitleOverlay();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
updateTooltips() {
|
|
599
|
+
if (!this.controls) return;
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
this.controls.querySelectorAll('[data-tooltip]').forEach(element => {
|
|
603
|
+
const key = element.getAttribute('data-tooltip');
|
|
604
|
+
element.title = this.t(key);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const autoOption = this.controls.querySelector('.quality-option[data-quality="auto"]');
|
|
608
|
+
if (autoOption) {
|
|
609
|
+
autoOption.textContent = this.t('auto');
|
|
610
|
+
}
|
|
611
|
+
} catch (error) {
|
|
612
|
+
if (this.options.debug) console.warn('Errore aggiornamento tooltip:', error);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
setLanguage(lang) {
|
|
617
|
+
if (this.isI18nAvailable()) {
|
|
618
|
+
try {
|
|
619
|
+
if (VideoPlayerTranslations.setLanguage(lang)) {
|
|
620
|
+
this.updateTooltips();
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
} catch (error) {
|
|
624
|
+
if (this.options.debug) console.warn('Errore cambio lingua:', error);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
setVideoTitle(title) {
|
|
631
|
+
this.options.videoTitle = title || '';
|
|
632
|
+
|
|
633
|
+
if (this.titleOverlay) {
|
|
634
|
+
const titleElement = this.titleOverlay.querySelector('.title-text');
|
|
635
|
+
if (titleElement) {
|
|
636
|
+
titleElement.textContent = this.options.videoTitle;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (title) {
|
|
640
|
+
this.showTitleOverlay();
|
|
641
|
+
|
|
642
|
+
if (!this.options.persistentTitle) {
|
|
643
|
+
this.clearTitleTimeout();
|
|
644
|
+
this.titleTimeout = setTimeout(() => {
|
|
645
|
+
this.hideTitleOverlay();
|
|
646
|
+
}, 3000);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return this;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
getVideoTitle() {
|
|
655
|
+
return this.options.videoTitle;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
setPersistentTitle(persistent) {
|
|
659
|
+
this.options.persistentTitle = persistent;
|
|
660
|
+
|
|
661
|
+
if (this.titleOverlay && this.options.videoTitle) {
|
|
662
|
+
if (persistent) {
|
|
663
|
+
this.showTitleOverlay();
|
|
664
|
+
this.clearTitleTimeout();
|
|
665
|
+
} else {
|
|
666
|
+
this.titleOverlay.classList.remove('persistent');
|
|
667
|
+
if (this.titleOverlay.classList.contains('show')) {
|
|
668
|
+
this.clearTitleTimeout();
|
|
669
|
+
this.titleTimeout = setTimeout(() => {
|
|
670
|
+
this.hideTitleOverlay();
|
|
671
|
+
}, 3000);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return this;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
enableTitleOverlay() {
|
|
680
|
+
if (!this.titleOverlay && !this.options.showTitleOverlay) {
|
|
681
|
+
this.options.showTitleOverlay = true;
|
|
682
|
+
this.createTitleOverlay();
|
|
683
|
+
}
|
|
684
|
+
return this;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
disableTitleOverlay() {
|
|
688
|
+
if (this.titleOverlay) {
|
|
689
|
+
this.titleOverlay.remove();
|
|
690
|
+
this.titleOverlay = null;
|
|
691
|
+
}
|
|
692
|
+
this.options.showTitleOverlay = false;
|
|
693
|
+
return this;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
getUniqueId() {
|
|
697
|
+
return Math.random().toString(36).substr(2, 9);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
initializeElements() {
|
|
701
|
+
this.progressContainer = this.controls?.querySelector('.progress-container');
|
|
702
|
+
this.progressFilled = this.controls?.querySelector('.progress-filled');
|
|
703
|
+
this.progressBuffer = this.controls?.querySelector('.progress-buffer');
|
|
704
|
+
this.progressHandle = this.controls?.querySelector('.progress-handle');
|
|
705
|
+
this.seekTooltip = this.controls?.querySelector('.seek-tooltip');
|
|
706
|
+
|
|
707
|
+
this.playPauseBtn = this.controls?.querySelector('.play-pause-btn');
|
|
708
|
+
this.muteBtn = this.controls?.querySelector('.mute-btn');
|
|
709
|
+
this.fullscreenBtn = this.controls?.querySelector('.fullscreen-btn');
|
|
710
|
+
this.speedBtn = this.controls?.querySelector('.speed-btn');
|
|
711
|
+
this.qualityBtn = this.controls?.querySelector('.quality-btn');
|
|
712
|
+
this.pipBtn = this.controls?.querySelector('.pip-btn');
|
|
713
|
+
this.subtitlesBtn = this.controls?.querySelector('.subtitles-btn');
|
|
714
|
+
this.playlistPrevBtn = this.controls?.querySelector('.playlist-prev-btn');
|
|
715
|
+
this.playlistNextBtn = this.controls?.querySelector('.playlist-next-btn');
|
|
716
|
+
|
|
717
|
+
this.playIcon = this.controls?.querySelector('.play-icon');
|
|
718
|
+
this.pauseIcon = this.controls?.querySelector('.pause-icon');
|
|
719
|
+
this.volumeIcon = this.controls?.querySelector('.volume-icon');
|
|
720
|
+
this.muteIcon = this.controls?.querySelector('.mute-icon');
|
|
721
|
+
this.fullscreenIcon = this.controls?.querySelector('.fullscreen-icon');
|
|
722
|
+
this.exitFullscreenIcon = this.controls?.querySelector('.exit-fullscreen-icon');
|
|
723
|
+
this.pipIcon = this.controls?.querySelector('.pip-icon');
|
|
724
|
+
this.pipExitIcon = this.controls?.querySelector('.pip-exit-icon');
|
|
725
|
+
|
|
726
|
+
this.volumeSlider = this.controls?.querySelector('.volume-slider');
|
|
727
|
+
this.currentTimeEl = this.controls?.querySelector('.current-time');
|
|
728
|
+
this.durationEl = this.controls?.querySelector('.duration');
|
|
729
|
+
this.speedMenu = this.controls?.querySelector('.speed-menu');
|
|
730
|
+
this.qualityMenu = this.controls?.querySelector('.quality-menu');
|
|
731
|
+
this.subtitlesMenu = this.controls?.querySelector('.subtitles-menu');
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
updateVolumeSliderVisual() {
|
|
735
|
+
if (!this.video || !this.container) return;
|
|
736
|
+
|
|
737
|
+
const volume = this.video.muted ? 0 : this.video.volume;
|
|
738
|
+
const percentage = Math.round(volume * 100);
|
|
739
|
+
|
|
740
|
+
this.container.style.setProperty('--player-volume-fill', percentage + '%');
|
|
741
|
+
|
|
742
|
+
if (this.volumeSlider) {
|
|
743
|
+
this.volumeSlider.value = percentage;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
createVolumeTooltip() {
|
|
748
|
+
const volumeContainer = this.controls?.querySelector('.volume-container');
|
|
749
|
+
if (!volumeContainer || volumeContainer.querySelector('.volume-tooltip')) {
|
|
750
|
+
return; // Tooltip already present
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const tooltip = document.createElement('div');
|
|
754
|
+
tooltip.className = 'volume-tooltip';
|
|
755
|
+
tooltip.textContent = '50%';
|
|
756
|
+
volumeContainer.appendChild(tooltip);
|
|
757
|
+
|
|
758
|
+
this.volumeTooltip = tooltip;
|
|
759
|
+
|
|
760
|
+
if (this.options.debug) {
|
|
761
|
+
console.log('Dynamic volume tooltip created');
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
updateVolumeTooltip() {
|
|
766
|
+
if (!this.volumeTooltip || !this.video) return;
|
|
767
|
+
|
|
768
|
+
const volume = Math.round(this.video.volume * 100);
|
|
769
|
+
this.volumeTooltip.textContent = volume + '%';
|
|
770
|
+
|
|
771
|
+
// Aggiorna la posizione del tooltip
|
|
772
|
+
this.updateVolumeTooltipPosition(this.video.volume);
|
|
773
|
+
|
|
774
|
+
if (this.options.debug) {
|
|
775
|
+
console.log('Volume tooltip updated:', volume + '%');
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
updateVolumeTooltipPosition(volumeValue = null) {
|
|
780
|
+
if (!this.volumeTooltip || !this.video) return;
|
|
781
|
+
|
|
782
|
+
const volumeSlider = this.controls?.querySelector('.volume-slider');
|
|
783
|
+
if (!volumeSlider) return;
|
|
784
|
+
|
|
785
|
+
// If no volume provided, use current volume
|
|
786
|
+
if (volumeValue === null) {
|
|
787
|
+
volumeValue = this.video.volume;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Calcola la posizione esatta del thumb
|
|
791
|
+
const sliderRect = volumeSlider.getBoundingClientRect();
|
|
792
|
+
const sliderWidth = sliderRect.width;
|
|
793
|
+
|
|
794
|
+
// Thumb size is typically 14px (as defined in CSS)
|
|
795
|
+
const thumbSize = 14; // var(--player-volume-handle-size)
|
|
796
|
+
|
|
797
|
+
// Calcola la posizione del centro del thumb
|
|
798
|
+
// Il thumb si muove da thumbSize/2 a (sliderWidth - thumbSize/2)
|
|
799
|
+
const availableWidth = sliderWidth - thumbSize;
|
|
800
|
+
const thumbCenterPosition = (thumbSize / 2) + (availableWidth * volumeValue);
|
|
801
|
+
|
|
802
|
+
// Converti in percentuale relativa al container dello slider
|
|
803
|
+
const percentage = (thumbCenterPosition / sliderWidth) * 100;
|
|
804
|
+
|
|
805
|
+
// Posiziona il tooltip
|
|
806
|
+
this.volumeTooltip.style.left = percentage + '%';
|
|
807
|
+
|
|
808
|
+
if (this.options.debug) {
|
|
809
|
+
console.log('Volume tooltip position updated:', {
|
|
810
|
+
volumeValue: volumeValue,
|
|
811
|
+
percentage: percentage + '%',
|
|
812
|
+
thumbCenter: thumbCenterPosition,
|
|
813
|
+
sliderWidth: sliderWidth
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
initVolumeTooltip() {
|
|
819
|
+
this.createVolumeTooltip();
|
|
820
|
+
this.setupVolumeTooltipEvents();
|
|
821
|
+
|
|
822
|
+
// Set initial position immediately
|
|
823
|
+
setTimeout(() => {
|
|
824
|
+
if (this.volumeTooltip && this.video) {
|
|
825
|
+
this.updateVolumeTooltipPosition(this.video.volume);
|
|
826
|
+
this.updateVolumeTooltip();
|
|
827
|
+
}
|
|
828
|
+
}, 50); // Shorter delay for faster initialization
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
updateVolumeSliderVisualWithTooltip() {
|
|
832
|
+
const volumeSlider = this.controls?.querySelector('.volume-slider');
|
|
833
|
+
if (!volumeSlider || !this.video) return;
|
|
834
|
+
|
|
835
|
+
const volume = this.video.volume || 0;
|
|
836
|
+
const percentage = Math.round(volume * 100);
|
|
837
|
+
|
|
838
|
+
volumeSlider.value = volume;
|
|
839
|
+
|
|
840
|
+
// Update CSS custom property per il riempimento visuale
|
|
841
|
+
const volumeFillPercentage = percentage + '%';
|
|
842
|
+
volumeSlider.style.setProperty('--player-volume-fill', volumeFillPercentage);
|
|
843
|
+
|
|
844
|
+
// Aggiorna anche il tooltip se presente (testo e posizione)
|
|
845
|
+
this.updateVolumeTooltip();
|
|
846
|
+
|
|
847
|
+
if (this.options.debug) {
|
|
848
|
+
console.log('Volume slider aggiornato:', {
|
|
849
|
+
volume: volume,
|
|
850
|
+
percentage: percentage,
|
|
851
|
+
fillPercentage: volumeFillPercentage
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Volume slider type: 'horizontal' or 'vertical'
|
|
857
|
+
setVolumeSliderOrientation(orientation) {
|
|
858
|
+
if (!['horizontal', 'vertical'].includes(orientation)) {
|
|
859
|
+
if (this.options.debug) console.warn('Invalid volume slider orientation:', orientation);
|
|
860
|
+
return this;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
this.options.volumeSlider = orientation;
|
|
864
|
+
const volumeContainer = this.controls?.querySelector('.volume-container');
|
|
865
|
+
if (volumeContainer) {
|
|
866
|
+
volumeContainer.setAttribute('data-orientation', orientation);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (this.options.debug) console.log('Volume slider orientation set to:', orientation);
|
|
870
|
+
return this;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
getVolumeSliderOrientation() {
|
|
874
|
+
return this.options.volumeSlider;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
initVolumeTooltip() {
|
|
879
|
+
|
|
880
|
+
this.createVolumeTooltip();
|
|
881
|
+
|
|
882
|
+
// Setup events
|
|
883
|
+
this.setupVolumeTooltipEvents();
|
|
884
|
+
|
|
885
|
+
setTimeout(() => {
|
|
886
|
+
this.updateVolumeTooltip();
|
|
887
|
+
}, 200);
|
|
888
|
+
|
|
889
|
+
if (this.options.debug) {
|
|
890
|
+
console.log('Dynamic volume tooltip inizializzato');
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
setupSeekTooltip() {
|
|
895
|
+
if (!this.options.showSeekTooltip || !this.progressContainer || !this.seekTooltip) return;
|
|
896
|
+
|
|
897
|
+
this.progressContainer.addEventListener('mouseenter', () => {
|
|
898
|
+
if (this.seekTooltip) {
|
|
899
|
+
this.seekTooltip.classList.add('visible');
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
this.progressContainer.addEventListener('mouseleave', () => {
|
|
904
|
+
if (this.seekTooltip) {
|
|
905
|
+
this.seekTooltip.classList.remove('visible');
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
this.progressContainer.addEventListener('mousemove', (e) => {
|
|
910
|
+
this.updateSeekTooltip(e);
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
updateSeekTooltip(e) {
|
|
915
|
+
if (!this.seekTooltip || !this.progressContainer || !this.video || !this.video.duration) return;
|
|
916
|
+
|
|
917
|
+
const rect = this.progressContainer.getBoundingClientRect();
|
|
918
|
+
const clickX = e.clientX - rect.left;
|
|
919
|
+
const percentage = Math.max(0, Math.min(1, clickX / rect.width));
|
|
920
|
+
const targetTime = percentage * this.video.duration;
|
|
921
|
+
|
|
922
|
+
this.seekTooltip.textContent = this.formatTime(targetTime);
|
|
923
|
+
|
|
924
|
+
const tooltipRect = this.seekTooltip.getBoundingClientRect();
|
|
925
|
+
let leftPosition = clickX;
|
|
926
|
+
|
|
927
|
+
const tooltipWidth = tooltipRect.width || 50;
|
|
928
|
+
const containerWidth = rect.width;
|
|
929
|
+
|
|
930
|
+
leftPosition = Math.max(tooltipWidth / 2, Math.min(containerWidth - tooltipWidth / 2, clickX));
|
|
931
|
+
|
|
932
|
+
this.seekTooltip.style.left = leftPosition + 'px';
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
play() {
|
|
936
|
+
if (!this.video || this.isChangingQuality) return;
|
|
937
|
+
|
|
938
|
+
this.video.play().catch(err => {
|
|
939
|
+
if (this.options.debug) console.log('Play failed:', err);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
if (this.playIcon) this.playIcon.classList.add('hidden');
|
|
943
|
+
if (this.pauseIcon) this.pauseIcon.classList.remove('hidden');
|
|
944
|
+
|
|
945
|
+
// Trigger event played
|
|
946
|
+
this.triggerEvent('played', {
|
|
947
|
+
currentTime: this.getCurrentTime(),
|
|
948
|
+
duration: this.getDuration()
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
pause() {
|
|
953
|
+
if (!this.video) return;
|
|
954
|
+
|
|
955
|
+
this.video.pause();
|
|
956
|
+
if (this.playIcon) this.playIcon.classList.remove('hidden');
|
|
957
|
+
if (this.pauseIcon) this.pauseIcon.classList.add('hidden');
|
|
958
|
+
|
|
959
|
+
// Trigger paused event
|
|
960
|
+
this.triggerEvent('paused', {
|
|
961
|
+
currentTime: this.getCurrentTime(),
|
|
962
|
+
duration: this.getDuration()
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
updateVolume(value) {
|
|
967
|
+
if (!this.video) return;
|
|
968
|
+
|
|
969
|
+
const previousVolume = this.video.volume;
|
|
970
|
+
const previousMuted = this.video.muted;
|
|
971
|
+
|
|
972
|
+
this.video.volume = Math.max(0, Math.min(1, value / 100));
|
|
973
|
+
if (this.volumeSlider) this.volumeSlider.value = value;
|
|
974
|
+
this.updateMuteButton();
|
|
975
|
+
this.updateVolumeSliderVisual();
|
|
976
|
+
this.initVolumeTooltip();
|
|
977
|
+
|
|
978
|
+
// Triggers volumechange event if there is a significant change
|
|
979
|
+
if (Math.abs(previousVolume - this.video.volume) > 0.01 || previousMuted !== this.video.muted) {
|
|
980
|
+
this.triggerEvent('volumechange', {
|
|
981
|
+
volume: this.getVolume(),
|
|
982
|
+
muted: this.isMuted(),
|
|
983
|
+
previousVolume: previousVolume,
|
|
984
|
+
previousMuted: previousMuted
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
changeVolume(delta) {
|
|
990
|
+
if (!this.video) return;
|
|
991
|
+
|
|
992
|
+
const newVolume = Math.max(0, Math.min(1, this.video.volume + delta));
|
|
993
|
+
this.updateVolume(newVolume * 100);
|
|
994
|
+
this.updateVolumeSliderVisual();
|
|
995
|
+
this.initVolumeTooltip();
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
updateProgress() {
|
|
999
|
+
if (!this.video || !this.progressFilled || !this.progressHandle || this.isUserSeeking) return;
|
|
1000
|
+
|
|
1001
|
+
if (this.video.duration && !isNaN(this.video.duration)) {
|
|
1002
|
+
const progress = (this.video.currentTime / this.video.duration) * 100;
|
|
1003
|
+
this.progressFilled.style.width = progress + '%';
|
|
1004
|
+
this.progressHandle.style.left = progress + '%';
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
this.updateTimeDisplay();
|
|
1008
|
+
|
|
1009
|
+
// Trigger timeupdate event (with throttling to avoid too many events)
|
|
1010
|
+
if (!this.lastTimeUpdate || Date.now() - this.lastTimeUpdate > 250) {
|
|
1011
|
+
this.triggerEvent('timeupdate', {
|
|
1012
|
+
currentTime: this.getCurrentTime(),
|
|
1013
|
+
duration: this.getDuration(),
|
|
1014
|
+
progress: (this.getCurrentTime() / this.getDuration()) * 100 || 0
|
|
1015
|
+
});
|
|
1016
|
+
this.lastTimeUpdate = Date.now();
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
updateBuffer() {
|
|
1021
|
+
if (!this.video || !this.progressBuffer) return;
|
|
1022
|
+
|
|
1023
|
+
try {
|
|
1024
|
+
if (this.video.buffered && this.video.buffered.length > 0 && this.video.duration) {
|
|
1025
|
+
const buffered = (this.video.buffered.end(0) / this.video.duration) * 100;
|
|
1026
|
+
this.progressBuffer.style.width = buffered + '%';
|
|
1027
|
+
}
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
if (this.options.debug) console.log('Buffer update error (non-critical):', error);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
startSeeking(e) {
|
|
1034
|
+
if (this.isChangingQuality) return;
|
|
1035
|
+
|
|
1036
|
+
this.isUserSeeking = true;
|
|
1037
|
+
this.seek(e);
|
|
1038
|
+
e.preventDefault();
|
|
1039
|
+
|
|
1040
|
+
// Show controls during seeking
|
|
1041
|
+
if (this.options.autoHide && this.autoHideInitialized) {
|
|
1042
|
+
this.showControlsNow();
|
|
1043
|
+
this.resetAutoHideTimer();
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
continueSeeking(e) {
|
|
1048
|
+
if (this.isUserSeeking && !this.isChangingQuality) {
|
|
1049
|
+
this.seek(e);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
endSeeking() {
|
|
1054
|
+
this.isUserSeeking = false;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
seek(e) {
|
|
1058
|
+
if (!this.video || !this.progressContainer || !this.progressFilled || !this.progressHandle || this.isChangingQuality) return;
|
|
1059
|
+
|
|
1060
|
+
const rect = this.progressContainer.getBoundingClientRect();
|
|
1061
|
+
const clickX = e.clientX - rect.left;
|
|
1062
|
+
const percentage = Math.max(0, Math.min(1, clickX / rect.width));
|
|
1063
|
+
|
|
1064
|
+
if (this.video.duration && !isNaN(this.video.duration)) {
|
|
1065
|
+
this.video.currentTime = percentage * this.video.duration;
|
|
1066
|
+
const progress = percentage * 100;
|
|
1067
|
+
this.progressFilled.style.width = progress + '%';
|
|
1068
|
+
this.progressHandle.style.left = progress + '%';
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
updateDuration() {
|
|
1073
|
+
if (this.durationEl && this.video && this.video.duration && !isNaN(this.video.duration)) {
|
|
1074
|
+
this.durationEl.textContent = this.formatTime(this.video.duration);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
changeSpeed(e) {
|
|
1079
|
+
if (!this.video || !e.target.classList.contains('speed-option') || this.isChangingQuality) return;
|
|
1080
|
+
|
|
1081
|
+
const speed = parseFloat(e.target.getAttribute('data-speed'));
|
|
1082
|
+
if (speed && speed > 0) {
|
|
1083
|
+
this.video.playbackRate = speed;
|
|
1084
|
+
if (this.speedBtn) this.speedBtn.textContent = speed + 'x';
|
|
1085
|
+
|
|
1086
|
+
if (this.speedMenu) {
|
|
1087
|
+
this.speedMenu.querySelectorAll('.speed-option').forEach(option => {
|
|
1088
|
+
option.classList.remove('active');
|
|
1089
|
+
});
|
|
1090
|
+
e.target.classList.add('active');
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Trigger speedchange event
|
|
1094
|
+
const previousSpeed = this.video.playbackRate;
|
|
1095
|
+
this.triggerEvent('speedchange', {
|
|
1096
|
+
speed: speed,
|
|
1097
|
+
previousSpeed: previousSpeed
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
onVideoEnded() {
|
|
1103
|
+
if (this.playIcon) this.playIcon.classList.remove('hidden');
|
|
1104
|
+
if (this.pauseIcon) this.pauseIcon.classList.add('hidden');
|
|
1105
|
+
|
|
1106
|
+
// Handle loop option
|
|
1107
|
+
if (this.options.loop) {
|
|
1108
|
+
if (this.options.debug) console.log('🔄 Video loop enabled - restarting from beginning');
|
|
1109
|
+
this.video.currentTime = 0;
|
|
1110
|
+
this.video.play().catch(error => {
|
|
1111
|
+
if (this.options.debug) console.warn('Loop play failed:', error);
|
|
1112
|
+
});
|
|
1113
|
+
return; // Don't show controls or trigger ended event when looping
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
this.showControlsNow();
|
|
1117
|
+
|
|
1118
|
+
// Trigger ended event
|
|
1119
|
+
this.triggerEvent('ended', {
|
|
1120
|
+
currentTime: this.getCurrentTime(),
|
|
1121
|
+
duration: this.getDuration(),
|
|
1122
|
+
playlistInfo: this.getPlaylistInfo()
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
getCurrentTime() { return this.video ? this.video.currentTime || 0 : 0; }
|
|
1127
|
+
|
|
1128
|
+
setCurrentTime(time) { if (this.video && typeof time === 'number' && time >= 0 && !this.isChangingQuality) { this.video.currentTime = time; } }
|
|
1129
|
+
|
|
1130
|
+
getDuration() { return this.video && this.video.duration ? this.video.duration : 0; }
|
|
1131
|
+
|
|
1132
|
+
getVolume() { return this.video ? this.video.volume || 0 : 0; }
|
|
1133
|
+
|
|
1134
|
+
setVolume(volume) {
|
|
1135
|
+
if (typeof volume === 'number' && volume >= 0 && volume <= 1) {
|
|
1136
|
+
this.updateVolume(volume * 100);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
isPaused() { return this.video ? this.video.paused : true; }
|
|
1141
|
+
|
|
1142
|
+
isMuted() { return this.video ? this.video.muted : false; }
|
|
1143
|
+
|
|
1144
|
+
setMuted(muted) {
|
|
1145
|
+
if (this.video && typeof muted === 'boolean') {
|
|
1146
|
+
this.video.muted = muted;
|
|
1147
|
+
this.updateMuteButton();
|
|
1148
|
+
this.updateVolumeSliderVisual();
|
|
1149
|
+
this.initVolumeTooltip();
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
getPlaybackRate() { return this.video ? this.video.playbackRate || 1 : 1; }
|
|
1154
|
+
|
|
1155
|
+
setPlaybackRate(rate) { if (this.video && typeof rate === 'number' && rate > 0 && !this.isChangingQuality) { this.video.playbackRate = rate; if (this.speedBtn) this.speedBtn.textContent = rate + 'x'; } }
|
|
1156
|
+
|
|
1157
|
+
isPictureInPictureActive() { return document.pictureInPictureElement === this.video; }
|
|
1158
|
+
|
|
1159
|
+
getCurrentLanguage() {
|
|
1160
|
+
return this.isI18nAvailable() ?
|
|
1161
|
+
VideoPlayerTranslations.getCurrentLanguage() : 'en';
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
getSupportedLanguages() {
|
|
1165
|
+
return this.isI18nAvailable() ?
|
|
1166
|
+
VideoPlayerTranslations.getSupportedLanguages() : ['en'];
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
createBrandLogo() {
|
|
1170
|
+
if (!this.options.brandLogoEnabled || !this.options.brandLogoUrl) return;
|
|
1171
|
+
|
|
1172
|
+
const controlsRight = this.controls?.querySelector('.controls-right');
|
|
1173
|
+
if (!controlsRight) return;
|
|
1174
|
+
|
|
1175
|
+
// Create brand logo image
|
|
1176
|
+
const logo = document.createElement('img');
|
|
1177
|
+
logo.className = 'brand-logo';
|
|
1178
|
+
logo.src = this.options.brandLogoUrl;
|
|
1179
|
+
logo.alt = this.t('brand_logo');
|
|
1180
|
+
|
|
1181
|
+
// Handle loading error
|
|
1182
|
+
logo.onerror = () => {
|
|
1183
|
+
if (this.options.debug) console.warn('Brand logo failed to load:', this.options.brandLogoUrl);
|
|
1184
|
+
logo.style.display = 'none';
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
logo.onload = () => {
|
|
1188
|
+
if (this.options.debug) console.log('Brand logo loaded successfully');
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
// Add click functionality if link URL is provided
|
|
1192
|
+
if (this.options.brandLogoLinkUrl) {
|
|
1193
|
+
logo.style.cursor = 'pointer';
|
|
1194
|
+
logo.addEventListener('click', (e) => {
|
|
1195
|
+
e.stopPropagation(); // Prevent video controls interference
|
|
1196
|
+
window.open(this.options.brandLogoLinkUrl, '_blank', 'noopener,noreferrer');
|
|
1197
|
+
if (this.options.debug) console.log('Brand logo clicked, opening:', this.options.brandLogoLinkUrl);
|
|
1198
|
+
});
|
|
1199
|
+
} else {
|
|
1200
|
+
logo.style.cursor = 'default';
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Position the brand logo at the right of the controlbar (at the left of the buttons)
|
|
1204
|
+
controlsRight.insertBefore(logo, controlsRight.firstChild);
|
|
1205
|
+
|
|
1206
|
+
if (this.options.debug) {
|
|
1207
|
+
if (this.options.brandLogoLinkUrl) {
|
|
1208
|
+
console.log('Brand logo with click handler created for:', this.options.brandLogoLinkUrl);
|
|
1209
|
+
} else {
|
|
1210
|
+
console.log('Brand logo created (no link)');
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
setBrandLogo(enabled, url = '', linkUrl = '') {
|
|
1216
|
+
this.options.brandLogoEnabled = enabled;
|
|
1217
|
+
if (url) {
|
|
1218
|
+
this.options.brandLogoUrl = url;
|
|
1219
|
+
}
|
|
1220
|
+
if (linkUrl !== '') {
|
|
1221
|
+
this.options.brandLogoLinkUrl = linkUrl;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Remove existing brand logo
|
|
1225
|
+
const existingLogo = this.controls?.querySelector('.brand-logo');
|
|
1226
|
+
if (existingLogo) {
|
|
1227
|
+
existingLogo.remove();
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Recreate the logo if enabled
|
|
1231
|
+
if (enabled && this.options.brandLogoUrl) {
|
|
1232
|
+
this.createBrandLogo();
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
return this;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
getBrandLogoSettings() {
|
|
1239
|
+
return {
|
|
1240
|
+
enabled: this.options.brandLogoEnabled,
|
|
1241
|
+
url: this.options.brandLogoUrl,
|
|
1242
|
+
linkUrl: this.options.brandLogoLinkUrl
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
switchToVideo(newVideoElement, shouldPlay = false) {
|
|
1247
|
+
if (!newVideoElement) {
|
|
1248
|
+
if (this.options.debug) console.error('🎵 New video element not found');
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// Pause current video
|
|
1253
|
+
this.video.pause();
|
|
1254
|
+
|
|
1255
|
+
// Get new video sources and qualities
|
|
1256
|
+
const newSources = Array.from(newVideoElement.querySelectorAll('source')).map(source => ({
|
|
1257
|
+
src: source.src,
|
|
1258
|
+
quality: source.getAttribute('data-quality') || 'auto',
|
|
1259
|
+
type: source.type || 'video/mp4'
|
|
1260
|
+
}));
|
|
1261
|
+
|
|
1262
|
+
if (newSources.length === 0) {
|
|
1263
|
+
if (this.options.debug) console.error('🎵 New video has no sources');
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// Check if new video is adaptive stream
|
|
1268
|
+
if (this.options.adaptiveStreaming && newSources.length > 0) {
|
|
1269
|
+
const firstSource = newSources[0];
|
|
1270
|
+
if (this.detectStreamType(firstSource.src)) {
|
|
1271
|
+
// Initialize adaptive streaming for new video
|
|
1272
|
+
this.initializeAdaptiveStreaming(firstSource.src).then((initialized) => {
|
|
1273
|
+
if (initialized && shouldPlay) {
|
|
1274
|
+
const playPromise = this.video.play();
|
|
1275
|
+
if (playPromise) {
|
|
1276
|
+
playPromise.catch(error => {
|
|
1277
|
+
if (this.options.debug) console.log('Autoplay prevented:', error);
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
return true;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// Update traditional video sources
|
|
1287
|
+
this.video.innerHTML = '';
|
|
1288
|
+
newSources.forEach(source => {
|
|
1289
|
+
const sourceEl = document.createElement('source');
|
|
1290
|
+
sourceEl.src = source.src;
|
|
1291
|
+
sourceEl.type = source.type;
|
|
1292
|
+
sourceEl.setAttribute('data-quality', source.quality);
|
|
1293
|
+
this.video.appendChild(sourceEl);
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
// Update subtitles if present
|
|
1297
|
+
const newTracks = Array.from(newVideoElement.querySelectorAll('track'));
|
|
1298
|
+
newTracks.forEach(track => {
|
|
1299
|
+
const trackEl = document.createElement('track');
|
|
1300
|
+
trackEl.kind = track.kind;
|
|
1301
|
+
trackEl.src = track.src;
|
|
1302
|
+
trackEl.srclang = track.srclang;
|
|
1303
|
+
trackEl.label = track.label;
|
|
1304
|
+
if (track.default) trackEl.default = true;
|
|
1305
|
+
this.video.appendChild(trackEl);
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
// Update video title
|
|
1309
|
+
const newTitle = newVideoElement.getAttribute('data-video-title');
|
|
1310
|
+
if (newTitle && this.options.showTitleOverlay) {
|
|
1311
|
+
this.options.videoTitle = newTitle;
|
|
1312
|
+
if (this.titleText) {
|
|
1313
|
+
this.titleText.textContent = newTitle;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Reload video
|
|
1318
|
+
this.video.load();
|
|
1319
|
+
|
|
1320
|
+
// Update qualities and quality selector
|
|
1321
|
+
this.collectVideoQualities();
|
|
1322
|
+
this.updateQualityMenu();
|
|
1323
|
+
|
|
1324
|
+
// Play if needed
|
|
1325
|
+
if (shouldPlay) {
|
|
1326
|
+
const playPromise = this.video.play();
|
|
1327
|
+
if (playPromise) {
|
|
1328
|
+
playPromise.catch(error => {
|
|
1329
|
+
if (this.options.debug) console.log('🎵 Autoplay prevented:', error);
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* POSTER IMAGE MANAGEMENT
|
|
1339
|
+
* Initialize and manage video poster image
|
|
1340
|
+
*/
|
|
1341
|
+
initializePoster() {
|
|
1342
|
+
if (!this.video) {
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// Set poster from options if provided
|
|
1347
|
+
if (this.options.poster) {
|
|
1348
|
+
this.video.setAttribute('poster', this.options.poster);
|
|
1349
|
+
if (this.options.debug) console.log('🖼️ Poster set from options:', this.options.poster);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// Create custom poster overlay to prevent disappearing
|
|
1353
|
+
this.createPosterOverlay();
|
|
1354
|
+
|
|
1355
|
+
// Bind poster events
|
|
1356
|
+
this.bindPosterEvents();
|
|
1357
|
+
|
|
1358
|
+
if (this.options.debug) console.log('🖼️ Poster management initialized');
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Create custom poster overlay element
|
|
1363
|
+
* This prevents the poster from disappearing after video loads
|
|
1364
|
+
*/
|
|
1365
|
+
createPosterOverlay() {
|
|
1366
|
+
if (!this.container || !this.video) {
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Check if poster exists (either from attribute or options)
|
|
1371
|
+
const posterUrl = this.video.getAttribute('poster') || this.options.poster;
|
|
1372
|
+
|
|
1373
|
+
if (!posterUrl) {
|
|
1374
|
+
if (this.options.debug) console.log('🖼️ No poster URL found');
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// Create poster overlay element
|
|
1379
|
+
const posterOverlay = document.createElement('div');
|
|
1380
|
+
posterOverlay.className = 'video-poster-overlay';
|
|
1381
|
+
posterOverlay.style.backgroundImage = `url(${posterUrl})`;
|
|
1382
|
+
|
|
1383
|
+
// Insert poster overlay before controls
|
|
1384
|
+
if (this.controls) {
|
|
1385
|
+
this.container.insertBefore(posterOverlay, this.controls);
|
|
1386
|
+
} else {
|
|
1387
|
+
this.container.appendChild(posterOverlay);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
this.posterOverlay = posterOverlay;
|
|
1391
|
+
|
|
1392
|
+
if (this.options.debug) console.log('🖼️ Custom poster overlay created');
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* Bind poster-related events
|
|
1397
|
+
*/
|
|
1398
|
+
bindPosterEvents() {
|
|
1399
|
+
if (!this.video || !this.posterOverlay) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Hide poster when video starts playing
|
|
1404
|
+
this.video.addEventListener('play', () => {
|
|
1405
|
+
this.hidePoster();
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
// Show poster when video ends (optional)
|
|
1409
|
+
this.video.addEventListener('ended', () => {
|
|
1410
|
+
if (this.options.showPosterOnEnd) {
|
|
1411
|
+
this.showPoster();
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
// Hide poster when video is loading/playing
|
|
1416
|
+
this.video.addEventListener('playing', () => {
|
|
1417
|
+
this.hidePoster();
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
// Show poster on load if not autoplay
|
|
1421
|
+
if (!this.options.autoplay) {
|
|
1422
|
+
this.showPoster();
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Click on poster to play video
|
|
1426
|
+
if (this.posterOverlay) {
|
|
1427
|
+
this.posterOverlay.addEventListener('click', (e) => {
|
|
1428
|
+
e.stopPropagation();
|
|
1429
|
+
if (this.video.paused) {
|
|
1430
|
+
this.play();
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (this.options.debug) console.log('🖼️ Poster events bound');
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Show poster overlay
|
|
1440
|
+
*/
|
|
1441
|
+
showPoster() {
|
|
1442
|
+
if (this.posterOverlay) {
|
|
1443
|
+
this.posterOverlay.classList.add('visible');
|
|
1444
|
+
this.posterOverlay.classList.remove('hidden');
|
|
1445
|
+
if (this.options.debug) console.log('🖼️ Poster shown');
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* Hide poster overlay
|
|
1451
|
+
*/
|
|
1452
|
+
hidePoster() {
|
|
1453
|
+
if (this.posterOverlay) {
|
|
1454
|
+
this.posterOverlay.classList.remove('visible');
|
|
1455
|
+
this.posterOverlay.classList.add('hidden');
|
|
1456
|
+
if (this.options.debug) console.log('🖼️ Poster hidden');
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Set poster image dynamically
|
|
1462
|
+
* @param {String} posterUrl - URL of the poster image
|
|
1463
|
+
*/
|
|
1464
|
+
setPoster(posterUrl) {
|
|
1465
|
+
if (!posterUrl) {
|
|
1466
|
+
if (this.options.debug) console.warn('🖼️ Invalid poster URL');
|
|
1467
|
+
return this;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
this.options.poster = posterUrl;
|
|
1471
|
+
|
|
1472
|
+
// Update video poster attribute
|
|
1473
|
+
if (this.video) {
|
|
1474
|
+
this.video.setAttribute('poster', posterUrl);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Update or create poster overlay
|
|
1478
|
+
if (this.posterOverlay) {
|
|
1479
|
+
this.posterOverlay.style.backgroundImage = `url(${posterUrl})`;
|
|
1480
|
+
} else {
|
|
1481
|
+
this.createPosterOverlay();
|
|
1482
|
+
this.bindPosterEvents();
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
if (this.options.debug) console.log('🖼️ Poster updated:', posterUrl);
|
|
1486
|
+
|
|
1487
|
+
return this;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
/**
|
|
1491
|
+
* Get current poster URL
|
|
1492
|
+
* @returns {String|null} Poster URL or null
|
|
1493
|
+
*/
|
|
1494
|
+
getPoster() {
|
|
1495
|
+
return this.options.poster || this.video?.getAttribute('poster') || null;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* Remove poster
|
|
1500
|
+
*/
|
|
1501
|
+
removePoster() {
|
|
1502
|
+
if (this.posterOverlay) {
|
|
1503
|
+
this.posterOverlay.remove();
|
|
1504
|
+
this.posterOverlay = null;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
if (this.video) {
|
|
1508
|
+
this.video.removeAttribute('poster');
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
this.options.poster = null;
|
|
1512
|
+
|
|
1513
|
+
if (this.options.debug) console.log('🖼️ Poster removed');
|
|
1514
|
+
|
|
1515
|
+
return this;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Toggle poster visibility
|
|
1520
|
+
* @param {Boolean|null} show - True to show, false to hide, null to toggle
|
|
1521
|
+
* @returns {Object} this
|
|
1522
|
+
*/
|
|
1523
|
+
togglePoster(show = null) {
|
|
1524
|
+
if (!this.posterOverlay) {
|
|
1525
|
+
return this;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
if (show === null) {
|
|
1529
|
+
// Toggle
|
|
1530
|
+
if (this.posterOverlay.classList.contains('visible')) {
|
|
1531
|
+
this.hidePoster();
|
|
1532
|
+
} else {
|
|
1533
|
+
this.showPoster();
|
|
1534
|
+
}
|
|
1535
|
+
} else if (show) {
|
|
1536
|
+
this.showPoster();
|
|
1537
|
+
} else {
|
|
1538
|
+
this.hidePoster();
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
return this;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* Check if poster is visible
|
|
1546
|
+
* @returns {Boolean} True if poster is visible
|
|
1547
|
+
*/
|
|
1548
|
+
isPosterVisible() {
|
|
1549
|
+
return this.posterOverlay ? this.posterOverlay.classList.contains('visible') : false;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
loadScript(src) {
|
|
1554
|
+
return new Promise((resolve, reject) => {
|
|
1555
|
+
if (document.querySelector(`script[src="${src}"]`)) {
|
|
1556
|
+
resolve();
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
const script = document.createElement('script');
|
|
1561
|
+
script.src = src;
|
|
1562
|
+
script.onload = resolve;
|
|
1563
|
+
script.onerror = reject;
|
|
1564
|
+
document.head.appendChild(script);
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
dispose() {
|
|
1569
|
+
if (this.qualityMonitorInterval) {
|
|
1570
|
+
clearInterval(this.qualityMonitorInterval);
|
|
1571
|
+
this.qualityMonitorInterval = null;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
if (this.autoHideTimer) {
|
|
1575
|
+
clearTimeout(this.autoHideTimer);
|
|
1576
|
+
this.autoHideTimer = null;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
this.cleanupQualityChange();
|
|
1580
|
+
this.clearControlsTimeout();
|
|
1581
|
+
this.clearTitleTimeout();
|
|
1582
|
+
|
|
1583
|
+
// Destroy adaptive streaming players
|
|
1584
|
+
this.destroyAdaptivePlayer();
|
|
1585
|
+
|
|
1586
|
+
if (this.controls) {
|
|
1587
|
+
this.controls.remove();
|
|
1588
|
+
}
|
|
1589
|
+
if (this.loadingOverlay) {
|
|
1590
|
+
this.loadingOverlay.remove();
|
|
1591
|
+
}
|
|
1592
|
+
if (this.titleOverlay) {
|
|
1593
|
+
this.titleOverlay.remove();
|
|
1594
|
+
}
|
|
1595
|
+
if (this.initialLoading) {
|
|
1596
|
+
this.initialLoading.remove();
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (this.video) {
|
|
1600
|
+
this.video.classList.remove('video-player');
|
|
1601
|
+
this.video.controls = true;
|
|
1602
|
+
this.video.style.visibility = '';
|
|
1603
|
+
this.video.style.opacity = '';
|
|
1604
|
+
this.video.style.pointerEvents = '';
|
|
1605
|
+
}
|
|
1606
|
+
if (this.chapterMarkersContainer) {
|
|
1607
|
+
this.chapterMarkersContainer.remove();
|
|
1608
|
+
}
|
|
1609
|
+
if (this.chapterTooltip) {
|
|
1610
|
+
this.chapterTooltip.remove();
|
|
1611
|
+
}
|
|
1612
|
+
if (this.posterOverlay) {
|
|
1613
|
+
this.posterOverlay.remove();
|
|
1614
|
+
}
|
|
1615
|
+
this.disposeAllPlugins();
|
|
1616
|
+
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
|
|
1621
|
+
* Apply specified resolution mode to video
|
|
1622
|
+
|
|
1623
|
+
* @param {string} resolution - The resolution mode to apply
|
|
1624
|
+
|
|
1625
|
+
*/
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
|
|
1629
|
+
* Get currently set resolution
|
|
1630
|
+
|
|
1631
|
+
* @returns {string} Current resolution
|
|
1632
|
+
|
|
1633
|
+
*/
|
|
1634
|
+
|
|
1635
|
+
/**
|
|
1636
|
+
|
|
1637
|
+
* Initialize resolution from options value
|
|
1638
|
+
|
|
1639
|
+
*/
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
|
|
1643
|
+
* Restore resolution after quality change - internal method
|
|
1644
|
+
|
|
1645
|
+
* @private
|
|
1646
|
+
|
|
1647
|
+
*/
|
|
1648
|
+
|
|
1649
|
+
// Core methods for main class
|
|
1650
|
+
// All original functionality preserved exactly
|