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.
@@ -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
+ }
@@ -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
- 'played': [],
115
- 'paused': [],
116
- 'subtitlechange': [],
117
- 'chapterchange': [],
118
- 'pipchange': [],
119
- 'fullscreenchange': [],
120
- 'speedchange': [],
121
- 'timeupdate': [],
122
- 'volumechange': [],
123
- 'qualitychange': [],
124
- 'playlistchange': [],
125
- 'ended': []
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 = this.t('brand_logo');
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(); // Prevent video controls interference
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
- if (this.options.brandLogoLinkUrl) {
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
- if (this.video) {
169
- this.video.addEventListener('loadedmetadata', () => {
170
- this.updateDuration();
171
- setTimeout(() => {
172
- this.initializeSubtitles();
173
- }, 100);
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
- this.video.addEventListener('timeupdate', () => this.updateProgress());
176
- this.video.addEventListener('progress', () => this.updateBuffer());
177
- this.video.addEventListener('waiting', () => {
178
- if (!this.isChangingQuality) {
179
- this.showLoading();
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
- this.video.addEventListener('canplay', () => {
183
- if (!this.isChangingQuality) {
184
- this.hideLoading();
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
- this.video.addEventListener('ended', () => this.onVideoEnded());
188
- this.video.addEventListener('loadstart', () => {
189
- if (!this.isChangingQuality) {
190
- this.showLoading();
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
- this.video.addEventListener('loadeddata', () => {
194
- if (!this.isChangingQuality) {
195
- this.hideLoading();
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
- updateTimeDisplay() {
28
- if (this.currentTimeEl && this.video) {
29
- this.currentTimeEl.textContent = this.formatTime(this.video.currentTime || 0);
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
- if (this.durationEl && this.video && this.video.duration && !isNaN(this.video.duration)) {
33
- this.durationEl.textContent = this.formatTime(this.video.duration);
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';