myetv-player 1.1.0 → 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 CHANGED
@@ -119,6 +119,7 @@ const player = new MYETVvideoplayer('my-video', {
119
119
  | `brandLogoEnabled` | boolean | `false` | Show/hide the brand logo in the controlbar |
120
120
  | `brandLogoUrl` | string | `''` | Brand logo url in the controlbar (png, jpg, gif) - image height 44px - image width 120px |
121
121
  | `brandLogoLinkUrl` | string | `''` | Optional URL to open in a new page when clicking the brand logo in the controlbar
122
+ | `brandLogoTooltipText` | string | `''` | Optional Custom tooltip of the brand logo (the default is the url of the brand logo, if present)
122
123
  | `watermarkUrl` | string | `''` | Optional URL of the image watermark over the video, reccomended dimension: width: 180px, height: 100px
123
124
  | `watermarkLink` | string | `''` | Optional URL to open in a new page when clicking the watermark logo in the video
124
125
  | `watermarkPosition` | string | `''` | Optional where to show the watermark logo in the video (values are: top-left, top-right, bottom-left, bottom-right)
@@ -283,8 +284,21 @@ console.log(player.getCurrentResolution()); // Get current resolution
283
284
  ```
284
285
  ## API Events
285
286
  The MYETV Video Player includes a comprehensive custom event system that allows you to monitor all player state changes in real-time.
287
+ ### on player ready
288
+ Description: Triggered when the video player is ready
289
+
290
+ When: Player is ready to receive other events
291
+ ```
292
+ player.addEventListener('playerready', (event) => {
293
+ console.log('Player is ready!', event);
294
+ //now it's secure to call other apis method
295
+ player.setVolume(0.8);
296
+ player.play();
297
+ });
298
+ ```
286
299
  ### on played
287
300
  Description: Triggered when the video starts playing
301
+
288
302
  When: User presses play or video starts automatically
289
303
  ```
290
304
  player.addEventListener('played', (event) => {
@@ -294,24 +308,72 @@ player.addEventListener('played', (event) => {
294
308
  });
295
309
  });
296
310
  ```
311
+ ### on playing
312
+ Description: Triggered when the video is playing
313
+
314
+ When: Video is effectively playing
315
+ ```
316
+ player.addEventListener('playing', (event) => {
317
+ console.log('Video is playing at', event.currentTime);
318
+ });
319
+ ```
297
320
  ### on paused
298
321
  Description: Triggered when the video is pause
322
+
299
323
  When: User presses pause or video stops
300
324
  ```
301
325
  player.addEventListener('paused', (event) => {
302
326
  console.log('Video paused at:', event.currentTime + 's');
303
327
  });
304
328
  ```
329
+ ### on waiting
330
+ Description: Triggered when the video is buffering
331
+
332
+ When: Video is buffering and is waiting
333
+ ```
334
+ player.addEventListener('waiting', (event) => {
335
+ console.log('Video is buffering...');
336
+ });
337
+ ```
338
+ ### on seeking
339
+ Description: Triggered when the video is being seeking
340
+
341
+ When: The user is seeking over the video
342
+ ```
343
+ player.addEventListener('seeking', (event) => {
344
+ console.log('User is seeking to', event.targetTime);
345
+ });
346
+ ```
347
+ ### on seeked
348
+ Description: Triggered when the video is finished seeked
349
+
350
+ When: The user have finished seeking and seeked the video
351
+ ```
352
+ player.addEventListener('seeked', (event) => {
353
+ console.log('Seek completed at', event.currentTime);
354
+ });
355
+ ```
305
356
  ### on ended
306
357
  Description: Triggered when the video is ended
358
+
307
359
  When: Video is ended
308
360
  ```
309
361
  player.addEventListener('ended', (e) => {
310
362
  console.log('Video terminato!', e.currentTime, e.duration, e.playlistInfo);
311
363
  });
312
364
  ```
365
+ ### on error
366
+ Description: Triggered when the video have some error
367
+
368
+ When: Video have some error on load
369
+ ```
370
+ player.addEventListener('error', (event) => {
371
+ console.error('Playback error:', event.message);
372
+ });
373
+ ```
313
374
  ### on subtitle change
314
375
  Description: Triggered when subtitles are enabled/disabled or track changes
376
+
315
377
  When: User toggles subtitles or switches subtitle tracks
316
378
  ```
317
379
  player.addEventListener('subtitlechange', (event) => {
@@ -324,6 +386,7 @@ player.addEventListener('subtitlechange', (event) => {
324
386
  ```
325
387
  ### on chapters change
326
388
  Description: Triggered when chapters are changes
389
+
327
390
  When: User switches chapters tracks
328
391
  ```
329
392
  player.on('chapterchange', (data) => {
@@ -332,6 +395,7 @@ player.on('chapterchange', (data) => {
332
395
  ```
333
396
  ### on pip change
334
397
  Description: Triggered when Picture-in-Picture mode changes
398
+
335
399
  When: Video enters or exits PiP mode
336
400
  ```
337
401
  player.addEventListener('pipchange', (event) => {
@@ -340,6 +404,7 @@ player.addEventListener('pipchange', (event) => {
340
404
  ```
341
405
  ### on fullscreen change
342
406
  Description: Triggered when fullscreen mode changes
407
+
343
408
  When: Player enters or exits fullscreen mode
344
409
  ```
345
410
  player.addEventListener('fullscreenchange', (event) => {
@@ -348,6 +413,7 @@ player.addEventListener('fullscreenchange', (event) => {
348
413
  ```
349
414
  ### on speed change
350
415
  Description: Triggered when playback speed changes
416
+
351
417
  When: User modifies playback speed (0.5x, 1x, 1.5x, 2x, etc.)
352
418
  ```
353
419
  player.addEventListener('speedchange', (event) => {
@@ -356,6 +422,7 @@ player.addEventListener('speedchange', (event) => {
356
422
  ```
357
423
  ### on time update
358
424
  Description: Triggered during playback to update progress
425
+
359
426
  When: Every 250ms during playback (throttled for performance)
360
427
  ```
361
428
  player.addEventListener('timeupdate', (event) => {
@@ -366,6 +433,7 @@ player.addEventListener('timeupdate', (event) => {
366
433
  ```
367
434
  ### on volumechange
368
435
  Description: Triggered when volume or mute state changes
436
+
369
437
  When: User modifies volume or toggles mute
370
438
  ```
371
439
  player.addEventListener('volumechange', (event) => {
@@ -384,6 +452,7 @@ player.addEventListener('playlistchange', (e) => {
384
452
  ```
385
453
  ### Main APIs
386
454
  getEventData()
455
+
387
456
  Returns all requested state data in a single object:
388
457
  ```
389
458
  const state = player.getEventData();
@@ -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
@@ -508,18 +509,42 @@ constructor(videoElement, options = {}) {
508
509
 
509
510
  // Custom event system
510
511
  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': []
512
+ // Core lifecycle events
513
+ 'playerready': [], // Fired when player is fully initialized and ready
514
+ 'played': [], // Fired when video starts playing
515
+ 'paused': [], // Fired when video is paused
516
+ 'ended': [], // Fired when video playback ends
517
+
518
+ // Playback state events
519
+ 'playing': [], // Fired when video is actually playing (after buffering)
520
+ 'waiting': [], // Fired when video is waiting for data (buffering)
521
+ 'seeking': [], // Fired when seek operation starts
522
+ 'seeked': [], // Fired when seek operation completes
523
+
524
+ // Loading events
525
+ 'loadstart': [], // Fired when browser starts looking for media
526
+ 'loadedmetadata': [], // Fired when metadata (duration, dimensions) is loaded
527
+ 'loadeddata': [], // Fired when data for current frame is loaded
528
+ 'canplay': [], // Fired when browser can start playing video
529
+ 'progress': [], // Fired periodically while downloading media
530
+ 'durationchange': [], // Fired when duration attribute changes
531
+
532
+ // Error events
533
+ 'error': [], // Fired when media loading or playback error occurs
534
+ 'stalled': [], // Fired when browser is trying to get data but it's not available
535
+
536
+ // Control events
537
+ 'timeupdate': [], // Fired when current playback position changes
538
+ 'volumechange': [], // Fired when volume or muted state changes
539
+ 'speedchange': [], // Fired when playback speed changes
540
+ 'qualitychange': [], // Fired when video quality changes
541
+
542
+ // Feature events
543
+ 'subtitlechange': [], // Fired when subtitle track changes
544
+ 'chapterchange': [], // Fired when video chapter changes
545
+ 'pipchange': [], // Fired when picture-in-picture mode changes
546
+ 'fullscreenchange': [], // Fired when fullscreen mode changes
547
+ 'playlistchange': [] // Fired when playlist item changes
523
548
  };
524
549
 
525
550
  // Playlist management
@@ -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 = '';
@@ -1742,7 +1775,14 @@ createBrandLogo() {
1742
1775
  const logo = document.createElement('img');
1743
1776
  logo.className = 'brand-logo';
1744
1777
  logo.src = this.options.brandLogoUrl;
1745
- logo.alt = this.t('brand_logo');
1778
+ logo.alt = 'Brand logo';
1779
+
1780
+ // Add tooltip ONLY if link URL is present
1781
+ if (this.options.brandLogoLinkUrl) {
1782
+ // Use custom tooltip text if provided, otherwise fallback to URL
1783
+ logo.title = this.options.brandLogoTooltipText || this.options.brandLogoLinkUrl;
1784
+ // NON usare data-tooltip per evitare che venga sovrascritto da updateTooltips()
1785
+ }
1746
1786
 
1747
1787
  // Handle loading error
1748
1788
  logo.onerror = () => {
@@ -1758,7 +1798,7 @@ createBrandLogo() {
1758
1798
  if (this.options.brandLogoLinkUrl) {
1759
1799
  logo.style.cursor = 'pointer';
1760
1800
  logo.addEventListener('click', (e) => {
1761
- e.stopPropagation(); // Prevent video controls interference
1801
+ e.stopPropagation();
1762
1802
  window.open(this.options.brandLogoLinkUrl, '_blank', 'noopener,noreferrer');
1763
1803
  if (this.options.debug) console.log('Brand logo clicked, opening:', this.options.brandLogoLinkUrl);
1764
1804
  });
@@ -1766,15 +1806,10 @@ createBrandLogo() {
1766
1806
  logo.style.cursor = 'default';
1767
1807
  }
1768
1808
 
1769
- // Position the brand logo at the right of the controlbar (at the left of the buttons)
1770
1809
  controlsRight.insertBefore(logo, controlsRight.firstChild);
1771
1810
 
1772
1811
  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
- }
1812
+ console.log('Brand logo created with tooltip:', logo.title || 'no tooltip');
1778
1813
  }
1779
1814
  }
1780
1815
 
@@ -2420,36 +2455,129 @@ addEventListener(eventType, callback) {
2420
2455
  }
2421
2456
 
2422
2457
  bindEvents() {
2423
- if (this.video) {
2424
- this.video.addEventListener('loadedmetadata', () => {
2425
- this.updateDuration();
2426
- setTimeout(() => {
2427
- this.initializeSubtitles();
2428
- }, 100);
2458
+ if (this.video) {
2459
+
2460
+ // Playback events
2461
+ this.video.addEventListener('playing', () => {
2462
+ this.hideLoading();
2463
+ // Trigger playing event - video is now actually playing
2464
+ this.triggerEvent('playing', {
2465
+ currentTime: this.getCurrentTime(),
2466
+ duration: this.getDuration()
2429
2467
  });
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
- }
2468
+ });
2469
+
2470
+ this.video.addEventListener('waiting', () => {
2471
+ if (!this.isChangingQuality) {
2472
+ this.showLoading();
2473
+ // Trigger waiting event - video is buffering
2474
+ this.triggerEvent('waiting', {
2475
+ currentTime: this.getCurrentTime()
2476
+ });
2477
+ }
2478
+ });
2479
+
2480
+ this.video.addEventListener('seeking', () => {
2481
+ // Trigger seeking event - seek operation started
2482
+ this.triggerEvent('seeking', {
2483
+ currentTime: this.getCurrentTime(),
2484
+ targetTime: this.video.currentTime
2436
2485
  });
2437
- this.video.addEventListener('canplay', () => {
2438
- if (!this.isChangingQuality) {
2439
- this.hideLoading();
2440
- }
2486
+ });
2487
+
2488
+ this.video.addEventListener('seeked', () => {
2489
+ // Trigger seeked event - seek operation completed
2490
+ this.triggerEvent('seeked', {
2491
+ currentTime: this.getCurrentTime()
2441
2492
  });
2442
- this.video.addEventListener('ended', () => this.onVideoEnded());
2443
- this.video.addEventListener('loadstart', () => {
2444
- if (!this.isChangingQuality) {
2445
- this.showLoading();
2446
- }
2493
+ });
2494
+
2495
+ // Loading events
2496
+ this.video.addEventListener('loadstart', () => {
2497
+ if (!this.isChangingQuality) {
2498
+ this.showLoading();
2499
+ }
2500
+ // Trigger loadstart event - browser started loading media
2501
+ this.triggerEvent('loadstart');
2502
+ });
2503
+
2504
+ this.video.addEventListener('loadedmetadata', () => {
2505
+ this.updateDuration();
2506
+
2507
+ // Trigger loadedmetadata event - video metadata loaded
2508
+ this.triggerEvent('loadedmetadata', {
2509
+ duration: this.getDuration(),
2510
+ videoWidth: this.video.videoWidth,
2511
+ videoHeight: this.video.videoHeight
2447
2512
  });
2448
- this.video.addEventListener('loadeddata', () => {
2449
- if (!this.isChangingQuality) {
2450
- this.hideLoading();
2451
- }
2513
+
2514
+ // Initialize subtitles after metadata is loaded
2515
+ setTimeout(() => {
2516
+ this.initializeSubtitles();
2517
+ }, 100);
2518
+ });
2519
+
2520
+ this.video.addEventListener('loadeddata', () => {
2521
+ if (!this.isChangingQuality) {
2522
+ this.hideLoading();
2523
+ }
2524
+ // Trigger loadeddata event - current frame data loaded
2525
+ this.triggerEvent('loadeddata', {
2526
+ currentTime: this.getCurrentTime()
2527
+ });
2528
+ });
2529
+
2530
+ this.video.addEventListener('canplay', () => {
2531
+ if (!this.isChangingQuality) {
2532
+ this.hideLoading();
2533
+ }
2534
+ // Trigger canplay event - video can start playing
2535
+ this.triggerEvent('canplay', {
2536
+ currentTime: this.getCurrentTime(),
2537
+ duration: this.getDuration()
2452
2538
  });
2539
+ });
2540
+
2541
+ this.video.addEventListener('progress', () => {
2542
+ this.updateBuffer();
2543
+ // Trigger progress event - browser is downloading media
2544
+ this.triggerEvent('progress', {
2545
+ buffered: this.getBufferedTime(),
2546
+ duration: this.getDuration()
2547
+ });
2548
+ });
2549
+
2550
+ this.video.addEventListener('durationchange', () => {
2551
+ this.updateDuration();
2552
+ // Trigger durationchange event - video duration changed
2553
+ this.triggerEvent('durationchange', {
2554
+ duration: this.getDuration()
2555
+ });
2556
+ });
2557
+
2558
+ // Error events
2559
+ this.video.addEventListener('error', (e) => {
2560
+ this.onVideoError(e);
2561
+ // Trigger error event - media loading/playback error occurred
2562
+ this.triggerEvent('error', {
2563
+ code: this.video.error?.code,
2564
+ message: this.video.error?.message,
2565
+ src: this.video.currentSrc || this.video.src
2566
+ });
2567
+ });
2568
+
2569
+ this.video.addEventListener('stalled', () => {
2570
+ // Trigger stalled event - browser is trying to fetch data but it's not available
2571
+ this.triggerEvent('stalled', {
2572
+ currentTime: this.getCurrentTime()
2573
+ });
2574
+ });
2575
+
2576
+
2577
+ this.video.addEventListener('timeupdate', () => this.updateProgress());
2578
+
2579
+ this.video.addEventListener('ended', () => this.onVideoEnded());
2580
+
2453
2581
  // Complete video click logic with doubleTapPause support (DESKTOP)
2454
2582
  this.video.addEventListener('click', () => {
2455
2583
  if (!this.options.pauseClick) return;
@@ -419,6 +419,7 @@ constructor(videoElement, options = {}) {
419
419
  brandLogoEnabled: false,
420
420
  brandLogoUrl: '',
421
421
  brandLogoLinkUrl: '',
422
+ brandLogoTooltipText: '',
422
423
  playlistEnabled: true,
423
424
  playlistAutoPlay: true,
424
425
  playlistLoop: false,
@@ -494,18 +495,42 @@ constructor(videoElement, options = {}) {
494
495
 
495
496
 
496
497
  this.eventCallbacks = {
497
- 'played': [],
498
- 'paused': [],
499
- 'subtitlechange': [],
500
- 'chapterchange': [],
501
- 'pipchange': [],
498
+
499
+ 'playerready': [],
500
+ 'played': [],
501
+ 'paused': [],
502
+ 'ended': [],
503
+
504
+
505
+ 'playing': [],
506
+ 'waiting': [],
507
+ 'seeking': [],
508
+ 'seeked': [],
509
+
510
+
511
+ 'loadstart': [],
512
+ 'loadedmetadata': [],
513
+ 'loadeddata': [],
514
+ 'canplay': [],
515
+ 'progress': [],
516
+ 'durationchange': [],
517
+
518
+
519
+ 'error': [],
520
+ 'stalled': [],
521
+
522
+
523
+ 'timeupdate': [],
524
+ 'volumechange': [],
525
+ 'speedchange': [],
526
+ 'qualitychange': [],
527
+
528
+
529
+ 'subtitlechange': [],
530
+ 'chapterchange': [],
531
+ 'pipchange': [],
502
532
  'fullscreenchange': [],
503
- 'speedchange': [],
504
- 'timeupdate': [],
505
- 'volumechange': [],
506
- 'qualitychange': [],
507
- 'playlistchange': [],
508
- 'ended': []
533
+ 'playlistchange': []
509
534
  };
510
535
 
511
536
 
@@ -867,6 +892,14 @@ markPlayerReady() {
867
892
  this.container.classList.add('player-initialized');
868
893
  }
869
894
 
895
+ this.triggerEvent('playerready', {
896
+ playerState: this.getPlayerState(),
897
+ qualities: this.qualities,
898
+ subtitles: this.textTracks,
899
+ chapters: this.chapters,
900
+ playlist: this.getPlaylistInfo()
901
+ });
902
+
870
903
  if (this.video) {
871
904
  this.video.style.visibility = '';
872
905
  this.video.style.opacity = '';
@@ -1716,7 +1749,14 @@ createBrandLogo() {
1716
1749
  const logo = document.createElement('img');
1717
1750
  logo.className = 'brand-logo';
1718
1751
  logo.src = this.options.brandLogoUrl;
1719
- logo.alt = this.t('brand_logo');
1752
+ logo.alt = 'Brand logo';
1753
+
1754
+
1755
+ if (this.options.brandLogoLinkUrl) {
1756
+
1757
+ logo.title = this.options.brandLogoTooltipText || this.options.brandLogoLinkUrl;
1758
+
1759
+ }
1720
1760
 
1721
1761
 
1722
1762
  logo.onerror = () => {
@@ -1740,15 +1780,10 @@ createBrandLogo() {
1740
1780
  logo.style.cursor = 'default';
1741
1781
  }
1742
1782
 
1743
-
1744
1783
  controlsRight.insertBefore(logo, controlsRight.firstChild);
1745
1784
 
1746
1785
  if (this.options.debug) {
1747
- if (this.options.brandLogoLinkUrl) {
1748
- console.log('Brand logo with click handler created for:', this.options.brandLogoLinkUrl);
1749
- } else {
1750
- console.log('Brand logo created (no link)');
1751
- }
1786
+ console.log('Brand logo created with tooltip:', logo.title || 'no tooltip');
1752
1787
  }
1753
1788
  }
1754
1789
 
@@ -2335,36 +2370,129 @@ addEventListener(eventType, callback) {
2335
2370
  }
2336
2371
 
2337
2372
  bindEvents() {
2338
- if (this.video) {
2339
- this.video.addEventListener('loadedmetadata', () => {
2340
- this.updateDuration();
2341
- setTimeout(() => {
2342
- this.initializeSubtitles();
2343
- }, 100);
2373
+ if (this.video) {
2374
+
2375
+
2376
+ this.video.addEventListener('playing', () => {
2377
+ this.hideLoading();
2378
+
2379
+ this.triggerEvent('playing', {
2380
+ currentTime: this.getCurrentTime(),
2381
+ duration: this.getDuration()
2344
2382
  });
2345
- this.video.addEventListener('timeupdate', () => this.updateProgress());
2346
- this.video.addEventListener('progress', () => this.updateBuffer());
2347
- this.video.addEventListener('waiting', () => {
2348
- if (!this.isChangingQuality) {
2349
- this.showLoading();
2350
- }
2383
+ });
2384
+
2385
+ this.video.addEventListener('waiting', () => {
2386
+ if (!this.isChangingQuality) {
2387
+ this.showLoading();
2388
+
2389
+ this.triggerEvent('waiting', {
2390
+ currentTime: this.getCurrentTime()
2391
+ });
2392
+ }
2393
+ });
2394
+
2395
+ this.video.addEventListener('seeking', () => {
2396
+
2397
+ this.triggerEvent('seeking', {
2398
+ currentTime: this.getCurrentTime(),
2399
+ targetTime: this.video.currentTime
2351
2400
  });
2352
- this.video.addEventListener('canplay', () => {
2353
- if (!this.isChangingQuality) {
2354
- this.hideLoading();
2355
- }
2401
+ });
2402
+
2403
+ this.video.addEventListener('seeked', () => {
2404
+
2405
+ this.triggerEvent('seeked', {
2406
+ currentTime: this.getCurrentTime()
2356
2407
  });
2357
- this.video.addEventListener('ended', () => this.onVideoEnded());
2358
- this.video.addEventListener('loadstart', () => {
2359
- if (!this.isChangingQuality) {
2360
- this.showLoading();
2361
- }
2408
+ });
2409
+
2410
+
2411
+ this.video.addEventListener('loadstart', () => {
2412
+ if (!this.isChangingQuality) {
2413
+ this.showLoading();
2414
+ }
2415
+
2416
+ this.triggerEvent('loadstart');
2417
+ });
2418
+
2419
+ this.video.addEventListener('loadedmetadata', () => {
2420
+ this.updateDuration();
2421
+
2422
+
2423
+ this.triggerEvent('loadedmetadata', {
2424
+ duration: this.getDuration(),
2425
+ videoWidth: this.video.videoWidth,
2426
+ videoHeight: this.video.videoHeight
2362
2427
  });
2363
- this.video.addEventListener('loadeddata', () => {
2364
- if (!this.isChangingQuality) {
2365
- this.hideLoading();
2366
- }
2428
+
2429
+
2430
+ setTimeout(() => {
2431
+ this.initializeSubtitles();
2432
+ }, 100);
2433
+ });
2434
+
2435
+ this.video.addEventListener('loadeddata', () => {
2436
+ if (!this.isChangingQuality) {
2437
+ this.hideLoading();
2438
+ }
2439
+
2440
+ this.triggerEvent('loadeddata', {
2441
+ currentTime: this.getCurrentTime()
2367
2442
  });
2443
+ });
2444
+
2445
+ this.video.addEventListener('canplay', () => {
2446
+ if (!this.isChangingQuality) {
2447
+ this.hideLoading();
2448
+ }
2449
+
2450
+ this.triggerEvent('canplay', {
2451
+ currentTime: this.getCurrentTime(),
2452
+ duration: this.getDuration()
2453
+ });
2454
+ });
2455
+
2456
+ this.video.addEventListener('progress', () => {
2457
+ this.updateBuffer();
2458
+
2459
+ this.triggerEvent('progress', {
2460
+ buffered: this.getBufferedTime(),
2461
+ duration: this.getDuration()
2462
+ });
2463
+ });
2464
+
2465
+ this.video.addEventListener('durationchange', () => {
2466
+ this.updateDuration();
2467
+
2468
+ this.triggerEvent('durationchange', {
2469
+ duration: this.getDuration()
2470
+ });
2471
+ });
2472
+
2473
+
2474
+ this.video.addEventListener('error', (e) => {
2475
+ this.onVideoError(e);
2476
+
2477
+ this.triggerEvent('error', {
2478
+ code: this.video.error?.code,
2479
+ message: this.video.error?.message,
2480
+ src: this.video.currentSrc || this.video.src
2481
+ });
2482
+ });
2483
+
2484
+ this.video.addEventListener('stalled', () => {
2485
+
2486
+ this.triggerEvent('stalled', {
2487
+ currentTime: this.getCurrentTime()
2488
+ });
2489
+ });
2490
+
2491
+
2492
+ this.video.addEventListener('timeupdate', () => this.updateProgress());
2493
+
2494
+ this.video.addEventListener('ended', () => this.onVideoEnded());
2495
+
2368
2496
 
2369
2497
  this.video.addEventListener('click', () => {
2370
2498
  if (!this.options.pauseClick) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myetv-player",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "MYETV Video Player - Modular JavaScript and SCSS Build System",
5
5
  "main": "dist/myetv-player.js",
6
6
  "scripts": {
@@ -33,3 +33,4 @@
33
33
 
34
34
 
35
35
 
36
+
@@ -14,7 +14,8 @@
14
14
  autoplay: options.autoplay !== undefined ? options.autoplay : false,
15
15
  showYouTubeUI: options.showYouTubeUI !== undefined ? options.showYouTubeUI : false,
16
16
  autoLoadFromData: options.autoLoadFromData !== undefined ? options.autoLoadFromData : true,
17
- quality: options.quality || 'auto',
17
+ quality: options.quality || 'default',
18
+
18
19
  enableQualityControl: options.enableQualityControl !== undefined ? options.enableQualityControl : true,
19
20
  enableCaptions: options.enableCaptions !== undefined ? options.enableCaptions : true,
20
21
 
@@ -31,6 +32,13 @@
31
32
  ...options
32
33
  };
33
34
 
35
+ // Normalize 'auto' to 'default' for YouTube API compatibility
36
+ if (this.options.quality === 'auto') {
37
+ this.options.quality = 'default';
38
+ }
39
+ // Track original user choice for UI display
40
+ this.userQualityChoice = options.quality || 'default';
41
+
34
42
  this.ytPlayer = null;
35
43
  this.isYouTubeReady = false;
36
44
  this.videoId = this.options.videoId;
@@ -716,6 +724,131 @@ width: fit-content;
716
724
  this.api.triggerEvent('youtubeplugin:videoloaded', { videoId });
717
725
  }
718
726
 
727
+ setAdaptiveQuality() {
728
+ if (!this.ytPlayer) return;
729
+
730
+ try {
731
+ // Check network connection speed if available
732
+ const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
733
+ let suggestedQuality = 'default';
734
+
735
+ if (connection) {
736
+ const effectiveType = connection.effectiveType; // '4g', '3g', '2g', 'slow-2g'
737
+ const downlink = connection.downlink; // Mbps
738
+
739
+ if (this.api.player.options.debug) {
740
+ console.log('[YT Plugin] Connection:', effectiveType, 'Downlink:', downlink, 'Mbps');
741
+ }
742
+
743
+ // Set quality based on connection speed
744
+ if (effectiveType === 'slow-2g' || downlink < 0.5) {
745
+ suggestedQuality = 'small'; // 240p
746
+ } else if (effectiveType === '2g' || downlink < 1) {
747
+ suggestedQuality = 'medium'; // 360p
748
+ } else if (effectiveType === '3g' || downlink < 2.5) {
749
+ suggestedQuality = 'large'; // 480p
750
+ } else if (downlink < 5) {
751
+ suggestedQuality = 'hd720'; // 720p
752
+ } else if (downlink < 10) {
753
+ suggestedQuality = 'hd1080'; // 1080p
754
+ } else if (downlink < 20) {
755
+ suggestedQuality = 'hd1440'; // 1440p (2K)
756
+ } else if (downlink < 35) {
757
+ suggestedQuality = 'hd2160'; // 2160p (4K)
758
+ } else {
759
+ suggestedQuality = 'highres'; // 8K o migliore disponibile
760
+ }
761
+
762
+ if (this.api.player.options.debug) {
763
+ console.log('[YT Plugin] Setting suggested quality:', suggestedQuality);
764
+ }
765
+
766
+ this.ytPlayer.setPlaybackQuality(suggestedQuality);
767
+ } else {
768
+ // Fallback: start with medium quality on unknown devices
769
+ if (this.api.player.options.debug) {
770
+ console.log('[YT Plugin] Connection API not available, using large (480p) as safe default');
771
+ }
772
+ this.ytPlayer.setPlaybackQuality('large'); // 480p come default sicuro
773
+ }
774
+ } catch (error) {
775
+ if (this.api.player.options.debug) {
776
+ console.error('[YT Plugin] Error setting adaptive quality:', error);
777
+ }
778
+ }
779
+ }
780
+
781
+ startBufferMonitoring() {
782
+ if (this.bufferMonitorInterval) {
783
+ clearInterval(this.bufferMonitorInterval);
784
+ }
785
+
786
+ let bufferingCount = 0;
787
+ let lastState = null;
788
+
789
+ this.bufferMonitorInterval = setInterval(() => {
790
+ if (!this.ytPlayer) return;
791
+
792
+ try {
793
+ const state = this.ytPlayer.getPlayerState();
794
+
795
+ // Detect buffering (state 3)
796
+ if (state === YT.PlayerState.BUFFERING) {
797
+ bufferingCount++;
798
+
799
+ if (this.api.player.options.debug) {
800
+ console.log('[YT Plugin] Buffering detected, count:', bufferingCount);
801
+ }
802
+
803
+ // If buffering happens too often, reduce quality
804
+ if (bufferingCount >= 3) {
805
+ const currentQuality = this.ytPlayer.getPlaybackQuality();
806
+ const availableQualities = this.ytPlayer.getAvailableQualityLevels();
807
+
808
+ if (this.api.player.options.debug) {
809
+ console.log('[YT Plugin] Too much buffering, current quality:', currentQuality);
810
+ console.log('[YT Plugin] Available qualities:', availableQualities);
811
+ }
812
+
813
+ // Quality hierarchy (highest to lowest)
814
+ const qualityLevels = ['highres', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny'];
815
+ const currentIndex = qualityLevels.indexOf(currentQuality);
816
+
817
+ // Try to go to next lower quality
818
+ if (currentIndex < qualityLevels.length - 1) {
819
+ const lowerQuality = qualityLevels[currentIndex + 1];
820
+
821
+ // Check if lower quality is available
822
+ if (availableQualities.includes(lowerQuality)) {
823
+ if (this.api.player.options.debug) {
824
+ console.log('[YT Plugin] Reducing quality to:', lowerQuality);
825
+ }
826
+
827
+ this.ytPlayer.setPlaybackQuality(lowerQuality);
828
+ bufferingCount = 0; // Reset counter
829
+ }
830
+ }
831
+ }
832
+ } else if (state === YT.PlayerState.PLAYING) {
833
+ // Reset buffering count when playing smoothly
834
+ if (lastState === YT.PlayerState.BUFFERING) {
835
+ setTimeout(() => {
836
+ if (this.ytPlayer.getPlayerState() === YT.PlayerState.PLAYING) {
837
+ bufferingCount = Math.max(0, bufferingCount - 1);
838
+ }
839
+ }, 5000); // Wait 5 seconds of smooth playback
840
+ }
841
+ }
842
+
843
+ lastState = state;
844
+ } catch (error) {
845
+ if (this.api.player.options.debug) {
846
+ console.error('[YT Plugin] Error in buffer monitoring:', error);
847
+ }
848
+ }
849
+ }, 1000); // Check every second
850
+ }
851
+
719
852
  createMouseMoveOverlay() {
720
853
  if (this.mouseMoveOverlay) return;
721
854
 
@@ -1010,6 +1143,8 @@ width: fit-content;
1010
1143
  this.injectYouTubeCSSOverride();
1011
1144
 
1012
1145
  this.syncControls();
1146
+ // Start buffer monitoring for auto quality adjustment
1147
+ this.startBufferMonitoring();
1013
1148
 
1014
1149
  // Hide custom controls when YouTube native UI is enabled
1015
1150
  if (this.options.showYouTubeUI) {
@@ -1051,6 +1186,8 @@ width: fit-content;
1051
1186
  // Check if this is a live stream
1052
1187
  setTimeout(() => this.checkIfLiveStream(), 2000);
1053
1188
  setTimeout(() => this.checkIfLiveStream(), 5000);
1189
+ // Set adaptive quality based on connection
1190
+ setTimeout(() => this.setAdaptiveQuality(), 1000);
1054
1191
 
1055
1192
 
1056
1193
  // Listen for window resize
@@ -1700,7 +1837,7 @@ width: fit-content;
1700
1837
  const actualQuality = this.ytPlayer.getPlaybackQuality();
1701
1838
 
1702
1839
  // Only update UI if in AUTO mode, otherwise respect manual selection
1703
- if (this.currentQuality === 'default' || this.currentQuality === 'auto') {
1840
+ if (this.userQualityChoice === 'default' || this.userQualityChoice === 'auto') {
1704
1841
  if (actualQuality !== this.currentPlayingQuality) {
1705
1842
  this.currentPlayingQuality = actualQuality;
1706
1843
  if (this.api.player.options.debug) {
@@ -1880,6 +2017,8 @@ width: fit-content;
1880
2017
  if (!this.ytPlayer || !this.ytPlayer.setPlaybackQuality) return false;
1881
2018
 
1882
2019
  try {
2020
+ // Track user's quality choice for display
2021
+ this.userQualityChoice = quality;
1883
2022
  if (this.api.player.options.debug) {
1884
2023
  console.log('[YT Plugin] Setting quality to:', quality);
1885
2024
  console.log('[YT Plugin] Current quality:', this.ytPlayer.getPlaybackQuality());
@@ -2651,18 +2790,39 @@ width: fit-content;
2651
2790
  }
2652
2791
 
2653
2792
  onPlayerError(event) {
2654
- const errorMessages = {
2655
- 2: 'Invalid video ID',
2656
- 5: 'HTML5 player error',
2657
- 100: 'Video not found or private',
2658
- 101: 'Video not allowed in embedded players',
2659
- 150: 'Video not allowed in embedded players'
2660
- };
2661
- const errorMsg = errorMessages[event.data] || 'Unknown error';
2662
- if (this.api.player.options.debug) console.error('[YT Plugin] Error:', errorMsg);
2793
+ const errorCode = event.data;
2794
+
2795
+ if (this.api.player.options.debug) {
2796
+ console.error('[YT Plugin] Player error:', errorCode);
2797
+ }
2798
+
2799
+ // Error codes che indicano video non disponibile
2800
+ const unavailableErrors = [
2801
+ 2, // Invalid video ID
2802
+ 5, // HTML5 player error
2803
+ 100, // Video not found / removed
2804
+ 101, // Video not allowed to be played in embedded players (private/restricted)
2805
+ 150 // Same as 101
2806
+ ];
2807
+
2808
+ if (unavailableErrors.includes(errorCode)) {
2809
+ if (this.api.player.options.debug) {
2810
+ console.log('[YT Plugin] Video unavailable, triggering ended event');
2811
+ }
2812
+
2813
+ // Trigger the ended event from your player API
2814
+ this.api.triggerEvent('ended', {
2815
+ reason: 'video_unavailable',
2816
+ errorCode: errorCode
2817
+ });
2818
+
2819
+ // Optional: show poster overlay again
2820
+ this.showPosterOverlay();
2821
+ }
2822
+
2663
2823
  this.api.triggerEvent('youtubeplugin:error', {
2664
- errorCode: event.data,
2665
- errorMessage: errorMsg
2824
+ errorCode: errorCode,
2825
+ videoId: this.videoId
2666
2826
  });
2667
2827
  }
2668
2828
 
@@ -3151,6 +3311,16 @@ width: fit-content;
3151
3311
  this.ytPlayerContainer = null;
3152
3312
  }
3153
3313
 
3314
+ if (this.bufferMonitorInterval) {
3315
+ clearInterval(this.bufferMonitorInterval);
3316
+ this.bufferMonitorInterval = null;
3317
+ }
3318
+
3319
+ if (this.qualityMonitorInterval) {
3320
+ clearInterval(this.qualityMonitorInterval);
3321
+ this.qualityMonitorInterval = null;
3322
+ }
3323
+
3154
3324
  this.removeMouseMoveOverlay();
3155
3325
 
3156
3326
  this.api.container.classList.remove('youtube-active');
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
@@ -112,18 +113,42 @@ constructor(videoElement, options = {}) {
112
113
 
113
114
  // Custom event system
114
115
  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': []
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
127
152
  };
128
153
 
129
154
  // Playlist management
@@ -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 = '';
@@ -1346,7 +1379,14 @@ createBrandLogo() {
1346
1379
  const logo = document.createElement('img');
1347
1380
  logo.className = 'brand-logo';
1348
1381
  logo.src = this.options.brandLogoUrl;
1349
- 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
+ }
1350
1390
 
1351
1391
  // Handle loading error
1352
1392
  logo.onerror = () => {
@@ -1362,7 +1402,7 @@ createBrandLogo() {
1362
1402
  if (this.options.brandLogoLinkUrl) {
1363
1403
  logo.style.cursor = 'pointer';
1364
1404
  logo.addEventListener('click', (e) => {
1365
- e.stopPropagation(); // Prevent video controls interference
1405
+ e.stopPropagation();
1366
1406
  window.open(this.options.brandLogoLinkUrl, '_blank', 'noopener,noreferrer');
1367
1407
  if (this.options.debug) console.log('Brand logo clicked, opening:', this.options.brandLogoLinkUrl);
1368
1408
  });
@@ -1370,15 +1410,10 @@ createBrandLogo() {
1370
1410
  logo.style.cursor = 'default';
1371
1411
  }
1372
1412
 
1373
- // Position the brand logo at the right of the controlbar (at the left of the buttons)
1374
1413
  controlsRight.insertBefore(logo, controlsRight.firstChild);
1375
1414
 
1376
1415
  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
- }
1416
+ console.log('Brand logo created with tooltip:', logo.title || 'no tooltip');
1382
1417
  }
1383
1418
  }
1384
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;