myetv-player 1.0.10 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -2
- package/css/myetv-player.css +69 -0
- package/css/myetv-player.min.css +1 -1
- package/dist/myetv-player.js +227 -52
- package/dist/myetv-player.min.js +225 -50
- package/package.json +3 -1
- package/plugins/cloudflare/README.md +26 -4
- package/plugins/cloudflare/myetv-player-cloudflare-stream-plugin.js +1273 -217
- package/plugins/facebook/myetv-player-facebook-plugin.js +1340 -164
- package/plugins/twitch/myetv-player-twitch-plugin.js +428 -167
- package/plugins/vimeo/README.md +1 -1
- package/plugins/vimeo/myetv-player-vimeo.js +560 -247
- package/plugins/youtube/README.md +5 -2
- package/plugins/youtube/myetv-player-youtube-plugin.js +572 -116
- package/scss/_controls.scss +53 -0
- package/scss/_title-overlay.scss +27 -0
- package/src/core.js +89 -21
- package/src/events.js +118 -25
- package/src/utils.js +20 -6
package/scss/_controls.scss
CHANGED
|
@@ -309,3 +309,56 @@
|
|
|
309
309
|
touch-action: none; // Disable browser's default touch gestures
|
|
310
310
|
-webkit-touch-callout: none; // Disable callout on iOS
|
|
311
311
|
}
|
|
312
|
+
|
|
313
|
+
// ===================================
|
|
314
|
+
// ENCODING BADGE - Video in encoding
|
|
315
|
+
// ===================================
|
|
316
|
+
|
|
317
|
+
/* Badge for video in encoding (duration Infinity/NaN) */
|
|
318
|
+
.encoding-badge {
|
|
319
|
+
display: inline-block;
|
|
320
|
+
background: rgba(128, 128, 128, 0.8); // Grigio semi-trasparente
|
|
321
|
+
color: white;
|
|
322
|
+
padding: 2px 8px;
|
|
323
|
+
border-radius: 4px;
|
|
324
|
+
font-size: 11px;
|
|
325
|
+
font-weight: 500;
|
|
326
|
+
text-transform: uppercase;
|
|
327
|
+
letter-spacing: 0.5px;
|
|
328
|
+
white-space: nowrap;
|
|
329
|
+
backdrop-filter: blur(4px);
|
|
330
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
331
|
+
animation: encoding-pulse 2s ease-in-out infinite;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* Class for the container when shows the badge */
|
|
335
|
+
.time-display .encoding-state {
|
|
336
|
+
display: flex;
|
|
337
|
+
align-items: center;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/* animation for the badge */
|
|
341
|
+
@keyframes encoding-pulse {
|
|
342
|
+
0%, 100% {
|
|
343
|
+
opacity: 0.8;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
50% {
|
|
347
|
+
opacity: 1;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Responsive for badge encoding
|
|
352
|
+
@media (max-width: 480px) {
|
|
353
|
+
.encoding-badge {
|
|
354
|
+
font-size: 9px;
|
|
355
|
+
padding: 1px 6px;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
@media (max-width: 350px) {
|
|
360
|
+
.encoding-badge {
|
|
361
|
+
font-size: 8px;
|
|
362
|
+
padding: 1px 4px;
|
|
363
|
+
}
|
|
364
|
+
}
|
package/scss/_title-overlay.scss
CHANGED
|
@@ -44,6 +44,33 @@
|
|
|
44
44
|
-moz-osx-font-smoothing: grayscale;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
.subtitle-text {
|
|
48
|
+
color: var(--player-text-color);
|
|
49
|
+
font-size: 14px; // Più piccolo del titolo (18px)
|
|
50
|
+
font-weight: 400; // Più leggero del titolo (600)
|
|
51
|
+
line-height: 1.3;
|
|
52
|
+
margin: 5px 0 0 0;
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
text-overflow: ellipsis;
|
|
56
|
+
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
|
|
57
|
+
opacity: 0.9;
|
|
58
|
+
-webkit-font-smoothing: antialiased;
|
|
59
|
+
-moz-osx-font-smoothing: grayscale;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@media (max-width: 768px) {
|
|
63
|
+
.subtitle-text {
|
|
64
|
+
font-size: 12px;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@media (max-width: 480px) {
|
|
69
|
+
.subtitle-text {
|
|
70
|
+
font-size: 11px;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
47
74
|
/* CONTROLS - IMPROVED RESPONSIVE DESIGN */
|
|
48
75
|
.controls {
|
|
49
76
|
position: absolute;
|
package/src/core.js
CHANGED
|
@@ -26,6 +26,7 @@ constructor(videoElement, options = {}) {
|
|
|
26
26
|
showSeekTooltip: true,
|
|
27
27
|
showTitleOverlay: false,
|
|
28
28
|
videoTitle: '',
|
|
29
|
+
videoSubtitle: '',
|
|
29
30
|
persistentTitle: false,
|
|
30
31
|
debug: false, // Enable/disable debug logging
|
|
31
32
|
autoplay: false, // if video should autoplay at start
|
|
@@ -36,6 +37,7 @@ constructor(videoElement, options = {}) {
|
|
|
36
37
|
brandLogoEnabled: false, // Enable/disable brand logo
|
|
37
38
|
brandLogoUrl: '', // URL for brand logo image
|
|
38
39
|
brandLogoLinkUrl: '', // Optional URL to open when clicking the logo
|
|
40
|
+
brandLogoTooltipText: '', // Tooltip text for brand logo
|
|
39
41
|
playlistEnabled: true, // Enable/disable playlist detection
|
|
40
42
|
playlistAutoPlay: true, // Auto-play next video when current ends
|
|
41
43
|
playlistLoop: false, // Loop playlist when reaching the end
|
|
@@ -111,18 +113,42 @@ constructor(videoElement, options = {}) {
|
|
|
111
113
|
|
|
112
114
|
// Custom event system
|
|
113
115
|
this.eventCallbacks = {
|
|
114
|
-
|
|
115
|
-
'
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
'
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
|
|
116
|
+
// Core lifecycle events
|
|
117
|
+
'playerready': [], // Fired when player is fully initialized and ready
|
|
118
|
+
'played': [], // Fired when video starts playing
|
|
119
|
+
'paused': [], // Fired when video is paused
|
|
120
|
+
'ended': [], // Fired when video playback ends
|
|
121
|
+
|
|
122
|
+
// Playback state events
|
|
123
|
+
'playing': [], // Fired when video is actually playing (after buffering)
|
|
124
|
+
'waiting': [], // Fired when video is waiting for data (buffering)
|
|
125
|
+
'seeking': [], // Fired when seek operation starts
|
|
126
|
+
'seeked': [], // Fired when seek operation completes
|
|
127
|
+
|
|
128
|
+
// Loading events
|
|
129
|
+
'loadstart': [], // Fired when browser starts looking for media
|
|
130
|
+
'loadedmetadata': [], // Fired when metadata (duration, dimensions) is loaded
|
|
131
|
+
'loadeddata': [], // Fired when data for current frame is loaded
|
|
132
|
+
'canplay': [], // Fired when browser can start playing video
|
|
133
|
+
'progress': [], // Fired periodically while downloading media
|
|
134
|
+
'durationchange': [], // Fired when duration attribute changes
|
|
135
|
+
|
|
136
|
+
// Error events
|
|
137
|
+
'error': [], // Fired when media loading or playback error occurs
|
|
138
|
+
'stalled': [], // Fired when browser is trying to get data but it's not available
|
|
139
|
+
|
|
140
|
+
// Control events
|
|
141
|
+
'timeupdate': [], // Fired when current playback position changes
|
|
142
|
+
'volumechange': [], // Fired when volume or muted state changes
|
|
143
|
+
'speedchange': [], // Fired when playback speed changes
|
|
144
|
+
'qualitychange': [], // Fired when video quality changes
|
|
145
|
+
|
|
146
|
+
// Feature events
|
|
147
|
+
'subtitlechange': [], // Fired when subtitle track changes
|
|
148
|
+
'chapterchange': [], // Fired when video chapter changes
|
|
149
|
+
'pipchange': [], // Fired when picture-in-picture mode changes
|
|
150
|
+
'fullscreenchange': [], // Fired when fullscreen mode changes
|
|
151
|
+
'playlistchange': [] // Fired when playlist item changes
|
|
126
152
|
};
|
|
127
153
|
|
|
128
154
|
// Playlist management
|
|
@@ -484,6 +510,14 @@ markPlayerReady() {
|
|
|
484
510
|
this.container.classList.add('player-initialized');
|
|
485
511
|
}
|
|
486
512
|
|
|
513
|
+
this.triggerEvent('playerready', {
|
|
514
|
+
playerState: this.getPlayerState(),
|
|
515
|
+
qualities: this.qualities,
|
|
516
|
+
subtitles: this.textTracks,
|
|
517
|
+
chapters: this.chapters,
|
|
518
|
+
playlist: this.getPlaylistInfo()
|
|
519
|
+
});
|
|
520
|
+
|
|
487
521
|
if (this.video) {
|
|
488
522
|
this.video.style.visibility = '';
|
|
489
523
|
this.video.style.opacity = '';
|
|
@@ -582,9 +616,16 @@ createTitleOverlay() {
|
|
|
582
616
|
const titleText = document.createElement('h2');
|
|
583
617
|
titleText.className = 'title-text';
|
|
584
618
|
titleText.textContent = this.options.videoTitle || '';
|
|
585
|
-
|
|
586
619
|
overlay.appendChild(titleText);
|
|
587
620
|
|
|
621
|
+
// add subtitles
|
|
622
|
+
if (this.options.videoSubtitle) {
|
|
623
|
+
const subtitleText = document.createElement('p');
|
|
624
|
+
subtitleText.className = 'subtitle-text';
|
|
625
|
+
subtitleText.textContent = this.options.videoSubtitle;
|
|
626
|
+
overlay.appendChild(subtitleText);
|
|
627
|
+
}
|
|
628
|
+
|
|
588
629
|
if (this.controls) {
|
|
589
630
|
this.container.insertBefore(overlay, this.controls);
|
|
590
631
|
} else {
|
|
@@ -658,6 +699,31 @@ getVideoTitle() {
|
|
|
658
699
|
return this.options.videoTitle;
|
|
659
700
|
}
|
|
660
701
|
|
|
702
|
+
setVideoSubtitle(subtitle) {
|
|
703
|
+
this.options.videoSubtitle = subtitle || '';
|
|
704
|
+
|
|
705
|
+
if (this.titleOverlay) {
|
|
706
|
+
let subtitleElement = this.titleOverlay.querySelector('.subtitle-text');
|
|
707
|
+
|
|
708
|
+
if (subtitle) {
|
|
709
|
+
if (!subtitleElement) {
|
|
710
|
+
subtitleElement = document.createElement('p');
|
|
711
|
+
subtitleElement.className = 'subtitle-text';
|
|
712
|
+
this.titleOverlay.appendChild(subtitleElement);
|
|
713
|
+
}
|
|
714
|
+
subtitleElement.textContent = subtitle;
|
|
715
|
+
} else if (subtitleElement) {
|
|
716
|
+
subtitleElement.remove();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return this;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
getVideoSubtitle() {
|
|
724
|
+
return this.options.videoSubtitle;
|
|
725
|
+
}
|
|
726
|
+
|
|
661
727
|
setPersistentTitle(persistent) {
|
|
662
728
|
this.options.persistentTitle = persistent;
|
|
663
729
|
|
|
@@ -1313,7 +1379,14 @@ createBrandLogo() {
|
|
|
1313
1379
|
const logo = document.createElement('img');
|
|
1314
1380
|
logo.className = 'brand-logo';
|
|
1315
1381
|
logo.src = this.options.brandLogoUrl;
|
|
1316
|
-
logo.alt =
|
|
1382
|
+
logo.alt = 'Brand logo';
|
|
1383
|
+
|
|
1384
|
+
// Add tooltip ONLY if link URL is present
|
|
1385
|
+
if (this.options.brandLogoLinkUrl) {
|
|
1386
|
+
// Use custom tooltip text if provided, otherwise fallback to URL
|
|
1387
|
+
logo.title = this.options.brandLogoTooltipText || this.options.brandLogoLinkUrl;
|
|
1388
|
+
// NON usare data-tooltip per evitare che venga sovrascritto da updateTooltips()
|
|
1389
|
+
}
|
|
1317
1390
|
|
|
1318
1391
|
// Handle loading error
|
|
1319
1392
|
logo.onerror = () => {
|
|
@@ -1329,7 +1402,7 @@ createBrandLogo() {
|
|
|
1329
1402
|
if (this.options.brandLogoLinkUrl) {
|
|
1330
1403
|
logo.style.cursor = 'pointer';
|
|
1331
1404
|
logo.addEventListener('click', (e) => {
|
|
1332
|
-
e.stopPropagation();
|
|
1405
|
+
e.stopPropagation();
|
|
1333
1406
|
window.open(this.options.brandLogoLinkUrl, '_blank', 'noopener,noreferrer');
|
|
1334
1407
|
if (this.options.debug) console.log('Brand logo clicked, opening:', this.options.brandLogoLinkUrl);
|
|
1335
1408
|
});
|
|
@@ -1337,15 +1410,10 @@ createBrandLogo() {
|
|
|
1337
1410
|
logo.style.cursor = 'default';
|
|
1338
1411
|
}
|
|
1339
1412
|
|
|
1340
|
-
// Position the brand logo at the right of the controlbar (at the left of the buttons)
|
|
1341
1413
|
controlsRight.insertBefore(logo, controlsRight.firstChild);
|
|
1342
1414
|
|
|
1343
1415
|
if (this.options.debug) {
|
|
1344
|
-
|
|
1345
|
-
console.log('Brand logo with click handler created for:', this.options.brandLogoLinkUrl);
|
|
1346
|
-
} else {
|
|
1347
|
-
console.log('Brand logo created (no link)');
|
|
1348
|
-
}
|
|
1416
|
+
console.log('Brand logo created with tooltip:', logo.title || 'no tooltip');
|
|
1349
1417
|
}
|
|
1350
1418
|
}
|
|
1351
1419
|
|
package/src/events.js
CHANGED
|
@@ -165,36 +165,129 @@
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
bindEvents() {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
168
|
+
if (this.video) {
|
|
169
|
+
|
|
170
|
+
// Playback events
|
|
171
|
+
this.video.addEventListener('playing', () => {
|
|
172
|
+
this.hideLoading();
|
|
173
|
+
// Trigger playing event - video is now actually playing
|
|
174
|
+
this.triggerEvent('playing', {
|
|
175
|
+
currentTime: this.getCurrentTime(),
|
|
176
|
+
duration: this.getDuration()
|
|
174
177
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
this.video.addEventListener('waiting', () => {
|
|
181
|
+
if (!this.isChangingQuality) {
|
|
182
|
+
this.showLoading();
|
|
183
|
+
// Trigger waiting event - video is buffering
|
|
184
|
+
this.triggerEvent('waiting', {
|
|
185
|
+
currentTime: this.getCurrentTime()
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
this.video.addEventListener('seeking', () => {
|
|
191
|
+
// Trigger seeking event - seek operation started
|
|
192
|
+
this.triggerEvent('seeking', {
|
|
193
|
+
currentTime: this.getCurrentTime(),
|
|
194
|
+
targetTime: this.video.currentTime
|
|
181
195
|
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this.video.addEventListener('seeked', () => {
|
|
199
|
+
// Trigger seeked event - seek operation completed
|
|
200
|
+
this.triggerEvent('seeked', {
|
|
201
|
+
currentTime: this.getCurrentTime()
|
|
186
202
|
});
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Loading events
|
|
206
|
+
this.video.addEventListener('loadstart', () => {
|
|
207
|
+
if (!this.isChangingQuality) {
|
|
208
|
+
this.showLoading();
|
|
209
|
+
}
|
|
210
|
+
// Trigger loadstart event - browser started loading media
|
|
211
|
+
this.triggerEvent('loadstart');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
this.video.addEventListener('loadedmetadata', () => {
|
|
215
|
+
this.updateDuration();
|
|
216
|
+
|
|
217
|
+
// Trigger loadedmetadata event - video metadata loaded
|
|
218
|
+
this.triggerEvent('loadedmetadata', {
|
|
219
|
+
duration: this.getDuration(),
|
|
220
|
+
videoWidth: this.video.videoWidth,
|
|
221
|
+
videoHeight: this.video.videoHeight
|
|
192
222
|
});
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
223
|
+
|
|
224
|
+
// Initialize subtitles after metadata is loaded
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
this.initializeSubtitles();
|
|
227
|
+
}, 100);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
this.video.addEventListener('loadeddata', () => {
|
|
231
|
+
if (!this.isChangingQuality) {
|
|
232
|
+
this.hideLoading();
|
|
233
|
+
}
|
|
234
|
+
// Trigger loadeddata event - current frame data loaded
|
|
235
|
+
this.triggerEvent('loadeddata', {
|
|
236
|
+
currentTime: this.getCurrentTime()
|
|
197
237
|
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
this.video.addEventListener('canplay', () => {
|
|
241
|
+
if (!this.isChangingQuality) {
|
|
242
|
+
this.hideLoading();
|
|
243
|
+
}
|
|
244
|
+
// Trigger canplay event - video can start playing
|
|
245
|
+
this.triggerEvent('canplay', {
|
|
246
|
+
currentTime: this.getCurrentTime(),
|
|
247
|
+
duration: this.getDuration()
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
this.video.addEventListener('progress', () => {
|
|
252
|
+
this.updateBuffer();
|
|
253
|
+
// Trigger progress event - browser is downloading media
|
|
254
|
+
this.triggerEvent('progress', {
|
|
255
|
+
buffered: this.getBufferedTime(),
|
|
256
|
+
duration: this.getDuration()
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
this.video.addEventListener('durationchange', () => {
|
|
261
|
+
this.updateDuration();
|
|
262
|
+
// Trigger durationchange event - video duration changed
|
|
263
|
+
this.triggerEvent('durationchange', {
|
|
264
|
+
duration: this.getDuration()
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Error events
|
|
269
|
+
this.video.addEventListener('error', (e) => {
|
|
270
|
+
this.onVideoError(e);
|
|
271
|
+
// Trigger error event - media loading/playback error occurred
|
|
272
|
+
this.triggerEvent('error', {
|
|
273
|
+
code: this.video.error?.code,
|
|
274
|
+
message: this.video.error?.message,
|
|
275
|
+
src: this.video.currentSrc || this.video.src
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
this.video.addEventListener('stalled', () => {
|
|
280
|
+
// Trigger stalled event - browser is trying to fetch data but it's not available
|
|
281
|
+
this.triggerEvent('stalled', {
|
|
282
|
+
currentTime: this.getCurrentTime()
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
this.video.addEventListener('timeupdate', () => this.updateProgress());
|
|
288
|
+
|
|
289
|
+
this.video.addEventListener('ended', () => this.onVideoEnded());
|
|
290
|
+
|
|
198
291
|
// Complete video click logic with doubleTapPause support (DESKTOP)
|
|
199
292
|
this.video.addEventListener('click', () => {
|
|
200
293
|
if (!this.options.pauseClick) return;
|
package/src/utils.js
CHANGED
|
@@ -24,15 +24,29 @@
|
|
|
24
24
|
this.video.currentTime = Math.max(0, Math.min(this.video.duration, this.video.currentTime + seconds));
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
updateTimeDisplay() {
|
|
28
|
+
// update current time
|
|
29
|
+
if (this.currentTimeEl && this.video) {
|
|
30
|
+
this.currentTimeEl.textContent = this.formatTime(this.video.currentTime || 0);
|
|
31
|
+
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
// update duration or show badge if encoding
|
|
34
|
+
if (this.durationEl && this.video) {
|
|
35
|
+
const duration = this.video.duration;
|
|
36
|
+
|
|
37
|
+
// check if duration is valid
|
|
38
|
+
if (!duration || isNaN(duration) || !isFinite(duration)) {
|
|
39
|
+
// Video in encoding - show badge instead of duration
|
|
40
|
+
this.durationEl.innerHTML = '<span class="encoding-badge">Encoding in progress</span>';
|
|
41
|
+
this.durationEl.classList.add('encoding-state');
|
|
42
|
+
} else {
|
|
43
|
+
// valid duration - show normal
|
|
44
|
+
this.durationEl.textContent = this.formatTime(duration);
|
|
45
|
+
this.durationEl.classList.remove('encoding-state');
|
|
34
46
|
}
|
|
35
47
|
}
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
|
|
37
51
|
formatTime(seconds) {
|
|
38
52
|
if (isNaN(seconds) || seconds < 0) return '0:00';
|