eb-player 2.0.9 → 2.0.11

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.
@@ -4,7 +4,7 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.EBPlayer = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- var __EB_PLAYER_VERSION__ = "2.0.9";
7
+ var __EB_PLAYER_VERSION__ = "2.0.11";
8
8
 
9
9
  /**
10
10
  * Finite State Machine for player playback state transitions.
@@ -4284,12 +4284,20 @@
4284
4284
  video.addEventListener('ended', () => {
4285
4285
  state.playbackState = 'ended';
4286
4286
  }, { signal });
4287
- // Safety net: clear stale 'buffering' state when playback is clearly advancing.
4288
- // In some edge cases (live stream segment boundaries), the browser fires 'waiting'
4289
- // but never fires 'playing' even though video resumes from buffer. The timeupdate
4290
- // event fires reliably while time advances, so we use it to recover.
4287
+ // Safety net: reconcile stale FSM state when playback is clearly advancing.
4288
+ // Covers two scenarios:
4289
+ // 1. Live stream segment boundaries where browser fires 'waiting' but never
4290
+ // fires 'playing' even though video resumes from buffer.
4291
+ // 2. Safari autoplay: when the browser starts playback via the autoplay attribute,
4292
+ // the 'playing' event can be missed or arrive out of order, leaving the FSM
4293
+ // stuck in 'loading' or 'buffering' while the video is actually playing.
4294
+ // The timeupdate event fires reliably while time advances, so we use it to recover.
4295
+ // Note: Safari with MSE/hls.js may report readyState=2 (HAVE_CURRENT_DATA) even
4296
+ // while actively playing and advancing currentTime. We don't gate on readyState
4297
+ // here — the fact that timeupdate fires with !paused is sufficient proof of playback.
4291
4298
  video.addEventListener('timeupdate', () => {
4292
- if (state.playbackState === 'buffering' && !video.paused && video.readyState >= 3) {
4299
+ const stuck = state.playbackState === 'buffering' || state.playbackState === 'loading';
4300
+ if (stuck && !video.paused) {
4293
4301
  state.playbackState = 'playing';
4294
4302
  }
4295
4303
  }, { signal });
@@ -4487,6 +4495,7 @@
4487
4495
  this.expirationMarginInSeconds = expirationMarginInSeconds;
4488
4496
  this.lastTokenResponse = null;
4489
4497
  this.resetAttemptCounterTimeout = null;
4498
+ this.inFlightFetch = null;
4490
4499
  }
4491
4500
  resetAttemptCounter() {
4492
4501
  if (this.resetAttemptCounterTimeout) {
@@ -4730,21 +4739,35 @@
4730
4739
  console.warn('CDNToken: Missing src to tokenize');
4731
4740
  return null;
4732
4741
  }
4733
- switch (this.tokenType) {
4734
- case TOKEN_TYPES.BUNNY:
4735
- this.lastTokenResponse = await this._fetchBunnyToken(src);
4736
- break;
4737
- case TOKEN_TYPES.AKAMAI:
4738
- this.lastTokenResponse = await this._fetchAkamaiToken(src);
4739
- break;
4740
- case TOKEN_TYPES.VENOM:
4741
- case TOKEN_TYPES.EASY_B:
4742
- this.lastTokenResponse = await this._fetchEasyBToken(src);
4743
- break;
4744
- default:
4745
- this.lastTokenResponse = await this._fetchDefaultToken(src);
4742
+ // Deduplicate concurrent fetches: if a fetch is already in flight,
4743
+ // return the same promise instead of firing a second network request.
4744
+ // This prevents the main player and snapshot handler from both triggering
4745
+ // separate generate calls when their manifest refreshes overlap.
4746
+ if (this.inFlightFetch) {
4747
+ return this.inFlightFetch;
4746
4748
  }
4747
- return this.lastTokenResponse;
4749
+ const doFetch = async () => {
4750
+ switch (this.tokenType) {
4751
+ case TOKEN_TYPES.BUNNY:
4752
+ return this._fetchBunnyToken(src);
4753
+ case TOKEN_TYPES.AKAMAI:
4754
+ return this._fetchAkamaiToken(src);
4755
+ case TOKEN_TYPES.VENOM:
4756
+ case TOKEN_TYPES.EASY_B:
4757
+ return this._fetchEasyBToken(src);
4758
+ default:
4759
+ return this._fetchDefaultToken(src);
4760
+ }
4761
+ };
4762
+ this.inFlightFetch = doFetch().then((response) => {
4763
+ this.lastTokenResponse = response;
4764
+ this.inFlightFetch = null;
4765
+ return response;
4766
+ }).catch((error) => {
4767
+ this.inFlightFetch = null;
4768
+ throw error;
4769
+ });
4770
+ return this.inFlightFetch;
4748
4771
  }
4749
4772
  // -------------------------------------------------------------------------
4750
4773
  // Public: updateUrlWithTokenParams
@@ -4847,6 +4870,7 @@
4847
4870
  }
4848
4871
  this.attempt = 0;
4849
4872
  this.lastTokenResponse = null;
4873
+ this.inFlightFetch = null;
4850
4874
  console.log('CDNTokenManager: destroyed');
4851
4875
  }
4852
4876
  // -------------------------------------------------------------------------
@@ -5382,6 +5406,39 @@
5382
5406
  }, { signal });
5383
5407
  // Start stall watchdog
5384
5408
  this.startWatchdog();
5409
+ // Post-init state reconciliation: during the async init() above, the browser
5410
+ // may have started or blocked playback in ways that leave the FSM out of sync.
5411
+ // Two cases:
5412
+ // A) Video is playing but FSM missed the 'playing' event (Safari event ordering)
5413
+ // B) Autoplay was blocked — Safari fires 'waiting' (→ buffering) but may not
5414
+ // fire 'pause', leaving the spinner visible over a paused video
5415
+ // A short periodic check covers both cases and self-cleans after convergence.
5416
+ const reconcileVideo = video;
5417
+ const reconcileState = state;
5418
+ let reconcileCount = 0;
5419
+ const reconcileTimer = setInterval(() => {
5420
+ reconcileCount++;
5421
+ if (reconcileCount >= 10) {
5422
+ clearInterval(reconcileTimer);
5423
+ return;
5424
+ }
5425
+ const fsm = reconcileState.playbackState;
5426
+ // Case A: video is playing but FSM disagrees
5427
+ // Note: Safari/MSE may report readyState=2 while actively playing, so we
5428
+ // check currentTime advancement instead of readyState >= 3.
5429
+ if (fsm !== 'playing' && !reconcileVideo.paused && reconcileVideo.currentTime > 0) {
5430
+ reconcileState.playbackState = 'playing';
5431
+ clearInterval(reconcileTimer);
5432
+ return;
5433
+ }
5434
+ // Case B: autoplay blocked — video is paused but FSM stuck in buffering/loading
5435
+ if ((fsm === 'buffering' || fsm === 'loading') && reconcileVideo.paused) {
5436
+ reconcileState.playbackState = 'paused';
5437
+ clearInterval(reconcileTimer);
5438
+ }
5439
+ }, 200);
5440
+ // Clean up on engine detach
5441
+ signal.addEventListener('abort', () => clearInterval(reconcileTimer), { once: true });
5385
5442
  }
5386
5443
  // -------------------------------------------------------------------------
5387
5444
  // Driver event mapping
@@ -5857,6 +5914,28 @@
5857
5914
  };
5858
5915
  player.on(dashjs.MediaPlayer.events['STREAM_INITIALIZED'], onInit);
5859
5916
  });
5917
+ // Post-init state reconciliation (same as HLS engine — see comment there)
5918
+ const reconcileVideo = video;
5919
+ const reconcileState = state;
5920
+ let reconcileCount = 0;
5921
+ const reconcileTimer = setInterval(() => {
5922
+ reconcileCount++;
5923
+ if (reconcileCount >= 10) {
5924
+ clearInterval(reconcileTimer);
5925
+ return;
5926
+ }
5927
+ const fsm = reconcileState.playbackState;
5928
+ if (fsm !== 'playing' && !reconcileVideo.paused && reconcileVideo.currentTime > 0) {
5929
+ reconcileState.playbackState = 'playing';
5930
+ clearInterval(reconcileTimer);
5931
+ return;
5932
+ }
5933
+ if ((fsm === 'buffering' || fsm === 'loading') && reconcileVideo.paused) {
5934
+ reconcileState.playbackState = 'paused';
5935
+ clearInterval(reconcileTimer);
5936
+ }
5937
+ }, 200);
5938
+ signal.addEventListener('abort', () => clearInterval(reconcileTimer), { once: true });
5860
5939
  }
5861
5940
  // -------------------------------------------------------------------------
5862
5941
  // Event mapping
@@ -6113,6 +6192,24 @@
6113
6192
  getVideo() {
6114
6193
  return this.offscreenVideo;
6115
6194
  }
6195
+ /**
6196
+ * Stop manifest refreshes (and thus CDN token requests) while the main player is paused.
6197
+ * For live streams, the snapshot hls.js instance continuously refreshes the manifest
6198
+ * on the playlist interval — this prevents unnecessary generate calls during pause.
6199
+ */
6200
+ stopLoad() {
6201
+ if (this.driver && typeof this.driver['stopLoad'] === 'function') {
6202
+ this.driver['stopLoad']();
6203
+ }
6204
+ }
6205
+ /**
6206
+ * Resume manifest refreshes when the main player resumes playback.
6207
+ */
6208
+ startLoad() {
6209
+ if (this.driver && typeof this.driver['startLoad'] === 'function') {
6210
+ this.driver['startLoad'](-1);
6211
+ }
6212
+ }
6116
6213
  /**
6117
6214
  * Destroy the snapshot Hls instance and clean up resources.
6118
6215
  */
@@ -6410,6 +6507,21 @@
6410
6507
  if (snapshotVideo !== null) {
6411
6508
  controller.bus.emit('snapshot-handler-ready', { take: (time) => handler.take(time), video: snapshotVideo });
6412
6509
  }
6510
+ // Stop/start snapshot manifest refreshes on pause/play to prevent
6511
+ // CDN token generate calls while the player is paused (live streams only).
6512
+ // Mirrors the same logic in hls.ts for the main engine.
6513
+ if (video) {
6514
+ video.addEventListener('pause', () => {
6515
+ if (controller.state?.isLive) {
6516
+ handler.stopLoad();
6517
+ }
6518
+ }, { signal: controller.signal });
6519
+ video.addEventListener('play', () => {
6520
+ if (controller.state?.isLive) {
6521
+ setTimeout(() => handler.startLoad(), 0);
6522
+ }
6523
+ }, { signal: controller.signal });
6524
+ }
6413
6525
  })
6414
6526
  .catch((error) => {
6415
6527
  console.warn('EBPlayer: HlsSnapshotHandler init failed:', error);