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