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