eb-player 2.0.21 → 2.1.0

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.21";
7
+ var __EB_PLAYER_VERSION__ = "2.1.0";
8
8
 
9
9
  /**
10
10
  * Finite State Machine for player playback state transitions.
@@ -3992,7 +3992,15 @@
3992
3992
  // sequential-await interaction: fixing G-07-9 alone would otherwise
3993
3993
  // install this listener only after the intro, behind the very click
3994
3994
  // that started it).
3995
- const gesturePromise = config.autoplay ? null : this.waitForUserGesture(video, signal);
3995
+ //
3996
+ // WR-01: wired to localController.signal, not the outer signal. The outer
3997
+ // abort already propagates into localController (above), so this still
3998
+ // covers player disposal — but it additionally lets completeAdLeg() settle
3999
+ // a still-pending gesture wait. Without it, an ad leg completed by the
4000
+ // watchdog, an AD_ERROR or ALL_ADS_COMPLETED before any click leaves this
4001
+ // promise unsettled forever: init() never resolves and the capture-phase
4002
+ // click listener stays attached to .eb-player.
4003
+ const gesturePromise = config.autoplay ? null : this.waitForUserGesture(video, localController.signal);
3996
4004
  // G-07-9: the decision is derived from configuration, not from the
3997
4005
  // PlayerState intro-playing flag. An async function body runs
3998
4006
  // synchronously up to its first await, so reading that flag here would
@@ -5747,6 +5755,153 @@
5747
5755
  };
5748
5756
  }
5749
5757
 
5758
+ /**
5759
+ * Segment (fragment) CDN token signing — shared primitive.
5760
+ *
5761
+ * 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.
5789
+ */
5790
+ // ---------------------------------------------------------------------------
5791
+ // Supported token-type family (D-03) — query-param tokens only. Akamai's
5792
+ // path-segment mechanic (`appendAkamaiTokenParams`) is a different shape and
5793
+ // deliberately not extended here.
5794
+ // ---------------------------------------------------------------------------
5795
+ const SEGMENT_SIGNING_TOKEN_TYPES = Object.freeze([
5796
+ TOKEN_TYPES.EASY_B,
5797
+ TOKEN_TYPES.VENOM,
5798
+ TOKEN_TYPES.BUNNY
5799
+ ]);
5800
+ /**
5801
+ * Builds the single diagnostic string used for every unsupported-token-type
5802
+ * case. Names the flag and the offending token type (or says none is
5803
+ * configured) and lists the supported ones. Never includes a token value.
5804
+ */
5805
+ function segmentSigningUnsupportedMessage(tokenType) {
5806
+ const supported = SEGMENT_SIGNING_TOKEN_TYPES.join(', ');
5807
+ const described = tokenType === undefined ? 'no token type configured' : `token type "${tokenType}"`;
5808
+ return `signSegmentsWithCdnToken: segment signing is not supported for ${described}. Supported token types: ${supported}.`;
5809
+ }
5810
+ /**
5811
+ * A fresh, all-zero `LoaderStats`-shaped object for the fail-loud path's
5812
+ * `onError` call. hls.js's own error-handling code reads fields off this
5813
+ * object (e.g. `stats.loaded`); passing `undefined` risks a `TypeError`
5814
+ * inside hls.js itself instead of the clean error this module intends to
5815
+ * produce. Returns a new object per call — never shared/mutated state — so
5816
+ * nothing downstream can observe stale values from a previous failed load.
5817
+ */
5818
+ function createEmptySegmentLoaderStats() {
5819
+ return {
5820
+ aborted: false,
5821
+ loaded: 0,
5822
+ total: 0,
5823
+ retry: 0,
5824
+ chunkCount: 0,
5825
+ bwEstimate: 0,
5826
+ loading: { start: 0, first: 0, end: 0 },
5827
+ parsing: { start: 0, end: 0 },
5828
+ buffering: { start: 0, first: 0, end: 0 }
5829
+ };
5830
+ }
5831
+ /**
5832
+ * Pure transfer: reads `token`, `expires` and `token_path` off the parent
5833
+ * playlist URL's query string and reapplies them to the segment URL via the
5834
+ * same static primitive that signs manifests, so a signed segment URL is
5835
+ * shaped exactly like a signed manifest URL. Returns the segment URL
5836
+ * unchanged when the playlist carries no token, or when either URL cannot be
5837
+ * parsed (a thrown exception inside a loader is a dead player, not a signing
5838
+ * failure).
5839
+ */
5840
+ function signSegmentUrlFromPlaylist(segmentUrl, parentPlaylistUrl) {
5841
+ try {
5842
+ const playlistUrl = new URL(parentPlaylistUrl);
5843
+ const token = playlistUrl.searchParams.get('token');
5844
+ if (!token)
5845
+ return segmentUrl;
5846
+ const expires = playlistUrl.searchParams.get('expires');
5847
+ const tokenPath = playlistUrl.searchParams.get('token_path');
5848
+ return CDNTokenManager.appendTokenParams(segmentUrl, token, expires, tokenPath);
5849
+ }
5850
+ catch {
5851
+ return segmentUrl;
5852
+ }
5853
+ }
5854
+ /**
5855
+ * Returns a loader class extending `BaseLoader`, built exactly like the
5856
+ * existing playlist loader: call `super()`, bind the inherited `load`, then
5857
+ * assign an own `load` property that reads `context.frag?.baseurl`.
5858
+ *
5859
+ * `tokenSource` is captured by closure — never stored on `this` and never
5860
+ * used to acquire or refresh a token.
5861
+ */
5862
+ function createSegmentTokenLoaderClass(BaseLoader, tokenSource) {
5863
+ // WR-02: the "unsupported token type" diagnostic is logged to the console
5864
+ // at most once per loader CLASS — i.e. once per engine session that calls
5865
+ // createSegmentTokenLoaderClass, since hls.js constructs a fresh loader
5866
+ // instance per fragment load and an instance-level flag would reset every
5867
+ // load. `callbacks.onError` still fires on every load below, unguarded —
5868
+ // hls.js needs the error signal each time to drive its own retry/fatal
5869
+ // escalation; only the console spam is deduplicated.
5870
+ let unsupportedTokenTypeLogged = false;
5871
+ class SegmentTokenLoader extends BaseLoader {
5872
+ constructor(loaderConfig) {
5873
+ super(loaderConfig);
5874
+ const originalLoad = this.load.bind(this);
5875
+ this.load = function (context, loadConfig, callbacks) {
5876
+ // D-03/D-04: a token type outside the query-param family (akamai, or
5877
+ // none configured) must fail visibly rather than emit an unsigned
5878
+ // request. A silent passthrough here is the same undiagnosable 403
5879
+ // from the edge that D-04 forbids, so it gets the same treatment as
5880
+ // the akamai case: no delegation to the base loader, and a named
5881
+ // error surfaced through hls.js's own error callback.
5882
+ const tokenType = tokenSource.tokenType;
5883
+ if (!tokenType || !SEGMENT_SIGNING_TOKEN_TYPES.includes(tokenType)) {
5884
+ const message = segmentSigningUnsupportedMessage(tokenType);
5885
+ if (!unsupportedTokenTypeLogged) {
5886
+ unsupportedTokenTypeLogged = true;
5887
+ console.error(message);
5888
+ }
5889
+ callbacks.onError({ code: 0, text: message }, context, null, createEmptySegmentLoaderStats());
5890
+ return;
5891
+ }
5892
+ const baseUrl = context.frag?.baseurl;
5893
+ if (!baseUrl || !context.url) {
5894
+ originalLoad(context, loadConfig, callbacks);
5895
+ return;
5896
+ }
5897
+ context.url = signSegmentUrlFromPlaylist(context.url, baseUrl);
5898
+ originalLoad(context, loadConfig, callbacks);
5899
+ };
5900
+ }
5901
+ }
5902
+ return SegmentTokenLoader;
5903
+ }
5904
+
5750
5905
  /**
5751
5906
  * HlsEngine — HLS playback engine.
5752
5907
  *
@@ -5930,6 +6085,7 @@
5930
6085
  const hlsSafeSettings = { ...hlsEngineSettings };
5931
6086
  delete hlsSafeSettings['extraParamsCallback'];
5932
6087
  delete hlsSafeSettings['onCDNTokenError'];
6088
+ delete hlsSafeSettings['signSegmentsWithCdnToken'];
5933
6089
  const driverConfig = {
5934
6090
  autoStartLoad: !config.startAt,
5935
6091
  enableWorker: true,
@@ -5973,6 +6129,12 @@
5973
6129
  }
5974
6130
  }
5975
6131
  driverConfig.pLoader = PLoader;
6132
+ // Opt-in (D-06): sign segment requests with the grant carried by their
6133
+ // own parent playlist. Off by default so segment request shapes stay
6134
+ // byte-identical to today's when the flag is unset.
6135
+ if (config.engineSettings.signSegmentsWithCdnToken === true) {
6136
+ driverConfig.fLoader = createSegmentTokenLoaderClass(Hls.DefaultConfig.loader, tm);
6137
+ }
5976
6138
  }
5977
6139
  // Inject custom ABR controller unless disabled
5978
6140
  if (!config.disableCustomAbr) {
@@ -6426,7 +6588,12 @@
6426
6588
  * Ported from component/engines/dash.js
6427
6589
  *
6428
6590
  * Extends BaseEngine to bridge dash.js MediaPlayer events to PlayerState.
6429
- * Uses CDN loader, CDNTokenManager, StallWatchdog, and DASH retry settings.
6591
+ * Uses the CDN loader and StallWatchdog, plus DASH retry settings and ABR rules.
6592
+ *
6593
+ * CDN token signing (manifests and segments alike) is HLS-only by design —
6594
+ * this engine has no `CDNTokenManager` integration at all (Phase 08.1 / D-05).
6595
+ * DASH segments are `.m4s`, so nothing written for HLS's fragment-loader
6596
+ * token signing should be assumed to generalise here.
6430
6597
  *
6431
6598
  * CRITICAL: driver instance is a private class field — NEVER stored in PlayerState.
6432
6599
  */
