@amazon/vinyl 3.2.0 → 3.2.2

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 (50) hide show
  1. package/dist/index.cjs +65 -45
  2. package/dist/index.cjs.map +2 -2
  3. package/dist/index.js +90 -69
  4. package/dist/index.js.map +3 -3
  5. package/dist/test/integ.es5.js +303 -287
  6. package/dist/test/integ.js +328 -71
  7. package/dist/test/integ.js.map +2 -2
  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 +6 -0
  12. package/dist/types/current/drm/DrmControllerImpl.d.ts.map +1 -1
  13. package/dist/types/current/drm/DrmControllerImpl.js +11 -1
  14. package/dist/types/current/playback/PlaybackControllerImpl.d.ts +8 -2
  15. package/dist/types/current/playback/PlaybackControllerImpl.d.ts.map +1 -1
  16. package/dist/types/current/playback/PlaybackControllerImpl.js +38 -33
  17. package/dist/types/current/playback/createStallMonitor.d.ts.map +1 -1
  18. package/dist/types/current/playback/createStallMonitor.js +19 -14
  19. package/dist/types/current/track/mse/MseTrack.d.ts +0 -1
  20. package/dist/types/current/track/mse/MseTrack.d.ts.map +1 -1
  21. package/dist/types/current/track/mse/MseTrack.js +5 -6
  22. package/dist/types/current/tsconfig.tsbuildinfo +1 -1
  23. package/dist/types/ts3.7/ad/AdControllerImpl.d.ts +18 -2
  24. package/dist/types/ts3.7/drm/DrmControllerImpl.d.ts +6 -0
  25. package/dist/types/ts3.7/playback/PlaybackControllerImpl.d.ts +8 -2
  26. package/dist/types/ts3.7/track/mse/MseTrack.d.ts +0 -1
  27. package/dist/types/ts4.1/ad/AdControllerImpl.d.ts +18 -2
  28. package/dist/types/ts4.1/drm/DrmControllerImpl.d.ts +6 -0
  29. package/dist/types/ts4.1/playback/PlaybackControllerImpl.d.ts +8 -2
  30. package/dist/types/ts4.1/track/mse/MseTrack.d.ts +0 -1
  31. package/dist/types/ts4.7/ad/AdControllerImpl.d.ts +18 -2
  32. package/dist/types/ts4.7/drm/DrmControllerImpl.d.ts +6 -0
  33. package/dist/types/ts4.7/playback/PlaybackControllerImpl.d.ts +8 -2
  34. package/dist/types/ts4.7/track/mse/MseTrack.d.ts +0 -1
  35. package/dist/unit/types/current/ad/AdControllerImpl.test.js +110 -4
  36. package/dist/unit/types/current/drm/DrmControllerImpl.test.js +19 -1
  37. package/dist/unit/types/current/playback/PlaybackControllerImpl.test.js +15 -0
  38. package/dist/unit/types/current/playback/createStallMonitor.test.js +38 -0
  39. package/dist/unit/types/current/streaming/MseTrack.test.js +15 -0
  40. package/dist/unit/types/current/tsconfig.tsbuildinfo +1 -1
  41. package/dist/vinylTestUtil/index.js +10 -1
  42. package/dist/vinylTestUtil/index.js.map +2 -2
  43. package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.d.ts +5 -0
  44. package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.d.ts.map +1 -1
  45. package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.js +9 -0
  46. package/dist/vinylTestUtil/types/current/tsconfig.tsbuildinfo +1 -1
  47. package/dist/vinylTestUtil/types/ts3.7/assets/vinylTestAssets.d.ts +5 -0
  48. package/dist/vinylTestUtil/types/ts4.1/assets/vinylTestAssets.d.ts +5 -0
  49. package/dist/vinylTestUtil/types/ts4.7/assets/vinylTestAssets.d.ts +5 -0
  50. package/package.json +10 -10
@@ -9953,6 +9953,7 @@ ${this.lastInitStack}` : "";
9953
9953
 
9954
9954
  // src/ad/AdControllerImpl.ts
9955
9955
  var AD_START_TOLERANCE = 0.05;
9956
+ var PLAYOUT_LIMIT_TOLERANCE = 1;
9956
9957
  var defaultAdControllerImplOptions = {
9957
9958
  adLoadTimeout: 15,
9958
9959
  preloadAheadTime: 10
@@ -10443,23 +10444,29 @@ ${this.lastInitStack}` : "";
10443
10444
  });
10444
10445
  }
10445
10446
  /**
10446
- * Ads breaks can have a playout limit that may cut off ad playback.
10447
- * 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.
10448
10456
  */
10449
10457
  enforcePlayoutLimits(adState) {
10458
+ if (!adState.started) return;
10459
+ const { parentBreak } = adState;
10460
+ const limit = parentBreak.adBreak.playoutLimit;
10461
+ if (limit == null) return;
10450
10462
  const pC = this.deps.playbackController;
10451
- const { ad, parentBreak } = adState;
10452
- if (adState.started) {
10453
- const elapsed = Math.max(0, pC.currentTime - adState.timeStart);
10454
- const adBreak = parentBreak.adBreak;
10455
- const limit = adBreak.playoutLimit;
10456
- if (limit != null && parentBreak.playoutElapsed + elapsed >= limit) {
10457
- parentBreak.clear();
10458
- this.endAd(adState);
10459
- } else if (ad.duration != null && elapsed >= ad.duration) {
10460
- this.endAd(adState);
10461
- }
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;
10462
10467
  }
10468
+ parentBreak.clear();
10469
+ this.endAd(adState);
10463
10470
  }
