myetv-player 1.1.0 → 1.1.2

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.
@@ -433,6 +433,7 @@ constructor(videoElement, options = {}) {
433
433
  brandLogoEnabled: false, // Enable/disable brand logo
434
434
  brandLogoUrl: '', // URL for brand logo image
435
435
  brandLogoLinkUrl: '', // Optional URL to open when clicking the logo
436
+ brandLogoTooltipText: '', // Tooltip text for brand logo
436
437
  playlistEnabled: true, // Enable/disable playlist detection
437
438
  playlistAutoPlay: true, // Auto-play next video when current ends
438
439
  playlistLoop: false, // Loop playlist when reaching the end
@@ -465,7 +466,6 @@ constructor(videoElement, options = {}) {
465
466
  this.currentQualityIndex = 0;
466
467
  this.qualities = [];
467
468
  this.originalSources = [];
468
- this.setupMenuToggles(); // Initialize menu toggle system
469
469
  this.isPiPSupported = this.checkPiPSupport();
470
470
  this.seekTooltip = null;
471
471
  this.titleOverlay = null;
@@ -508,18 +508,42 @@ constructor(videoElement, options = {}) {
508
508
 
509
509
  // Custom event system
510
510
  this.eventCallbacks = {
511
- 'played': [],
512
- 'paused': [],
513
- 'subtitlechange': [],
514
- 'chapterchange': [],
515
- 'pipchange': [],
516
- 'fullscreenchange': [],
517
- 'speedchange': [],
518
- 'timeupdate': [],
519
- 'volumechange': [],
520
- 'qualitychange': [],
521
- 'playlistchange': [],
522
- 'ended': []
511
+ // Core lifecycle events
512
+ 'playerready': [], // Fired when player is fully initialized and ready
513
+ 'played': [], // Fired when video starts playing
514
+ 'paused': [], // Fired when video is paused
515
+ 'ended': [], // Fired when video playback ends
516
+
517
+ // Playback state events
518
+ 'playing': [], // Fired when video is actually playing (after buffering)
519
+ 'waiting': [], // Fired when video is waiting for data (buffering)
520
+ 'seeking': [], // Fired when seek operation starts
521
+ 'seeked': [], // Fired when seek operation completes
522
+
523
+ // Loading events
524
+ 'loadstart': [], // Fired when browser starts looking for media
525
+ 'loadedmetadata': [], // Fired when metadata (duration, dimensions) is loaded
526
+ 'loadeddata': [], // Fired when data for current frame is loaded
527
+ 'canplay': [], // Fired when browser can start playing video
528
+ 'progress': [], // Fired periodically while downloading media
529
+ 'durationchange': [], // Fired when duration attribute changes
530
+
531
+ // Error events
532
+ 'error': [], // Fired when media loading or playback error occurs
533
+ 'stalled': [], // Fired when browser is trying to get data but it's not available
534
+
535
+ // Control events
536
+ 'timeupdate': [], // Fired when current playback position changes
537
+ 'volumechange': [], // Fired when volume or muted state changes
538
+ 'speedchange': [], // Fired when playback speed changes
539
+ 'qualitychange': [], // Fired when video quality changes
540
+
541
+ // Feature events
542
+ 'subtitlechange': [], // Fired when subtitle track changes
543
+ 'chapterchange': [], // Fired when video chapter changes
544
+ 'pipchange': [], // Fired when picture-in-picture mode changes
545
+ 'fullscreenchange': [], // Fired when fullscreen mode changes
546
+ 'playlistchange': [] // Fired when playlist item changes
523
547
  };
524
548
 
525
549
  // Playlist management
@@ -553,6 +577,7 @@ constructor(videoElement, options = {}) {
553
577
  this.interceptAutoLoading();
554
578
  this.createPlayerStructure();
555
579
  this.initializeElements();
580
+ this.setupMenuToggles(); // Initialize menu toggle system
556
581
  // audio player adaptation
557
582
  this.adaptToAudioFile = function () {
558
583
  if (this.options.audiofile) {
@@ -881,6 +906,14 @@ markPlayerReady() {
881
906
  this.container.classList.add('player-initialized');
882
907
  }
883
908
 
909
+ this.triggerEvent('playerready', {
910
+ playerState: this.getPlayerState(),
911
+ qualities: this.qualities,
912
+ subtitles: this.textTracks,
913
+ chapters: this.chapters,
914
+ playlist: this.getPlaylistInfo()
915
+ });
916
+
884
917
  if (this.video) {
885
918
  this.video.style.visibility = '';
886
919
  this.video.style.opacity = '';
@@ -1161,74 +1194,84 @@ initializeElements() {
1161
1194
  this.speedMenu = this.controls?.querySelector('.speed-menu');
1162
1195
  this.qualityMenu = this.controls?.querySelector('.quality-menu');
1163
1196
  this.subtitlesMenu = this.controls?.querySelector('.subtitles-menu');
1197
+ // Apply seek handle shape from options
1198
+ if (this.progressHandle && this.options.seekHandleShape) {
1199
+ this.setSeekHandleShape(this.options.seekHandleShape);
1200
+ }
1164
1201
  }
1165
1202
 
1166
1203
  // Generic method to close all active menus (works with plugins too)
1167
1204
  closeAllMenus() {
1168
- // Find all elements with class ending in '-menu' that have 'active' class
1169
- const allMenus = this.controls?.querySelectorAll('[class*="-menu"].active');
1170
- allMenus?.forEach(menu => {
1171
- menu.classList.remove('active');
1172
- });
1205
+ if (!this.controls) return;
1173
1206
 
1174
- // Remove active state from all control buttons
1175
- const allButtons = this.controls?.querySelectorAll('.control-btn.active');
1176
- allButtons?.forEach(btn => {
1177
- btn.classList.remove('active');
1178
- });
1207
+ const menus = this.controls.querySelectorAll('.speed-menu, .quality-menu, .subtitles-menu, .settings-menu');
1208
+ const buttons = this.controls.querySelectorAll('.control-btn');
1209
+
1210
+ menus.forEach(menu => menu.classList.remove('active'));
1211
+ buttons.forEach(btn => btn.classList.remove('active'));
1212
+
1213
+ this.currentOpenMenu = null;
1214
+
1215
+ if (this.options.debug) {
1216
+ console.log('All menus closed');
1217
+ }
1179
1218
  }
1180
1219
 
1181
1220
  // Generic menu toggle setup (works with core menus and plugin menus)
1182
1221
  setupMenuToggles() {
1183
- // Delegate click events to control bar for any button with associated menu
1184
- if (this.controls) {
1185
- this.controls.addEventListener('click', (e) => {
1186
- // Find if clicked element is a control button or inside one
1187
- const button = e.target.closest('.control-btn');
1188
-
1189
- if (!button) return;
1190
-
1191
- // Get button classes to find associated menu
1192
- const buttonClasses = button.className.split(' ');
1193
- let menuClass = null;
1194
-
1195
- // Find if this button has an associated menu (e.g., speed-btn -> speed-menu)
1196
- for (const cls of buttonClasses) {
1197
- if (cls.endsWith('-btn')) {
1198
- const menuName = cls.replace('-btn', '-menu');
1199
- const menu = this.controls.querySelector('.' + menuName);
1200
- if (menu) {
1201
- menuClass = menuName;
1202
- break;
1203
- }
1204
- }
1222
+ if (!this.controls) return;
1223
+
1224
+ this.currentOpenMenu = null;
1225
+
1226
+ this.controls.addEventListener('click', (e) => {
1227
+ const button = e.target.closest('.control-btn');
1228
+ if (!button) return;
1229
+
1230
+ const buttonClasses = Array.from(button.classList);
1231
+ let menuElement = null;
1232
+
1233
+ for (const cls of buttonClasses) {
1234
+ if (cls.endsWith('-btn')) {
1235
+ const menuClass = cls.replace('-btn', '-menu');
1236
+ menuElement = this.controls.querySelector(`.${menuClass}`);
1237
+ if (menuElement) break;
1205
1238
  }
1239
+ }
1206
1240
 
1207
- if (!menuClass) return;
1241
+ if (!menuElement) return;
1208
1242
 
1209
- e.stopPropagation();
1243
+ e.stopPropagation();
1244
+ e.preventDefault();
1210
1245
 
1211
- // Get the menu element
1212
- const menu = this.controls.querySelector('.' + menuClass);
1213
- const isOpen = menu.classList.contains('active');
1246
+ const isOpen = menuElement.classList.contains('active');
1214
1247
 
1215
- // Close all menus first
1216
- this.closeAllMenus();
1248
+ this.closeAllMenus();
1217
1249
 
1218
- // If menu was closed, open it
1219
- if (!isOpen) {
1220
- menu.classList.add('active');
1221
- button.classList.add('active');
1250
+ if (!isOpen) {
1251
+ menuElement.classList.add('active');
1252
+ button.classList.add('active');
1253
+ this.currentOpenMenu = menuElement;
1254
+ if (this.options.debug) {
1255
+ console.log('Menu opened:', menuElement.className);
1222
1256
  }
1223
- });
1224
- }
1257
+ } else {
1258
+ this.currentOpenMenu = null;
1259
+ if (this.options.debug) {
1260
+ console.log('Menu closed:', menuElement.className);
1261
+ }
1262
+ }
1263
+ });
1225
1264
 
1226
- // Close menus when clicking outside controls
1227
1265
  document.addEventListener('click', (e) => {
1228
- if (!this.controls?.contains(e.target)) {
1266
+ if (!this.controls) return;
1267
+ if (!this.controls.contains(e.target)) {
1229
1268
  this.closeAllMenus();
1230
1269
  }
1231
1270
  });
1271
+
1272
+ if (this.options.debug) {
1273
+ console.log('✅ Menu toggle system initialized (click-based, auto-close)');
1274
+ }
1232
1275
  }
1233
1276
 
1234
1277
  updateVolumeSliderVisual() {
@@ -1539,9 +1582,11 @@ updateBuffer() {
1539
1582
  }
1540
1583
 
1541
1584
  startSeeking(e) {
1585
+ if (e.cancelable) e.preventDefault();
1542
1586
  if (this.isChangingQuality) return;
1543
1587
 
1544
1588
  this.isUserSeeking = true;
1589
+ this.progressContainer.classList.add('seeking');
1545
1590
  this.seek(e);
1546
1591
  e.preventDefault();
1547
1592
 
@@ -1553,6 +1598,7 @@ startSeeking(e) {
1553
1598
  }
1554
1599
 
1555
1600
  continueSeeking(e) {
1601
+ if (e.cancelable) e.preventDefault();
1556
1602
  if (this.isUserSeeking && !this.isChangingQuality) {
1557
1603
  this.seek(e);
1558
1604
  }
@@ -1560,9 +1606,13 @@ continueSeeking(e) {
1560
1606
 
1561
1607
  endSeeking() {
1562
1608
  this.isUserSeeking = false;
1609
+ this.progressContainer.classList.remove('seeking');
1563
1610
  }
1564
1611
 
1565
1612
  seek(e) {
1613
+ if (e.cancelable) {
1614
+ e.preventDefault();
1615
+ }
1566
1616
  if (!this.video || !this.progressContainer || !this.progressFilled || !this.progressHandle || this.isChangingQuality) return;
1567
1617
 
1568
1618
  const rect = this.progressContainer.getBoundingClientRect();
@@ -1742,7 +1792,14 @@ createBrandLogo() {
1742
1792
  const logo = document.createElement('img');
1743
1793
  logo.className = 'brand-logo';
1744
1794
  logo.src = this.options.brandLogoUrl;
1745
- logo.alt = this.t('brand_logo');
1795
+ logo.alt = 'Brand logo';
1796
+
1797
+ // Add tooltip ONLY if link URL is present
1798
+ if (this.options.brandLogoLinkUrl) {
1799
+ // Use custom tooltip text if provided, otherwise fallback to URL
1800
+ logo.title = this.options.brandLogoTooltipText || this.options.brandLogoLinkUrl;
1801
+ // NON usare data-tooltip per evitare che venga sovrascritto da updateTooltips()
1802
+ }
1746
1803
 
1747
1804
  // Handle loading error
1748
1805
  logo.onerror = () => {
@@ -1758,7 +1815,7 @@ createBrandLogo() {
1758
1815
  if (this.options.brandLogoLinkUrl) {
1759
1816
  logo.style.cursor = 'pointer';
1760
1817
  logo.addEventListener('click', (e) => {
1761
- e.stopPropagation(); // Prevent video controls interference
1818
+ e.stopPropagation();
1762
1819
  window.open(this.options.brandLogoLinkUrl, '_blank', 'noopener,noreferrer');
1763
1820
  if (this.options.debug) console.log('Brand logo clicked, opening:', this.options.brandLogoLinkUrl);
1764
1821
  });
@@ -1766,15 +1823,10 @@ createBrandLogo() {
1766
1823
  logo.style.cursor = 'default';
1767
1824
  }
1768
1825
 
1769
- // Position the brand logo at the right of the controlbar (at the left of the buttons)
1770
1826
  controlsRight.insertBefore(logo, controlsRight.firstChild);
1771
1827
 
1772
1828
  if (this.options.debug) {
1773
- if (this.options.brandLogoLinkUrl) {
1774
- console.log('Brand logo with click handler created for:', this.options.brandLogoLinkUrl);
1775
- } else {
1776
- console.log('Brand logo created (no link)');
1777
- }
1829
+ console.log('Brand logo created with tooltip:', logo.title || 'no tooltip');
1778
1830
  }
1779
1831
  }
1780
1832
 
@@ -1981,7 +2033,7 @@ bindPosterEvents() {
1981
2033
  // Hide poster when video is loading/playing
1982
2034
  this.video.addEventListener('playing', () => {
1983
2035
  this.hidePoster();
1984
- });
2036
+ });
1985
2037
 
1986
2038
  // Show poster on load if not autoplay
1987
2039
  if (!this.options.autoplay) {
@@ -2420,36 +2472,130 @@ addEventListener(eventType, callback) {
2420
2472
  }
2421
2473
 
2422
2474
  bindEvents() {
2423
- if (this.video) {
2424
- this.video.addEventListener('loadedmetadata', () => {
2425
- this.updateDuration();
2426
- setTimeout(() => {
2427
- this.initializeSubtitles();
2428
- }, 100);
2475
+ if (this.video) {
2476
+
2477
+ // Playback events
2478
+ this.video.addEventListener('playing', () => {
2479
+ this.hideLoading();
2480
+ this.closeAllMenus();
2481
+ // Trigger playing event - video is now actually playing
2482
+ this.triggerEvent('playing', {
2483
+ currentTime: this.getCurrentTime(),
2484
+ duration: this.getDuration()
2429
2485
  });
2430
- this.video.addEventListener('timeupdate', () => this.updateProgress());
2431
- this.video.addEventListener('progress', () => this.updateBuffer());
2432
- this.video.addEventListener('waiting', () => {
2433
- if (!this.isChangingQuality) {
2434
- this.showLoading();
2435
- }
2486
+ });
2487
+
2488
+ this.video.addEventListener('waiting', () => {
2489
+ if (!this.isChangingQuality) {
2490
+ this.showLoading();
2491
+ // Trigger waiting event - video is buffering
2492
+ this.triggerEvent('waiting', {
2493
+ currentTime: this.getCurrentTime()
2494
+ });
2495
+ }
2496
+ });
2497
+
2498
+ this.video.addEventListener('seeking', () => {
2499
+ // Trigger seeking event - seek operation started
2500
+ this.triggerEvent('seeking', {
2501
+ currentTime: this.getCurrentTime(),
2502
+ targetTime: this.video.currentTime
2436
2503
  });
2437
- this.video.addEventListener('canplay', () => {
2438
- if (!this.isChangingQuality) {
2439
- this.hideLoading();
2440
- }
2504
+ });
2505
+
2506
+ this.video.addEventListener('seeked', () => {
2507
+ // Trigger seeked event - seek operation completed
2508
+ this.triggerEvent('seeked', {
2509
+ currentTime: this.getCurrentTime()
2441
2510
  });
2442
- this.video.addEventListener('ended', () => this.onVideoEnded());
2443
- this.video.addEventListener('loadstart', () => {
2444
- if (!this.isChangingQuality) {
2445
- this.showLoading();
2446
- }
2511
+ });
2512
+
2513
+ // Loading events
2514
+ this.video.addEventListener('loadstart', () => {
2515
+ if (!this.isChangingQuality) {
2516
+ this.showLoading();
2517
+ }
2518
+ // Trigger loadstart event - browser started loading media
2519
+ this.triggerEvent('loadstart');
2520
+ });
2521
+
2522
+ this.video.addEventListener('loadedmetadata', () => {
2523
+ this.updateDuration();
2524
+
2525
+ // Trigger loadedmetadata event - video metadata loaded
2526
+ this.triggerEvent('loadedmetadata', {
2527
+ duration: this.getDuration(),
2528
+ videoWidth: this.video.videoWidth,
2529
+ videoHeight: this.video.videoHeight
2447
2530
  });
2448
- this.video.addEventListener('loadeddata', () => {
2449
- if (!this.isChangingQuality) {
2450
- this.hideLoading();
2451
- }
2531
+
2532
+ // Initialize subtitles after metadata is loaded
2533
+ setTimeout(() => {
2534
+ this.initializeSubtitles();
2535
+ }, 100);
2536
+ });
2537
+
2538
+ this.video.addEventListener('loadeddata', () => {
2539
+ if (!this.isChangingQuality) {
2540
+ this.hideLoading();
2541
+ }
2542
+ // Trigger loadeddata event - current frame data loaded
2543
+ this.triggerEvent('loadeddata', {
2544
+ currentTime: this.getCurrentTime()
2545
+ });
2546
+ });
2547
+
2548
+ this.video.addEventListener('canplay', () => {
2549
+ if (!this.isChangingQuality) {
2550
+ this.hideLoading();
2551
+ }
2552
+ // Trigger canplay event - video can start playing
2553
+ this.triggerEvent('canplay', {
2554
+ currentTime: this.getCurrentTime(),
2555
+ duration: this.getDuration()
2452
2556
  });
2557
+ });
2558
+
2559
+ this.video.addEventListener('progress', () => {
2560
+ this.updateBuffer();
2561
+ // Trigger progress event - browser is downloading media
2562
+ this.triggerEvent('progress', {
2563
+ buffered: this.getBufferedTime(),
2564
+ duration: this.getDuration()
2565
+ });
2566
+ });
2567
+
2568
+ this.video.addEventListener('durationchange', () => {
2569
+ this.updateDuration();
2570
+ // Trigger durationchange event - video duration changed
2571
+ this.triggerEvent('durationchange', {
2572
+ duration: this.getDuration()
2573
+ });
2574
+ });
2575
+
2576
+ // Error events
2577
+ this.video.addEventListener('error', (e) => {
2578
+ this.onVideoError(e);
2579
+ // Trigger error event - media loading/playback error occurred
2580
+ this.triggerEvent('error', {
2581
+ code: this.video.error?.code,
2582
+ message: this.video.error?.message,
2583
+ src: this.video.currentSrc || this.video.src
2584
+ });
2585
+ });
2586
+
2587
+ this.video.addEventListener('stalled', () => {
2588
+ // Trigger stalled event - browser is trying to fetch data but it's not available
2589
+ this.triggerEvent('stalled', {
2590
+ currentTime: this.getCurrentTime()
2591
+ });
2592
+ });
2593
+
2594
+
2595
+ this.video.addEventListener('timeupdate', () => this.updateProgress());
2596
+
2597
+ this.video.addEventListener('ended', () => this.onVideoEnded());
2598
+
2453
2599
  // Complete video click logic with doubleTapPause support (DESKTOP)
2454
2600
  this.video.addEventListener('click', () => {
2455
2601
  if (!this.options.pauseClick) return;
@@ -2556,6 +2702,10 @@ addEventListener(eventType, callback) {
2556
2702
  // Mouse events (desktop)
2557
2703
  this.progressContainer.addEventListener('click', (e) => this.seek(e));
2558
2704
  this.progressContainer.addEventListener('mousedown', (e) => this.startSeeking(e));
2705
+ if (this.progressHandle) {
2706
+ this.progressHandle.addEventListener('mousedown', this.startSeeking.bind(this));
2707
+ this.progressHandle.addEventListener('touchstart', this.startSeeking.bind(this), { passive: false });
2708
+ }
2559
2709
 
2560
2710
  // Touch events (mobile)
2561
2711
  this.progressContainer.addEventListener('touchstart', (e) => {
@@ -2978,13 +3128,13 @@ createControls() {
2978
3128
  const controlsHTML = `
2979
3129
  <div class="controls" id="${controlsId}">
2980
3130
  <div class="progress-container">
2981
- <div class="progress-bar">
2982
- <div class="progress-buffer"></div>
2983
- <div class="progress-filled"></div>
2984
- <div class="progress-handle progress-handle-${this.options.seekHandleShape}"></div>
2985
- </div>
2986
- ${this.options.showSeekTooltip ? '<div class="seek-tooltip">0:00</div>' : ''}
2987
- </div>
3131
+ <div class="progress-bar">
3132
+ <div class="progress-buffer"></div>
3133
+ <div class="progress-filled"></div>
3134
+ </div>
3135
+ <div class="progress-handle progress-handle-${this.options.seekHandleShape}"></div> <!-- ✅ Fuori da progress-bar -->
3136
+ ${this.options.showSeekTooltip ? '<div class="seek-tooltip">0:00</div>' : ''}
3137
+ </div>
2988
3138
 
2989
3139
  <div class="controls-main">
2990
3140
  <div class="controls-left">
@@ -4979,11 +5129,11 @@ updateSubtitlesButton() {
4979
5129
  var subtitlesBtn = this.controls && this.controls.querySelector('.subtitles-btn');
4980
5130
  if (!subtitlesBtn) return;
4981
5131
 
5132
+ subtitlesBtn.classList.remove('active');
5133
+
4982
5134
  if (this.subtitlesEnabled) {
4983
- subtitlesBtn.classList.add('active');
4984
5135
  subtitlesBtn.title = this.t('subtitlesdisable');
4985
5136
  } else {
4986
- subtitlesBtn.classList.remove('active');
4987
5137
  subtitlesBtn.title = this.t('subtitlesenable');
4988
5138
  }
4989
5139
  }
@@ -5019,14 +5169,6 @@ updateSubtitlesUI() {
5019
5169
  bindSubtitleEvents() {
5020
5170
  var self = this;
5021
5171
 
5022
- var subtitlesBtn = this.controls && this.controls.querySelector('.subtitles-btn');
5023
- if (subtitlesBtn) {
5024
- subtitlesBtn.addEventListener('click', function (e) {
5025
- e.stopPropagation();
5026
- self.toggleSubtitles();
5027
- });
5028
- }
5029
-
5030
5172
  var subtitlesMenu = this.controls && this.controls.querySelector('.subtitles-menu');
5031
5173
  if (subtitlesMenu) {
5032
5174
  subtitlesMenu.addEventListener('click', function (e) {
@@ -5035,6 +5177,7 @@ bindSubtitleEvents() {
5035
5177
  }
5036
5178
  }
5037
5179
 
5180
+
5038
5181
  handleSubtitlesMenuClick(e) {
5039
5182
  if (!e.target.classList.contains('subtitles-option')) return;
5040
5183