eb-player 2.1.1 → 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.1";
7
+ var __EB_PLAYER_VERSION__ = "2.1.2";
8
8
 
9
9
  /**
10
10
  * Finite State Machine for player playback state transitions.
@@ -5937,6 +5937,17 @@
5937
5937
  * CRITICAL: Call applyDiscontinuityWorkaround BEFORE driver.attachMedia/loadSource (Pitfall 4).
5938
5938
  * CRITICAL: Never call driver.startLoad() from video 'play' event handler (Pitfall 3).
5939
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;
5940
5951
  // ---------------------------------------------------------------------------
5941
5952
  // HlsEngine
5942
5953
  // ---------------------------------------------------------------------------
@@ -5949,6 +5960,9 @@
5949
5960
  this.autoQuality = true;
5950
5961
  // Holds state reference for named driver event handlers
5951
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;
5952
5966
  this.liveSyncDisabled = false;
5953
5967
  }
5954
5968
  getDriver() {
@@ -5982,6 +5996,7 @@
5982
5996
  }
5983
5997
  onDetach() {
5984
5998
  this.stopWatchdog();
5999
+ this.clearLoadNeverStartedWatchdog();
5985
6000
  if (this.driver !== null) {
5986
6001
  try {
5987
6002
  this.driver.detachMedia();
@@ -6108,8 +6123,17 @@
6108
6123
  delete hlsSafeSettings['extraParamsCallback'];
6109
6124
  delete hlsSafeSettings['onCDNTokenError'];
6110
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.
6111
6136
  const driverConfig = {
6112
- autoStartLoad: !config.startAt,
6113
6137
  enableWorker: true,
6114
6138
  backBufferLength: 30,
6115
6139
  ...hlsSafeSettings,
@@ -6191,6 +6215,11 @@
6191
6215
  this.driver = driver;
6192
6216
  // Pitfall 4: apply discontinuity workaround BEFORE attachMedia/loadSource
6193
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;
6194
6223
  // Wire retry handler — pass engine context so unrecoverable DRM errors
6195
6224
  // stop the stall watchdog (prevents useless reload loops) and surface to UI.
6196
6225
  // For the intro pass (loadSourceUrl matches config.preroll), also wire
@@ -6199,7 +6228,6 @@
6199
6228
  // This callback is intentionally NOT set for the main stream so transient
6200
6229
  // network blips still trigger the normal startLoad() recovery.
6201
6230
  if (config.retry) {
6202
- const isIntroPass = Boolean(config.preroll) && this.loadSourceUrl === config.preroll;
6203
6231
  handleHlsRetry(driver, {
6204
6232
  get chromecast_casting() { return state.isCasting; },
6205
6233
  onUnrecoverableError: (message) => {
@@ -6266,6 +6294,8 @@
6266
6294
  this.resolveDriverReady();
6267
6295
  // Register driver event handlers
6268
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);
6269
6299
  // Pause/resume loading on video pause/play to stop manifest refreshes
6270
6300
  // (and thus CDN token requests) while the player is paused.
6271
6301
  // Only applies to live streams where hls.js continuously refreshes the manifest.
@@ -6334,6 +6364,43 @@
6334
6364
  driver.on(Events.SUBTITLE_TRACKS_UPDATED, this._onSubtitleTracksUpdated.bind(this));
6335
6365
  driver.on(Events.SUBTITLE_TRACK_SWITCH, this._onSubtitleTrackSwitch.bind(this));
6336
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
+ }
6337
6404
  _onManifestLoaded(_event, data) {
6338
6405
  const state = this.eventState;
6339
6406
  const driver = this.driver;
@@ -7438,6 +7505,7 @@
7438
7505
  // skin-root.ts:141/318 and is a no-op for runtime mute.
7439
7506
  const originalMuted = mergedConfig.muted ?? false;
7440
7507
  const originalManager = mergedConfig.manager;
7508
+ const originalStartAt = mergedConfig.startAt;
7441
7509
  const controller = new PlayerController(runtimeConfig);
7442
7510
  controller.mount(container);
7443
7511
  // Video element is available after mount() creates the skin DOM
@@ -7663,6 +7731,10 @@
7663
7731
  // during the intro pass. Synchronize both objects on every swap.
7664
7732
  mergedConfig.manager = originalManager;
7665
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;
7666
7738
  // CR-03: clear any stale error that leaked from the intro/ad pass.
7667
7739
  // IntroStreamManager's silent fall-through (D-04) treats error-fatal as a
7668
7740
  // normal completion but leaves state.error populated with the intro
@@ -7811,6 +7883,13 @@
7811
7883
  // CR-05: keep the dual-merged configs in sync (see openMainStream).
7812
7884
  mergedConfig.manager = false;
7813
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;
7814
7893
  reference.open(mergedConfig.preroll);
7815
7894
  }
7816
7895
  // D-02/D-08-02 (Phase 8): ad-only configuration — no intro stream, so this