10464
10471
  setPendingBreaks(newPending) {
10465
10472
  this.pendingAdBreaks = newPending.filter(
@@ -14000,6 +14007,7 @@ ${this.lastInitStack}` : "";
14000
14007
  // src/drm/DrmControllerImpl.ts
14001
14008
  var LICENSE_TIMEOUT = 90;
14002
14009
  var LICENSE_TIMEOUT_MESSAGE = "License provider timed out after {time}s";
14010
+ var EXCESSIVE_SESSION_COUNT = 20;
14003
14011
  var DrmControllerImpl = class extends EventHostImpl {
14004
14012
  constructor(deps, options) {
14005
14013
  super();
@@ -14216,6 +14224,13 @@ ${this.lastInitStack}` : "";
14216
14224
  trackUri
14217
14225
  });
14218
14226
  this.sessions.push(newSession);
14227
+ logVerbose(this, `total sessions: ${this.sessions.length}`);
14228
+ if (this.sessions.length === EXCESSIVE_SESSION_COUNT + 1) {
14229
+ logWarn(
14230
+ this,
14231
+ `${this.sessions.length} open key sessions exceeds ${EXCESSIVE_SESSION_COUNT}; sessions may be leaking (not closing on track deactivation)`
14232
+ );
14233
+ }
14219
14234
  this.dispatch("sessionCreate", {
14220
14235
  initDataType: newSession.initDataType,
14221
14236
  mimeType: newSession.mimeType
@@ -15214,7 +15229,7 @@ ${this.lastInitStack}` : "";
15214
15229
  // src/global/environment.ts
15215
15230
  var vinylVersion = parseVersion(
15216
15231
  /* istanbul ignore next */
15217
- "3.2.0"
15232
+ "3.2.2"
15218
15233
  );
15219
15234
 
15220
15235
  // src/global/globalRef.ts
@@ -15371,6 +15386,7 @@ ${this.lastInitStack}` : "";
15371
15386
  ];
15372
15387
 
15373
15388
  // src/playback/createStallMonitor.ts
15389
+ var PROGRESS_EPSILON = 1e-6;
15374
15390
  function createStallMonitor(controller, callbacks, options = {}) {
15375
15391
  var _a3, _b;
15376
15392
  const stallThresholdMs = ((_a3 = options.stallThreshold) != null ? _a3 : 1) * 1e3;
@@ -15379,6 +15395,7 @@ ${this.lastInitStack}` : "";
15379
15395
  let lastProgressAt = Date.now();
15380
15396
  let inStall = false;
15381
15397
  let pollId = null;
15398
+ const { add, dispose } = createDisposer();
15382
15399
  const endStall = (reason) => {
15383
15400
  if (!inStall) return;
15384
15401
  inStall = false;
@@ -15395,6 +15412,7 @@ ${this.lastInitStack}` : "";
15395
15412
  clearInterval(pollId);
15396
15413
  pollId = null;
15397
15414
  };
15415
+ add(stopPoll);
15398
15416
  const startPoll = () => {
15399
15417
  if (pollId != null) return;
15400
15418
  pollId = setInterval(() => {
@@ -15410,17 +15428,18 @@ ${this.lastInitStack}` : "";
15410
15428
  observedPlaying = false;
15411
15429
  stopPoll();
15412
15430
  };
15413
- const { add, dispose } = createDisposer();
15414
15431
  add(
15415
15432
  controller.on("playing", () => {
15416
15433
  if (inStall) endStall("playing" /* PLAYING */);
15417
- else lastProgressAt = Date.now();
15434
+ lastProgressAt = Date.now();
15418
15435
  observedPlaying = true;
15419
15436
  startPoll();
15420
15437
  })
15421
15438
  );
15422
15439
  add(
15423
- controller.on("timeUpdate", () => {
15440
+ controller.on("timeUpdate", ({ previous, current }) => {
15441
+ if (previous != null && current - previous <= PROGRESS_EPSILON)
15442
+ return;
15424
15443
  if (inStall) endStall("playing" /* PLAYING */);
15425
15444
  lastProgressAt = Date.now();
15426
15445
  })
@@ -15428,13 +15447,7 @@ ${this.lastInitStack}` : "";
15428
15447
  add(controller.on("pause", () => suspend("pause" /* PAUSE */)));
15429
15448
  add(controller.on("seeking", () => suspend("seeking" /* SEEKING */)));
15430
15449
  add(controller.on("emptied", () => suspend("emptied" /* EMPTIED */)));
15431
- let stopped = false;
15432
- return () => {
15433
- if (stopped) return;
15434
- stopped = true;
15435
- stopPoll();
15436
- dispose();
15437
- };
15450
+ return dispose;
15438
15451
  }
15439
15452
 
15440
15453
  // src/playback/error/InvalidSeekError.ts
@@ -15710,15 +15723,13 @@ ${this.lastInitStack}` : "";
15710
15723
  __publicField(this, "_playing", false);
15711
15724
  __publicField(this, "_seeking", false);
15712
15725
  __publicField(this, "_waiting", false);
15713
- // Monotonic counter identifying the most recent seekTo call.
15714
- __publicField(this, "_seekId", 0);
15726
+ __publicField(this, "seekAbortSlot", createAbortSlot());
15715
15727
  // Stops the stall detector started after the last seek, if any.
15716
15728
  __publicField(this, "stopStallDetection", null);
15717
15729
  // default user volume (0.0 to 1.0 scale)
15718
15730
  __publicField(this, "userVolume", 1);
15719
15731
  __publicField(this, "_pendingPlay", null);
15720
15732
  __publicField(this, "playAbort", null);
15721
- __publicField(this, "disposeAbort", new Abort());
15722
15733
  __publicField(this, "options");
15723
15734
  this.options = __spreadValues(__spreadProps(__spreadValues({}, defaultPlaybackControllerImplOptions), {
15724
15735
  minSeekableBuffer: getMinSeekableBufferDefault()
@@ -15729,8 +15740,7 @@ ${this.lastInitStack}` : "";
15729
15740
  const { add } = this.disposer;
15730
15741
  add(() => {
15731
15742
  var _a3;
15732
- this.disposeAbort.abort();
15733
- this.disposeAbort.dispose();
15743
+ this.seekAbortSlot.abort();
15734
15744
  (_a3 = this.stopStallDetection) == null ? void 0 : _a3.call(this);
15735
15745
  });
15736
15746
  this.domEvents = add(
@@ -16143,14 +16153,10 @@ ${this.lastInitStack}` : "";
16143
16153
  async seekTo(time, tolerance = 0.5) {
16144
16154
  var _a3;
16145
16155
  logDebug(this, `seekTo: ${time}`);
16146
- const seekId = ++this._seekId;
16147
16156
  (_a3 = this.stopStallDetection) == null ? void 0 : _a3.call(this);
16148
16157
  this.stopStallDetection = null;
16149
- const superseded = () => {
16150
- if (this._seekId === seekId) return false;
16151
- logDebug(this, `seekTo: ${time} superseded by a newer seek`);
16152
- return true;
16153
- };
16158
+ this.seekAbortSlot.abort();
16159
+ const seekAbort = this.seekAbortSlot.value;
16154
16160
  const nextEvent = (type, options) => {
16155
16161
  var _a4;
16156
16162
  return nextEventAsPromise(this, type, {
@@ -16158,12 +16164,25 @@ ${this.lastInitStack}` : "";
16158
16164
  timeoutMessage: `seek timed out on ${type} event after {time}s`,
16159
16165
  timeoutOrigin: ErrorOrigin.INTERNAL,
16160
16166
  timeoutLevel: "warn" /* WARN */,
16161
- abort: this.disposeAbort
16167
+ abort: seekAbort
16162
16168
  });
16163
16169
  };
16170
+ try {
16171
+ await this.doSeek(time, tolerance, seekAbort, nextEvent);
16172
+ } catch (error) {
16173
+ if (seekAbort.aborted() && !this.disposed) return;
16174
+ throw error;
16175
+ }
16176
+ }
16177
+ /**
16178
+ * The body of a single {@link seekTo} attempt. Extracted so {@link seekTo}
16179
+ * can wrap it with per-seek abort/supersession handling. `seekAbort` is
16180
+ * aborted when a newer seek supersedes this one (or on dispose).
16181
+ * @private
16182
+ */
16183
+ async doSeek(time, tolerance, seekAbort, nextEvent) {
16164
16184
  if (!this.hasMetadata) {
16165
16185
  await nextEvent("loadedMetadata");
16166
- if (superseded()) return;
16167
16186
  }
16168
16187
  const seekable = this.seekable;
16169
16188
  const range = seekable.getRangeAt(
@@ -16194,7 +16213,7 @@ ${this.lastInitStack}` : "";
16194
16213
  );
16195
16214
  } else {
16196
16215
  const nullIfStuck = (error) => {
16197
- if (this.disposeAbort.aborted()) throw error;
16216
+ if (this.disposed) throw error;
16198
16217
  return null;
16199
16218
  };
16200
16219
  const seekOnce = async (reSeeking) => {
@@ -16208,7 +16227,7 @@ ${this.lastInitStack}` : "";
16208
16227
  }).catch(nullIfStuck);
