eb-player 2.1.3 → 2.1.4

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.3";
7
+ var __EB_PLAYER_VERSION__ = "2.1.4";
8
8
 
9
9
  /**
10
10
  * Finite State Machine for player playback state transitions.
@@ -4397,8 +4397,9 @@
4397
4397
  * Mount order:
4398
4398
  * 1. CSS vars / data-theme applied to container
4399
4399
  * 2. Config-derived state (isRtl, isRadio) set
4400
- * 3. SkinRoot connected (needs DOM) — creates video element and ads container
4401
- * 4. AutoHideController + KeyboardController wired
4400
+ * 3. SkinRoot connected (needs DOM) — creates the video element on every
4401
+ * mount, including noUi; the ads container is only rendered in UI mode
4402
+ * 4. AutoHideController + KeyboardController wired (UI mode only)
4402
4403
  * 5. Integrations initialized (need skin DOM for ads)
4403
4404
  *
4404
4405
  * Dispose order (reverse of mount):
@@ -4483,21 +4484,32 @@
4483
4484
  // Set config-derived state flags
4484
4485
  this.state.isRtl = this.config.lang === 'ar';
4485
4486
  this.state.isRadio = this.config.radio === true;
4486
- // Step 3: Create SkinRoot (unless noUi mode)
4487
+ // Step 3: Create SkinRoot. This runs on EVERY mount, including noUi —
4488
+ // SkinRoot is the only creator of the <video> element, and its own
4489
+ // template() already branches to a headless template that renders just
4490
+ // the bare element (see renderNoUiTemplate() in skin-root.ts). This block
4491
+ // used to be gated on `!this.config.noUi`, which left that headless
4492
+ // branch unreachable from the real player: a noUi mount produced an
4493
+ // empty container, no <video> was ever handed to the engine, and
4494
+ // therefore start() issued no CDN token, no manifest and no segment
4495
+ // request at all.
4496
+ const skinRoot = new SkinRoot();
4497
+ this.skinRoot = skinRoot;
4498
+ skinRoot.connect(container, this.state, this.bus, this.config, this.i18n);
4499
+ // Sync initial muted/volume from the actual video element so UI matches
4500
+ // reality. The volumechange event only fires on *changes*, not on initial
4501
+ // attribute values set during element creation. The browser is the source
4502
+ // of truth — it may override the muted attribute based on autoplay policy.
4503
+ const videoEl = skinRoot.getVideoElement();
4504
+ if (videoEl !== null) {
4505
+ this.state.muted = videoEl.muted;
4506
+ this.state.volume = videoEl.volume;
4507
+ }
4508
+ // Step 4: Wire activity and keyboard controllers — chrome-only, so they
4509
+ // stay gated on noUi (D-03). AutoHide drives visibility for controls that
4510
+ // don't exist headlessly; Keyboard would claim page-level key handling
4511
+ // that a headless integrator expects to own itself.
4487
4512
  if (!this.config.noUi) {
4488
- const skinRoot = new SkinRoot();
4489
- this.skinRoot = skinRoot;
4490
- skinRoot.connect(container, this.state, this.bus, this.config, this.i18n);
4491
- // Sync initial muted/volume from the actual video element so UI matches
4492
- // reality. The volumechange event only fires on *changes*, not on initial
4493
- // attribute values set during element creation. The browser is the source
4494
- // of truth — it may override the muted attribute based on autoplay policy.
4495
- const videoEl = skinRoot.getVideoElement();
4496
- if (videoEl !== null) {
4497
- this.state.muted = videoEl.muted;
4498
- this.state.volume = videoEl.volume;
4499
- }
4500
- // Step 4: Wire activity and keyboard controllers
4501
4513
  new AutoHideController(container, this.state, this.signal);
4502
4514
  new KeyboardController(container, this.state, this.bus, this.config, this.signal);
4503
4515
  }
@@ -4592,12 +4604,16 @@
4592
4604
  const playlistManager = new PlaylistManager();
4593
4605
  playlistManager.init(config, state, bus, signal);
4594
4606
  }
4595
- // AdsManager: skip when config.ad is undefined (D-09)
4607
+ // AdsManager: skip when config.ad is undefined (D-09), or when no ads
4608
+ // container exists — the headless template (D-04) renders none, so a
4609
+ // noUi mount must not construct AdsManager at all, not merely skip
4610
+ // init(). Constructing it unconditionally on skinRoot !== null would
4611
+ // regress now that SkinRoot is created in noUi mode too (D-01).
4596
4612
  if (config.ad && this.skinRoot !== null) {
4597
- const adsManager = new AdsManager();
4598
4613
  const video = this.skinRoot.getVideoElement();
4599
4614
  const adsContainer = this.skinRoot.getAdsContainer();
4600
4615
  if (video !== null && adsContainer !== null) {
4616
+ const adsManager = new AdsManager();
4601
4617
  adsManager.init(config, state, bus, video, adsContainer, signal).catch((error) => {
4602
4618
  console.error('EBPlayer: AdsManager init failed:', error);
4603
4619
  });
@@ -7531,10 +7547,11 @@
7531
7547
  * (20s) so that more precise, better-logged inner mechanism wins first in a
7532
7548
  * live player — this is the outer safety net, not the primary signal. Covers
7533
7549
  * two cases the inner mechanism cannot: an AdsManager that was never
7534
- * constructed at all (noUi:true, or a null ads container — lifecycle.ts
7535
- * skips the branch that would create one), and an AdsManager that obtained
7536
- * its manager from the loader (disarming its own bound) whose creative then
7537
- * never reached the screen.
7550
+ * constructed at all (a headless player renders no ads container, so the
7551
+ * ads branch in lifecycle.ts is still skipped for that reason (D-04) — this
7552
+ * backstop applies unchanged to noUi + ad configurations), and an AdsManager
7553
+ * that obtained its manager from the loader (disarming its own bound) whose
7554
+ * creative then never reached the screen.
7538
7555
  */
