eb-player 2.4.2 → 2.5.0

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.4.2";
7
+ var __EB_PLAYER_VERSION__ = "2.5.0";
8
8
 
9
9
  /**
10
10
  * Finite State Machine for player playback state transitions.
@@ -5236,6 +5236,11 @@
5236
5236
  if (text.trim().startsWith('{')) {
5237
5237
  return TokenResponse.fromJson({ src, json: text });
5238
5238
  }
5239
+ const fullUrlResponse = parseFullUrlResponse(text);
5240
+ if (fullUrlResponse) {
5241
+ const responseParameters = { token: null, tokenPath: null, ...fullUrlResponse };
5242
+ return new TokenResponse({ src, responseParameters });
5243
+ }
5239
5244
  const params = new URLSearchParams(text.trim().replace(/^\?/, ''));
5240
5245
  const token = params.get('token') ?? null;
5241
5246
  const expiresStr = params.get('expires');
@@ -5274,8 +5279,11 @@
5274
5279
  // Akamai response parameter parsing (inlined from CDNtoken/akamaiResponseParameters.js)
5275
5280
  // ---------------------------------------------------------------------------
5276
5281
  function parseAkamaiResponse({ src, text }) {
5277
- const fullUrl = `${src}${text ? `?${text}` : ''}`;
5278
- const srcUrl = new URL(fullUrl);
5282
+ // The token server may answer with a complete replacement URL (Akamai "URL
5283
+ // rewrite": a token path segment inserted after the origin, plus a query
5284
+ // `hdnea`). That URL is the result, not a fragment to merge into `src`.
5285
+ const fullUrl = parseFullUrlResponse(text)?.fullUrl ?? null;
5286
+ const srcUrl = new URL(fullUrl ?? `${src}${text ? `?${text}` : ''}`);
5279
5287
  let hdntl = null;
5280
5288
  let exp = null;
5281
5289
  let hmac = null;
@@ -5309,7 +5317,83 @@
5309
5317
  if (aclMatch)
5310
5318
  acl = aclMatch[1];
5311
5319
  }
5312
- return { expires: exp, hdntl: tokenValue, hmac, acl, hdnea };
5320
+ return { expires: exp, hdntl: tokenValue, hmac, acl, hdnea, fullUrl };
5321
+ }
5322
+ // ---------------------------------------------------------------------------
5323
+ // Full-URL token responses (Akamai ESI Token Authority / URL rewrite)
5324
+ // ---------------------------------------------------------------------------
5325
+ const ABSOLUTE_URL_PATTERN = /^https?:\/\//i;
5326
+ /**
5327
+ * Detects a token response that is itself a complete URL, e.g.
5328
+ * `https://cdn/<token-segment>/path/asset.m3u8?hdnea=exp=...~acl=...~hmac=...`.
5329
+ * `expires` comes from an `expires` query param when present, otherwise from
5330
+ * the `exp=` field of the `hdnea`/`hdntl` token.
5331
+ */
5332
+ function parseFullUrlResponse(text) {
5333
+ const fullUrl = text.trim();
5334
+ if (!ABSOLUTE_URL_PATTERN.test(fullUrl)) {
5335
+ return null;
5336
+ }
5337
+ const expiresParam = new URL(fullUrl).searchParams.get('expires');
5338
+ if (expiresParam) {
5339
+ return { fullUrl, expires: parseInt(expiresParam, 10) };
5340
+ }
5341
+ return { fullUrl, expires: parseAkamaiResponse({ src: fullUrl, text: '' }).expires };
5342
+ }
5343
+ function fullUrlOf(tokenResponse) {
5344
+ const params = tokenResponse?.responseParameters;
5345
+ return params && 'fullUrl' in params ? params.fullUrl ?? null : null;
5346
+ }
5347
+ function rewriteGrantOf(tokenResponse) {
5348
+ const params = tokenResponse?.responseParameters;
5349
+ const fullUrl = fullUrlOf(tokenResponse);
5350
+ if (!tokenResponse || !params || !fullUrl) {
5351
+ return null;
5352
+ }
5353
+ const original = new URL(tokenResponse.src);
5354
+ const rewritten = new URL(fullUrl);
5355
+ if (!rewritten.pathname.endsWith(original.pathname)) {
5356
+ return null;
5357
+ }
5358
+ const insertedPath = rewritten.pathname.slice(0, rewritten.pathname.length - original.pathname.length);
5359
+ return {
5360
+ fullUrl,
5361
+ originalBase: original.origin,
5362
+ rewrittenBase: rewritten.origin + insertedPath,
5363
+ search: rewritten.search,
5364
+ expires: params.expires
5365
+ };
5366
+ }
5367
+ /**
5368
+ * Maps a URL back to its un-rewritten form (token segment and `hdnea`/`hdntl`
5369
+ * query removed). Feeding an already-rewritten URL to the token server makes
5370
+ * it stack a second token segment, so refreshes must go through this first.
5371
+ * Returns null when the URL is not under the grant.
5372
+ */
5373
+ function unrewriteUrl(url, grant) {
5374
+ if (!url.startsWith(`${grant.rewrittenBase}/`)) {
5375
+ return null;
5376
+ }
5377
+ const original = new URL(grant.originalBase + url.slice(grant.rewrittenBase.length));
5378
+ original.searchParams.delete('hdnea');
5379
+ original.searchParams.delete('hdntl');
5380
+ return original.toString();
5381
+ }
5382
+ /**
5383
+ * Applies a grant to a URL (original or already rewritten): the token segment
5384
+ * is inserted after the origin and the server's query string appended
5385
+ * verbatim. Returns null when the URL lives on another origin.
5386
+ */
5387
+ function rewriteUrl(url, grant) {
5388
+ const original = new URL(unrewriteUrl(url, grant) ?? url);
5389
+ if (original.origin !== grant.originalBase) {
5390
+ return null;
5391
+ }
5392
+ original.searchParams.delete('hdnea');
5393
+ original.searchParams.delete('hdntl');
5394
+ const ownQuery = original.search;
5395
+ const query = ownQuery ? `${ownQuery}&${grant.search.slice(1)}` : grant.search;
5396
+ return `${grant.rewrittenBase}${original.pathname}${query}`;
5313
5397
  }
