eb-player 2.1.0 → 2.1.2

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.0";
7
+ var __EB_PLAYER_VERSION__ = "2.1.2";
8
8
 
9
9
  /**
10
10
  * Finite State Machine for player playback state transitions.
@@ -5852,9 +5852,22 @@
5852
5852
  }
5853
5853
  }
5854
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`.
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.
5858
5871
  *
5859
5872
  * `tokenSource` is captured by closure — never stored on `this` and never
5860
5873
  * used to acquire or refresh a token.
@@ -5868,37 +5881,46 @@
5868
5881
  // hls.js needs the error signal each time to drive its own retry/fatal
5869
5882
  // escalation; only the console spam is deduplicated.
5870
5883
  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;
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);
5896
5899
  }
5897
- context.url = signSegmentUrlFromPlaylist(context.url, baseUrl);
5900
+ callbacks.onError({ code: 0, text: message }, context, null, createEmptySegmentLoaderStats());
5901
+ return;
5902
+ }
5903
+ const baseUrl = context.frag?.baseurl;
5904
+ if (!baseUrl || !context.url) {
5898
5905
  originalLoad(context, loadConfig, callbacks);
5899
- };
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;
5900
5917
  }
5918
+ BaseLoader.call(this, loaderConfig);
5919
+ attachSigningLoad(this);
5920
+ return undefined;
5901
5921
  }
5922
+ SegmentTokenLoader.prototype = Object.create(BaseLoader.prototype);
5923
+ SegmentTokenLoader.prototype.constructor = SegmentTokenLoader;
5902
5924
  return SegmentTokenLoader;
5903
5925
  }
5904
5926
 
@@ -5915,6 +5937,17 @@
5915
5937
  * CRITICAL: Call applyDiscontinuityWorkaround BEFORE driver.attachMedia/loadSource (Pitfall 4).
5916
5938
  * CRITICAL: Never call driver.startLoad() from video 'play' event handler (Pitfall 3).
5917
5939
  */
5940
+ /**
5941
+ * Bound for the "load never started" watchdog: hls.js's own manifest-loading
5942
+ * budget (default manifestLoadingTimeOut 10 000 ms with one retry plus
5943
+ * backoff) tops out around 21s before hls.js surfaces its own fatal network
5944
+ * error, so 30s guarantees hls.js's own error path always wins first and
5945
+ * this watchdog only ever sees the case hls.js is silent about — a
5946
+ * StreamController parked in STOPPED with no error and no event. 30s also
5947
+ * matches the existing MAIN_OPEN_FALLBACK_MS precedent in src/eb-player.ts
5948
+ * and sits well below INTRO_MAX_DURATION_MS (90 000).
5949
+ */
5950
+ const LOAD_NEVER_STARTED_TIMEOUT_MS = 30000;
5918
5951
  // ---------------------------------------------------------------------------
5919
5952
  // HlsEngine
5920
5953
  // ---------------------------------------------------------------------------
@@ -5927,6 +5960,9 @@
5927
5960
  this.autoQuality = true;
5928
5961
  // Holds state reference for named driver event handlers
5929
5962
  this.eventState = null;
5963
+ // Bound "load never started" watchdog timer — 0-initialized (not null) so
5964
+ // clearTimeout(0) is a no-op, eliminating a null-guard phantom V8 branch.
5965
+ this.loadNeverStartedTimer = 0;
5930
5966
  this.liveSyncDisabled = false;
5931
5967
  }
5932
5968
  getDriver() {
@@ -5960,6 +5996,7 @@
5960
5996
  }