7539
7556
  const MAIN_OPEN_FALLBACK_MS = 30000;
7540
7557
  // ---------------------------------------------------------------------------
@@ -7569,7 +7586,13 @@
7569
7586
  // so the config-level swap is legitimate for that field only). NEVER mutate
7570
7587
  // `mergedConfig.muted` — it is consumed only at video element creation in
7571
7588
  // skin-root.ts:141/318 and is a no-op for runtime mute.
7572
- const originalMuted = mergedConfig.muted ?? false;
7589
+ //
7590
+ // `mergedConfig.muted` is typed `boolean` (required, non-optional) on
7591
+ // PlayerConfig, and DEFAULT_CONFIG.muted is the boolean literal `true` —
7592
+ // mergeConfig()'s deepMerge always falls back to the base value for an
7593
+ // undefined override (see config.ts), so this field can never be nullish
7594
+ // through the real merge. No `?? false` fallback needed.
7595
+ const originalMuted = mergedConfig.muted;
7573
7596
  const originalManager = mergedConfig.manager;
7574
7597
  const originalStartAt = mergedConfig.startAt;
7575
7598
  const controller = new PlayerController(runtimeConfig);
@@ -7748,6 +7771,21 @@
7748
7771
  controller.bus.on('error-fatal', () => {
7749
7772
  inst.p2p?.stop();
7750
7773
  }, { signal: controller.signal });
7774
+ // Fail loud when mount() produced no video element (D-06). Without one,
7775
+ // engine.setVideo() is skipped inside open() (see the `video !== null`
7776
+ // guard above), so no engine ever attaches media and nothing is requested
7777
+ // at all — no CDN token, no manifest, no segments. No watchdog covers
7778
+ // this: the load-never-started watchdog lives inside the engine and is
7779
+ // only armed once a driver exists. state.error (not a bus emit) is the
7780
+ // channel here because start() has not returned the reference yet, so no
7781
+ // consumer can have subscribed to the bus at this point, whereas the
7782
+ // reactive state store is readable at any time afterwards.
7783
+ if (video === null) {
7784
+ const message = 'EBPlayer: no <video> element found in the container after mount() — playback cannot start.';
7785
+ console.error(message);
7786
+ controller.state.error = message;
7787
+ return reference;
7788
+ }
7751
7789
  // Phase 7/8 — pre-main leg orchestration.
7752
7790
  // When mergedConfig.preroll is set, open the intro stream first, then on
7753
7791
  // bus 'intro-stream-complete' (emitted by IntroStreamManager on video.ended,
@@ -7784,9 +7822,11 @@
7784
7822
  // same tick. complete() is idempotent, so on the normal ordering (intro
7785
7823
  // already finished) this is a no-op; the listener is already torn down.
7786
7824
  controller.bus.emit('force-intro-complete');
7787
- if (video !== null) {
7788
- video.muted = originalMuted;
7789
- }
7825
+ // D-06: `video` is guaranteed non-null here — the guard above (line ~415)
7826
+ // already returned early when mount() produced no <video>, and `video`
7827
+ // is a `const`, so TypeScript's control-flow narrowing carries the
7828
+ // non-null guarantee into this closure without a redundant re-check.
7829
+ video.muted = originalMuted;
7790
7830
  controller.state.muted = originalMuted;
7791
7831
  // CR-05: keep mergedConfig.manager and controller.config.manager in sync.
7792
7832
  // start() builds mergedConfig and PlayerController builds its own merged
@@ -7871,7 +7911,9 @@
7871
7911
  return;
7872
7912
  deferredPlaybackStarted = true;
7873
7913
  const engine = inst.engine;
7874
- if (engine === null || video === null)
7914
+ // D-06: `video === null` is unreachable here — see the comment on the
7915
+ // equivalent guard in openMainStream() above.
7916
+ if (engine === null)
7875
7917
  return;
7876
7918
  // Honor the user's click intent. CommandHandler's earlier bus.on('play')
7877
7919
  // listener fired before us with no source attached, so its video.play()
@@ -7941,9 +7983,9 @@
7941
7983
  }, { signal: controller.signal });
7942
7984
  // Force runtime mute off for the intro pass — applied to the LIVE <video>
7943
7985
  // element and PlayerState, NOT to mergedConfig.muted (no-op for runtime).
7944
- if (video !== null) {
7945
- video.muted = false;
7946
- }
7986
+ // D-06: `video` is guaranteed non-null here — see the comment on the
7987
+ // equivalent restore in openMainStream() above.
7988
+ video.muted = false;
7947
7989
  controller.state.muted = false;
7948
7990
  // Skip P2P attach for the intro stream (D-08); restored before main open.
7949
7991
  // CR-05: keep the dual-merged configs in sync (see openMainStream).