5314
5398
  // ---------------------------------------------------------------------------
5315
5399
  // Internal wait helper
@@ -5636,6 +5720,13 @@
5636
5720
  if (!this.isTokenExpired(akamaiParams.expires)) {
5637
5721
  return url;
5638
5722
  }
5723
+ // A cached full-URL grant covers every URL under the same origin
5724
+ // (child playlists, segments) without another token request.
5725
+ const grant = rewriteGrantOf(this.lastTokenResponse);
5726
+ const reused = grant && !this.isTokenExpired(grant.expires) ? rewriteUrl(url, grant) : null;
5727
+ if (reused) {
5728
+ return reused;
5729
+ }
5639
5730
  // Remove hdntl / hdnea path segments before requesting new token
5640
5731
  const pathSegments = srcUrl.pathname.split('/');
5641
5732
  srcUrl.pathname = pathSegments
@@ -5655,6 +5746,10 @@
5655
5746
  if (cachedResponse) {
5656
5747
  const cachedParams = cachedResponse.responseParameters;
5657
5748
  if (cachedParams && !this.isTokenExpired(cachedParams.expires)) {
5749
+ const grant = rewriteGrantOf(cachedResponse);
5750
+ if (grant) {
5751
+ return rewriteUrl(url, grant) ?? url;
5752
+ }
5658
5753
  const { token, expires: cachedExpires, tokenPath } = cachedParams;
5659
5754
  return CDNTokenManager.appendTokenParams(url, token, cachedExpires, tokenPath);
5660
5755
  }
@@ -5675,7 +5770,11 @@
5675
5770
  const sharesPrefixScopedGrant = this.tokenType === TOKEN_TYPES.EASY_B
5676
5771
  || this.tokenType === TOKEN_TYPES.VENOM
5677
5772
  || this.tokenType === TOKEN_TYPES.BUNNY;
5678
- const fetchSrc = (sharesPrefixScopedGrant && this.sourceUrl) ? this.sourceUrl : srcUrl.toString();
5773
+ // A previous full-URL grant may have rewritten this URL; ask the token
5774
+ // server about the original, or it stacks a second token segment.
5775
+ const previousGrant = rewriteGrantOf(this.lastTokenResponse);
5776
+ const originalUrl = (previousGrant && unrewriteUrl(srcUrl.toString(), previousGrant)) ?? srcUrl.toString();
5777
+ const fetchSrc = (sharesPrefixScopedGrant && this.sourceUrl) ? this.sourceUrl : originalUrl;
5679
5778
  try {
5680
5779
  const tokenResponse = await this.fetchToken({ src: fetchSrc });
5681
5780
  if (tokenResponse === null) {
@@ -5691,6 +5790,20 @@
5691
5790
  }
5692
5791
  return url;
5693
5792
  }
5793
+ // Full-URL response: the server's URL is the result. When the response
5794
+ // was minted for another URL (deduplicated in-flight fetch, or the
5795
+ // easy_b asset-level source), apply its grant to this one instead.
5796
+ const fullUrl = fullUrlOf(tokenResponse);
5797
+ if (fullUrl) {
5798
+ if (tokenResponse.src === originalUrl) {
5799
+ return fullUrl;
5800
+ }
5801
+ const grant = rewriteGrantOf(tokenResponse);
5802
+ const rewritten = grant ? rewriteUrl(originalUrl, grant) : null;
5803
+ if (rewritten) {
5804
+ return rewritten;
5805
+ }
5806
+ }
5694
5807
  switch (this.tokenType) {
5695
5808
  case TOKEN_TYPES.BUNNY:
5696
5809
  case TOKEN_TYPES.VENOM: