eb-player 2.1.2 → 2.1.3

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.1.2";
7
+ var __EB_PLAYER_VERSION__ = "2.1.3";
8
8
 
9
9
  /**
10
10
  * Finite State Machine for player playback state transitions.
@@ -5119,6 +5119,7 @@
5119
5119
  this.retryInterval = retryInterval;
5120
5120
  this.expirationMarginInSeconds = expirationMarginInSeconds;
5121
5121
  this.lastTokenResponse = null;
5122
+ this.sourceUrl = null;
5122
5123
  this.resetAttemptCounterTimeout = null;
5123
5124
  this.inFlightFetch = null;
5124
5125
  }
@@ -5437,8 +5438,17 @@
5437
5438
  }
5438
5439
  break;
5439
5440
  }
5441
+ // Query-param tokens (easy_b/venom/bunny) carry a prefix-scoped `token_path`
5442
+ // and share one grant across manifests and segments, so the grant must be
5443
+ // minted against the asset-level source URL rather than whichever URL
5444
+ // triggered this refresh. Akamai's path-segment mechanic is scoped
5445
+ // differently and is deliberately left on the requested URL.
5446
+ const sharesPrefixScopedGrant = this.tokenType === TOKEN_TYPES.EASY_B
5447
+ || this.tokenType === TOKEN_TYPES.VENOM
5448
+ || this.tokenType === TOKEN_TYPES.BUNNY;
5449
+ const fetchSrc = (sharesPrefixScopedGrant && this.sourceUrl) ? this.sourceUrl : srcUrl.toString();
5440
5450
  try {
5441
- const tokenResponse = await this.fetchToken({ src: srcUrl.toString() });
5451
+ const tokenResponse = await this.fetchToken({ src: fetchSrc });
5442
5452
  if (tokenResponse === null) {
5443
5453
  return url;
5444
5454
  }
@@ -5759,33 +5769,39 @@
5759
5769
  * Segment (fragment) CDN token signing — shared primitive.
5760
5770
  *
5761
5771
  * Extends CDN token signing from manifests to segments, behind an opt-in flag
5762
- * (`EngineSettings.signSegmentsWithCdnToken`). Each segment is signed with the
5763
- * grant carried by its OWN parent playlist — read from `context.frag.baseurl`,
5764
- * the object hls.js itself parsed that fragment from — never from any store,
5765
- * cache, or shared manager state. This makes cross-rendition mis-signing
5766
- * structurally impossible rather than merely unlikely.
5767
- *
5768
- * CRITICAL: this module must never acquire or refresh a token. It only reads
5769
- * data already present on the fragment context handed to it by hls.js. Any
5770
- * acquisition call here would reintroduce the single-slot cache reuse in
5771
- * `CDNTokenManager.lastTokenResponse`, which hands one rendition's grant to
5772
- * another — the exact failure this design forbids.
5773
- *
5774
- * Token expiry model (CDN-04, D-07): a segment carries whatever grant its
5775
- * parent playlist carried when hls.js parsed it. Live playlists reload on
5776
- * their own cadence, so their fragments pick up refreshed grants with no
5777
- * refresh machinery here — reload IS the refresh path. VOD playlists are
5778
- * parsed exactly once by hls.js 1.6.10 (its level controller only
5779
- * (re)requests a playlist it has none of, or one that is live), so a VOD
5780
- * asset's segment grants are frozen at first parse for the asset's entire
5781
- * duration. If a token's lifetime is shorter than the asset's duration, later
5782
- * segments will be rejected by the edge and surface through the existing
5783
- * error/retry path. This is an accepted operational constraint — the token
5784
- * service's lifetime must exceed the longest VOD asset — not an oversight;
5785
- * D-07 chose reload-cadence refresh and CONTEXT.md defers a proactive
5786
- * near-expiry refresh path with a named revisit condition (a real asset shown
5787
- * to outlive its grant). No timer, interval, or scheduled re-fetch belongs in
5788
- * this module.
5772
+ * (`EngineSettings.signSegmentsWithCdnToken`). The fast path signs each segment
5773
+ * with the grant carried by its OWN parent playlist — read from
5774
+ * `context.frag.baseurl`, the object hls.js itself parsed that fragment from.
5775
+ *
5776
+ * Token expiry model (CDN-04, D-07, revised):
5777
+ *
5778
+ * `frag.baseurl` is a string frozen at the moment hls.js parsed the level
5779
+ * playlist. Live playlists reload on their own cadence, so their fragments pick
5780
+ * up refreshed grants for free — reload IS the refresh path. VOD playlists are
5781
+ * parsed exactly once by hls.js 1.6.10 (its level controller only (re)requests a
5782
+ * playlist it has none of, or one that is live), so for VOD that string never
5783
+ * changes. Pure transfer alone therefore pins a VOD asset's segments to the
5784
+ * first grant for the asset's entire duration, and every segment 403s once that
5785
+ * grant lapses — the condition CONTEXT.md named as D-07's revisit trigger, since
5786
+ * observed in production (a ~294s grant against a ~26min asset).
5787
+ *
5788
+ * So when the inherited grant is inside the manager's expiry margin, this module
5789
+ * now defers to the shared grant via `updateUrlWithTokenParams` rather than
5790
+ * copying a dead string.
5791
+ *
5792
+ * Why sharing a grant across renditions is safe (supersedes this module's
5793
+ * previous prohibition): the token service derives `token_path` from the
5794
+ * directory of the URL it signs, and `token_path` is a PREFIX scope. An
5795
+ * asset-level grant returns 200 on every rendition beneath it — verified live
5796
+ * across all four renditions of a real asset. The hazard this module previously
5797
+ * guarded against — one rendition's grant handed to another — is real ONLY for a
5798
+ * rendition-scoped grant, and that is prevented at its source by
5799
+ * `CDNTokenManager.sourceUrl`, which pins acquisition of the shared grant to the
5800
+ * asset-level source URL. The invariant is therefore "the shared grant must be
5801
+ * asset-level", not "segments must never refresh".
5802
+ *
5803
+ * Still true: no timer, interval, or scheduled re-fetch belongs in this module.
5804
+ * Refresh here is demand-driven, on the load that needs it.
5789
5805
  */
