eb-player 2.1.1 → 2.1.3
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 +178 -33
- package/dist/build/ebplayer.bundle.js.map +1 -1
- package/dist/build/types/src/core/event-bus.d.ts +3 -0
- package/dist/build/types/src/core/event-bus.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 +24 -0
- 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.3";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Finite State Machine for player playback state transitions.
|
|
@@ -5119,6 +5119,7 @@
|
|
|
5119
5119
|
this.retryInterval = retryInterval;
|
|
5120
5120
|
this.expirationMarginInSeconds = expirationMarginInSeconds;
|
|
5121
5121
|
this.lastTokenResponse = null;
|
|
5122
|
+
this.sourceUrl = null;
|
|
5122
5123
|
this.resetAttemptCounterTimeout = null;
|
|
5123
5124
|
this.inFlightFetch = null;
|
|
5124
5125
|
}
|
|
@@ -5437,8 +5438,17 @@
|
|
|
5437
5438
|
}
|
|
5438
5439
|
break;
|
|
5439
5440
|
}
|
|
5441
|
+
// Query-param tokens (easy_b/venom/bunny) carry a prefix-scoped `token_path`
|
|
5442
|
+
// and share one grant across manifests and segments, so the grant must be
|
|
5443
|
+
// minted against the asset-level source URL rather than whichever URL
|
|
5444
|
+
// triggered this refresh. Akamai's path-segment mechanic is scoped
|
|
5445
|
+
// differently and is deliberately left on the requested URL.
|
|
5446
|
+
const sharesPrefixScopedGrant = this.tokenType === TOKEN_TYPES.EASY_B
|
|
5447
|
+
|| this.tokenType === TOKEN_TYPES.VENOM
|
|
5448
|
+
|| this.tokenType === TOKEN_TYPES.BUNNY;
|
|
5449
|
+
const fetchSrc = (sharesPrefixScopedGrant && this.sourceUrl) ? this.sourceUrl : srcUrl.toString();
|
|
5440
5450
|
try {
|
|
5441
|
-
const tokenResponse = await this.fetchToken({ src:
|
|
5451
|
+
const tokenResponse = await this.fetchToken({ src: fetchSrc });
|
|
5442
5452
|
if (tokenResponse === null) {
|
|
5443
5453
|
return url;
|
|
5444
5454
|
}
|
|
@@ -5759,33 +5769,39 @@
|
|
|
5759
5769
|
* Segment (fragment) CDN token signing — shared primitive.
|
|
5760
5770
|
*
|
|
5761
5771
|
* 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
|
-
*
|
|
5772
|
+
* (`EngineSettings.signSegmentsWithCdnToken`). The fast path signs each segment
|
|
5773
|
+
* with the grant carried by its OWN parent playlist — read from
|
|
5774
|
+
* `context.frag.baseurl`, the object hls.js itself parsed that fragment from.
|
|
5775
|
+
*
|
|
5776
|
+
* Token expiry model (CDN-04, D-07, revised):
|
|
5777
|
+
*
|
|
5778
|
+
* `frag.baseurl` is a string frozen at the moment hls.js parsed the level
|
|
5779
|
+
* playlist. Live playlists reload on their own cadence, so their fragments pick
|
|
5780
|
+
* up refreshed grants for free — reload IS the refresh path. VOD playlists are
|
|
5781
|
+
* parsed exactly once by hls.js 1.6.10 (its level controller only (re)requests a
|
|
5782
|
+
* playlist it has none of, or one that is live), so for VOD that string never
|
|
5783
|
+
* changes. Pure transfer alone therefore pins a VOD asset's segments to the
|
|
5784
|
+
* first grant for the asset's entire duration, and every segment 403s once that
|
|
5785
|
+
* grant lapses — the condition CONTEXT.md named as D-07's revisit trigger, since
|
|
5786
|
+
* observed in production (a ~294s grant against a ~26min asset).
|
|
5787
|
+
*
|
|
5788
|
+
* So when the inherited grant is inside the manager's expiry margin, this module
|
|
5789
|
+
* now defers to the shared grant via `updateUrlWithTokenParams` rather than
|
|
5790
|
+
* copying a dead string.
|
|
5791
|
+
*
|
|
5792
|
+
* Why sharing a grant across renditions is safe (supersedes this module's
|
|
5793
|
+
* previous prohibition): the token service derives `token_path` from the
|
|
5794
|
+
* directory of the URL it signs, and `token_path` is a PREFIX scope. An
|
|
5795
|
+
* asset-level grant returns 200 on every rendition beneath it — verified live
|
|
5796
|
+
* across all four renditions of a real asset. The hazard this module previously
|
|
5797
|
+
* guarded against — one rendition's grant handed to another — is real ONLY for a
|
|
5798
|
+
* rendition-scoped grant, and that is prevented at its source by
|
|
5799
|
+
* `CDNTokenManager.sourceUrl`, which pins acquisition of the shared grant to the
|
|
5800
|
+
* asset-level source URL. The invariant is therefore "the shared grant must be
|
|
5801
|
+
* asset-level", not "segments must never refresh".
|
|
5802
|
+
*
|
|
5803
|
+
* Still true: no timer, interval, or scheduled re-fetch belongs in this module.
|
|
5804
|
+
* Refresh here is demand-driven, on the load that needs it.
|
|
5789
5805
|
*/
|
|
5790
5806
|
// ---------------------------------------------------------------------------
|
|
5791
5807
|
// Supported token-type family (D-03) — query-param tokens only. Akamai's
|
|
@@ -5851,6 +5867,23 @@
|
|
|
5851
5867
|
return segmentUrl;
|
|
5852
5868
|
}
|
|
5853
5869
|
}
|
|
5870
|
+
/**
|
|
5871
|
+
* Reads the `expires` value off a parent playlist URL, or null when the URL
|
|
5872
|
+
* carries no token at all or cannot be parsed. Used to decide between the pure
|
|
5873
|
+
* transfer fast path and a shared-grant refresh.
|
|
5874
|
+
*/
|
|
5875
|
+
function readPlaylistGrantExpiry(parentPlaylistUrl) {
|
|
5876
|
+
try {
|
|
5877
|
+
const playlistUrl = new URL(parentPlaylistUrl);
|
|
5878
|
+
const token = playlistUrl.searchParams.get('token');
|
|
5879
|
+
if (!token)
|
|
5880
|
+
return { hasToken: false, expires: null };
|
|
5881
|
+
return { hasToken: true, expires: playlistUrl.searchParams.get('expires') };
|
|
5882
|
+
}
|
|
5883
|
+
catch {
|
|
5884
|
+
return { hasToken: false, expires: null };
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5854
5887
|
/**
|
|
5855
5888
|
* Returns a loader constructor extending `BaseLoader`: construct the base,
|
|
5856
5889
|
* bind the inherited `load`, then assign an own `load` property that reads
|
|
@@ -5905,8 +5938,36 @@
|
|
|
5905
5938
|
originalLoad(context, loadConfig, callbacks);
|
|
5906
5939
|
return;
|
|
5907
5940
|
}
|
|
5908
|
-
|
|
5909
|
-
|
|
5941
|
+
const { hasToken, expires } = readPlaylistGrantExpiry(baseUrl);
|
|
5942
|
+
// Parent playlist carried no grant: nothing to transfer, and nothing to
|
|
5943
|
+
// refresh — an unsigned playlist means this path is not token-protected.
|
|
5944
|
+
if (!hasToken) {
|
|
5945
|
+
originalLoad(context, loadConfig, callbacks);
|
|
5946
|
+
return;
|
|
5947
|
+
}
|
|
5948
|
+
// Fast path: the inherited grant is still good. Pure synchronous transfer,
|
|
5949
|
+
// no manager involvement, no network.
|
|
5950
|
+
if (!tokenSource.isTokenExpired(expires)) {
|
|
5951
|
+
context.url = signSegmentUrlFromPlaylist(context.url, baseUrl);
|
|
5952
|
+
originalLoad(context, loadConfig, callbacks);
|
|
5953
|
+
return;
|
|
5954
|
+
}
|
|
5955
|
+
// Inherited grant is expired or inside the expiry margin. For VOD this is
|
|
5956
|
+
// terminal — hls.js will never re-parse the playlist, so the frozen
|
|
5957
|
+
// baseurl can never recover on its own. Defer to the shared asset-level
|
|
5958
|
+
// grant, which `CDNTokenManager.sourceUrl` keeps broad enough to cover
|
|
5959
|
+
// this segment.
|
|
5960
|
+
tokenSource.updateUrlWithTokenParams({ url: context.url })
|
|
5961
|
+
.then((updatedUrl) => {
|
|
5962
|
+
context.url = updatedUrl;
|
|
5963
|
+
originalLoad(context, loadConfig, callbacks);
|
|
5964
|
+
})
|
|
5965
|
+
.catch(() => {
|
|
5966
|
+
// Token error: fall back to the inherited grant rather than emitting an
|
|
5967
|
+
// unsigned request, and let the edge's 403 drive hls.js's own retry.
|
|
5968
|
+
context.url = signSegmentUrlFromPlaylist(context.url, baseUrl);
|
|
5969
|
+
originalLoad(context, loadConfig, callbacks);
|
|
5970
|
+
});
|
|
5910
5971
|
};
|
|
5911
5972
|
}
|
|
5912
5973
|
function SegmentTokenLoader(loaderConfig) {
|
|
@@ -5937,6 +5998,17 @@
|
|
|
5937
5998
|
* CRITICAL: Call applyDiscontinuityWorkaround BEFORE driver.attachMedia/loadSource (Pitfall 4).
|
|
5938
5999
|
* CRITICAL: Never call driver.startLoad() from video 'play' event handler (Pitfall 3).
|
|
5939
6000
|
*/
|
|
6001
|
+
/**
|
|
6002
|
+
* Bound for the "load never started" watchdog: hls.js's own manifest-loading
|
|
6003
|
+
* budget (default manifestLoadingTimeOut 10 000 ms with one retry plus
|
|
6004
|
+
* backoff) tops out around 21s before hls.js surfaces its own fatal network
|
|
6005
|
+
* error, so 30s guarantees hls.js's own error path always wins first and
|
|
6006
|
+
* this watchdog only ever sees the case hls.js is silent about — a
|
|
6007
|
+
* StreamController parked in STOPPED with no error and no event. 30s also
|
|
6008
|
+
* matches the existing MAIN_OPEN_FALLBACK_MS precedent in src/eb-player.ts
|
|
6009
|
+
* and sits well below INTRO_MAX_DURATION_MS (90 000).
|
|
6010
|
+
*/
|
|
6011
|
+
const LOAD_NEVER_STARTED_TIMEOUT_MS = 30000;
|
|
5940
6012
|
// ---------------------------------------------------------------------------
|
|
5941
6013
|
// HlsEngine
|
|
5942
6014
|
// ---------------------------------------------------------------------------
|
|
@@ -5949,6 +6021,9 @@
|
|
|
5949
6021
|
this.autoQuality = true;
|
|
5950
6022
|
// Holds state reference for named driver event handlers
|
|
5951
6023
|
this.eventState = null;
|
|
6024
|
+
// Bound "load never started" watchdog timer — 0-initialized (not null) so
|
|
6025
|
+
// clearTimeout(0) is a no-op, eliminating a null-guard phantom V8 branch.
|
|
6026
|
+
this.loadNeverStartedTimer = 0;
|
|
5952
6027
|
this.liveSyncDisabled = false;
|
|
5953
6028
|
}
|
|
5954
6029
|
getDriver() {
|
|
@@ -5982,6 +6057,7 @@
|
|
|
5982
6057
|
}
|
|
5983
6058
|
onDetach() {
|
|
5984
6059
|
this.stopWatchdog();
|
|
6060
|
+
this.clearLoadNeverStartedWatchdog();
|
|
5985
6061
|
if (this.driver !== null) {
|
|
5986
6062
|
try {
|
|
5987
6063
|
this.driver.detachMedia();
|
|
@@ -6108,8 +6184,17 @@
|
|
|
6108
6184
|
delete hlsSafeSettings['extraParamsCallback'];
|
|
6109
6185
|
delete hlsSafeSettings['onCDNTokenError'];
|
|
6110
6186
|
delete hlsSafeSettings['signSegmentsWithCdnToken'];
|
|
6187
|
+
// Do NOT set an explicit auto-start disable here. hls.js starts loading
|
|
6188
|
+
// segments on its own by default, and the resume position is already
|
|
6189
|
+
// carried by startPosition below — hls.js honours it without any help.
|
|
6190
|
+
// Nothing on the VOD path would ever restart a stopped loader: the only
|
|
6191
|
+
// two startLoad() call sites in this file are recoverFromStall() (only
|
|
6192
|
+
// reachable after playback has started) and the video 'play' listener
|
|
6193
|
+
// (gated on state.isLive, so VOD never reaches it). Disabling auto-start
|
|
6194
|
+
// here parks the StreamController in STOPPED forever for any non-zero
|
|
6195
|
+
// resume position — no variant playlist, no segments, no error, no bus
|
|
6196
|
+
// event, spinner forever.
|
|
6111
6197
|
const driverConfig = {
|
|
6112
|
-
autoStartLoad: !config.startAt,
|
|
6113
6198
|
enableWorker: true,
|
|
6114
6199
|
backBufferLength: 30,
|
|
6115
6200
|
...hlsSafeSettings,
|
|
@@ -6191,6 +6276,11 @@
|
|
|
6191
6276
|
this.driver = driver;
|
|
6192
6277
|
// Pitfall 4: apply discontinuity workaround BEFORE attachMedia/loadSource
|
|
6193
6278
|
applyDiscontinuityWorkaround(driver, Hls.Events);
|
|
6279
|
+
// Hoisted once, used both for the retry wiring below and for gating the
|
|
6280
|
+
// "load never started" watchdog — the intro pass already has its own
|
|
6281
|
+
// bounds (INTRO_MAX_DURATION_MS, D-04 silent fall-through) and a second
|
|
6282
|
+
// overlapping bound would only race them.
|
|
6283
|
+
const isIntroPass = Boolean(config.preroll) && this.loadSourceUrl === config.preroll;
|
|
6194
6284
|
// Wire retry handler — pass engine context so unrecoverable DRM errors
|
|
6195
6285
|
// stop the stall watchdog (prevents useless reload loops) and surface to UI.
|
|
6196
6286
|
// For the intro pass (loadSourceUrl matches config.preroll), also wire
|
|
@@ -6199,7 +6289,6 @@
|
|
|
6199
6289
|
// This callback is intentionally NOT set for the main stream so transient
|
|
6200
6290
|
// network blips still trigger the normal startLoad() recovery.
|
|
6201
6291
|
if (config.retry) {
|
|
6202
|
-
const isIntroPass = Boolean(config.preroll) && this.loadSourceUrl === config.preroll;
|
|
6203
6292
|
handleHlsRetry(driver, {
|
|
6204
6293
|
get chromecast_casting() { return state.isCasting; },
|
|
6205
6294
|
onUnrecoverableError: (message) => {
|
|
@@ -6251,6 +6340,11 @@
|
|
|
6251
6340
|
// stream URL is loaded rather than config.src which always points at the main stream.
|
|
6252
6341
|
let src = this.loadSourceUrl || config.src || '';
|
|
6253
6342
|
if (this.tokenManager && src) {
|
|
6343
|
+
// Pin shared-grant acquisition to the asset-level source URL before the
|
|
6344
|
+
// first fetch. The token service scopes `token_path` to the directory of
|
|
6345
|
+
// the URL it signs, so a grant minted against a level playlist (as an ABR
|
|
6346
|
+
// switch would trigger) is rendition-scoped and 403s on every sibling.
|
|
6347
|
+
this.tokenManager.sourceUrl = src;
|
|
6254
6348
|
src = await this.tokenManager.updateUrlWithTokenParams({ url: src });
|
|
6255
6349
|
// Guard: abort if detached during token URL update
|
|
6256
6350
|
if (this.detached) {
|
|
@@ -6266,6 +6360,8 @@
|
|
|
6266
6360
|
this.resolveDriverReady();
|
|
6267
6361
|
// Register driver event handlers
|
|
6268
6362
|
this.registerDriverEvents(Hls, state);
|
|
6363
|
+
// Arm the "load never started" watchdog — see LOAD_NEVER_STARTED_TIMEOUT_MS.
|
|
6364
|
+
this.armLoadNeverStartedWatchdog(driver, Hls, src, isIntroPass);
|
|
6269
6365
|
// Pause/resume loading on video pause/play to stop manifest refreshes
|
|
6270
6366
|
// (and thus CDN token requests) while the player is paused.
|
|
6271
6367
|
// Only applies to live streams where hls.js continuously refreshes the manifest.
|
|
@@ -6334,6 +6430,43 @@
|
|
|
6334
6430
|
driver.on(Events.SUBTITLE_TRACKS_UPDATED, this._onSubtitleTracksUpdated.bind(this));
|
|
6335
6431
|
driver.on(Events.SUBTITLE_TRACK_SWITCH, this._onSubtitleTrackSwitch.bind(this));
|
|
6336
6432
|
}
|
|
6433
|
+
// -------------------------------------------------------------------------
|
|
6434
|
+
// "Load never started" watchdog
|
|
6435
|
+
// -------------------------------------------------------------------------
|
|
6436
|
+
/**
|
|
6437
|
+
* Arms a one-shot bound that turns an unobservable hang into `state.error`
|
|
6438
|
+
* + a typed bus event: hls.js emits neither when its StreamController
|
|
6439
|
+
* never leaves STOPPED. Disarmed by the first FRAG_LOADING (proof that a
|
|
6440
|
+
* fragment request was actually issued, whether or not playback was
|
|
6441
|
+
* allowed to start — so autoplay-blocked playback never false-positives).
|
|
6442
|
+
* Never armed for the intro pass — it already has its own bounds.
|
|
6443
|
+
*/
|
|
6444
|
+
armLoadNeverStartedWatchdog(driver, Hls, src, isIntroPass) {
|
|
6445
|
+
if (isIntroPass)
|
|
6446
|
+
return;
|
|
6447
|
+
driver.on(Hls.Events.FRAG_LOADING, this._onFragLoading.bind(this));
|
|
6448
|
+
this.loadNeverStartedTimer = setTimeout(this._onLoadNeverStartedTimeout.bind(this, src), LOAD_NEVER_STARTED_TIMEOUT_MS);
|
|
6449
|
+
}
|
|
6450
|
+
clearLoadNeverStartedWatchdog() {
|
|
6451
|
+
clearTimeout(this.loadNeverStartedTimer);
|
|
6452
|
+
this.loadNeverStartedTimer = 0;
|
|
6453
|
+
}
|
|
6454
|
+
_onFragLoading() {
|
|
6455
|
+
this.clearLoadNeverStartedWatchdog();
|
|
6456
|
+
}
|
|
6457
|
+
_onLoadNeverStartedTimeout(src) {
|
|
6458
|
+
if (this.detached)
|
|
6459
|
+
return;
|
|
6460
|
+
// Two-signal live gate (mirrors _onLevelLoaded): a stream that never
|
|
6461
|
+
// loaded a level detail never got its isLive set from the driver, so
|
|
6462
|
+
// the config-level signal must also be checked.
|
|
6463
|
+
if (this.state?.isLive === true || this.config?.isLive === true)
|
|
6464
|
+
return;
|
|
6465
|
+
if (this.state) {
|
|
6466
|
+
this.state.error = 'HlsEngine: load never started — no fragment request was issued within the bound';
|
|
6467
|
+
}
|
|
6468
|
+
this.bus?.emit('load-never-started', { src });
|
|
6469
|
+
}
|
|
6337
6470
|
_onManifestLoaded(_event, data) {
|
|
6338
6471
|
const state = this.eventState;
|
|
6339
6472
|
const driver = this.driver;
|
|
@@ -7438,6 +7571,7 @@
|
|
|
7438
7571
|
// skin-root.ts:141/318 and is a no-op for runtime mute.
|
|
7439
7572
|
const originalMuted = mergedConfig.muted ?? false;
|
|
7440
7573
|
const originalManager = mergedConfig.manager;
|
|
7574
|
+
const originalStartAt = mergedConfig.startAt;
|
|
7441
7575
|
const controller = new PlayerController(runtimeConfig);
|
|
7442
7576
|
controller.mount(container);
|
|
7443
7577
|
// Video element is available after mount() creates the skin DOM
|
|
@@ -7663,6 +7797,10 @@
|
|
|
7663
7797
|
// during the intro pass. Synchronize both objects on every swap.
|
|
7664
7798
|
mergedConfig.manager = originalManager;
|
|
7665
7799
|
controller.config.manager = originalManager;
|
|
7800
|
+
// Restore the viewer's resume position for the main asset — see the
|
|
7801
|
+
// rationale comment at the intro-side swap below (CR-05 sync applies here too).
|
|
7802
|
+
mergedConfig.startAt = originalStartAt;
|
|
7803
|
+
controller.config.startAt = originalStartAt;
|
|
7666
7804
|
// CR-03: clear any stale error that leaked from the intro/ad pass.
|
|
7667
7805
|
// IntroStreamManager's silent fall-through (D-04) treats error-fatal as a
|
|
7668
7806
|
// normal completion but leaves state.error populated with the intro
|
|
@@ -7811,6 +7949,13 @@
|
|
|
7811
7949
|
// CR-05: keep the dual-merged configs in sync (see openMainStream).
|
|
7812
7950
|
mergedConfig.manager = false;
|
|
7813
7951
|
controller.config.manager = false;
|
|
7952
|
+
// A viewer resume position belongs to the main asset only — applying it
|
|
7953
|
+
// to a short intro clip seeks past the whole clip. This defect survives
|
|
7954
|
+
// the startAt/autoStartLoad fix: after that fix, startPosition: 300 is
|
|
7955
|
+
// still applied to the intro, which is still wrong, just differently
|
|
7956
|
+
// wrong. Cleared here and restored in openMainStream() (CR-05 sync).
|
|
7957
|
+
mergedConfig.startAt = undefined;
|
|
7958
|
+
controller.config.startAt = undefined;
|
|
7814
7959
|
reference.open(mergedConfig.preroll);
|
|
7815
7960
|
}
|
|
7816
7961
|
// D-02/D-08-02 (Phase 8): ad-only configuration — no intro stream, so this
|