@amazon/vinyl 3.1.0 → 3.2.1

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.
Files changed (69) hide show
  1. package/dist/index.cjs +236 -187
  2. package/dist/index.cjs.map +4 -4
  3. package/dist/index.js +224 -174
  4. package/dist/index.js.map +4 -4
  5. package/dist/test/integ.es5.js +245 -231
  6. package/dist/test/integ.js +676 -271
  7. package/dist/test/integ.js.map +4 -4
  8. package/dist/types/current/ad/AdControllerImpl.d.ts +18 -2
  9. package/dist/types/current/ad/AdControllerImpl.d.ts.map +1 -1
  10. package/dist/types/current/ad/AdControllerImpl.js +42 -20
  11. package/dist/types/current/drm/DrmControllerImpl.d.ts +17 -1
  12. package/dist/types/current/drm/DrmControllerImpl.d.ts.map +1 -1
  13. package/dist/types/current/drm/DrmControllerImpl.js +45 -10
  14. package/dist/types/current/playback/createStallMonitor.d.ts.map +1 -1
  15. package/dist/types/current/playback/createStallMonitor.js +19 -14
  16. package/dist/types/current/streaming/LoadMetric.d.ts +15 -1
  17. package/dist/types/current/streaming/LoadMetric.d.ts.map +1 -1
  18. package/dist/types/current/streaming/mediaTimelineFilter.d.ts +4 -2
  19. package/dist/types/current/streaming/mediaTimelineFilter.d.ts.map +1 -1
  20. package/dist/types/current/streaming/mediaTimelineFilter.js +23 -8
  21. package/dist/types/current/track/filters/sampleRateFilter.d.ts +8 -4
  22. package/dist/types/current/track/filters/sampleRateFilter.d.ts.map +1 -1
  23. package/dist/types/current/track/filters/sampleRateFilter.js +31 -21
  24. package/dist/types/current/track/mse/MseTrack.d.ts +0 -9
  25. package/dist/types/current/track/mse/MseTrack.d.ts.map +1 -1
  26. package/dist/types/current/track/mse/MseTrack.js +2 -29
  27. package/dist/types/current/tsconfig.tsbuildinfo +1 -1
  28. package/dist/types/current/vinyl/VinylPlayer.d.ts +5 -0
  29. package/dist/types/current/vinyl/VinylPlayer.d.ts.map +1 -1
  30. package/dist/types/current/vinyl/VinylPlayer.js +23 -1
  31. package/dist/types/ts3.7/ad/AdControllerImpl.d.ts +18 -2
  32. package/dist/types/ts3.7/drm/DrmControllerImpl.d.ts +17 -1
  33. package/dist/types/ts3.7/streaming/LoadMetric.d.ts +15 -1
  34. package/dist/types/ts3.7/streaming/mediaTimelineFilter.d.ts +4 -2
  35. package/dist/types/ts3.7/track/filters/sampleRateFilter.d.ts +8 -4
  36. package/dist/types/ts3.7/track/mse/MseTrack.d.ts +0 -9
  37. package/dist/types/ts3.7/vinyl/VinylPlayer.d.ts +5 -0
  38. package/dist/types/ts4.1/ad/AdControllerImpl.d.ts +18 -2
  39. package/dist/types/ts4.1/drm/DrmControllerImpl.d.ts +17 -1
  40. package/dist/types/ts4.1/streaming/LoadMetric.d.ts +15 -1
  41. package/dist/types/ts4.1/streaming/mediaTimelineFilter.d.ts +4 -2
  42. package/dist/types/ts4.1/track/filters/sampleRateFilter.d.ts +8 -4
  43. package/dist/types/ts4.1/track/mse/MseTrack.d.ts +0 -9
  44. package/dist/types/ts4.1/vinyl/VinylPlayer.d.ts +5 -0
  45. package/dist/types/ts4.7/ad/AdControllerImpl.d.ts +18 -2
  46. package/dist/types/ts4.7/drm/DrmControllerImpl.d.ts +17 -1
  47. package/dist/types/ts4.7/streaming/LoadMetric.d.ts +15 -1
  48. package/dist/types/ts4.7/streaming/mediaTimelineFilter.d.ts +4 -2
  49. package/dist/types/ts4.7/track/filters/sampleRateFilter.d.ts +8 -4
  50. package/dist/types/ts4.7/track/mse/MseTrack.d.ts +0 -9
  51. package/dist/types/ts4.7/vinyl/VinylPlayer.d.ts +5 -0
  52. package/dist/unit/types/current/ad/AdControllerImpl.test.js +110 -4
  53. package/dist/unit/types/current/drm/DrmControllerImpl.test.js +53 -9
  54. package/dist/unit/types/current/playback/createStallMonitor.test.js +38 -0
  55. package/dist/unit/types/current/streaming/MseTrack.test.js +11 -8
  56. package/dist/unit/types/current/streaming/mediaTimelineFilter.test.js +60 -54
  57. package/dist/unit/types/current/track/filters/sampleRateFilter.test.js +75 -0
  58. package/dist/unit/types/current/tsconfig.tsbuildinfo +1 -1
  59. package/dist/unit/types/current/vinyl/VinylPlayer.test.js +33 -2
  60. package/dist/vinylTestUtil/index.js +10 -1
  61. package/dist/vinylTestUtil/index.js.map +2 -2
  62. package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.d.ts +5 -0
  63. package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.d.ts.map +1 -1
  64. package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.js +9 -0
  65. package/dist/vinylTestUtil/types/current/tsconfig.tsbuildinfo +1 -1
  66. package/dist/vinylTestUtil/types/ts3.7/assets/vinylTestAssets.d.ts +5 -0
  67. package/dist/vinylTestUtil/types/ts4.1/assets/vinylTestAssets.d.ts +5 -0
  68. package/dist/vinylTestUtil/types/ts4.7/assets/vinylTestAssets.d.ts +5 -0
  69. package/package.json +10 -10
@@ -5592,9 +5592,6 @@
5592
5592
  function map(arrayLike, transform, thisArg) {
5593
5593
  return Array.prototype.map.call(arrayLike, transform, thisArg);
5594
5594
  }
5595
- function every(arrayLike, predicate, thisArg) {
5596
- return Array.prototype.every.call(arrayLike, predicate, thisArg);
5597
- }
5598
5595
  function first(arrayLike) {
5599
5596
  if (!arrayLike) return void 0;
5600
5597
  return arrayLike[0];
@@ -9956,6 +9953,7 @@ ${this.lastInitStack}` : "";
9956
9953
 
9957
9954
  // src/ad/AdControllerImpl.ts
9958
9955
  var AD_START_TOLERANCE = 0.05;
9956
+ var PLAYOUT_LIMIT_TOLERANCE = 1;
9959
9957
  var defaultAdControllerImplOptions = {
9960
9958
  adLoadTimeout: 15,
9961
9959
  preloadAheadTime: 10
@@ -10446,23 +10444,29 @@ ${this.lastInitStack}` : "";
10446
10444
  });
10447
10445
  }
10448
10446
  /**
10449
- * Ads breaks can have a playout limit that may cut off ad playback.
10450
- * End the ad if that limit is reached.
10447
+ * Enforces the break-level playout limit (X-PLAYOUT-LIMIT): a budget shared
10448
+ * across all of the break's ads. When cumulative playout reaches it, the
10449
+ * whole break ends, dropping any ads that have not started.
10450
+ *
10451
+ * This is deliberately NOT a per-ad cap. Each ad asset is its own source and
10452
+ * ends on its media `ended` (see the 'ended' handler), which is what advances
10453
+ * a multi-ad break. Ending an ad at its declared per-ad DURATION instead would
10454
+ * truncate the real asset whenever a packager under-declares it — e.g. rounds
10455
+ * a 6.732s asset down to a whole-second 6 — cutting off the fractional tail.
10451
10456
  */
10452
10457
  enforcePlayoutLimits(adState) {
10458
+ if (!adState.started) return;
10459
+ const { parentBreak } = adState;
10460
+ const limit = parentBreak.adBreak.playoutLimit;
10461
+ if (limit == null) return;
10453
10462
  const pC = this.deps.playbackController;
10454
- const { ad, parentBreak } = adState;
10455
- if (adState.started) {
10456
- const elapsed = Math.max(0, pC.currentTime - adState.timeStart);
10457
- const adBreak = parentBreak.adBreak;
10458
- const limit = adBreak.playoutLimit;
10459
- if (limit != null && parentBreak.playoutElapsed + elapsed >= limit) {
10460
- parentBreak.clear();
10461
- this.endAd(adState);
10462
- } else if (ad.duration != null && elapsed >= ad.duration) {
10463
- this.endAd(adState);
10464
- }
10463
+ const elapsed = Math.max(0, pC.currentTime - adState.timeStart);
10464
+ if (parentBreak.playoutElapsed + elapsed < limit) return;
10465
+ if (isFinite(pC.duration) && pC.duration > 0 && pC.duration - pC.currentTime <= PLAYOUT_LIMIT_TOLERANCE) {
10466
+ return;
10465
10467
  }
10468
+ parentBreak.clear();
10469
+ this.endAd(adState);
10466
10470
  }
10467
10471
  setPendingBreaks(newPending) {
10468
10472
  this.pendingAdBreaks = newPending.filter(
@@ -14017,6 +14021,10 @@ ${this.lastInitStack}` : "";
14017
14021
  __publicField(this, "bufferingTrackUri", null);
14018
14022
  __publicField(this, "sessionAbort", null);
14019
14023
  __publicField(this, "pendingMessageProps", null);
14024
+ // Armed per track load; latches on the first session so only the
14025
+ // critical-path session's EME/CDM setup is measured.
14026
+ __publicField(this, "keySetupSpanArmed", true);
14027
+ __publicField(this, "keySetupSpanStart", null);
14020
14028
  __publicField(this, "disposer", createDisposer());
14021
14029
  __publicField(this, "checkIsSupportedCached", memoize(
14022
14030
  async (params) => {
@@ -14182,6 +14190,7 @@ ${this.lastInitStack}` : "";
14182
14190
  if (!hasMimeType(drmInfo)) {
14183
14191
  throw new DrmError("Encrypted content must have a mimeType.");
14184
14192
  }
14193
+ this.markKeySetupStart();
14185
14194
  const mediaKeys = await this.attachMediaKeys(drmInfo);
14186
14195
  this.abortIfDisposed();
14187
14196
  const keySystemOptions = (_a3 = this.options.keySystems[mediaKeys.keySystem]) != null ? _a3 : this.options.keySystems["*"];
@@ -14226,6 +14235,28 @@ ${this.lastInitStack}` : "";
14226
14235
  });
14227
14236
  return newSession;
14228
14237
  }
14238
+ /**
14239
+ * Captures the key-setup span start at the top of the session-creation path
14240
+ * (before media keys are attached), latching so only the first session of
14241
+ * the current track load is measured.
14242
+ */
14243
+ markKeySetupStart() {
14244
+ if (!this.keySetupSpanArmed) return;
14245
+ this.keySetupSpanArmed = false;
14246
+ this.keySetupSpanStart = Date.now();
14247
+ }
14248
+ dispatchLoadSpan(kind, startTime, endTime, messageProps) {
14249
+ this.dispatch("loadSpanMeasured", __spreadValues(__spreadValues({
14250
+ kind,
14251
+ startTime,
14252
+ endTime,
14253
+ mimeType: messageProps.mimeType
14254
+ }, messageProps.trackUri != null && {
14255
+ trackUri: messageProps.trackUri
14256
+ }), messageProps.contentType != null && {
14257
+ contentType: messageProps.contentType
14258
+ }));
14259
+ }
14229
14260
  initializeForPlayback(drmInfo, options) {
14230
14261
  var _a3;
14231
14262
  logDebug(this, "initializeForPlayback", drmInfo);
@@ -14249,6 +14280,8 @@ ${this.lastInitStack}` : "";
14249
14280
  this.drmInfo = drmInfo;
14250
14281
  this.bufferingTrackUri = (_a3 = options == null ? void 0 : options.trackUri) != null ? _a3 : null;
14251
14282
  this.sessionAbort = (_b = options == null ? void 0 : options.abort) != null ? _b : null;
14283
+ this.keySetupSpanArmed = true;
14284
+ this.keySetupSpanStart = null;
14252
14285
  }
14253
14286
  /**
14254
14287
  * If the drm metadata contains initialization data for the current key system, create a new session.
@@ -14260,6 +14293,7 @@ ${this.lastInitStack}` : "";
14260
14293
  this.handleError(new DrmError("DRM not supported."));
14261
14294
  return;
14262
14295
  }
14296
+ this.markKeySetupStart();
14263
14297
  const mediaKeys = await this.attachMediaKeys(drmInfo);
14264
14298
  const drmProtection = (_a3 = drmInfo.contentProtections.find((cP) => {
14265
14299
  return cP.keySystem === mediaKeys.keySystem;
@@ -14398,7 +14432,9 @@ ${this.lastInitStack}` : "";
14398
14432
  event,
14399
14433
  // Captured per session, so a concurrent preloaded track's
14400
14434
  // license exchange keeps its own attribution.
14401
- trackUri
14435
+ trackUri,
14436
+ mimeType: drmInfo.mimeType,
14437
+ contentType: drmInfo.contentType
14402
14438
  }),
14403
14439
  LICENSE_TIMEOUT,
14404
14440
  { message: LICENSE_TIMEOUT_MESSAGE }
@@ -14420,6 +14456,15 @@ ${this.lastInitStack}` : "";
14420
14456
  this.pendingMessageProps = messageProps;
14421
14457
  const keySystem = mediaKeys.keySystem;
14422
14458
  logDebug(this, "message", keySystem, event.message.byteLength);
14459
+ if (this.keySetupSpanStart != null) {
14460
+ this.dispatchLoadSpan(
14461
+ "keySetup",
14462
+ this.keySetupSpanStart,
14463
+ Date.now(),
14464
+ messageProps
14465
+ );
14466
+ this.keySetupSpanStart = null;
14467
+ }
14423
14468
  const keySystemOptions = (_a3 = this.options.keySystems[keySystem]) != null ? _a3 : this.options.keySystems["*"];
14424
14469
  const licenseServerOptionsProvider = keySystemOptions == null ? void 0 : keySystemOptions.licenseServer;
14425
14470
  const licenseServerOptions = (_b = clone(await resolveValueProvider(licenseServerOptionsProvider))) != null ? _b : {};
@@ -14441,13 +14486,12 @@ ${this.lastInitStack}` : "";
14441
14486
  challenge
14442
14487
  );
14443
14488
  if (this.disposed) return;
14444
- this.dispatch("loadSpanMeasured", __spreadValues({
14445
- kind: "license",
14446
- startTime: licenseSpanStart,
14447
- endTime: Date.now()
14448
- }, messageProps.trackUri != null && {
14449
- trackUri: messageProps.trackUri
14450
- }));
14489
+ this.dispatchLoadSpan(
14490
+ "license",
14491
+ licenseSpanStart,
14492
+ Date.now(),
14493
+ messageProps
14494
+ );
14451
14495
  this.pendingMessageProps = null;
14452
14496
  if (session.disposed) return;
14453
14497
  logDebug(this, "update");
@@ -15177,7 +15221,7 @@ ${this.lastInitStack}` : "";
15177
15221
  // src/global/environment.ts
15178
15222
  var vinylVersion = parseVersion(
15179
15223
  /* istanbul ignore next */
15180
- "3.1.0"
15224
+ "3.2.1"
15181
15225
  );
15182
15226
 
15183
15227
  // src/global/globalRef.ts
@@ -15334,6 +15378,7 @@ ${this.lastInitStack}` : "";
15334
15378
  ];
15335
15379
 
15336
15380
  // src/playback/createStallMonitor.ts
15381
+ var PROGRESS_EPSILON = 1e-6;
15337
15382
  function createStallMonitor(controller, callbacks, options = {}) {
15338
15383
  var _a3, _b;
15339
15384
  const stallThresholdMs = ((_a3 = options.stallThreshold) != null ? _a3 : 1) * 1e3;
@@ -15342,6 +15387,7 @@ ${this.lastInitStack}` : "";
15342
15387
  let lastProgressAt = Date.now();
15343
15388
  let inStall = false;
15344
15389
  let pollId = null;
15390
+ const { add, dispose } = createDisposer();
15345
15391
  const endStall = (reason) => {
15346
15392
  if (!inStall) return;
15347
15393
  inStall = false;
@@ -15358,6 +15404,7 @@ ${this.lastInitStack}` : "";
15358
15404
  clearInterval(pollId);
15359
15405
  pollId = null;
15360
15406
  };
15407
+ add(stopPoll);
15361
15408
  const startPoll = () => {
15362
15409
  if (pollId != null) return;
15363
15410
  pollId = setInterval(() => {
@@ -15373,17 +15420,18 @@ ${this.lastInitStack}` : "";
15373
15420
  observedPlaying = false;
15374
15421
  stopPoll();
15375
15422
  };
15376
- const { add, dispose } = createDisposer();
15377
15423
  add(
15378
15424
  controller.on("playing", () => {
15379
15425
  if (inStall) endStall("playing" /* PLAYING */);
15380
- else lastProgressAt = Date.now();
15426
+ lastProgressAt = Date.now();
15381
15427
  observedPlaying = true;
15382
15428
  startPoll();
15383
15429
  })
15384
15430
  );
15385
15431
  add(
15386
- controller.on("timeUpdate", () => {
15432
+ controller.on("timeUpdate", ({ previous, current }) => {
15433
+ if (previous != null && current - previous <= PROGRESS_EPSILON)
15434
+ return;
15387
15435
  if (inStall) endStall("playing" /* PLAYING */);
15388
15436
  lastProgressAt = Date.now();
15389
15437
  })
@@ -15391,13 +15439,7 @@ ${this.lastInitStack}` : "";
15391
15439
  add(controller.on("pause", () => suspend("pause" /* PAUSE */)));
15392
15440
  add(controller.on("seeking", () => suspend("seeking" /* SEEKING */)));
15393
15441
  add(controller.on("emptied", () => suspend("emptied" /* EMPTIED */)));
15394
- let stopped = false;
15395
- return () => {
15396
- if (stopped) return;
15397
- stopped = true;
15398
- stopPoll();
15399
- dispose();
15400
- };
15442
+ return dispose;
15401
15443
  }
15402
15444
 
15403
15445
  // src/playback/error/InvalidSeekError.ts
@@ -17743,6 +17785,86 @@ ${this.lastInitStack}` : "";
17743
17785
  return filtered;
17744
17786
  }
17745
17787
 
17788
+ // src/streaming/DescriptorRecord.ts
17789
+ function getDescriptorValue(record2, uri, id) {
17790
+ var _a3, _b;
17791
+ let arr = record2[uri];
17792
+ if (!arr) return null;
17793
+ if (id !== void 0) arr = arr.filter((descriptor) => descriptor.id === id);
17794
+ return (_b = (_a3 = first(arr)) == null ? void 0 : _a3.value) != null ? _b : null;
17795
+ }
17796
+ var descriptorValidator = object({
17797
+ id: string().maybe().optional(),
17798
+ value: string().maybe().optional()
17799
+ });
17800
+ var descriptorRecordValidator = record(string(), array(descriptorValidator).readonly().orUndefined());
17801
+
17802
+ // src/streaming/MediaQualityMetadata.ts
17803
+ var ALL_CONTENT_TYPES = [
17804
+ "video",
17805
+ "audio",
17806
+ "text"
17807
+ ];
17808
+ var RESTRICTABLE_CONTENT_TYPES = [
17809
+ "video",
17810
+ "audio"
17811
+ ];
17812
+ var drmProtectionValidator = descriptorValidator.extend({
17813
+ keySystem: enumOf(DrmKeySystem),
17814
+ pro: string().maybe().optional(),
17815
+ pssh: string().maybe().optional()
17816
+ });
17817
+ var mediaFormatMetadataValidator = object({
17818
+ contentProtections: array(drmProtectionValidator).readonly(),
17819
+ contentType: isOneOf("audio", "video", "text").orNull(),
17820
+ initDataType: string().orNull(),
17821
+ encryptionScheme: string().orNull(),
17822
+ mimeType: string().orNull()
17823
+ });
17824
+ function createEmptyMediaFormatMetadata() {
17825
+ return {
17826
+ contentType: null,
17827
+ mimeType: null,
17828
+ initDataType: null,
17829
+ encryptionScheme: null,
17830
+ contentProtections: []
17831
+ };
17832
+ }
17833
+ function createEmptyMediaQualityMetadata() {
17834
+ return __spreadProps(__spreadValues({}, createEmptyMediaFormatMetadata()), {
17835
+ decoderId: createShortUid(),
17836
+ qualityId: createShortUid(),
17837
+ codecs: null,
17838
+ bandwidth: null,
17839
+ bandwidthTotal: null,
17840
+ supplementalProperties: {},
17841
+ audioSamplingRate: null,
17842
+ frameRate: null,
17843
+ height: null,
17844
+ width: null,
17845
+ lang: null,
17846
+ characteristics: [],
17847
+ groupId: createShortUid(),
17848
+ switchingGroupIds: null
17849
+ });
17850
+ }
17851
+ var mediaQualityMetadataValidator = mediaFormatMetadataValidator.extend({
17852
+ audioSamplingRate: array(number()).readonly().orNull(),
17853
+ bandwidth: number().orNull(),
17854
+ bandwidthTotal: number().orNull(),
17855
+ codecs: string().orNull(),
17856
+ decoderId: string(),
17857
+ frameRate: tuple(number(), number()).readonly().orNull(),
17858
+ height: number().orNull(),
17859
+ qualityId: string(),
17860
+ supplementalProperties: descriptorRecordValidator,
17861
+ width: number().orNull(),
17862
+ lang: string().orNull(),
17863
+ characteristics: array(string()).readonly(),
17864
+ groupId: string(),
17865
+ switchingGroupIds: array(string()).readonly().orNull()
17866
+ });
17867
+
17746
17868
  // src/track/filters/languageFilter.ts
17747
17869
  function throwLanguagesUnsupported() {
17748
17870
  throw new MediaUnsupportedError(
@@ -18024,6 +18146,13 @@ ${this.lastInitStack}` : "";
18024
18146
  }
18025
18147
 
18026
18148
  // src/streaming/mediaTimelineFilter.ts
18149
+ function assertMediaStreamsRetained(original, filtered, throwError) {
18150
+ for (const contentType2 of RESTRICTABLE_CONTENT_TYPES) {
18151
+ if (original.some((q) => q.metadata.contentType === contentType2) && !filtered.some((q) => q.metadata.contentType === contentType2)) {
18152
+ throwError();
18153
+ }
18154
+ }
18155
+ }
18027
18156
  function filterTimelineQualities(filter, throwError, timeline) {
18028
18157
  if (!filter) return timeline;
18029
18158
  const mapMetadataArray = memoize(
@@ -18035,11 +18164,9 @@ ${this.lastInitStack}` : "";
18035
18164
  const filtered = period.qualities.filter(
18036
18165
  (q, index) => filter(q.metadata, index, mapMetadataArray(period.qualities))
18037
18166
  );
18167
+ assertMediaStreamsRetained(period.qualities, filtered, throwError);
18038
18168
  return __spreadProps(__spreadValues({}, period), { qualities: filtered });
18039
18169
  });
18040
- if (periods.some((p) => p.qualities.length === 0)) {
18041
- throwError();
18042
- }
18043
18170
  return __spreadProps(__spreadValues({}, timeline), { periods });
18044
18171
  }
18045
18172
  async function filterTimelineQualitiesAsync(filter, throwError, timeline) {
@@ -18057,11 +18184,9 @@ ${this.lastInitStack}` : "";
18057
18184
  filtered.push(period.qualities[i]);
18058
18185
  }
18059
18186
  }
18187
+ assertMediaStreamsRetained(period.qualities, filtered, throwError);
18060
18188
  periods.push(__spreadProps(__spreadValues({}, period), { qualities: filtered }));
18061
18189
  }
18062
- if (periods.some((p) => p.qualities.length === 0)) {
18063
- throwError();
18064
- }
18065
18190
  return __spreadProps(__spreadValues({}, timeline), { periods });
18066
18191
  }
18067
18192
 
@@ -18135,23 +18260,28 @@ ${this.lastInitStack}` : "";
18135
18260
  if (metadata.contentType !== "audio") return true;
18136
18261
  const samplingRate = last(metadata.audioSamplingRate);
18137
18262
  if (!samplingRate) return true;
18263
+ const { minSampleRate } = options;
18264
+ const maxSampleRate = (_a3 = options.maxSampleRate) != null ? _a3 : options.capabilities.sampleRate;
18138
18265
  const audioRates = () => map(
18139
18266
  array2,
18140
18267
  (item) => item.contentType === "audio" ? last(item.audioSamplingRate) : void 0
18141
18268
  ).filter((rate) => rate != null);
18142
- const { minSampleRate } = options;
18269
+ const outOfBandFallback = () => {
18270
+ const rates = audioRates();
18271
+ const inBand = (rate) => (minSampleRate == null || rate >= minSampleRate) && (!maxSampleRate || rate <= maxSampleRate);
18272
+ if (rates.some(inBand)) return void 0;
18273
+ const decodable = maxSampleRate ? rates.filter((rate) => rate <= maxSampleRate) : rates;
18274
+ return decodable.length ? max(decodable) : min(rates);
18275
+ };
18143
18276
  if (minSampleRate != null && samplingRate < minSampleRate) {
18144
- const rates2 = audioRates();
18145
- return every(rates2, (rate) => rate < minSampleRate) && samplingRate === max(rates2);
18277
+ return samplingRate === outOfBandFallback();
18146
18278
  }
18147
- const maxSampleRate = (_a3 = options.maxSampleRate) != null ? _a3 : options.capabilities.sampleRate;
18148
18279
  if (!maxSampleRate) return true;
18149
18280
  if (hasBrowser("Firefox" /* FIREFOX */)) {
18150
18281
  return samplingRate <= 48e3;
18151
18282
  }
18152
18283
  if (samplingRate <= maxSampleRate) return true;
18153
- const rates = audioRates();
18154
- return every(rates, (rate) => rate > maxSampleRate) && samplingRate === min(rates);
18284
+ return samplingRate === outOfBandFallback();
18155
18285
  }
18156
18286
 
18157
18287
  // src/util/media/codecOverrides.ts
@@ -18238,20 +18368,6 @@ ${this.lastInitStack}` : "";
18238
18368
  });
18239
18369
  }
18240
18370
 
18241
- // src/streaming/DescriptorRecord.ts
18242
- function getDescriptorValue(record2, uri, id) {
18243
- var _a3, _b;
18244
- let arr = record2[uri];
18245
- if (!arr) return null;
18246
- if (id !== void 0) arr = arr.filter((descriptor) => descriptor.id === id);
18247
- return (_b = (_a3 = first(arr)) == null ? void 0 : _a3.value) != null ? _b : null;
18248
- }
18249
- var descriptorValidator = object({
18250
- id: string().maybe().optional(),
18251
- value: string().maybe().optional()
18252
- });
18253
- var descriptorRecordValidator = record(string(), array(descriptorValidator).readonly().orUndefined());
18254
-
18255
18371
  // src/streaming/ManifestControllerImpl.ts
18256
18372
  var ManifestControllerImpl = class {
18257
18373
  constructor(deps) {
@@ -18351,72 +18467,6 @@ ${this.lastInitStack}` : "";
18351
18467
  }
18352
18468
  };
18353
18469
 
18354
- // src/streaming/MediaQualityMetadata.ts
18355
- var ALL_CONTENT_TYPES = [
18356
- "video",
18357
- "audio",
18358
- "text"
18359
- ];
18360
- var RESTRICTABLE_CONTENT_TYPES = [
18361
- "video",
18362
- "audio"
18363
- ];
18364
- var drmProtectionValidator = descriptorValidator.extend({
18365
- keySystem: enumOf(DrmKeySystem),
18366
- pro: string().maybe().optional(),
18367
- pssh: string().maybe().optional()
18368
- });
18369
- var mediaFormatMetadataValidator = object({
18370
- contentProtections: array(drmProtectionValidator).readonly(),
18371
- contentType: isOneOf("audio", "video", "text").orNull(),
18372
- initDataType: string().orNull(),
18373
- encryptionScheme: string().orNull(),
18374
- mimeType: string().orNull()
18375
- });
18376
- function createEmptyMediaFormatMetadata() {
18377
- return {
18378
- contentType: null,
18379
- mimeType: null,
18380
- initDataType: null,
18381
- encryptionScheme: null,
18382
- contentProtections: []
18383
- };
18384
- }
18385
- function createEmptyMediaQualityMetadata() {
18386
- return __spreadProps(__spreadValues({}, createEmptyMediaFormatMetadata()), {
18387
- decoderId: createShortUid(),
18388
- qualityId: createShortUid(),
18389
- codecs: null,
18390
- bandwidth: null,
18391
- bandwidthTotal: null,
18392
- supplementalProperties: {},
18393
- audioSamplingRate: null,
18394
- frameRate: null,
18395
- height: null,
18396
- width: null,
18397
- lang: null,
18398
- characteristics: [],
18399
- groupId: createShortUid(),
18400
- switchingGroupIds: null
18401
- });
18402
- }
18403
- var mediaQualityMetadataValidator = mediaFormatMetadataValidator.extend({
18404
- audioSamplingRate: array(number()).readonly().orNull(),
18405
- bandwidth: number().orNull(),
18406
- bandwidthTotal: number().orNull(),
18407
- codecs: string().orNull(),
18408
- decoderId: string(),
18409
- frameRate: tuple(number(), number()).readonly().orNull(),
18410
- height: number().orNull(),
18411
- qualityId: string(),
18412
- supplementalProperties: descriptorRecordValidator,
18413
- width: number().orNull(),
18414
- lang: string().orNull(),
18415
- characteristics: array(string()).readonly(),
18416
- groupId: string(),
18417
- switchingGroupIds: array(string()).readonly().orNull()
18418
- });
18419
-
18420
18470
  // src/streaming/MediaTimeline.ts
18421
18471
  function getMediaPeriodAtTime(timeline, time) {
18422
18472
  const index = sortedInsertionIndex(timeline.periods, time, (time2, period2) => {
@@ -20529,10 +20579,7 @@ ${this.lastInitStack}` : "";
20529
20579
  this.deps = deps;
20530
20580
  add(
20531
20581
  deps.contentTypesValue.onData((contentTypesPromise) => {
20532
- contentTypesPromise.then((value) => {
20533
- this.setContentTypes(value);
20534
- this.validateRequiredContentTypesPlayable();
20535
- }).catch(this.errorHandler);
20582
+ contentTypesPromise.then((value) => this.setContentTypes(value)).catch(this.errorHandler);
20536
20583
  })
20537
20584
  );
20538
20585
  this.on("streamingQualityChange", (event) => {
@@ -20633,33 +20680,8 @@ ${this.lastInitStack}` : "";
20633
20680
  });
20634
20681
  }
20635
20682
  }
20636
- this.validateRequiredContentTypesPlayable();
20637
20683
  });
20638
20684
  }
20639
- /**
20640
- * Fails fast when a media content type the manifest declared (audio/video)
20641
- * has no playable qualities left after timeline filtering. Its stream can
20642
- * never create a source buffer, so without this the track waits forever for
20643
- * a source buffer that never arrives, surfacing only as an opaque
20644
- * `readyToAppend` timeout. Runs once both the required content types and the
20645
- * filtered timeline qualities are known (either order).
20646
- */
20647
- validateRequiredContentTypesPlayable() {
20648
- const qualities = this._qualities;
20649
- if (!qualities) return;
20650
- const present = new Set(qualities.map((q) => q.contentType));
20651
- for (const contentType2 of this._contentTypes) {
20652
- if (!present.has(contentType2)) {
20653
- this.errorHandler(
20654
- new MediaUnsupportedError(
20655
- `No playable ${contentType2} rendition`,
20656
- `no-playable-${contentType2}`
20657
- )
20658
- );
20659
- return;
20660
- }
20661
- }
20662
- }
20663
20685
  async getAds() {
20664
20686
  const timeline = await this.deps.mediaTimeline.value;
20665
20687
  return {
@@ -24081,6 +24103,7 @@ ${this.lastInitStack}` : "";
24081
24103
  playerRegistryRef.value.addPlayer(this);
24082
24104
  this.redispatchSubControllerEvents();
24083
24105
  this.redispatchCurrentTrackEvents();
24106
+ this.initializeLoadSpanLogging();
24084
24107
  this.initializeAutoResetHandling();
24085
24108
  }
24086
24109
  get [Symbol.toStringTag]() {
@@ -24098,6 +24121,26 @@ ${this.lastInitStack}` : "";
24098
24121
  get autoResetController() {
24099
24122
  return this.deps.autoResetController;
24100
24123
  }
24124
+ /**
24125
+ * Logs each republished load span once at the verbose level, formatting its
24126
+ * bounds as ISO date strings and its duration in milliseconds.
24127
+ */
24128
+ initializeLoadSpanLogging() {
24129
+ this.disposer.add(
24130
+ this.on("loadSpan", (span) => {
24131
+ if (getLogLevel() > 0 /* VERBOSE */) return;
24132
+ logVerbose(this, "loadSpan", {
24133
+ kind: span.kind,
24134
+ trackUri: span.trackUri,
24135
+ mimeType: span.mimeType,
24136
+ contentType: span.contentType,
24137
+ startTime: new Date(span.startTime).toISOString(),
24138
+ endTime: new Date(span.endTime).toISOString(),
24139
+ durationMs: (span.endTime - span.startTime).toFixed(2)
24140
+ });
24141
+ })
24142
+ );
24143
+ }
24101
24144
  redispatchSubControllerEvents() {
24102
24145
  const add = this.disposer.add;
24103
24146
  add(
@@ -24913,7 +24956,8 @@ ${this.lastInitStack}` : "";
24913
24956
  var vinylTestAssets = {
24914
24957
  prog: {
24915
24958
  libmp3lame_60s_2ch_16bit_44100Hz_48kbps: `https://assets.dev.vinyl.music.amazon.dev/prog/libmp3lame_60s_2ch_16bit_44100Hz_48kbps.mp3`,
24916
- libmp3lame_1800s_2ch_16bit_44100Hz_48kbps: `https://assets.dev.vinyl.music.amazon.dev/prog/libmp3lame_1800s_2ch_16bit_44100Hz_48kbps.mp3`
24959
+ libmp3lame_1800s_2ch_16bit_44100Hz_48kbps: `https://assets.dev.vinyl.music.amazon.dev/prog/libmp3lame_1800s_2ch_16bit_44100Hz_48kbps.mp3`,
24960
+ libmp3lame_1s_2ch_16bit_44100Hz_48kbps: `https://assets.dev.vinyl.music.amazon.dev/prog/libmp3lame_1s_2ch_16bit_44100Hz_48kbps.mp3`
24917
24961
  },
24918
24962
  dash: {
24919
24963
  live_static_aac_opus_flac_60s_segmentTemplate: `https://assets.dev.vinyl.music.amazon.dev/dash/live_static_aac_opus_flac_60s_segmentTemplate/manifest.mpd`,
@@ -24928,6 +24972,10 @@ ${this.lastInitStack}` : "";
24928
24972
  live_static_aac_opus_flac_60s_segmentBase_widevine_cl10: `https://assets.dev.vinyl.music.amazon.dev/dash/live_static_aac_opus_flac_60s_segmentBase_widevine_cl10/manifest.mpd`,
24929
24973
  live_static_aac_opus_flac_60s_segmentBase_widevine: `https://assets.dev.vinyl.music.amazon.dev/dash/live_static_aac_opus_flac_60s_segmentBase_widevine/manifest.mpd`,
24930
24974
  live_static_aac_1800s_segmentBase_widevine_cl30: `https://assets.dev.vinyl.music.amazon.dev/dash/live_static_aac_1800s_segmentBase_widevine_cl30/manifest.mpd`,
24975
+ // Edge-case tracks: ultra-short 1s assets (audio-only + video), no
24976
+ // captions or translations.
24977
+ edge_static_aac_1s_segmentTemplate: `https://assets.dev.vinyl.music.amazon.dev/dash/edge_static_aac_1s_segmentTemplate/manifest.mpd`,
24978
+ edge_static_video_1s_segmentTemplate: `https://assets.dev.vinyl.music.amazon.dev/dash/edge_static_video_1s_segmentTemplate/manifest.mpd`,
24931
24979
  // Ad-break assets (see DMTestAssetBuilder src/adBreaks): a ~3-min
24932
24980
  // labeled ABR ladder with five-language narration, full + forced
24933
24981
  // captions. Base content only — DASH ad-break scenarios are not
@@ -24947,6 +24995,10 @@ ${this.lastInitStack}` : "";
24947
24995
  live_static_video_60s_4s_mpegts: `https://assets.dev.vinyl.music.amazon.dev/hls/live_static_video_60s_4s_mpegts/main.m3u8`,
24948
24996
  live_static_video_audio_60s_2s_mpegts: `https://assets.dev.vinyl.music.amazon.dev/hls/live_static_video_audio_60s_2s_mpegts/main.m3u8`,
24949
24997
  live_static_video_audio_60s_4s_mpegts: `https://assets.dev.vinyl.music.amazon.dev/hls/live_static_video_audio_60s_4s_mpegts/main.m3u8`,
24998
+ // Edge-case tracks: ultra-short 1s assets (audio-only + video), no
24999
+ // captions or translations.
25000
+ edge_static_aac_1s: `https://assets.dev.vinyl.music.amazon.dev/hls/edge_static_aac_1s/main.m3u8`,
25001
+ edge_static_video_1s: `https://assets.dev.vinyl.music.amazon.dev/hls/edge_static_video_1s/main.m3u8`,
24950
25002
  // Ad-break assets: base content + HLS Interstitial scenario variants.
24951
25003
  // `*_av` = video+audio, `*_audio` = audio-only; each `__<scenario>`
24952
25004
  // injects a different ad-break configuration. See DMTestAssetBuilder
@@ -27707,7 +27759,6 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27707
27759
  makeBreak({
27708
27760
  startTime: 10,
27709
27761
  duration: 20,
27710
- // Null per-ad duration so the per-ad cap does not end it.
27711
27762
  ads: [
27712
27763
  {
27713
27764
  id: "a1",
@@ -27811,8 +27862,8 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27811
27862
  });
27812
27863
  });
27813
27864
  describe("ad duration and playout limits", () => {
27814
- it("advances to the next ad when one reaches its declared duration", async () => {
27815
- var _a3, _b;
27865
+ it("advances to the next ad on the ad media ended, not its declared duration", async () => {
27866
+ var _a3, _b, _c;
27816
27867
  const c = createController();
27817
27868
  await setContent(
27818
27869
  c,
@@ -27825,6 +27876,7 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27825
27876
  id: "a1",
27826
27877
  startTime: 0,
27827
27878
  duration: 5,
27879
+ // declared, server-rounded
27828
27880
  uri: "ad1.m3u8"
27829
27881
  },
27830
27882
  {
@@ -27843,7 +27895,51 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27843
27895
  playbackController.dispatch("playing", {});
27844
27896
  updateTime(6);
27845
27897
  await flush();
27846
- expect((_b = c.currentAd) == null ? void 0 : _b.id).toBe("a2");
27898
+ expect((_b = c.currentAd) == null ? void 0 : _b.id).toBe("a1");
27899
+ playbackController.dispatch("ended", {
27900
+ previous: false,
27901
+ current: true
27902
+ });
27903
+ await flush();
27904
+ expect((_c = c.currentAd) == null ? void 0 : _c.id).toBe("a2");
27905
+ });
27906
+ it("does not cut the final/only ad at its declared duration; it ends on media ended", async () => {
27907
+ var _a3, _b;
27908
+ const c = createController();
27909
+ const ended = [];
27910
+ c.on("adEnded", (e) => ended.push(e.ad.id));
27911
+ await setContent(
27912
+ c,
27913
+ trackAds(
27914
+ makeBreak({
27915
+ startTime: 0,
27916
+ duration: 30,
27917
+ ads: [
27918
+ {
27919
+ id: "a1",
27920
+ startTime: 0,
27921
+ duration: 6,
27922
+ // declared, server-rounded
27923
+ uri: "ad1.m3u8"
27924
+ }
27925
+ ]
27926
+ })
27927
+ )
27928
+ );
27929
+ updateTime(0);
27930
+ await flush();
27931
+ expect((_a3 = c.currentAd) == null ? void 0 : _a3.id).toBe("a1");
27932
+ playbackController.dispatch("playing", {});
27933
+ updateTime(6.5);
27934
+ await flush();
27935
+ expect((_b = c.currentAd) == null ? void 0 : _b.id).toBe("a1");
27936
+ expect(ended).toEqual([]);
27937
+ playbackController.dispatch("ended", {
27938
+ previous: false,
27939
+ current: true
27940
+ });
27941
+ expect(ended).toEqual(["a1"]);
27942
+ expect(c.currentAdBreak).toBeNull();
27847
27943
  });
27848
27944
  it("ends the whole break when the playout limit is reached, dropping remaining ads", async () => {
27849
27945
  var _a3;
@@ -27884,6 +27980,70 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27884
27980
  expect(c.currentAdBreak).toBeNull();
27885
27981
  expect(resumes.at(-1)).toBe(0);
27886
27982
  });
27983
+ it("does not cut at the playout limit when the ad is within tolerance of its media end", async () => {
27984
+ var _a3;
27985
+ const c = createController();
27986
+ await setContent(
27987
+ c,
27988
+ trackAds(
27989
+ makeBreak({
27990
+ startTime: 0,
27991
+ duration: null,
27992
+ playoutLimit: 8,
27993
+ ads: [
27994
+ {
27995
+ id: "a1",
27996
+ startTime: 0,
27997
+ duration: 20,
27998
+ uri: "ad1.m3u8"
27999
+ }
28000
+ ]
28001
+ })
28002
+ )
28003
+ );
28004
+ updateTime(0);
28005
+ await flush();
28006
+ playbackController.dispatch("playing", {});
28007
+ playbackController.duration = 8.5;
28008
+ updateTime(8);
28009
+ await flush();
28010
+ expect(c.currentAdBreak).not.toBeNull();
28011
+ expect((_a3 = c.currentAd) == null ? void 0 : _a3.id).toBe("a1");
28012
+ playbackController.dispatch("ended", {
28013
+ previous: false,
28014
+ current: true
28015
+ });
28016
+ await flush();
28017
+ expect(c.currentAdBreak).toBeNull();
28018
+ });
28019
+ it("cuts at the playout limit when the ad has more than the tolerance of media left", async () => {
28020
+ const c = createController();
28021
+ await setContent(
28022
+ c,
28023
+ trackAds(
28024
+ makeBreak({
28025
+ startTime: 0,
28026
+ duration: null,
28027
+ playoutLimit: 8,
28028
+ ads: [
28029
+ {
28030
+ id: "a1",
28031
+ startTime: 0,
28032
+ duration: 20,
28033
+ uri: "ad1.m3u8"
28034
+ }
28035
+ ]
28036
+ })
28037
+ )
28038
+ );
28039
+ updateTime(0);
28040
+ await flush();
28041
+ playbackController.dispatch("playing", {});
28042
+ playbackController.duration = 20;
28043
+ updateTime(8);
28044
+ await flush();
28045
+ expect(c.currentAdBreak).toBeNull();
28046
+ });
27887
28047
  });
27888
28048
  describe("CUE=ONCE and replay", () => {
27889
28049
  it("replays a non-ONCE break after the playhead seeks back before it", async () => {
@@ -31554,7 +31714,7 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
31554
31714
  drmController.setBufferingDrmInfo(drmInfo);
31555
31715
  await emitEncrypted(new Uint8Array([1, 2, 3]), "cenc");
31556
31716
  await emitMessage(0, new ArrayBuffer(1));
31557
- expect(spy).toHaveBeenCalledOnceWith(
31717
+ expect(spy).toHaveBeenCalledWith(
31558
31718
  objectContaining2({
31559
31719
  kind: "license",
31560
31720
  startTime: any4(Number),
@@ -31562,22 +31722,65 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
31562
31722
  })
31563
31723
  );
31564
31724
  });
31725
+ it("measures the key setup, contiguous with and before the license", async () => {
31726
+ licenseProvider.and.resolveTo(new Uint8Array([1]).buffer);
31727
+ const spy = createEventSpy(drmController, "loadSpanMeasured");
31728
+ drmController.setBufferingDrmInfo(drmInfo, { trackUri: "track-a" });
31729
+ await emitEncrypted(new Uint8Array([1, 2, 3]), "cenc");
31730
+ await emitMessage(0, new ArrayBuffer(1));
31731
+ const spans = spy.calls.all().map((call) => call.args[0]);
31732
+ expect(spans.map((span) => span.kind)).toEqual([
31733
+ "keySetup",
31734
+ "license"
31735
+ ]);
31736
+ const [keySetup, license] = spans;
31737
+ expect(keySetup.trackUri).toEqual("track-a");
31738
+ expect(keySetup.endTime).toEqual(license.startTime);
31739
+ });
31740
+ it("carries the content mime and content type on the license span", async () => {
31741
+ licenseProvider.and.resolveTo(new Uint8Array([1]).buffer);
31742
+ const spy = createEventSpy(drmController, "loadSpanMeasured");
31743
+ drmController.setBufferingDrmInfo(drmInfo);
31744
+ await emitEncrypted(new Uint8Array([1, 2, 3]), "cenc");
31745
+ await emitMessage(0, new ArrayBuffer(1));
31746
+ expect(spy).toHaveBeenCalledWith(
31747
+ objectContaining2({
31748
+ kind: "license",
31749
+ mimeType: "audio/mp4",
31750
+ contentType: "audio"
31751
+ })
31752
+ );
31753
+ });
31565
31754
  it("does not measure a failed license exchange", async () => {
31566
31755
  licenseProvider.and.rejectWith(new Error("license failed"));
31567
31756
  const spy = createEventSpy(drmController, "loadSpanMeasured");
31568
31757
  drmController.setBufferingDrmInfo(drmInfo);
31569
31758
  await emitEncrypted(new Uint8Array([1, 2, 3]), "cenc");
31570
31759
  await emitMessage(0, new ArrayBuffer(1));
31571
- expect(spy).not.toHaveBeenCalled();
31760
+ expect(spy).not.toHaveBeenCalledWith(
31761
+ objectContaining2({ kind: "license" })
31762
+ );
31572
31763
  errorSpy.calls.reset();
31573
31764
  });
31765
+ it("measures key setup only for the first session of a track load", async () => {
31766
+ licenseProvider.and.resolveTo(new Uint8Array([1]).buffer);
31767
+ const spy = createEventSpy(drmController, "loadSpanMeasured");
31768
+ drmController.setBufferingDrmInfo(drmInfo, { trackUri: "track-a" });
31769
+ await emitEncrypted(new Uint8Array([1]), "cenc");
31770
+ await emitEncrypted(new Uint8Array([2]), "cenc");
31771
+ expect(mediaKeys.createSession).toHaveBeenCalledTimes(2);
31772
+ await emitMessage(0, new ArrayBuffer(1));
31773
+ await emitMessage(1, new ArrayBuffer(1));
31774
+ const keySetupSpans = spy.calls.all().filter((call) => call.args[0].kind === "keySetup");
31775
+ expect(keySetupSpans.length).toEqual(1);
31776
+ });
31574
31777
  it("attributes the license span to the buffering track uri", async () => {
31575
31778
  licenseProvider.and.resolveTo(new Uint8Array([1]).buffer);
31576
31779
  const spy = createEventSpy(drmController, "loadSpanMeasured");
31577
31780
  drmController.setBufferingDrmInfo(drmInfo, { trackUri: "track-a" });
31578
31781
  await emitEncrypted(new Uint8Array([1, 2, 3]), "cenc");
31579
31782
  await emitMessage(0, new ArrayBuffer(1));
31580
- expect(spy).toHaveBeenCalledOnceWith(
31783
+ expect(spy).toHaveBeenCalledWith(
31581
31784
  objectContaining2({
31582
31785
  kind: "license",
31583
31786
  trackUri: "track-a"
@@ -31606,16 +31809,10 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
31606
31809
  expect(mediaKeys.createSession).toHaveBeenCalledTimes(2);
31607
31810
  await emitMessage(1, new ArrayBuffer(1));
31608
31811
  await emitMessage(0, new ArrayBuffer(1));
31609
- expect(spy).toHaveBeenCalledTimes(2);
31610
- expect(spy).toHaveBeenCalledWith(
31611
- objectContaining2({
31612
- kind: "license",
31613
- trackUri: "track-preloaded"
31614
- })
31615
- );
31616
- expect(spy).toHaveBeenCalledWith(
31617
- objectContaining2({ kind: "license", trackUri: "track-active" })
31618
- );
31812
+ const licenseTrackUris = spy.calls.all().map((call) => call.args[0]).filter((span) => span.kind === "license").map((span) => span.trackUri);
31813
+ expect(licenseTrackUris.length).toEqual(2);
31814
+ expect(licenseTrackUris).toContain("track-preloaded");
31815
+ expect(licenseTrackUris).toContain("track-active");
31619
31816
  });
31620
31817
  });
31621
31818
  describe("serverCertificate", () => {
@@ -33128,10 +33325,38 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
33128
33325
  expect(onStallEntered).toHaveBeenCalledTimes(2);
33129
33326
  expect(onStallEnded).toHaveBeenCalledTimes(2);
33130
33327
  });
33328
+ it("does not end and re-detect a stall on a non-advancing timeUpdate", async () => {
33329
+ monitor();
33330
+ host.dispatch("playing", {});
33331
+ host.dispatch("timeUpdate", { previous: 4, current: 5 });
33332
+ await poll2(4);
33333
+ expect(onStallEntered).toHaveBeenCalledTimes(1);
33334
+ host.dispatch("timeUpdate", { previous: 5, current: 5 });
33335
+ host.dispatch("timeUpdate", { previous: 5, current: 4.99 });
33336
+ host.dispatch("timeUpdate", { previous: 5, current: 5 + 1e-9 });
33337
+ await poll2(4);
33338
+ expect(onStallEnded).not.toHaveBeenCalled();
33339
+ expect(onStallEntered).toHaveBeenCalledTimes(1);
33340
+ host.dispatch("timeUpdate", { previous: 5, current: 6 });
33341
+ expect(onStallEnded).toHaveBeenCalledTimes(1);
33342
+ expect(onStallEntered).toHaveBeenCalledTimes(1);
33343
+ });
33344
+ it("does not re-detect a stall immediately after resuming via a playing event", async () => {
33345
+ monitor();
33346
+ host.dispatch("playing", {});
33347
+ host.dispatch("timeUpdate", { previous: 0, current: 1 });
33348
+ await poll2(4);
33349
+ expect(onStallEntered).toHaveBeenCalledTimes(1);
33350
+ host.dispatch("playing", {});
33351
+ expect(onStallEnded).toHaveBeenCalledTimes(1);
33352
+ await poll2(3);
33353
+ expect(onStallEntered).toHaveBeenCalledTimes(1);
33354
+ });
33131
33355
  it("stops reporting after disposal", async () => {
33132
33356
  monitor();
33133
33357
  host.dispatch("playing", {});
33134
33358
  stop();
33359
+ stop = void 0;
33135
33360
  await poll2(8);
33136
33361
  expect(onStallEntered).not.toHaveBeenCalled();
33137
33362
  });
@@ -38396,105 +38621,101 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
38396
38621
  getSegment: () => Promise.resolve(null)
38397
38622
  };
38398
38623
  }
38399
- describe("filterTimelineQualities", () => {
38400
- const timeline = {
38401
- periods: [
38402
- {
38403
- startTime: 0,
38404
- endTime: 10,
38405
- qualities: [
38406
- createQuality2("audio", 128e3),
38407
- createQuality2("audio", 256e3),
38408
- createQuality2("video", 5e5)
38409
- ]
38410
- }
38411
- ],
38624
+ function timelineOf2(qualities) {
38625
+ return {
38626
+ periods: [{ startTime: 0, endTime: 10, qualities: [...qualities] }],
38412
38627
  minBufferTime: 2,
38413
38628
  getAdBreaks: () => Promise.resolve([]),
38414
38629
  getDuration: () => Promise.resolve(Infinity)
38415
38630
  };
38631
+ }
38632
+ function contentTypesOf(timeline) {
38633
+ return new Set(
38634
+ timeline.periods[0].qualities.map((q) => q.metadata.contentType)
38635
+ );
38636
+ }
38637
+ function throwEmpty() {
38638
+ throw new Error("no qualities");
38639
+ }
38640
+ describe("filterTimelineQualities", () => {
38641
+ const timeline = timelineOf2([
38642
+ createQuality2("audio", 128e3),
38643
+ createQuality2("audio", 256e3),
38644
+ createQuality2("video", 5e5)
38645
+ ]);
38416
38646
  it("returns the timeline unchanged when the filter is null", () => {
38417
- const result = filterTimelineQualities(
38418
- null,
38419
- () => {
38420
- throw new Error("empty");
38421
- },
38422
- timeline
38423
- );
38647
+ const result = filterTimelineQualities(null, throwEmpty, timeline);
38424
38648
  expect(result).toBe(timeline);
38425
38649
  });
38426
- it("filters qualities by predicate", () => {
38650
+ it("filters qualities by predicate while keeping every present stream", () => {
38427
38651
  const result = filterTimelineQualities(
38428
- (q) => q.contentType === "audio",
38429
- () => {
38430
- throw new Error("empty");
38431
- },
38652
+ (q) => q.bandwidth !== 256e3,
38653
+ throwEmpty,
38432
38654
  timeline
38433
38655
  );
38434
38656
  expect(result.periods[0].qualities.length).toBe(2);
38435
- expect(
38436
- result.periods[0].qualities.every(
38437
- (q) => q.metadata.contentType === "audio"
38438
- )
38439
- ).toBeTrue();
38657
+ expect(contentTypesOf(result)).toEqual(/* @__PURE__ */ new Set(["audio", "video"]));
38440
38658
  });
38441
38659
  it("preserves minBufferTime", () => {
38442
- const result = filterTimelineQualities(
38443
- () => true,
38444
- () => {
38445
- throw new Error("empty");
38446
- },
38447
- timeline
38448
- );
38660
+ const result = filterTimelineQualities(() => true, throwEmpty, timeline);
38449
38661
  expect(result.minBufferTime).toBe(2);
38450
38662
  });
38451
38663
  it("throws when filtering removes all qualities", () => {
38664
+ expect(
38665
+ () => filterTimelineQualities(() => false, throwEmpty, timeline)
38666
+ ).toThrowError("no qualities");
38667
+ });
38668
+ it("throws when filtering drops a present video stream to zero", () => {
38452
38669
  expect(
38453
38670
  () => filterTimelineQualities(
38454
- () => false,
38455
- () => {
38456
- throw new Error("no qualities");
38457
- },
38671
+ (q) => q.contentType === "audio",
38672
+ throwEmpty,
38458
38673
  timeline
38459
38674
  )
38460
38675
  ).toThrowError("no qualities");
38461
38676
  });
38677
+ it("throws when filtering drops a present audio stream to zero", () => {
38678
+ expect(
38679
+ () => filterTimelineQualities(
38680
+ (q) => q.contentType === "video",
38681
+ throwEmpty,
38682
+ timeline
38683
+ )
38684
+ ).toThrowError("no qualities");
38685
+ });
38686
+ it("does not throw when losing only an optional text sidecar stream", () => {
38687
+ const withText = timelineOf2([
38688
+ createQuality2("audio", 128e3),
38689
+ createQuality2("video", 5e5),
38690
+ createQuality2("text", 1e3)
38691
+ ]);
38692
+ const result = filterTimelineQualities(
38693
+ (q) => q.contentType !== "text",
38694
+ throwEmpty,
38695
+ withText
38696
+ );
38697
+ expect(contentTypesOf(result)).toEqual(/* @__PURE__ */ new Set(["audio", "video"]));
38698
+ });
38462
38699
  });
38463
38700
  describe("filterTimelineQualitiesAsync", () => {
38464
- const timeline = {
38465
- periods: [
38466
- {
38467
- startTime: 0,
38468
- endTime: 10,
38469
- qualities: [
38470
- createQuality2("audio", 128e3),
38471
- createQuality2("video", 5e5)
38472
- ]
38473
- }
38474
- ],
38475
- minBufferTime: 2,
38476
- getAdBreaks: () => Promise.resolve([]),
38477
- getDuration: () => Promise.resolve(Infinity)
38478
- };
38479
- it("filters qualities by async predicate", async () => {
38701
+ const timeline = timelineOf2([
38702
+ createQuality2("audio", 128e3),
38703
+ createQuality2("audio", 256e3),
38704
+ createQuality2("video", 5e5)
38705
+ ]);
38706
+ it("filters qualities by async predicate while keeping every present stream", async () => {
38480
38707
  const result = await filterTimelineQualitiesAsync(
38481
- (q) => Promise.resolve(q.contentType === "audio"),
38482
- () => {
38483
- throw new Error("empty");
38484
- },
38708
+ (q) => Promise.resolve(q.bandwidth !== 256e3),
38709
+ throwEmpty,
38485
38710
  timeline
38486
38711
  );
38487
- expect(result.periods[0].qualities.length).toBe(1);
38488
- expect(result.periods[0].qualities[0].metadata.contentType).toBe(
38489
- "audio"
38490
- );
38712
+ expect(result.periods[0].qualities.length).toBe(2);
38713
+ expect(contentTypesOf(result)).toEqual(/* @__PURE__ */ new Set(["audio", "video"]));
38491
38714
  });
38492
38715
  it("preserves minBufferTime", async () => {
38493
38716
  const result = await filterTimelineQualitiesAsync(
38494
38717
  () => Promise.resolve(true),
38495
- () => {
38496
- throw new Error("empty");
38497
- },
38718
+ throwEmpty,
38498
38719
  timeline
38499
38720
  );
38500
38721
  expect(result.minBufferTime).toBe(2);
@@ -38503,13 +38724,33 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
38503
38724
  await expectAsync(
38504
38725
  filterTimelineQualitiesAsync(
38505
38726
  () => Promise.resolve(false),
38506
- () => {
38507
- throw new Error("no qualities");
38508
- },
38727
+ throwEmpty,
38728
+ timeline
38729
+ )
38730
+ ).toBeRejectedWithError("no qualities");
38731
+ });
38732
+ it("throws when filtering drops a present video stream to zero", async () => {
38733
+ await expectAsync(
38734
+ filterTimelineQualitiesAsync(
38735
+ (q) => Promise.resolve(q.contentType === "audio"),
38736
+ throwEmpty,
38509
38737
  timeline
38510
38738
  )
38511
38739
  ).toBeRejectedWithError("no qualities");
38512
38740
  });
38741
+ it("does not throw when losing only an optional text sidecar stream", async () => {
38742
+ const withText = timelineOf2([
38743
+ createQuality2("audio", 128e3),
38744
+ createQuality2("video", 5e5),
38745
+ createQuality2("text", 1e3)
38746
+ ]);
38747
+ const result = await filterTimelineQualitiesAsync(
38748
+ (q) => Promise.resolve(q.contentType !== "text"),
38749
+ throwEmpty,
38750
+ withText
38751
+ );
38752
+ expect(contentTypesOf(result)).toEqual(/* @__PURE__ */ new Set(["audio", "video"]));
38753
+ });
38513
38754
  });
38514
38755
 
38515
38756
  // test/unit/streaming/mediaTimelineLanguageFilter.test.ts
@@ -39341,7 +39582,8 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
39341
39582
  expect(track2.qualities).toEqual([qualityMetadata]);
39342
39583
  expect(track2.qualitiesUnfiltered).toEqual([qualityMetadata]);
39343
39584
  });
39344
- it("fails fast when a required content type is entirely filtered out", async () => {
39585
+ it("plays the surviving content type when another is entirely filtered out", async () => {
39586
+ deps.contentTypesValue.value = Promise.resolve(/* @__PURE__ */ new Set(["video"]));
39345
39587
  const videoQuality2 = __spreadProps(__spreadValues({}, createEmptyMediaQualityMetadata()), {
39346
39588
  contentType: "video",
39347
39589
  qualityId: "v1"
@@ -39365,10 +39607,10 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
39365
39607
  });
39366
39608
  track2 = createTrack();
39367
39609
  await flushPromises();
39368
- expect(track2.error).toEqual(any14(MediaUnsupportedError));
39369
- expect(track2.error.code).toBe(
39370
- "no-playable-audio"
39371
- );
39610
+ expect(track2.error).toBeNull();
39611
+ expect(track2.contentTypes).toEqual(/* @__PURE__ */ new Set(["video"]));
39612
+ expect(getStream("video")).not.toBeNull();
39613
+ expect(getStream("audio")).toBeNull();
39372
39614
  });
39373
39615
  it("reads qualities from the filtered timeline but qualitiesUnfiltered from the raw one", async () => {
39374
39616
  const makeTimeline = (qualities) => ({
@@ -47894,6 +48136,25 @@ x`);
47894
48136
  expect(withinMin(mid, 1, [low, mid])).toBe(true);
47895
48137
  expect(withinMin(low, 0, [low, mid])).toBe(false);
47896
48138
  });
48139
+ it("keeps the highest when every rate (of three) is below the floor", () => {
48140
+ const low = audioQuality2([16e3]);
48141
+ const mid = audioQuality2([24e3]);
48142
+ const high = audioQuality2([32e3]);
48143
+ expect(withinMin(high, 2, [low, mid, high])).toBe(true);
48144
+ expect(withinMin(mid, 1, [low, mid, high])).toBe(false);
48145
+ expect(withinMin(low, 0, [low, mid, high])).toBe(false);
48146
+ });
48147
+ it("keeps the highest below-floor rate when no platform max is reported", () => {
48148
+ capabilities.sampleRate = null;
48149
+ const low = audioQuality2([16e3]);
48150
+ const high = audioQuality2([32e3]);
48151
+ expect(withinMin(high, 1, [low, high])).toBe(true);
48152
+ expect(withinMin(low, 0, [low, high])).toBe(false);
48153
+ });
48154
+ it("keeps a single audio rendition below the floor", () => {
48155
+ const only = audioQuality2([24e3]);
48156
+ expect(withinMin(only, 0, [only])).toBe(true);
48157
+ });
47897
48158
  it("ignores non-audio qualities in the floor fallback", () => {
47898
48159
  const audio = audioQuality2([24e3]);
47899
48160
  const video = createEmptyMediaQualityMetadata();
@@ -47903,6 +48164,55 @@ x`);
47903
48164
  expect(withinMin(video, 1, [audio, video])).toBe(true);
47904
48165
  });
47905
48166
  });
48167
+ describe("withinAudioSampleRateRange straddling an empty band", () => {
48168
+ let capabilities;
48169
+ beforeEach(() => {
48170
+ setUserAgent("Chrome");
48171
+ capabilities = new MockCapabilities();
48172
+ capabilities.sampleRate = 48e3;
48173
+ });
48174
+ function audioQuality2(rate) {
48175
+ const quality = createEmptyMediaQualityMetadata();
48176
+ quality.contentType = "audio";
48177
+ quality.audioSamplingRate = rate;
48178
+ return quality;
48179
+ }
48180
+ function within(metadata, index, array2) {
48181
+ return withinAudioSampleRateRange(
48182
+ { capabilities, minSampleRate: 44100 },
48183
+ metadata,
48184
+ index,
48185
+ array2
48186
+ );
48187
+ }
48188
+ it("keeps the highest decodable rate when one is below the floor and one above the ceiling", () => {
48189
+ const low = audioQuality2([22050]);
48190
+ const high = audioQuality2([96e3]);
48191
+ expect(within(low, 0, [low, high])).toBe(true);
48192
+ expect(within(high, 1, [low, high])).toBe(false);
48193
+ });
48194
+ it("keeps the below-floor rate when the ceiling matches the output device rate", () => {
48195
+ capabilities.sampleRate = 44100;
48196
+ const low = audioQuality2([22050]);
48197
+ const high = audioQuality2([48e3]);
48198
+ expect(within(low, 0, [low, high])).toBe(true);
48199
+ expect(within(high, 1, [low, high])).toBe(false);
48200
+ });
48201
+ it("still prefers an in-band rate over the straddle fallback", () => {
48202
+ const low = audioQuality2([22050]);
48203
+ const mid = audioQuality2([44100]);
48204
+ const high = audioQuality2([96e3]);
48205
+ expect(within(mid, 1, [low, mid, high])).toBe(true);
48206
+ expect(within(low, 0, [low, mid, high])).toBe(false);
48207
+ expect(within(high, 2, [low, mid, high])).toBe(false);
48208
+ });
48209
+ it("keeps the lowest when every rate is above the ceiling", () => {
48210
+ const low = audioQuality2([88200]);
48211
+ const high = audioQuality2([96e3]);
48212
+ expect(within(low, 0, [low, high])).toBe(true);
48213
+ expect(within(high, 1, [low, high])).toBe(false);
48214
+ });
48215
+ });
47906
48216
 
47907
48217
  // test/unit/track/hls/buildHlsMediaTimeline.test.ts
47908
48218
  describe("buildHlsMediaTimeline", () => {
@@ -53347,6 +53657,7 @@ x`);
53347
53657
  let depFactories;
53348
53658
  let player;
53349
53659
  let mockOptions;
53660
+ const loggerRef2 = useMockLogger();
53350
53661
  polyfillCustomEvent();
53351
53662
  beforeEach(() => {
53352
53663
  mockOptions = { media: new MockHTMLAudioElement() };
@@ -53571,6 +53882,40 @@ x`);
53571
53882
  });
53572
53883
  expect(spy).not.toHaveBeenCalled();
53573
53884
  });
53885
+ it("logs each republished span at the verbose level with formatted times", () => {
53886
+ loggerRef2.value.logLevel = 0 /* VERBOSE */;
53887
+ deps.drmController.dispatch("loadSpanMeasured", {
53888
+ kind: "license",
53889
+ startTime: 1e3,
53890
+ endTime: 3500,
53891
+ trackUri: "track-a",
53892
+ mimeType: "audio/mp4",
53893
+ contentType: "audio"
53894
+ });
53895
+ expect(loggerRef2.value.verbose).toHaveBeenCalledOnceWith(
53896
+ player,
53897
+ "loadSpan",
53898
+ {
53899
+ kind: "license",
53900
+ trackUri: "track-a",
53901
+ mimeType: "audio/mp4",
53902
+ contentType: "audio",
53903
+ startTime: (/* @__PURE__ */ new Date(1e3)).toISOString(),
53904
+ endTime: (/* @__PURE__ */ new Date(3500)).toISOString(),
53905
+ durationMs: "2500.00"
53906
+ }
53907
+ );
53908
+ });
53909
+ it("does not format or log a span when the verbose level is off", () => {
53910
+ loggerRef2.value.logLevel = 1 /* DEBUG */;
53911
+ deps.drmController.dispatch("loadSpanMeasured", {
53912
+ kind: "license",
53913
+ startTime: 1e3,
53914
+ endTime: 3500,
53915
+ trackUri: "track-a"
53916
+ });
53917
+ expect(loggerRef2.value.verbose).not.toHaveBeenCalled();
53918
+ });
53574
53919
  });
53575
53920
  it("reloads the current track on a codecUnsupported event", () => {
53576
53921
  const trackController = deps.trackController;
@@ -54816,27 +55161,31 @@ x`);
54816
55161
  if (!supportsMse()) pending("MSE not supported");
54817
55162
  });
54818
55163
  const suite = createVinylSuite();
55164
+ async function arrangeStall() {
55165
+ const player = suite.player;
55166
+ const stallEnteredSpy = createEventSpy(player, "stallEntered");
55167
+ const stallEndedSpy = createEventSpy(player, "stallEnded");
55168
+ const mediaSource = new ControlledMediaSource();
55169
+ player.load({
55170
+ type: "src",
55171
+ uri: URL.createObjectURL(await mediaSource.getMediaSource())
55172
+ });
55173
+ void player.play();
55174
+ await mediaSource.appendNext();
55175
+ await mediaSource.appendNext();
55176
+ await onPlaying(player);
55177
+ expect(stallEnteredSpy).not.toHaveBeenCalled();
55178
+ const entered = stallEnteredSpy.next(
55179
+ 30,
55180
+ "stallEntered after hitting end of buffer timed out after {timeout}s"
55181
+ );
55182
+ await player.seekTo(5);
55183
+ await entered;
55184
+ return { player, mediaSource, stallEnteredSpy, stallEndedSpy };
55185
+ }
54819
55186
  describe("when the play head freezes mid-playback", () => {
54820
55187
  it("emits stallEntered then stallEnded when playback resumes", async () => {
54821
- const player = suite.player;
54822
- const stallEnteredSpy = createEventSpy(player, "stallEntered");
54823
- const stallEndedSpy = createEventSpy(player, "stallEnded");
54824
- const mediaSource = new ControlledMediaSource();
54825
- player.load({
54826
- type: "src",
54827
- uri: URL.createObjectURL(await mediaSource.getMediaSource())
54828
- });
54829
- void player.play();
54830
- await mediaSource.appendNext();
54831
- await mediaSource.appendNext();
54832
- await onPlaying(player);
54833
- expect(stallEnteredSpy).not.toHaveBeenCalled();
54834
- const nextEntered = stallEnteredSpy.next(
54835
- 30,
54836
- "stallEntered after hitting end of buffer timed out after {timeout}s"
54837
- );
54838
- await player.seekTo(5);
54839
- await nextEntered;
55188
+ const { player, mediaSource, stallEnteredSpy, stallEndedSpy } = await arrangeStall();
54840
55189
  const nextEnded = stallEndedSpy.next(
54841
55190
  10,
54842
55191
  "stallEnded after data buffered timed out after {timeout}s"
@@ -54845,7 +55194,62 @@ x`);
54845
55194
  const ended = await nextEnded;
54846
55195
  expect(ended.reason).toBe("playing");
54847
55196
  expect(stallEnteredSpy).toHaveBeenCalledTimes(1);
55197
+ expect(stallEndedSpy).toHaveBeenCalledTimes(1);
54848
55198
  expect(ended.duration).toBeGreaterThan(0.5);
55199
+ for (let i = 0; i < 5; i++) await onTimeUpdate(player);
55200
+ expect(stallEnteredSpy).toHaveBeenCalledTimes(1);
55201
+ expect(stallEndedSpy).toHaveBeenCalledTimes(1);
55202
+ });
55203
+ it('ends the stall with reason "seeking" when the user seeks away mid-stall', async () => {
55204
+ const { player, stallEnteredSpy, stallEndedSpy } = await arrangeStall();
55205
+ const nextEnded = stallEndedSpy.next(
55206
+ 10,
55207
+ "stallEnded after seeking away timed out after {timeout}s"
55208
+ );
55209
+ await player.seekTo(1);
55210
+ const ended = await nextEnded;
55211
+ expect(ended.reason).toBe("seeking");
55212
+ expect(stallEnteredSpy).toHaveBeenCalledTimes(1);
55213
+ expect(stallEndedSpy).toHaveBeenCalledTimes(1);
55214
+ });
55215
+ it('ends the stall with reason "emptied" when the track is unloaded mid-stall', async () => {
55216
+ const { player, stallEnteredSpy, stallEndedSpy } = await arrangeStall();
55217
+ const nextEnded = stallEndedSpy.next(
55218
+ 10,
55219
+ "stallEnded after unload timed out after {timeout}s"
55220
+ );
55221
+ player.unload();
55222
+ const ended = await nextEnded;
55223
+ expect(ended.reason).toBe("emptied");
55224
+ expect(stallEnteredSpy).toHaveBeenCalledTimes(1);
55225
+ expect(stallEndedSpy).toHaveBeenCalledTimes(1);
55226
+ });
55227
+ it("reports each distinct freeze exactly once", async () => {
55228
+ const { player, mediaSource, stallEnteredSpy, stallEndedSpy } = await arrangeStall();
55229
+ const firstEnded = stallEndedSpy.next(
55230
+ 10,
55231
+ "stallEnded for freeze 1 timed out after {timeout}s"
55232
+ );
55233
+ await mediaSource.appendNext();
55234
+ expect((await firstEnded).reason).toBe("playing");
55235
+ expect(stallEnteredSpy).toHaveBeenCalledTimes(1);
55236
+ expect(stallEndedSpy).toHaveBeenCalledTimes(1);
55237
+ const secondEntered = stallEnteredSpy.next(
55238
+ 30,
55239
+ "stallEntered for freeze 2 timed out after {timeout}s"
55240
+ );
55241
+ await player.seekTo(15);
55242
+ await secondEntered;
55243
+ expect(stallEnteredSpy).toHaveBeenCalledTimes(2);
55244
+ const secondEnded = stallEndedSpy.next(
55245
+ 10,
55246
+ "stallEnded for freeze 2 timed out after {timeout}s"
55247
+ );
55248
+ await mediaSource.appendNext();
55249
+ expect((await secondEnded).reason).toBe("playing");
55250
+ for (let i = 0; i < 5; i++) await onTimeUpdate(player);
55251
+ expect(stallEnteredSpy).toHaveBeenCalledTimes(2);
55252
+ expect(stallEndedSpy).toHaveBeenCalledTimes(2);
54849
55253
  });
54850
55254
  });
54851
55255
  });
@@ -55574,6 +55978,7 @@ x`);
55574
55978
  var ANCHOR_MS = Date.parse(ANCHOR_ISO);
55575
55979
  var CONTENT_ASSET = vinylTestAssets.hls.live_static_video_audio_60s_4s;
55576
55980
  var AD_ASSET = vinylTestAssets.hls.live_static_video_audio_60s_2s;
55981
+ var SHORT_AD_ASSET = vinylTestAssets.hls.edge_static_video_1s;
55577
55982
  function injectingManifestProvider(mainUrl, interstitials) {
55578
55983
  return async () => {
55579
55984
  const mainText = await (await fetch(mainUrl)).text();
@@ -56208,8 +56613,8 @@ x`);
56208
56613
  id: "postroll-1",
56209
56614
  startTime: 0,
56210
56615
  duration: 4,
56211
- // Short ad so the break ends quickly on its own.
56212
- assetList: [{ uri: AD_ASSET, duration: 3 }],
56616
+ // A 1s ad so the break ends quickly on its own `ended`.
56617
+ assetList: [{ uri: SHORT_AD_ASSET, duration: 1 }],
56213
56618
  cue: "POST"
56214
56619
  }
56215
56620
  ])