5790
5806
  // ---------------------------------------------------------------------------
5791
5807
  // Supported token-type family (D-03) — query-param tokens only. Akamai's
@@ -5851,6 +5867,23 @@
5851
5867
  return segmentUrl;
5852
5868
  }
5853
5869
  }
5870
+ /**
5871
+ * Reads the `expires` value off a parent playlist URL, or null when the URL
5872
+ * carries no token at all or cannot be parsed. Used to decide between the pure
5873
+ * transfer fast path and a shared-grant refresh.
5874
+ */
5875
+ function readPlaylistGrantExpiry(parentPlaylistUrl) {
5876
+ try {
5877
+ const playlistUrl = new URL(parentPlaylistUrl);
5878
+ const token = playlistUrl.searchParams.get('token');
5879
+ if (!token)
5880
+ return { hasToken: false, expires: null };
5881
+ return { hasToken: true, expires: playlistUrl.searchParams.get('expires') };
5882
+ }
5883
+ catch {
5884
+ return { hasToken: false, expires: null };
5885
+ }
5886
+ }
5854
5887
  /**
5855
5888
  * Returns a loader constructor extending `BaseLoader`: construct the base,
5856
5889
  * bind the inherited `load`, then assign an own `load` property that reads
@@ -5905,8 +5938,36 @@
5905
5938
  originalLoad(context, loadConfig, callbacks);
5906
5939
  return;
5907
5940
  }
5908
- context.url = signSegmentUrlFromPlaylist(context.url, baseUrl);
5909
- originalLoad(context, loadConfig, callbacks);
5941
+ const { hasToken, expires } = readPlaylistGrantExpiry(baseUrl);
5942
+ // Parent playlist carried no grant: nothing to transfer, and nothing to
5943
+ // refresh — an unsigned playlist means this path is not token-protected.
5944
+ if (!hasToken) {
5945
+ originalLoad(context, loadConfig, callbacks);
5946
+ return;
5947
+ }
5948
+ // Fast path: the inherited grant is still good. Pure synchronous transfer,
5949
+ // no manager involvement, no network.
5950
+ if (!tokenSource.isTokenExpired(expires)) {
5951
+ context.url = signSegmentUrlFromPlaylist(context.url, baseUrl);
5952
+ originalLoad(context, loadConfig, callbacks);
5953
+ return;
5954
+ }
5955
+ // Inherited grant is expired or inside the expiry margin. For VOD this is
5956
+ // terminal — hls.js will never re-parse the playlist, so the frozen
5957
+ // baseurl can never recover on its own. Defer to the shared asset-level
5958
+ // grant, which `CDNTokenManager.sourceUrl` keeps broad enough to cover
5959
+ // this segment.
5960
+ tokenSource.updateUrlWithTokenParams({ url: context.url })
5961
+ .then((updatedUrl) => {
5962
+ context.url = updatedUrl;
5963
+ originalLoad(context, loadConfig, callbacks);
5964
+ })
5965
+ .catch(() => {
5966
+ // Token error: fall back to the inherited grant rather than emitting an
5967
+ // unsigned request, and let the edge's 403 drive hls.js's own retry.
5968
+ context.url = signSegmentUrlFromPlaylist(context.url, baseUrl);
5969
+ originalLoad(context, loadConfig, callbacks);
5970
+ });
5910
5971
  };
5911
5972
  }
5912
5973
  function SegmentTokenLoader(loaderConfig) {
@@ -6279,6 +6340,11 @@
6279
6340
  // stream URL is loaded rather than config.src which always points at the main stream.
6280
6341
  let src = this.loadSourceUrl || config.src || '';
6281
6342
  if (this.tokenManager && src) {
6343
+ // Pin shared-grant acquisition to the asset-level source URL before the
6344
+ // first fetch. The token service scopes `token_path` to the directory of
6345
+ // the URL it signs, so a grant minted against a level playlist (as an ABR
6346
+ // switch would trigger) is rendition-scoped and 403s on every sibling.
6347
+ this.tokenManager.sourceUrl = src;
6282
6348
  src = await this.tokenManager.updateUrlWithTokenParams({ url: src });
6283
6349
  // Guard: abort if detached during token URL update
6284
6350
  if (this.detached) {