16209
16228
  };
16210
16229
  let seekedEvent = await seekOnce(false);
16211
- for (let attempt = 0; !superseded() && attempt < SEEK_RETRY_LIMIT && (seekedEvent === null || seekedEvent.reason === "seeked" && !closeTo(clampedTime, this.currentTime, tolerance)); attempt++) {
16230
+ for (let attempt = 0; !seekAbort.aborted() && attempt < SEEK_RETRY_LIMIT && (seekedEvent === null || seekedEvent.reason === "seeked" && !closeTo(clampedTime, this.currentTime, tolerance)); attempt++) {
16212
16231
  if (seekedEvent === null) void this.media.play().catch(noop);
16213
16232
  logDebug(this, `seekTo: re-seeking to ${clampedTime}`);
16214
16233
  seekedEvent = await seekOnce(true);
@@ -16216,7 +16235,7 @@ ${this.lastInitStack}` : "";
16216
16235
  if ((seekedEvent == null ? void 0 : seekedEvent.reason) === "emptied") {
16217
16236
  throw new AbortError();
16218
16237
  }
16219
- if (!this.paused) this.startStallDetection();
16238
+ if (!this.paused && !seekAbort.aborted()) this.startStallDetection();
16220
16239
  }
16221
16240
  }
16222
16241
  /**
@@ -20556,7 +20575,6 @@ ${this.lastInitStack}` : "";
20556
20575
  const deps = depsContainer.dependencies;
20557
20576
  super(uri, type, deps);
20558
20577
  __publicField(this, "streams", []);
20559
- __publicField(this, "disposeAbort", new Abort());
20560
20578
  __publicField(this, "lastPreloadOptions", null);
20561
20579
  __publicField(this, "allFetchedRanges", []);
20562
20580
  __publicField(this, "_fetchedRanges", new IntersectionRanges(
@@ -20581,7 +20599,7 @@ ${this.lastInitStack}` : "";
20581
20599
  if (this.active) {
20582
20600
  this.deps.drmController.initializeForPlayback(event.current, {
20583
20601
  trackUri: this.uri,
20584
- abort: this.disposeAbort
20602
+ abort: this.drmSessionAbort.value
20585
20603
  });
20586
20604
  }
20587
20605
  });
@@ -20589,7 +20607,7 @@ ${this.lastInitStack}` : "";
20589
20607
  if (event.current == null) return;
20590
20608
  this.deps.drmController.setBufferingDrmInfo(event.current, {
20591
20609
  trackUri: this.uri,
20592
- abort: this.disposeAbort
20610
+ abort: this.drmSessionAbort.value
20593
20611
  });
20594
20612
  });
20595
20613
  add(
@@ -20827,7 +20845,7 @@ ${this.lastInitStack}` : "";
20827
20845
  this.streams.forEach((stream) => {
20828
20846
  this.deps.drmController.initializeForPlayback(
20829
20847
  stream.streamingQuality,
20830
- { trackUri: this.uri, abort: this.disposeAbort }
20848
+ { trackUri: this.uri, abort: this.drmSessionAbort.value }
20831
20849
  );
20832
20850
  });
20833
20851
  this.deps.playbackSource.src = this.deps.mediaSourceController.activate();
@@ -20853,7 +20871,7 @@ ${this.lastInitStack}` : "";
20853
20871
  this.callOnStreams("deactivate");
20854
20872
  this.deps.drmController.setBufferingDrmInfo(null, {
20855
20873
  trackUri: this.uri,
20856
- abort: this.disposeAbort
20874
+ abort: this.drmSessionAbort.value
20857
20875
  });
20858
20876
  (_b = this.deps.textTrackController) == null ? void 0 : _b.deactivate();
20859
20877
  this.deps.playbackSource.src = null;
@@ -24951,7 +24969,8 @@ ${this.lastInitStack}` : "";
24951
24969
  var vinylTestAssets = {
24952
24970
  prog: {
24953
24971
  libmp3lame_60s_2ch_16bit_44100Hz_48kbps: `https://assets.dev.vinyl.music.amazon.dev/prog/libmp3lame_60s_2ch_16bit_44100Hz_48kbps.mp3`,
24954
- libmp3lame_1800s_2ch_16bit_44100Hz_48kbps: `https://assets.dev.vinyl.music.amazon.dev/prog/libmp3lame_1800s_2ch_16bit_44100Hz_48kbps.mp3`
24972
+ libmp3lame_1800s_2ch_16bit_44100Hz_48kbps: `https://assets.dev.vinyl.music.amazon.dev/prog/libmp3lame_1800s_2ch_16bit_44100Hz_48kbps.mp3`,
24973
+ libmp3lame_1s_2ch_16bit_44100Hz_48kbps: `https://assets.dev.vinyl.music.amazon.dev/prog/libmp3lame_1s_2ch_16bit_44100Hz_48kbps.mp3`
24955
24974
  },
24956
24975
  dash: {
24957
24976
  live_static_aac_opus_flac_60s_segmentTemplate: `https://assets.dev.vinyl.music.amazon.dev/dash/live_static_aac_opus_flac_60s_segmentTemplate/manifest.mpd`,
@@ -24966,6 +24985,10 @@ ${this.lastInitStack}` : "";
24966
24985
  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`,
24967
24986
  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`,
24968
24987
  live_static_aac_1800s_segmentBase_widevine_cl30: `https://assets.dev.vinyl.music.amazon.dev/dash/live_static_aac_1800s_segmentBase_widevine_cl30/manifest.mpd`,
24988
+ // Edge-case tracks: ultra-short 1s assets (audio-only + video), no
24989
+ // captions or translations.
24990
+ edge_static_aac_1s_segmentTemplate: `https://assets.dev.vinyl.music.amazon.dev/dash/edge_static_aac_1s_segmentTemplate/manifest.mpd`,
24991
+ edge_static_video_1s_segmentTemplate: `https://assets.dev.vinyl.music.amazon.dev/dash/edge_static_video_1s_segmentTemplate/manifest.mpd`,
24969
24992
  // Ad-break assets (see DMTestAssetBuilder src/adBreaks): a ~3-min
24970
24993
  // labeled ABR ladder with five-language narration, full + forced
24971
24994
  // captions. Base content only — DASH ad-break scenarios are not
@@ -24985,6 +25008,10 @@ ${this.lastInitStack}` : "";
24985
25008
  live_static_video_60s_4s_mpegts: `https://assets.dev.vinyl.music.amazon.dev/hls/live_static_video_60s_4s_mpegts/main.m3u8`,
24986
25009
  live_static_video_audio_60s_2s_mpegts: `https://assets.dev.vinyl.music.amazon.dev/hls/live_static_video_audio_60s_2s_mpegts/main.m3u8`,
24987
25010
  live_static_video_audio_60s_4s_mpegts: `https://assets.dev.vinyl.music.amazon.dev/hls/live_static_video_audio_60s_4s_mpegts/main.m3u8`,
25011
+ // Edge-case tracks: ultra-short 1s assets (audio-only + video), no
25012
+ // captions or translations.
25013
+ edge_static_aac_1s: `https://assets.dev.vinyl.music.amazon.dev/hls/edge_static_aac_1s/main.m3u8`,
25014
+ edge_static_video_1s: `https://assets.dev.vinyl.music.amazon.dev/hls/edge_static_video_1s/main.m3u8`,
24988
25015
  // Ad-break assets: base content + HLS Interstitial scenario variants.
24989
25016
  // `*_av` = video+audio, `*_audio` = audio-only; each `__<scenario>`
24990
25017
  // injects a different ad-break configuration. See DMTestAssetBuilder
@@ -27745,7 +27772,6 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27745
27772
  makeBreak({
27746
27773
  startTime: 10,
27747
27774
  duration: 20,
27748
- // Null per-ad duration so the per-ad cap does not end it.
27749
27775
  ads: [
27750
27776
  {
27751
27777
  id: "a1",
@@ -27849,8 +27875,8 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27849
27875
  });
27850
27876
  });
27851
27877
  describe("ad duration and playout limits", () => {
27852
- it("advances to the next ad when one reaches its declared duration", async () => {
27853
- var _a3, _b;
27878
+ it("advances to the next ad on the ad media ended, not its declared duration", async () => {
27879
+ var _a3, _b, _c;
27854
27880
  const c = createController();
27855
27881
  await setContent(
27856
27882
  c,
@@ -27863,6 +27889,7 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27863
27889
  id: "a1",
27864
27890
  startTime: 0,
27865
27891
  duration: 5,
27892
+ // declared, server-rounded
27866
27893
  uri: "ad1.m3u8"
27867
27894
  },
27868
27895
  {
@@ -27881,7 +27908,51 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27881
27908
  playbackController.dispatch("playing", {});
27882
27909
  updateTime(6);
27883
27910
  await flush();
27884
- expect((_b = c.currentAd) == null ? void 0 : _b.id).toBe("a2");
27911
+ expect((_b = c.currentAd) == null ? void 0 : _b.id).toBe("a1");
27912
+ playbackController.dispatch("ended", {
27913
+ previous: false,
27914
+ current: true
27915
+ });
27916
+ await flush();
27917
+ expect((_c = c.currentAd) == null ? void 0 : _c.id).toBe("a2");
27918
+ });
27919
+ it("does not cut the final/only ad at its declared duration; it ends on media ended", async () => {
27920
+ var _a3, _b;
27921
+ const c = createController();
27922
+ const ended = [];
27923
+ c.on("adEnded", (e) => ended.push(e.ad.id));
27924
+ await setContent(
27925
+ c,
27926
+ trackAds(
27927
+ makeBreak({
27928
+ startTime: 0,
27929
+ duration: 30,
27930
+ ads: [
27931
+ {
27932
+ id: "a1",
27933
+ startTime: 0,
27934
+ duration: 6,
27935
+ // declared, server-rounded
27936
+ uri: "ad1.m3u8"
27937
+ }
27938
+ ]
27939
+ })
27940
+ )
27941
+ );
27942
+ updateTime(0);
27943
+ await flush();
27944
+ expect((_a3 = c.currentAd) == null ? void 0 : _a3.id).toBe("a1");
27945
+ playbackController.dispatch("playing", {});
27946
+ updateTime(6.5);
27947
+ await flush();
27948
+ expect((_b = c.currentAd) == null ? void 0 : _b.id).toBe("a1");
27949
+ expect(ended).toEqual([]);
27950
+ playbackController.dispatch("ended", {
27951
+ previous: false,
27952
+ current: true
27953
+ });
27954
+ expect(ended).toEqual(["a1"]);
27955
+ expect(c.currentAdBreak).toBeNull();
27885
27956
  });
27886
27957
  it("ends the whole break when the playout limit is reached, dropping remaining ads", async () => {
27887
27958
  var _a3;
@@ -27922,6 +27993,70 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27922
27993
  expect(c.currentAdBreak).toBeNull();
27923
27994
  expect(resumes.at(-1)).toBe(0);
27924
27995
  });
27996
+ it("does not cut at the playout limit when the ad is within tolerance of its media end", async () => {
27997
+ var _a3;
27998
+ const c = createController();
27999
+ await setContent(
28000
+ c,
28001
+ trackAds(
28002
+ makeBreak({
28003
+ startTime: 0,
28004
+ duration: null,
28005
+ playoutLimit: 8,
28006
+ ads: [
28007
+ {
28008
+ id: "a1",
28009
+ startTime: 0,
28010
+ duration: 20,
28011
+ uri: "ad1.m3u8"
28012
+ }
28013
+ ]
28014
+ })
28015
+ )
28016
+ );
28017
+ updateTime(0);
28018
+ await flush();
28019
+ playbackController.dispatch("playing", {});
28020
+ playbackController.duration = 8.5;
28021
+ updateTime(8);
28022
+ await flush();
28023
+ expect(c.currentAdBreak).not.toBeNull();
28024
+ expect((_a3 = c.currentAd) == null ? void 0 : _a3.id).toBe("a1");
28025
+ playbackController.dispatch("ended", {
28026
+ previous: false,
28027
+ current: true
28028
+ });
28029
+ await flush();
28030
+ expect(c.currentAdBreak).toBeNull();
28031
+ });
28032
+ it("cuts at the playout limit when the ad has more than the tolerance of media left", async () => {
28033
+ const c = createController();
28034
+ await setContent(
28035
+ c,
28036
+ trackAds(
28037
+ makeBreak({
28038
+ startTime: 0,
28039
+ duration: null,
28040
+ playoutLimit: 8,
28041
+ ads: [
28042
+ {
28043
+ id: "a1",
28044
+ startTime: 0,
28045
+ duration: 20,
28046
+ uri: "ad1.m3u8"
28047
+ }
28048
+ ]
28049
+ })
28050
+ )
28051
+ );
28052
+ updateTime(0);
28053
+ await flush();
28054
+ playbackController.dispatch("playing", {});
28055
+ playbackController.duration = 20;
28056
+ updateTime(8);
28057
+ await flush();
28058
+ expect(c.currentAdBreak).toBeNull();
28059
+ });
27925
28060
  });
27926
28061
  describe("CUE=ONCE and replay", () => {
27927
28062
  it("replays a non-ONCE break after the playhead seeks back before it", async () => {
@@ -32017,6 +32152,23 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
32017
32152
  expect(drmController.isEmeSupported()).toBeFalse();
32018
32153
  });
32019
32154
  });
32155
+ describe("excessive sessions", () => {
32156
+ it("warns once when the open session count exceeds the threshold", async () => {
32157
+ drmController.setBufferingDrmInfo(drmInfo);
32158
+ for (let i = 0; i < EXCESSIVE_SESSION_COUNT; i++) {
32159
+ await emitEncrypted(new Uint8Array([i]));
32160
+ }
32161
+ expect(drmController.activeSessions).toBe(EXCESSIVE_SESSION_COUNT);
32162
+ expect(logger.value.warn).not.toHaveBeenCalled();
32163
+ await emitEncrypted(new Uint8Array([EXCESSIVE_SESSION_COUNT]));
32164
+ expect(drmController.activeSessions).toBe(
32165
+ EXCESSIVE_SESSION_COUNT + 1
32166
+ );
32167
+ expect(logger.value.warn).toHaveBeenCalledTimes(1);
32168
+ await emitEncrypted(new Uint8Array([EXCESSIVE_SESSION_COUNT + 1]));
32169
+ expect(logger.value.warn).toHaveBeenCalledTimes(1);
32170
+ });
32171
+ });
32020
32172
  describe("closeSessions", () => {
32021
32173
  it("clears active sessions", async () => {
32022
32174
  const sessionCloseSpy = createEventSpy(
@@ -33203,10 +33355,38 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
33203
33355
  expect(onStallEntered).toHaveBeenCalledTimes(2);
33204
33356
  expect(onStallEnded).toHaveBeenCalledTimes(2);
33205
33357
  });
33358
+ it("does not end and re-detect a stall on a non-advancing timeUpdate", async () => {
33359
+ monitor();
33360
+ host.dispatch("playing", {});
33361
+ host.dispatch("timeUpdate", { previous: 4, current: 5 });
33362
+ await poll2(4);
33363
+ expect(onStallEntered).toHaveBeenCalledTimes(1);
33364
+ host.dispatch("timeUpdate", { previous: 5, current: 5 });
33365
+ host.dispatch("timeUpdate", { previous: 5, current: 4.99 });
33366
+ host.dispatch("timeUpdate", { previous: 5, current: 5 + 1e-9 });
33367
+ await poll2(4);
33368
+ expect(onStallEnded).not.toHaveBeenCalled();
33369
+ expect(onStallEntered).toHaveBeenCalledTimes(1);
33370
+ host.dispatch("timeUpdate", { previous: 5, current: 6 });
33371
+ expect(onStallEnded).toHaveBeenCalledTimes(1);
33372
+ expect(onStallEntered).toHaveBeenCalledTimes(1);
33373
+ });
33374
+ it("does not re-detect a stall immediately after resuming via a playing event", async () => {
33375
+ monitor();
33376
+ host.dispatch("playing", {});
33377
+ host.dispatch("timeUpdate", { previous: 0, current: 1 });
33378
+ await poll2(4);
33379
+ expect(onStallEntered).toHaveBeenCalledTimes(1);
33380
+ host.dispatch("playing", {});
33381
+ expect(onStallEnded).toHaveBeenCalledTimes(1);
33382
+ await poll2(3);
33383
+ expect(onStallEntered).toHaveBeenCalledTimes(1);
33384
+ });
33206
33385
  it("stops reporting after disposal", async () => {
33207
33386
  monitor();
33208
33387
  host.dispatch("playing", {});
33209
33388
  stop();
33389
+ stop = void 0;
33210
33390
  await poll2(8);
33211
33391
  expect(onStallEntered).not.toHaveBeenCalled();
33212
33392
  });
@@ -33957,6 +34137,13 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
33957
34137
  await clock.tick(controller.options.seekTimeout);
33958
34138
  await seekExpectation;
33959
34139
  });
34140
+ it("resolves a superseded seek at once when its seeking event never fires", async () => {
34141
+ setSeekable([[0, 100]]);
34142
+ const first2 = controller.seekTo(50);
34143
+ const second = controller.seekTo(60);
34144
+ await expectAsync(first2).toBeResolved();
34145
+ second.catch(() => void 0);
34146
+ });
33960
34147
  it("times out pending seeks after seekTimeout", async () => {
33961
34148
  const pendingSeekExpectation = expectAsync(
33962
34149
  controller.seekTo(60)
@@ -39067,6 +39254,16 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
39067
39254
  abort: any14(Abort)
39068
39255
  });
39069
39256
  });
39257
+ it("aborts the drm session abort so key sessions close", async () => {
39258
+ track2 = createTrack();
39259
+ await awaitContentTypes();
39260
+ getAudioStream().streamingQuality = createEmptyMediaQualityMetadata();
39261
+ track2.activate({});
39262
+ const { abort } = deps.drmController.initializeForPlayback.calls.mostRecent().args[1];
39263
+ expect(abort.aborted()).toBeFalse();
39264
+ track2.deactivate();
39265
+ expect(abort.aborted()).toBeTrue();
39266
+ });
39070
39267
  });
39071
39268
  describe("error", () => {
39072
39269
  it("returns the first emitted error", async () => {
@@ -55011,27 +55208,31 @@ x`);
55011
55208
  if (!supportsMse()) pending("MSE not supported");
55012
55209
  });
55013
55210
  const suite = createVinylSuite();
55211
+ async function arrangeStall() {
55212
+ const player = suite.player;
55213
+ const stallEnteredSpy = createEventSpy(player, "stallEntered");
55214
+ const stallEndedSpy = createEventSpy(player, "stallEnded");
55215
+ const mediaSource = new ControlledMediaSource();
55216
+ player.load({
55217
+ type: "src",
55218
+ uri: URL.createObjectURL(await mediaSource.getMediaSource())
55219
+ });
55220
+ void player.play();
55221
+ await mediaSource.appendNext();
55222
+ await mediaSource.appendNext();
55223
+ await onPlaying(player);
55224
+ expect(stallEnteredSpy).not.toHaveBeenCalled();
55225
+ const entered = stallEnteredSpy.next(
55226
+ 30,
55227
+ "stallEntered after hitting end of buffer timed out after {timeout}s"
55228
+ );
55229
+ await player.seekTo(5);
55230
+ await entered;
55231
+ return { player, mediaSource, stallEnteredSpy, stallEndedSpy };
55232
+ }
55014
55233
  describe("when the play head freezes mid-playback", () => {
55015
55234
  it("emits stallEntered then stallEnded when playback resumes", async () => {
55016
- const player = suite.player;
55017
- const stallEnteredSpy = createEventSpy(player, "stallEntered");
55018
- const stallEndedSpy = createEventSpy(player, "stallEnded");
55019
- const mediaSource = new ControlledMediaSource();
55020
- player.load({
55021
- type: "src",
55022
- uri: URL.createObjectURL(await mediaSource.getMediaSource())
55023
- });
55024
- void player.play();
55025
- await mediaSource.appendNext();
55026
- await mediaSource.appendNext();
55027
- await onPlaying(player);
55028
- expect(stallEnteredSpy).not.toHaveBeenCalled();
55029
- const nextEntered = stallEnteredSpy.next(
55030
- 30,
55031
- "stallEntered after hitting end of buffer timed out after {timeout}s"
55032
- );
55033
- await player.seekTo(5);
55034
- await nextEntered;
55235
+ const { player, mediaSource, stallEnteredSpy, stallEndedSpy } = await arrangeStall();
55035
55236
  const nextEnded = stallEndedSpy.next(
55036
55237
  10,
55037
55238
  "stallEnded after data buffered timed out after {timeout}s"
@@ -55040,7 +55241,62 @@ x`);
55040
55241
  const ended = await nextEnded;
55041
55242
  expect(ended.reason).toBe("playing");
55042
55243
  expect(stallEnteredSpy).toHaveBeenCalledTimes(1);
55244
+ expect(stallEndedSpy).toHaveBeenCalledTimes(1);
55043
55245
  expect(ended.duration).toBeGreaterThan(0.5);
55246
+ for (let i = 0; i < 5; i++) await onTimeUpdate(player);
55247
+ expect(stallEnteredSpy).toHaveBeenCalledTimes(1);
55248
+ expect(stallEndedSpy).toHaveBeenCalledTimes(1);
55249
+ });
55250
+ it('ends the stall with reason "seeking" when the user seeks away mid-stall', async () => {
55251
+ const { player, stallEnteredSpy, stallEndedSpy } = await arrangeStall();
55252
+ const nextEnded = stallEndedSpy.next(
55253
+ 10,
55254
+ "stallEnded after seeking away timed out after {timeout}s"
55255
+ );
55256
+ await player.seekTo(1);
55257
+ const ended = await nextEnded;
55258
+ expect(ended.reason).toBe("seeking");
55259
+ expect(stallEnteredSpy).toHaveBeenCalledTimes(1);
55260
+ expect(stallEndedSpy).toHaveBeenCalledTimes(1);
55261
+ });
55262
+ it('ends the stall with reason "emptied" when the track is unloaded mid-stall', async () => {
55263
+ const { player, stallEnteredSpy, stallEndedSpy } = await arrangeStall();
55264
+ const nextEnded = stallEndedSpy.next(
55265
+ 10,
55266
+ "stallEnded after unload timed out after {timeout}s"
55267
+ );
55268
+ player.unload();
55269
+ const ended = await nextEnded;
55270
+ expect(ended.reason).toBe("emptied");
55271
+ expect(stallEnteredSpy).toHaveBeenCalledTimes(1);
55272
+ expect(stallEndedSpy).toHaveBeenCalledTimes(1);
55273
+ });
55274
+ it("reports each distinct freeze exactly once", async () => {
55275
+ const { player, mediaSource, stallEnteredSpy, stallEndedSpy } = await arrangeStall();
55276
+ const firstEnded = stallEndedSpy.next(
55277
+ 10,
55278
+ "stallEnded for freeze 1 timed out after {timeout}s"
55279
+ );
55280
+ await mediaSource.appendNext();
55281
+ expect((await firstEnded).reason).toBe("playing");
55282
+ expect(stallEnteredSpy).toHaveBeenCalledTimes(1);
55283
+ expect(stallEndedSpy).toHaveBeenCalledTimes(1);
55284
+ const secondEntered = stallEnteredSpy.next(
55285
+ 30,
55286
+ "stallEntered for freeze 2 timed out after {timeout}s"
55287
+ );
55288
+ await player.seekTo(15);
55289
+ await secondEntered;
55290
+ expect(stallEnteredSpy).toHaveBeenCalledTimes(2);
55291
+ const secondEnded = stallEndedSpy.next(
55292
+ 10,
55293
+ "stallEnded for freeze 2 timed out after {timeout}s"
55294
+ );
55295
+ await mediaSource.appendNext();
55296
+ expect((await secondEnded).reason).toBe("playing");
55297
+ for (let i = 0; i < 5; i++) await onTimeUpdate(player);
55298
+ expect(stallEnteredSpy).toHaveBeenCalledTimes(2);
55299
+ expect(stallEndedSpy).toHaveBeenCalledTimes(2);
55044
55300
  });
55045
55301
  });
55046
55302
  });
@@ -55769,6 +56025,7 @@ x`);
55769
56025
  var ANCHOR_MS = Date.parse(ANCHOR_ISO);
55770
56026
  var CONTENT_ASSET = vinylTestAssets.hls.live_static_video_audio_60s_4s;
55771
56027
  var AD_ASSET = vinylTestAssets.hls.live_static_video_audio_60s_2s;
56028
+ var SHORT_AD_ASSET = vinylTestAssets.hls.edge_static_video_1s;
55772
56029
  function injectingManifestProvider(mainUrl, interstitials) {
55773
56030
  return async () => {
55774
56031
  const mainText = await (await fetch(mainUrl)).text();
@@ -56403,8 +56660,8 @@ x`);
56403
56660
  id: "postroll-1",
56404
56661
  startTime: 0,
56405
56662
  duration: 4,
56406
- // Short ad so the break ends quickly on its own.
56407
- assetList: [{ uri: AD_ASSET, duration: 3 }],
56663
+ // A 1s ad so the break ends quickly on its own `ended`.
56664
+ assetList: [{ uri: SHORT_AD_ASSET, duration: 1 }],
56408
56665
  cue: "POST"
56409
56666
  }
56410
56667
  ])