eb-player 2.0.21 → 2.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.
@@ -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.1";
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,175 @@
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 constructor extending `BaseLoader`: construct the base,
5856
+ * bind the inherited `load`, then assign an own `load` property that reads
5857
+ * `context.frag?.baseurl`.
5858
+ *
5859
+ * NOT a native `class`: this constructor is registered in the base
5860
+ * `config.loader` slot, which the EasyBroadcast P2P SDK's hls adapter
5861
+ * subclasses with transpiled ES5 inheritance — its wrapper constructor
5862
+ * invokes the parent as `Parent.apply(this, arguments)`, which throws
5863
+ * "Class constructor ... cannot be invoked without 'new'" on a native
5864
+ * class (hls.js's own dist loaders are function-based for the same
5865
+ * reason). The function below therefore supports both invocation modes:
5866
+ * under `new` (hls.js pLoader/fLoader/loader slots) it defers to
5867
+ * `Reflect.construct` so a native-class base still works; under an
5868
+ * `.apply()`-style super call it initializes the wrapper's `this` in
5869
+ * place, which requires the base to be function-based — true of every
5870
+ * hls.js dist build this player loads.
5871
+ *
5872
+ * `tokenSource` is captured by closure — never stored on `this` and never
5873
+ * used to acquire or refresh a token.
5874
+ */
5875
+ function createSegmentTokenLoaderClass(BaseLoader, tokenSource) {
5876
+ // WR-02: the "unsupported token type" diagnostic is logged to the console
5877
+ // at most once per loader CLASS — i.e. once per engine session that calls
5878
+ // createSegmentTokenLoaderClass, since hls.js constructs a fresh loader
5879
+ // instance per fragment load and an instance-level flag would reset every
5880
+ // load. `callbacks.onError` still fires on every load below, unguarded —
5881
+ // hls.js needs the error signal each time to drive its own retry/fatal
5882
+ // escalation; only the console spam is deduplicated.
5883
+ let unsupportedTokenTypeLogged = false;
5884
+ function attachSigningLoad(instance) {
5885
+ const originalLoad = instance.load.bind(instance);
5886
+ instance.load = function (context, loadConfig, callbacks) {
5887
+ // D-03/D-04: a token type outside the query-param family (akamai, or
5888
+ // none configured) must fail visibly rather than emit an unsigned
5889
+ // request. A silent passthrough here is the same undiagnosable 403
5890
+ // from the edge that D-04 forbids, so it gets the same treatment as
5891
+ // the akamai case: no delegation to the base loader, and a named
5892
+ // error surfaced through hls.js's own error callback.
5893
+ const tokenType = tokenSource.tokenType;
5894
+ if (!tokenType || !SEGMENT_SIGNING_TOKEN_TYPES.includes(tokenType)) {
5895
+ const message = segmentSigningUnsupportedMessage(tokenType);
5896
+ if (!unsupportedTokenTypeLogged) {
5897
+ unsupportedTokenTypeLogged = true;
5898
+ console.error(message);
5899
+ }
5900
+ callbacks.onError({ code: 0, text: message }, context, null, createEmptySegmentLoaderStats());
5901
+ return;
5902
+ }
5903
+ const baseUrl = context.frag?.baseurl;
5904
+ if (!baseUrl || !context.url) {
5905
+ originalLoad(context, loadConfig, callbacks);
5906
+ return;
5907
+ }
5908
+ context.url = signSegmentUrlFromPlaylist(context.url, baseUrl);
5909
+ originalLoad(context, loadConfig, callbacks);
5910
+ };
5911
+ }
5912
+ function SegmentTokenLoader(loaderConfig) {
5913
+ if (new.target) {
5914
+ const instance = Reflect.construct(BaseLoader, [loaderConfig], new.target);
5915
+ attachSigningLoad(instance);
5916
+ return instance;
5917
+ }
5918
+ BaseLoader.call(this, loaderConfig);
5919
+ attachSigningLoad(this);
5920
+ return undefined;
5921
+ }
5922
+ SegmentTokenLoader.prototype = Object.create(BaseLoader.prototype);
5923
+ SegmentTokenLoader.prototype.constructor = SegmentTokenLoader;
5924
+ return SegmentTokenLoader;
5925
+ }
5926
+
5750
5927
  /**
5751
5928
  * HlsEngine — HLS playback engine.
5752
5929
  *
@@ -5930,6 +6107,7 @@
5930
6107
  const hlsSafeSettings = { ...hlsEngineSettings };
5931
6108
  delete hlsSafeSettings['extraParamsCallback'];
5932
6109
  delete hlsSafeSettings['onCDNTokenError'];
6110
+ delete hlsSafeSettings['signSegmentsWithCdnToken'];
5933
6111
  const driverConfig = {
5934
6112
  autoStartLoad: !config.startAt,
5935
6113
  enableWorker: true,
@@ -5973,6 +6151,24 @@
5973
6151
  }
5974
6152
  }
5975
6153
  driverConfig.pLoader = PLoader;
6154
+ // Opt-in (D-06): sign segment requests with the grant carried by their
6155
+ // own parent playlist. Off by default so segment request shapes stay
6156
+ // byte-identical to today's when the flag is unset.
6157
+ if (config.engineSettings.signSegmentsWithCdnToken === true) {
6158
+ const SegmentTokenLoader = createSegmentTokenLoaderClass(Hls.DefaultConfig.loader, tm);
6159
+ driverConfig.fLoader = SegmentTokenLoader;
6160
+ // The base `loader` slot must sign too: the EasyBroadcast P2P SDK's
6161
+ // hls adapter replaces `config.fLoader` at integrate() time with a
6162
+ // wrapper class that extends `config.loader` and overrides only
6163
+ // loadInternal(), routing segment fetches through its own
6164
+ // resource manager (V2V + CDN-fallback fetch of context.url
6165
+ // verbatim). Signing lives in load(), which the wrapper inherits,
6166
+ // so every URL entering that path — and hls.js's key loader, which
6167
+ // also instantiates `config.loader` — is signed before any request
6168
+ // is issued. Without this, V2V CDN fallbacks go out unsigned (403
6169
+ // on token-protected origins).
6170
+ driverConfig.loader = SegmentTokenLoader;
6171
+ }
5976
6172
  }
5977
6173
  // Inject custom ABR controller unless disabled
5978
6174
  if (!config.disableCustomAbr) {
@@ -6426,7 +6622,12 @@
6426
6622
  * Ported from component/engines/dash.js
6427
6623
  *
6428
6624
  * Extends BaseEngine to bridge dash.js MediaPlayer events to PlayerState.
6429
- * Uses CDN loader, CDNTokenManager, StallWatchdog, and DASH retry settings.
6625
+ * Uses the CDN loader and StallWatchdog, plus DASH retry settings and ABR rules.
6626
+ *
6627
+ * CDN token signing (manifests and segments alike) is HLS-only by design —
6628
+ * this engine has no `CDNTokenManager` integration at all (Phase 08.1 / D-05).
6629
+ * DASH segments are `.m4s`, so nothing written for HLS's fragment-loader
6630
+ * token signing should be assumed to generalise here.
6430
6631
  *
6431
6632
  * CRITICAL: driver instance is a private class field — NEVER stored in PlayerState.
6432
6633
  */
@@ -6867,18 +7068,30 @@
6867
7068
  }
6868
7069
  PLoader = SnapshotPLoader;
6869
7070
  }
7071
+ // Build fLoader class with segment CDN token signing — opt-in (D-06), off by
7072
+ // default so segment request shapes stay byte-identical to today's when the
7073
+ // flag is unset. Reuses the shared factory built for the main engine's
7074
+ // fragment loader (`segment-token-loader.ts`) rather than re-implementing the
7075
+ // signing logic here. tokenManager is the same closure-captured reference
7076
+ // used above for pLoader (Pitfall 6) — never read off `this`.
7077
+ let FLoader;
7078
+ if (tokenManager && this.config.engineSettings?.signSegmentsWithCdnToken === true) {
7079
+ FLoader = createSegmentTokenLoaderClass(HlsConstructor.DefaultConfig.loader, tokenManager);
7080
+ }
6870
7081
  // Strip player-specific keys that are NOT hls.js config options — they contain
6871
7082
  // non-serializable functions that cause DataCloneError when hls.js posts config to its worker.
6872
7083
  const rawSettings = { ...(this.config.engineSettings ?? {}) };
6873
7084
  delete rawSettings['extraParamsCallback'];
6874
7085
  delete rawSettings['onCDNTokenError'];
7086
+ delete rawSettings['signSegmentsWithCdnToken'];
6875
7087
  const driverConfig = {
6876
7088
  startLevel: 0,
6877
7089
  enableWebVTT: false,
6878
7090
  enableWorker: false,
6879
7091
  maxBufferLength: 1,
6880
7092
  ...rawSettings,
6881
- ...(PLoader ? { pLoader: PLoader } : {})
7093
+ ...(PLoader ? { pLoader: PLoader } : {}),
7094
+ ...(FLoader ? { fLoader: FLoader } : {})
6882
7095
  };
6883
7096
  const driver = new HlsConstructor(driverConfig);
6884
7097
  this.driver = driver;