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.
package/src/controls.js CHANGED
@@ -368,13 +368,13 @@ createControls() {
368
368
  const controlsHTML = `
369
369
  <div class="controls" id="${controlsId}">
370
370
  <div class="progress-container">
371
- <div class="progress-bar">
372
- <div class="progress-buffer"></div>
373
- <div class="progress-filled"></div>
374
- <div class="progress-handle progress-handle-${this.options.seekHandleShape}"></div>
375
- </div>
376
- ${this.options.showSeekTooltip ? '<div class="seek-tooltip">0:00</div>' : ''}
377
- </div>
371
+ <div class="progress-bar">
372
+ <div class="progress-buffer"></div>
373
+ <div class="progress-filled"></div>
374
+ </div>
375
+ <div class="progress-handle progress-handle-${this.options.seekHandleShape}"></div> <!-- ✅ Fuori da progress-bar -->
376
+ ${this.options.showSeekTooltip ? '<div class="seek-tooltip">0:00</div>' : ''}
377
+ </div>
378
378
 
379
379
  <div class="controls-main">
380
380
  <div class="controls-left">
package/src/core.js CHANGED
@@ -37,6 +37,7 @@ constructor(videoElement, options = {}) {
37
37
  brandLogoEnabled: false, // Enable/disable brand logo
38
38
  brandLogoUrl: '', // URL for brand logo image
39
39
  brandLogoLinkUrl: '', // Optional URL to open when clicking the logo
40
+ brandLogoTooltipText: '', // Tooltip text for brand logo
40
41
  playlistEnabled: true, // Enable/disable playlist detection
41
42
  playlistAutoPlay: true, // Auto-play next video when current ends
42
43
  playlistLoop: false, // Loop playlist when reaching the end
@@ -69,7 +70,6 @@ constructor(videoElement, options = {}) {
69
70
  this.currentQualityIndex = 0;
70
71
  this.qualities = [];
71
72
  this.originalSources = [];
72
- this.setupMenuToggles(); // Initialize menu toggle system
73
73
  this.isPiPSupported = this.checkPiPSupport();
74
74
  this.seekTooltip = null;
75
75
  this.titleOverlay = null;
@@ -112,18 +112,42 @@ constructor(videoElement, options = {}) {
112
112
 
113
113
  // Custom event system
114
114
  this.eventCallbacks = {
115
- 'played': [],
116
- 'paused': [],
117
- 'subtitlechange': [],
118
- 'chapterchange': [],
119
- 'pipchange': [],
120
- 'fullscreenchange': [],
121
- 'speedchange': [],
122
- 'timeupdate': [],
123
- 'volumechange': [],
124
- 'qualitychange': [],
125
- 'playlistchange': [],
126
- 'ended': []
115
+ // Core lifecycle events
116
+ 'playerready': [], // Fired when player is fully initialized and ready
117
+ 'played': [], // Fired when video starts playing
118
+ 'paused': [], // Fired when video is paused
119
+ 'ended': [], // Fired when video playback ends
120
+
121
+ // Playback state events
122
+ 'playing': [], // Fired when video is actually playing (after buffering)
123
+ 'waiting': [], // Fired when video is waiting for data (buffering)
124
+ 'seeking': [], // Fired when seek operation starts
125
+ 'seeked': [], // Fired when seek operation completes
126
+
127
+ // Loading events
128
+ 'loadstart': [], // Fired when browser starts looking for media
129
+ 'loadedmetadata': [], // Fired when metadata (duration, dimensions) is loaded
130
+ 'loadeddata': [], // Fired when data for current frame is loaded
131
+ 'canplay': [], // Fired when browser can start playing video
132
+ 'progress': [], // Fired periodically while downloading media
133
+ 'durationchange': [], // Fired when duration attribute changes
134
+
135
+ // Error events
136
+ 'error': [], // Fired when media loading or playback error occurs
137
+ 'stalled': [], // Fired when browser is trying to get data but it's not available
138
+
139
+ // Control events
140
+ 'timeupdate': [], // Fired when current playback position changes
141
+ 'volumechange': [], // Fired when volume or muted state changes
142
+ 'speedchange': [], // Fired when playback speed changes
143
+ 'qualitychange': [], // Fired when video quality changes
144
+
145
+ // Feature events
146
+ 'subtitlechange': [], // Fired when subtitle track changes
147
+ 'chapterchange': [], // Fired when video chapter changes
148
+ 'pipchange': [], // Fired when picture-in-picture mode changes
149
+ 'fullscreenchange': [], // Fired when fullscreen mode changes
150
+ 'playlistchange': [] // Fired when playlist item changes
127
151
  };
128
152
 
129
153
  // Playlist management
@@ -157,6 +181,7 @@ constructor(videoElement, options = {}) {
157
181
  this.interceptAutoLoading();
158
182
  this.createPlayerStructure();
159
183
  this.initializeElements();
184
+ this.setupMenuToggles(); // Initialize menu toggle system
160
185
  // audio player adaptation
161
186
  this.adaptToAudioFile = function () {
162
187
  if (this.options.audiofile) {
@@ -485,6 +510,14 @@ markPlayerReady() {
485
510
  this.container.classList.add('player-initialized');
486
511
  }
487
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
+
488
521
  if (this.video) {
489
522
  this.video.style.visibility = '';
490
523
  this.video.style.opacity = '';
@@ -765,74 +798,84 @@ initializeElements() {
765
798
  this.speedMenu = this.controls?.querySelector('.speed-menu');
766
799
  this.qualityMenu = this.controls?.querySelector('.quality-menu');
767
800
  this.subtitlesMenu = this.controls?.querySelector('.subtitles-menu');
801
+ // Apply seek handle shape from options
802
+ if (this.progressHandle && this.options.seekHandleShape) {
803
+ this.setSeekHandleShape(this.options.seekHandleShape);
804
+ }
768
805
  }
769
806
 
770
807
  // Generic method to close all active menus (works with plugins too)
771
808
  closeAllMenus() {
772
- // Find all elements with class ending in '-menu' that have 'active' class
773
- const allMenus = this.controls?.querySelectorAll('[class*="-menu"].active');
774
- allMenus?.forEach(menu => {
775
- menu.classList.remove('active');
776
- });
809
+ if (!this.controls) return;
777
810
 
778
- // Remove active state from all control buttons
779
- const allButtons = this.controls?.querySelectorAll('.control-btn.active');
780
- allButtons?.forEach(btn => {
781
- btn.classList.remove('active');
782
- });
811
+ const menus = this.controls.querySelectorAll('.speed-menu, .quality-menu, .subtitles-menu, .settings-menu');
812
+ const buttons = this.controls.querySelectorAll('.control-btn');
813
+
814
+ menus.forEach(menu => menu.classList.remove('active'));
815
+ buttons.forEach(btn => btn.classList.remove('active'));
816
+
817
+ this.currentOpenMenu = null;
818
+
819
+ if (this.options.debug) {
820
+ console.log('All menus closed');
821
+ }
783
822
  }
784
823
 
785
824
  // Generic menu toggle setup (works with core menus and plugin menus)
786
825
  setupMenuToggles() {
787
- // Delegate click events to control bar for any button with associated menu
788
- if (this.controls) {
789
- this.controls.addEventListener('click', (e) => {
790
- // Find if clicked element is a control button or inside one
791
- const button = e.target.closest('.control-btn');
792
-
793
- if (!button) return;
794
-
795
- // Get button classes to find associated menu
796
- const buttonClasses = button.className.split(' ');
797
- let menuClass = null;
798
-
799
- // Find if this button has an associated menu (e.g., speed-btn -> speed-menu)
800
- for (const cls of buttonClasses) {
801
- if (cls.endsWith('-btn')) {
802
- const menuName = cls.replace('-btn', '-menu');
803
- const menu = this.controls.querySelector('.' + menuName);
804
- if (menu) {
805
- menuClass = menuName;
806
- break;
807
- }
808
- }
826
+ if (!this.controls) return;
827
+
828
+ this.currentOpenMenu = null;
829
+
830
+ this.controls.addEventListener('click', (e) => {
831
+ const button = e.target.closest('.control-btn');
832
+ if (!button) return;
833
+
834
+ const buttonClasses = Array.from(button.classList);
835
+ let menuElement = null;
836
+
837
+ for (const cls of buttonClasses) {
838
+ if (cls.endsWith('-btn')) {
839
+ const menuClass = cls.replace('-btn', '-menu');
840
+ menuElement = this.controls.querySelector(`.${menuClass}`);
841
+ if (menuElement) break;
809
842
  }
843
+ }
810
844
 
811
- if (!menuClass) return;
845
+ if (!menuElement) return;
812
846
 
813
- e.stopPropagation();
847
+ e.stopPropagation();
848
+ e.preventDefault();
814
849
 
815
- // Get the menu element
816
- const menu = this.controls.querySelector('.' + menuClass);
817
- const isOpen = menu.classList.contains('active');
850
+ const isOpen = menuElement.classList.contains('active');
818
851
 
819
- // Close all menus first
820
- this.closeAllMenus();
852
+ this.closeAllMenus();
821
853
 
822
- // If menu was closed, open it
823
- if (!isOpen) {
824
- menu.classList.add('active');
825
- button.classList.add('active');
854
+ if (!isOpen) {
855
+ menuElement.classList.add('active');
856
+ button.classList.add('active');
857
+ this.currentOpenMenu = menuElement;
858
+ if (this.options.debug) {
859
+ console.log('Menu opened:', menuElement.className);
826
860
  }
827
- });
828
- }
861
+ } else {
862
+ this.currentOpenMenu = null;
863
+ if (this.options.debug) {
864
+ console.log('Menu closed:', menuElement.className);
865
+ }
866
+ }
867
+ });
829
868
 
830
- // Close menus when clicking outside controls
831
869
  document.addEventListener('click', (e) => {
832
- if (!this.controls?.contains(e.target)) {
870
+ if (!this.controls) return;
871
+ if (!this.controls.contains(e.target)) {
833
872
  this.closeAllMenus();
834
873
  }
835
874
  });
875
+
876
+ if (this.options.debug) {
877
+ console.log('✅ Menu toggle system initialized (click-based, auto-close)');
878
+ }
836
879
  }
837
880
 
838
881
  updateVolumeSliderVisual() {
@@ -1143,9 +1186,11 @@ updateBuffer() {
1143
1186
  }
1144
1187
 
1145
1188
  startSeeking(e) {
1189
+ if (e.cancelable) e.preventDefault();
1146
1190
  if (this.isChangingQuality) return;
1147
1191
 
1148
1192
  this.isUserSeeking = true;
1193
+ this.progressContainer.classList.add('seeking');
1149
1194
  this.seek(e);
1150
1195
  e.preventDefault();
1151
1196
 
@@ -1157,6 +1202,7 @@ startSeeking(e) {
1157
1202
  }
1158
1203
 
1159
1204
  continueSeeking(e) {
1205
+ if (e.cancelable) e.preventDefault();
1160
1206
  if (this.isUserSeeking && !this.isChangingQuality) {
1161
1207
  this.seek(e);
1162
1208
  }
@@ -1164,9 +1210,13 @@ continueSeeking(e) {
1164
1210
 
1165
1211
  endSeeking() {
1166
1212
  this.isUserSeeking = false;
1213
+ this.progressContainer.classList.remove('seeking');
1167
1214
  }
1168
1215
 
1169
1216
  seek(e) {
1217
+ if (e.cancelable) {
1218
+ e.preventDefault();
1219
+ }
1170
1220
  if (!this.video || !this.progressContainer || !this.progressFilled || !this.progressHandle || this.isChangingQuality) return;
1171
1221
 
1172
1222
  const rect = this.progressContainer.getBoundingClientRect();
@@ -1346,7 +1396,14 @@ createBrandLogo() {
1346
1396
  const logo = document.createElement('img');
1347
1397
  logo.className = 'brand-logo';
1348
1398
  logo.src = this.options.brandLogoUrl;
1349
- logo.alt = this.t('brand_logo');
1399
+ logo.alt = 'Brand logo';
1400
+
1401
+ // Add tooltip ONLY if link URL is present
1402
+ if (this.options.brandLogoLinkUrl) {
1403
+ // Use custom tooltip text if provided, otherwise fallback to URL
1404
+ logo.title = this.options.brandLogoTooltipText || this.options.brandLogoLinkUrl;
1405
+ // NON usare data-tooltip per evitare che venga sovrascritto da updateTooltips()
1406
+ }
1350
1407
 
1351
1408
  // Handle loading error
1352
1409
  logo.onerror = () => {
@@ -1362,7 +1419,7 @@ createBrandLogo() {
1362
1419
  if (this.options.brandLogoLinkUrl) {
1363
1420
  logo.style.cursor = 'pointer';
1364
1421
  logo.addEventListener('click', (e) => {
1365
- e.stopPropagation(); // Prevent video controls interference
1422
+ e.stopPropagation();
1366
1423
  window.open(this.options.brandLogoLinkUrl, '_blank', 'noopener,noreferrer');
1367
1424
  if (this.options.debug) console.log('Brand logo clicked, opening:', this.options.brandLogoLinkUrl);
1368
1425
  });
@@ -1370,15 +1427,10 @@ createBrandLogo() {
1370
1427
  logo.style.cursor = 'default';
1371
1428
  }
1372
1429
 
1373
- // Position the brand logo at the right of the controlbar (at the left of the buttons)
1374
1430
  controlsRight.insertBefore(logo, controlsRight.firstChild);
1375
1431
 
1376
1432
  if (this.options.debug) {
1377
- if (this.options.brandLogoLinkUrl) {
1378
- console.log('Brand logo with click handler created for:', this.options.brandLogoLinkUrl);
1379
- } else {
1380
- console.log('Brand logo created (no link)');
1381
- }
1433
+ console.log('Brand logo created with tooltip:', logo.title || 'no tooltip');
1382
1434
  }
1383
1435
  }
1384
1436
 
@@ -1585,7 +1637,7 @@ bindPosterEvents() {
1585
1637
  // Hide poster when video is loading/playing
1586
1638
  this.video.addEventListener('playing', () => {
1587
1639
  this.hidePoster();
1588
- });
1640
+ });
1589
1641
 
1590
1642
  // Show poster on load if not autoplay
1591
1643
  if (!this.options.autoplay) {
package/src/events.js CHANGED
@@ -165,36 +165,130 @@
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
+ this.closeAllMenus();
174
+ // Trigger playing event - video is now actually playing
175
+ this.triggerEvent('playing', {
176
+ currentTime: this.getCurrentTime(),
177
+ duration: this.getDuration()
174
178
  });
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
- }
179
+ });
180
+
181
+ this.video.addEventListener('waiting', () => {
182
+ if (!this.isChangingQuality) {
183
+ this.showLoading();
184
+ // Trigger waiting event - video is buffering
185
+ this.triggerEvent('waiting', {
186
+ currentTime: this.getCurrentTime()
187
+ });
188
+ }
189
+ });
190
+
191
+ this.video.addEventListener('seeking', () => {
192
+ // Trigger seeking event - seek operation started
193
+ this.triggerEvent('seeking', {
194
+ currentTime: this.getCurrentTime(),
195
+ targetTime: this.video.currentTime
181
196
  });
182
- this.video.addEventListener('canplay', () => {
183
- if (!this.isChangingQuality) {
184
- this.hideLoading();
185
- }
197
+ });
198
+
199
+ this.video.addEventListener('seeked', () => {
200
+ // Trigger seeked event - seek operation completed
201
+ this.triggerEvent('seeked', {
202
+ currentTime: this.getCurrentTime()
186
203
  });
187
- this.video.addEventListener('ended', () => this.onVideoEnded());
188
- this.video.addEventListener('loadstart', () => {
189
- if (!this.isChangingQuality) {
190
- this.showLoading();
191
- }
204
+ });
205
+
206
+ // Loading events
207
+ this.video.addEventListener('loadstart', () => {
208
+ if (!this.isChangingQuality) {
209
+ this.showLoading();
210
+ }
211
+ // Trigger loadstart event - browser started loading media
212
+ this.triggerEvent('loadstart');
213
+ });
214
+
215
+ this.video.addEventListener('loadedmetadata', () => {
216
+ this.updateDuration();
217
+
218
+ // Trigger loadedmetadata event - video metadata loaded
219
+ this.triggerEvent('loadedmetadata', {
220
+ duration: this.getDuration(),
221
+ videoWidth: this.video.videoWidth,
222
+ videoHeight: this.video.videoHeight
192
223
  });
193
- this.video.addEventListener('loadeddata', () => {
194
- if (!this.isChangingQuality) {
195
- this.hideLoading();
196
- }
224
+
225
+ // Initialize subtitles after metadata is loaded
226
+ setTimeout(() => {
227
+ this.initializeSubtitles();
228
+ }, 100);
229
+ });
230
+
231
+ this.video.addEventListener('loadeddata', () => {
232
+ if (!this.isChangingQuality) {
233
+ this.hideLoading();
234
+ }
235
+ // Trigger loadeddata event - current frame data loaded
236
+ this.triggerEvent('loadeddata', {
237
+ currentTime: this.getCurrentTime()
197
238
  });
239
+ });
240
+
241
+ this.video.addEventListener('canplay', () => {
242
+ if (!this.isChangingQuality) {
243
+ this.hideLoading();
244
+ }
245
+ // Trigger canplay event - video can start playing
246
+ this.triggerEvent('canplay', {
247
+ currentTime: this.getCurrentTime(),
248
+ duration: this.getDuration()
249
+ });
250
+ });
251
+
252
+ this.video.addEventListener('progress', () => {
253
+ this.updateBuffer();
254
+ // Trigger progress event - browser is downloading media
255
+ this.triggerEvent('progress', {
256
+ buffered: this.getBufferedTime(),
257
+ duration: this.getDuration()
258
+ });
259
+ });
260
+
261
+ this.video.addEventListener('durationchange', () => {
262
+ this.updateDuration();
263
+ // Trigger durationchange event - video duration changed
264
+ this.triggerEvent('durationchange', {
265
+ duration: this.getDuration()
266
+ });
267
+ });
268
+
269
+ // Error events
270
+ this.video.addEventListener('error', (e) => {
271
+ this.onVideoError(e);
272
+ // Trigger error event - media loading/playback error occurred
273
+ this.triggerEvent('error', {
274
+ code: this.video.error?.code,
275
+ message: this.video.error?.message,
276
+ src: this.video.currentSrc || this.video.src
277
+ });
278
+ });
279
+
280
+ this.video.addEventListener('stalled', () => {
281
+ // Trigger stalled event - browser is trying to fetch data but it's not available
282
+ this.triggerEvent('stalled', {
283
+ currentTime: this.getCurrentTime()
284
+ });
285
+ });
286
+
287
+
288
+ this.video.addEventListener('timeupdate', () => this.updateProgress());
289
+
290
+ this.video.addEventListener('ended', () => this.onVideoEnded());
291
+
198
292
  // Complete video click logic with doubleTapPause support (DESKTOP)
199
293
  this.video.addEventListener('click', () => {
200
294
  if (!this.options.pauseClick) return;
@@ -301,6 +395,10 @@
301
395
  // Mouse events (desktop)
302
396
  this.progressContainer.addEventListener('click', (e) => this.seek(e));
303
397
  this.progressContainer.addEventListener('mousedown', (e) => this.startSeeking(e));
398
+ if (this.progressHandle) {
399
+ this.progressHandle.addEventListener('mousedown', this.startSeeking.bind(this));
400
+ this.progressHandle.addEventListener('touchstart', this.startSeeking.bind(this), { passive: false });
401
+ }
304
402
 
305
403
  // Touch events (mobile)
306
404
  this.progressContainer.addEventListener('touchstart', (e) => {
package/src/subtitles.js CHANGED
@@ -343,11 +343,11 @@ updateSubtitlesButton() {
343
343
  var subtitlesBtn = this.controls && this.controls.querySelector('.subtitles-btn');
344
344
  if (!subtitlesBtn) return;
345
345
 
346
+ subtitlesBtn.classList.remove('active');
347
+
346
348
  if (this.subtitlesEnabled) {
347
- subtitlesBtn.classList.add('active');
348
349
  subtitlesBtn.title = this.t('subtitlesdisable');
349
350
  } else {
350
- subtitlesBtn.classList.remove('active');
351
351
  subtitlesBtn.title = this.t('subtitlesenable');
352
352
  }
353
353
  }
@@ -383,14 +383,6 @@ updateSubtitlesUI() {
383
383
  bindSubtitleEvents() {
384
384
  var self = this;
385
385
 
386
- var subtitlesBtn = this.controls && this.controls.querySelector('.subtitles-btn');
387
- if (subtitlesBtn) {
388
- subtitlesBtn.addEventListener('click', function (e) {
389
- e.stopPropagation();
390
- self.toggleSubtitles();
391
- });
392
- }
393
-
394
386
  var subtitlesMenu = this.controls && this.controls.querySelector('.subtitles-menu');
395
387
  if (subtitlesMenu) {
396
388
  subtitlesMenu.addEventListener('click', function (e) {
@@ -399,6 +391,7 @@ bindSubtitleEvents() {
399
391
  }
400
392
  }
401
393
 
394
+
402
395
  handleSubtitlesMenuClick(e) {
403
396
  if (!e.target.classList.contains('subtitles-option')) return;
404
397