eb-player 2.1.2 → 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.
- package/dist/build/ebplayer.bundle.js +169 -61
- package/dist/build/ebplayer.bundle.js.map +1 -1
- package/dist/build/types/src/core/lifecycle.d.ts +3 -2
- package/dist/build/types/src/core/lifecycle.d.ts.map +1 -1
- package/dist/build/types/src/eb-player.d.ts.map +1 -1
- package/dist/build/types/src/engines/cdn-token-manager.d.ts +18 -0
- package/dist/build/types/src/engines/cdn-token-manager.d.ts.map +1 -1
- package/dist/build/types/src/engines/hls.d.ts.map +1 -1
- package/dist/build/types/src/engines/segment-token-loader.d.ts +46 -25
- package/dist/build/types/src/engines/segment-token-loader.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
|
4401
|
-
*
|
|
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
|
|
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
|
});
|
|
@@ -5119,6 +5135,7 @@
|
|
|
5119
5135
|
this.retryInterval = retryInterval;
|
|
5120
5136
|
this.expirationMarginInSeconds = expirationMarginInSeconds;
|
|
5121
5137
|
this.lastTokenResponse = null;
|
|
5138
|
+
this.sourceUrl = null;
|
|
5122
5139
|
this.resetAttemptCounterTimeout = null;
|
|
5123
5140
|
this.inFlightFetch = null;
|
|
5124
5141
|
}
|
|
@@ -5437,8 +5454,17 @@
|
|
|
5437
5454
|
}
|
|
5438
5455
|
break;
|
|
5439
5456
|
}
|
|
5457
|
+
// Query-param tokens (easy_b/venom/bunny) carry a prefix-scoped `token_path`
|
|
5458
|
+
// and share one grant across manifests and segments, so the grant must be
|
|
5459
|
+
// minted against the asset-level source URL rather than whichever URL
|
|
5460
|
+
// triggered this refresh. Akamai's path-segment mechanic is scoped
|
|
5461
|
+
// differently and is deliberately left on the requested URL.
|
|
5462
|
+
const sharesPrefixScopedGrant = this.tokenType === TOKEN_TYPES.EASY_B
|
|
5463
|
+
|| this.tokenType === TOKEN_TYPES.VENOM
|
|
5464
|
+
|| this.tokenType === TOKEN_TYPES.BUNNY;
|
|
5465
|
+
const fetchSrc = (sharesPrefixScopedGrant && this.sourceUrl) ? this.sourceUrl : srcUrl.toString();
|
|
5440
5466
|
try {
|
|
5441
|
-
const tokenResponse = await this.fetchToken({ src:
|
|
5467
|
+
const tokenResponse = await this.fetchToken({ src: fetchSrc });
|
|
5442
5468
|
if (tokenResponse === null) {
|
|
5443
5469
|
return url;
|
|
5444
5470
|
}
|
|
@@ -5759,33 +5785,39 @@
|
|
|
5759
5785
|
* Segment (fragment) CDN token signing — shared primitive.
|
|
5760
5786
|
*
|
|
5761
5787
|
* Extends CDN token signing from manifests to segments, behind an opt-in flag
|
|
5762
|
-
* (`EngineSettings.signSegmentsWithCdnToken`).
|
|
5763
|
-
* grant carried by its OWN parent playlist — read from
|
|
5764
|
-
* the object hls.js itself parsed that fragment from
|
|
5765
|
-
*
|
|
5766
|
-
*
|
|
5767
|
-
*
|
|
5768
|
-
*
|
|
5769
|
-
*
|
|
5770
|
-
*
|
|
5771
|
-
*
|
|
5772
|
-
*
|
|
5773
|
-
*
|
|
5774
|
-
*
|
|
5775
|
-
*
|
|
5776
|
-
*
|
|
5777
|
-
*
|
|
5778
|
-
*
|
|
5779
|
-
*
|
|
5780
|
-
*
|
|
5781
|
-
*
|
|
5782
|
-
*
|
|
5783
|
-
*
|
|
5784
|
-
*
|
|
5785
|
-
*
|
|
5786
|
-
*
|
|
5787
|
-
*
|
|
5788
|
-
*
|
|
5788
|
+
* (`EngineSettings.signSegmentsWithCdnToken`). The fast path signs each segment
|
|
5789
|
+
* with the grant carried by its OWN parent playlist — read from
|
|
5790
|
+
* `context.frag.baseurl`, the object hls.js itself parsed that fragment from.
|
|
5791
|
+
*
|
|
5792
|
+
* Token expiry model (CDN-04, D-07, revised):
|
|
5793
|
+
*
|
|
5794
|
+
* `frag.baseurl` is a string frozen at the moment hls.js parsed the level
|
|
5795
|
+
* playlist. Live playlists reload on their own cadence, so their fragments pick
|
|
5796
|
+
* up refreshed grants for free — reload IS the refresh path. VOD playlists are
|
|
5797
|
+
* parsed exactly once by hls.js 1.6.10 (its level controller only (re)requests a
|
|
5798
|
+
* playlist it has none of, or one that is live), so for VOD that string never
|
|
5799
|
+
* changes. Pure transfer alone therefore pins a VOD asset's segments to the
|
|
5800
|
+
* first grant for the asset's entire duration, and every segment 403s once that
|
|
5801
|
+
* grant lapses — the condition CONTEXT.md named as D-07's revisit trigger, since
|
|
5802
|
+
* observed in production (a ~294s grant against a ~26min asset).
|
|
5803
|
+
*
|
|
5804
|
+
* So when the inherited grant is inside the manager's expiry margin, this module
|
|
5805
|
+
* now defers to the shared grant via `updateUrlWithTokenParams` rather than
|
|
5806
|
+
* copying a dead string.
|
|
5807
|
+
*
|
|
5808
|
+
* Why sharing a grant across renditions is safe (supersedes this module's
|
|
5809
|
+
* previous prohibition): the token service derives `token_path` from the
|
|
5810
|
+
* directory of the URL it signs, and `token_path` is a PREFIX scope. An
|
|
5811
|
+
* asset-level grant returns 200 on every rendition beneath it — verified live
|
|
5812
|
+
* across all four renditions of a real asset. The hazard this module previously
|
|
5813
|
+
* guarded against — one rendition's grant handed to another — is real ONLY for a
|
|
5814
|
+
* rendition-scoped grant, and that is prevented at its source by
|
|
5815
|
+
* `CDNTokenManager.sourceUrl`, which pins acquisition of the shared grant to the
|
|
5816
|
+
* asset-level source URL. The invariant is therefore "the shared grant must be
|
|
5817
|
+
* asset-level", not "segments must never refresh".
|
|
5818
|
+
*
|
|
5819
|
+
* Still true: no timer, interval, or scheduled re-fetch belongs in this module.
|
|
5820
|
+
* Refresh here is demand-driven, on the load that needs it.
|
|
5789
5821
|
*/
|
|
5790
5822
|
// ---------------------------------------------------------------------------
|
|
5791
5823
|
// Supported token-type family (D-03) — query-param tokens only. Akamai's
|
|
@@ -5851,6 +5883,23 @@
|
|
|
5851
5883
|
return segmentUrl;
|
|
5852
5884
|
}
|
|
5853
5885
|
}
|
|
5886
|
+
/**
|
|
5887
|
+
* Reads the `expires` value off a parent playlist URL, or null when the URL
|
|
5888
|
+
* carries no token at all or cannot be parsed. Used to decide between the pure
|
|
5889
|
+
* transfer fast path and a shared-grant refresh.
|
|
5890
|
+
*/
|
|
5891
|
+
function readPlaylistGrantExpiry(parentPlaylistUrl) {
|
|
5892
|
+
try {
|
|
5893
|
+
const playlistUrl = new URL(parentPlaylistUrl);
|
|
5894
|
+
const token = playlistUrl.searchParams.get('token');
|
|
5895
|
+
if (!token)
|
|
5896
|
+
return { hasToken: false, expires: null };
|
|
5897
|
+
return { hasToken: true, expires: playlistUrl.searchParams.get('expires') };
|
|
5898
|
+
}
|
|
5899
|
+
catch {
|
|
5900
|
+
return { hasToken: false, expires: null };
|
|
5901
|
+
}
|
|
5902
|
+
}
|
|
5854
5903
|
/**
|
|
5855
5904
|
* Returns a loader constructor extending `BaseLoader`: construct the base,
|
|
5856
5905
|
* bind the inherited `load`, then assign an own `load` property that reads
|
|
@@ -5905,8 +5954,36 @@
|
|
|
5905
5954
|
originalLoad(context, loadConfig, callbacks);
|
|
5906
5955
|
return;
|
|
5907
5956
|
}
|
|
5908
|
-
|
|
5909
|
-
|
|
5957
|
+
const { hasToken, expires } = readPlaylistGrantExpiry(baseUrl);
|
|
5958
|
+
// Parent playlist carried no grant: nothing to transfer, and nothing to
|
|
5959
|
+
// refresh — an unsigned playlist means this path is not token-protected.
|
|
5960
|
+
if (!hasToken) {
|
|
5961
|
+
originalLoad(context, loadConfig, callbacks);
|
|
5962
|
+
return;
|
|
5963
|
+
}
|
|
5964
|
+
// Fast path: the inherited grant is still good. Pure synchronous transfer,
|
|
5965
|
+
// no manager involvement, no network.
|
|
5966
|
+
if (!tokenSource.isTokenExpired(expires)) {
|
|
5967
|
+
context.url = signSegmentUrlFromPlaylist(context.url, baseUrl);
|
|
5968
|
+
originalLoad(context, loadConfig, callbacks);
|
|
5969
|
+
return;
|
|
5970
|
+
}
|
|
5971
|
+
// Inherited grant is expired or inside the expiry margin. For VOD this is
|
|
5972
|
+
// terminal — hls.js will never re-parse the playlist, so the frozen
|
|
5973
|
+
// baseurl can never recover on its own. Defer to the shared asset-level
|
|
5974
|
+
// grant, which `CDNTokenManager.sourceUrl` keeps broad enough to cover
|
|
5975
|
+
// this segment.
|
|
5976
|
+
tokenSource.updateUrlWithTokenParams({ url: context.url })
|
|
5977
|
+
.then((updatedUrl) => {
|
|
5978
|
+
context.url = updatedUrl;
|
|
5979
|
+
originalLoad(context, loadConfig, callbacks);
|
|
5980
|
+
})
|
|
5981
|
+
.catch(() => {
|
|
5982
|
+
// Token error: fall back to the inherited grant rather than emitting an
|
|
5983
|
+
// unsigned request, and let the edge's 403 drive hls.js's own retry.
|
|
5984
|
+
context.url = signSegmentUrlFromPlaylist(context.url, baseUrl);
|
|
5985
|
+
originalLoad(context, loadConfig, callbacks);
|
|
5986
|
+
});
|
|
5910
5987
|
};
|
|
5911
5988
|
}
|
|
5912
5989
|
function SegmentTokenLoader(loaderConfig) {
|
|
@@ -6279,6 +6356,11 @@
|
|
|
6279
6356
|
// stream URL is loaded rather than config.src which always points at the main stream.
|
|
6280
6357
|
let src = this.loadSourceUrl || config.src || '';
|
|
6281
6358
|
if (this.tokenManager && src) {
|
|
6359
|
+
// Pin shared-grant acquisition to the asset-level source URL before the
|
|
6360
|
+
// first fetch. The token service scopes `token_path` to the directory of
|
|
6361
|
+
// the URL it signs, so a grant minted against a level playlist (as an ABR
|
|
6362
|
+
// switch would trigger) is rendition-scoped and 403s on every sibling.
|
|
6363
|
+
this.tokenManager.sourceUrl = src;
|
|
6282
6364
|
src = await this.tokenManager.updateUrlWithTokenParams({ url: src });
|
|
6283
6365
|
// Guard: abort if detached during token URL update
|
|
6284
6366
|
if (this.detached) {
|
|
@@ -7465,10 +7547,11 @@
|
|
|
7465
7547
|
* (20s) so that more precise, better-logged inner mechanism wins first in a
|
|
7466
7548
|
* live player — this is the outer safety net, not the primary signal. Covers
|
|
7467
7549
|
* two cases the inner mechanism cannot: an AdsManager that was never
|
|
7468
|
-
* constructed at all (
|
|
7469
|
-
*
|
|
7470
|
-
*
|
|
7471
|
-
*
|
|
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.
|
|
7472
7555
|
*/
|
|
7473
7556
|
const MAIN_OPEN_FALLBACK_MS = 30000;
|
|
7474
7557
|
// ---------------------------------------------------------------------------
|
|
@@ -7503,7 +7586,13 @@
|
|
|
7503
7586
|
// so the config-level swap is legitimate for that field only). NEVER mutate
|
|
7504
7587
|
// `mergedConfig.muted` — it is consumed only at video element creation in
|
|
7505
7588
|
// skin-root.ts:141/318 and is a no-op for runtime mute.
|
|
7506
|
-
|
|
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;
|
|
7507
7596
|
const originalManager = mergedConfig.manager;
|
|
7508
7597
|
const originalStartAt = mergedConfig.startAt;
|
|
7509
7598
|
const controller = new PlayerController(runtimeConfig);
|
|
@@ -7682,6 +7771,21 @@
|
|
|
7682
7771
|
controller.bus.on('error-fatal', () => {
|
|
7683
7772
|
inst.p2p?.stop();
|
|
7684
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
|
+
}
|
|
7685
7789
|
// Phase 7/8 — pre-main leg orchestration.
|
|
7686
7790
|
// When mergedConfig.preroll is set, open the intro stream first, then on
|
|
7687
7791
|
// bus 'intro-stream-complete' (emitted by IntroStreamManager on video.ended,
|
|
@@ -7718,9 +7822,11 @@
|
|
|
7718
7822
|
// same tick. complete() is idempotent, so on the normal ordering (intro
|
|
7719
7823
|
// already finished) this is a no-op; the listener is already torn down.
|
|
7720
7824
|
controller.bus.emit('force-intro-complete');
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
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;
|
|
7724
7830
|
controller.state.muted = originalMuted;
|
|
7725
7831
|
// CR-05: keep mergedConfig.manager and controller.config.manager in sync.
|
|
7726
7832
|
// start() builds mergedConfig and PlayerController builds its own merged
|
|
@@ -7805,7 +7911,9 @@
|
|
|
7805
7911
|
return;
|
|
7806
7912
|
deferredPlaybackStarted = true;
|
|
7807
7913
|
const engine = inst.engine;
|
|
7808
|
-
|
|
7914
|
+
// D-06: `video === null` is unreachable here — see the comment on the
|
|
7915
|
+
// equivalent guard in openMainStream() above.
|
|
7916
|
+
if (engine === null)
|
|
7809
7917
|
return;
|
|
7810
7918
|
// Honor the user's click intent. CommandHandler's earlier bus.on('play')
|
|
7811
7919
|
// listener fired before us with no source attached, so its video.play()
|
|
@@ -7875,9 +7983,9 @@
|
|
|
7875
7983
|
}, { signal: controller.signal });
|
|
7876
7984
|
// Force runtime mute off for the intro pass — applied to the LIVE <video>
|
|
7877
7985
|
// element and PlayerState, NOT to mergedConfig.muted (no-op for runtime).
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7986
|
+
// D-06: `video` is guaranteed non-null here — see the comment on the
|
|
7987
|
+
// equivalent restore in openMainStream() above.
|
|
7988
|
+
video.muted = false;
|
|
7881
7989
|
controller.state.muted = false;
|
|
7882
7990
|
// Skip P2P attach for the intro stream (D-08); restored before main open.
|
|
7883
7991
|
// CR-05: keep the dual-merged configs in sync (see openMainStream).
|