5961
5997
  onDetach() {
5962
5998
  this.stopWatchdog();
5999
+ this.clearLoadNeverStartedWatchdog();
5963
6000
  if (this.driver !== null) {
5964
6001
  try {
5965
6002
  this.driver.detachMedia();
@@ -6086,8 +6123,17 @@
6086
6123
  delete hlsSafeSettings['extraParamsCallback'];
6087
6124
  delete hlsSafeSettings['onCDNTokenError'];
6088
6125
  delete hlsSafeSettings['signSegmentsWithCdnToken'];
6126
+ // Do NOT set an explicit auto-start disable here. hls.js starts loading
6127
+ // segments on its own by default, and the resume position is already
6128
+ // carried by startPosition below — hls.js honours it without any help.
6129
+ // Nothing on the VOD path would ever restart a stopped loader: the only
6130
+ // two startLoad() call sites in this file are recoverFromStall() (only
6131
+ // reachable after playback has started) and the video 'play' listener
6132
+ // (gated on state.isLive, so VOD never reaches it). Disabling auto-start
6133
+ // here parks the StreamController in STOPPED forever for any non-zero
6134
+ // resume position — no variant playlist, no segments, no error, no bus
6135
+ // event, spinner forever.
6089
6136
  const driverConfig = {
6090
- autoStartLoad: !config.startAt,
6091
6137
  enableWorker: true,
6092
6138
  backBufferLength: 30,
6093
6139
  ...hlsSafeSettings,
@@ -6133,7 +6179,19 @@
6133
6179
  // own parent playlist. Off by default so segment request shapes stay
6134
6180
  // byte-identical to today's when the flag is unset.
6135
6181
  if (config.engineSettings.signSegmentsWithCdnToken === true) {
6136
- driverConfig.fLoader = createSegmentTokenLoaderClass(Hls.DefaultConfig.loader, tm);
6182
+ const SegmentTokenLoader = createSegmentTokenLoaderClass(Hls.DefaultConfig.loader, tm);
6183
+ driverConfig.fLoader = SegmentTokenLoader;
6184
+ // The base `loader` slot must sign too: the EasyBroadcast P2P SDK's
6185
+ // hls adapter replaces `config.fLoader` at integrate() time with a
6186
+ // wrapper class that extends `config.loader` and overrides only
6187
+ // loadInternal(), routing segment fetches through its own
6188
+ // resource manager (V2V + CDN-fallback fetch of context.url
6189
+ // verbatim). Signing lives in load(), which the wrapper inherits,
6190
+ // so every URL entering that path — and hls.js's key loader, which
6191
+ // also instantiates `config.loader` — is signed before any request
6192
+ // is issued. Without this, V2V CDN fallbacks go out unsigned (403
6193
+ // on token-protected origins).
6194
+ driverConfig.loader = SegmentTokenLoader;
6137
6195
  }
6138
6196
  }
6139
6197
  // Inject custom ABR controller unless disabled
@@ -6157,6 +6215,11 @@
6157
6215
  this.driver = driver;
6158
6216
  // Pitfall 4: apply discontinuity workaround BEFORE attachMedia/loadSource
6159
6217
  applyDiscontinuityWorkaround(driver, Hls.Events);
6218
+ // Hoisted once, used both for the retry wiring below and for gating the
6219
+ // "load never started" watchdog — the intro pass already has its own
6220
+ // bounds (INTRO_MAX_DURATION_MS, D-04 silent fall-through) and a second
6221
+ // overlapping bound would only race them.
6222
+ const isIntroPass = Boolean(config.preroll) && this.loadSourceUrl === config.preroll;
6160
6223
  // Wire retry handler — pass engine context so unrecoverable DRM errors
6161
6224
  // stop the stall watchdog (prevents useless reload loops) and surface to UI.
6162
6225
  // For the intro pass (loadSourceUrl matches config.preroll), also wire
@@ -6165,7 +6228,6 @@
6165
6228
  // This callback is intentionally NOT set for the main stream so transient
6166
6229
  // network blips still trigger the normal startLoad() recovery.
6167
6230
  if (config.retry) {
6168
- const isIntroPass = Boolean(config.preroll) && this.loadSourceUrl === config.preroll;
6169
6231
  handleHlsRetry(driver, {
6170
6232
  get chromecast_casting() { return state.isCasting; },
6171
6233
  onUnrecoverableError: (message) => {
@@ -6232,6 +6294,8 @@
6232
6294
  this.resolveDriverReady();
6233
6295
  // Register driver event handlers
6234
6296
  this.registerDriverEvents(Hls, state);
6297
+ // Arm the "load never started" watchdog — see LOAD_NEVER_STARTED_TIMEOUT_MS.
6298
+ this.armLoadNeverStartedWatchdog(driver, Hls, src, isIntroPass);
6235
6299
  // Pause/resume loading on video pause/play to stop manifest refreshes
6236
6300
  // (and thus CDN token requests) while the player is paused.
6237
6301
  // Only applies to live streams where hls.js continuously refreshes the manifest.
@@ -6300,6 +6364,43 @@
6300
6364
  driver.on(Events.SUBTITLE_TRACKS_UPDATED, this._onSubtitleTracksUpdated.bind(this));
6301
6365
  driver.on(Events.SUBTITLE_TRACK_SWITCH, this._onSubtitleTrackSwitch.bind(this));
6302
6366
  }
6367
+ // -------------------------------------------------------------------------
6368
+ // "Load never started" watchdog
6369
+ // -------------------------------------------------------------------------
6370
+ /**
6371
+ * Arms a one-shot bound that turns an unobservable hang into `state.error`
6372
+ * + a typed bus event: hls.js emits neither when its StreamController
6373
+ * never leaves STOPPED. Disarmed by the first FRAG_LOADING (proof that a
6374
+ * fragment request was actually issued, whether or not playback was
6375
+ * allowed to start — so autoplay-blocked playback never false-positives).
6376
+ * Never armed for the intro pass — it already has its own bounds.
6377
+ */
6378
+ armLoadNeverStartedWatchdog(driver, Hls, src, isIntroPass) {
6379
+ if (isIntroPass)
6380
+ return;
6381
+ driver.on(Hls.Events.FRAG_LOADING, this._onFragLoading.bind(this));
6382
+ this.loadNeverStartedTimer = setTimeout(this._onLoadNeverStartedTimeout.bind(this, src), LOAD_NEVER_STARTED_TIMEOUT_MS);
6383
+ }
6384
+ clearLoadNeverStartedWatchdog() {
6385
+ clearTimeout(this.loadNeverStartedTimer);
6386
+ this.loadNeverStartedTimer = 0;
6387
+ }
6388
+ _onFragLoading() {
6389
+ this.clearLoadNeverStartedWatchdog();
6390
+ }
6391
+ _onLoadNeverStartedTimeout(src) {
6392
+ if (this.detached)
6393
+ return;
6394
+ // Two-signal live gate (mirrors _onLevelLoaded): a stream that never
6395
+ // loaded a level detail never got its isLive set from the driver, so
6396
+ // the config-level signal must also be checked.
6397
+ if (this.state?.isLive === true || this.config?.isLive === true)
6398
+ return;
6399
+ if (this.state) {
6400
+ this.state.error = 'HlsEngine: load never started — no fragment request was issued within the bound';
6401
+ }
6402
+ this.bus?.emit('load-never-started', { src });
6403
+ }
6303
6404
  _onManifestLoaded(_event, data) {
6304
6405
  const state = this.eventState;
6305
6406
  const driver = this.driver;
@@ -7404,6 +7505,7 @@
7404
7505
  // skin-root.ts:141/318 and is a no-op for runtime mute.
7405
7506
  const originalMuted = mergedConfig.muted ?? false;
7406
7507
  const originalManager = mergedConfig.manager;
7508
+ const originalStartAt = mergedConfig.startAt;
7407
7509
  const controller = new PlayerController(runtimeConfig);
7408
7510
  controller.mount(container);
7409
7511
  // Video element is available after mount() creates the skin DOM
@@ -7629,6 +7731,10 @@
7629
7731
  // during the intro pass. Synchronize both objects on every swap.
7630
7732
  mergedConfig.manager = originalManager;
7631
7733
  controller.config.manager = originalManager;
7734
+ // Restore the viewer's resume position for the main asset — see the
7735
+ // rationale comment at the intro-side swap below (CR-05 sync applies here too).
7736
+ mergedConfig.startAt = originalStartAt;
7737
+ controller.config.startAt = originalStartAt;
7632
7738
  // CR-03: clear any stale error that leaked from the intro/ad pass.
7633
7739
  // IntroStreamManager's silent fall-through (D-04) treats error-fatal as a
7634
7740
  // normal completion but leaves state.error populated with the intro
@@ -7777,6 +7883,13 @@
7777
7883
  // CR-05: keep the dual-merged configs in sync (see openMainStream).
7778
7884
  mergedConfig.manager = false;
7779
7885
  controller.config.manager = false;
7886
+ // A viewer resume position belongs to the main asset only — applying it
7887
+ // to a short intro clip seeks past the whole clip. This defect survives
7888
+ // the startAt/autoStartLoad fix: after that fix, startPosition: 300 is
7889
+ // still applied to the intro, which is still wrong, just differently
7890
+ // wrong. Cleared here and restored in openMainStream() (CR-05 sync).
7891
+ mergedConfig.startAt = undefined;
7892
+ controller.config.startAt = undefined;
7780
7893
  reference.open(mergedConfig.preroll);
7781
7894
  }
7782
7895
  // D-02/D-08-02 (Phase 8): ad-only configuration — no intro stream, so this