@@ -6867,18 +7034,30 @@
6867
7034
  }
6868
7035
  PLoader = SnapshotPLoader;
6869
7036
  }
7037
+ // Build fLoader class with segment CDN token signing — opt-in (D-06), off by
7038
+ // default so segment request shapes stay byte-identical to today's when the
7039
+ // flag is unset. Reuses the shared factory built for the main engine's
7040
+ // fragment loader (`segment-token-loader.ts`) rather than re-implementing the
7041
+ // signing logic here. tokenManager is the same closure-captured reference
7042
+ // used above for pLoader (Pitfall 6) — never read off `this`.
7043
+ let FLoader;
7044
+ if (tokenManager && this.config.engineSettings?.signSegmentsWithCdnToken === true) {
7045
+ FLoader = createSegmentTokenLoaderClass(HlsConstructor.DefaultConfig.loader, tokenManager);
7046
+ }
6870
7047
  // Strip player-specific keys that are NOT hls.js config options — they contain
6871
7048
  // non-serializable functions that cause DataCloneError when hls.js posts config to its worker.
6872
7049
  const rawSettings = { ...(this.config.engineSettings ?? {}) };
6873
7050
  delete rawSettings['extraParamsCallback'];
6874
7051
  delete rawSettings['onCDNTokenError'];
7052
+ delete rawSettings['signSegmentsWithCdnToken'];
6875
7053
  const driverConfig = {
6876
7054
  startLevel: 0,
6877
7055
  enableWebVTT: false,
6878
7056
  enableWorker: false,
6879
7057
  maxBufferLength: 1,
6880
7058
  ...rawSettings,
6881
- ...(PLoader ? { pLoader: PLoader } : {})
7059
+ ...(PLoader ? { pLoader: PLoader } : {}),
7060
+ ...(FLoader ? { fLoader: FLoader } : {})
6882
7061
  };
6883
7062
  const driver = new HlsConstructor(driverConfig);
6884
7063
  this.driver = driver;