@amazon/vinyl 3.2.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 (29) hide show
  1. package/dist/index.cjs +30 -24
  2. package/dist/index.cjs.map +2 -2
  3. package/dist/index.js +30 -24
  4. package/dist/index.js.map +2 -2
  5. package/dist/test/integ.es5.js +167 -155
  6. package/dist/test/integ.js +260 -50
  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/playback/createStallMonitor.d.ts.map +1 -1
  12. package/dist/types/current/playback/createStallMonitor.js +19 -14
  13. package/dist/types/current/tsconfig.tsbuildinfo +1 -1
  14. package/dist/types/ts3.7/ad/AdControllerImpl.d.ts +18 -2
  15. package/dist/types/ts4.1/ad/AdControllerImpl.d.ts +18 -2
  16. package/dist/types/ts4.7/ad/AdControllerImpl.d.ts +18 -2
  17. package/dist/unit/types/current/ad/AdControllerImpl.test.js +110 -4
  18. package/dist/unit/types/current/playback/createStallMonitor.test.js +38 -0
  19. package/dist/unit/types/current/tsconfig.tsbuildinfo +1 -1
  20. package/dist/vinylTestUtil/index.js +10 -1
  21. package/dist/vinylTestUtil/index.js.map +2 -2
  22. package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.d.ts +5 -0
  23. package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.d.ts.map +1 -1
  24. package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.js +9 -0
  25. package/dist/vinylTestUtil/types/current/tsconfig.tsbuildinfo +1 -1
  26. package/dist/vinylTestUtil/types/ts3.7/assets/vinylTestAssets.d.ts +5 -0
  27. package/dist/vinylTestUtil/types/ts4.1/assets/vinylTestAssets.d.ts +5 -0
  28. package/dist/vinylTestUtil/types/ts4.7/assets/vinylTestAssets.d.ts +5 -0
  29. 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(
@@ -15214,7 +15221,7 @@ ${this.lastInitStack}` : "";
15214
15221
  // src/global/environment.ts
15215
15222
  var vinylVersion = parseVersion(
15216
15223
  /* istanbul ignore next */
15217
- "3.2.0"
15224
+ "3.2.1"
15218
15225
  );
15219
15226
 
15220
15227
  // src/global/globalRef.ts
@@ -15371,6 +15378,7 @@ ${this.lastInitStack}` : "";
15371
15378
  ];
15372
15379
 
15373
15380
  // src/playback/createStallMonitor.ts
15381
+ var PROGRESS_EPSILON = 1e-6;
15374
15382
  function createStallMonitor(controller, callbacks, options = {}) {
15375
15383
  var _a3, _b;
15376
15384
  const stallThresholdMs = ((_a3 = options.stallThreshold) != null ? _a3 : 1) * 1e3;
@@ -15379,6 +15387,7 @@ ${this.lastInitStack}` : "";
15379
15387
  let lastProgressAt = Date.now();
15380
15388
  let inStall = false;
15381
15389
  let pollId = null;
15390
+ const { add, dispose } = createDisposer();
15382
15391
  const endStall = (reason) => {
15383
15392
  if (!inStall) return;
15384
15393
  inStall = false;
@@ -15395,6 +15404,7 @@ ${this.lastInitStack}` : "";
15395
15404
  clearInterval(pollId);
15396
15405
  pollId = null;
15397
15406
  };
15407
+ add(stopPoll);
15398
15408
  const startPoll = () => {
15399
15409
  if (pollId != null) return;
15400
15410
  pollId = setInterval(() => {
@@ -15410,17 +15420,18 @@ ${this.lastInitStack}` : "";
15410
15420
  observedPlaying = false;
15411
15421
  stopPoll();
15412
15422
  };
15413
- const { add, dispose } = createDisposer();
15414
15423
  add(
15415
15424
  controller.on("playing", () => {
15416
15425
  if (inStall) endStall("playing" /* PLAYING */);
15417
- else lastProgressAt = Date.now();
15426
+ lastProgressAt = Date.now();
15418
15427
  observedPlaying = true;
15419
15428
  startPoll();
15420
15429
  })
15421
15430
  );
15422
15431
  add(
15423
- controller.on("timeUpdate", () => {
15432
+ controller.on("timeUpdate", ({ previous, current }) => {
15433
+ if (previous != null && current - previous <= PROGRESS_EPSILON)
15434
+ return;
15424
15435
  if (inStall) endStall("playing" /* PLAYING */);
15425
15436
  lastProgressAt = Date.now();
15426
15437
  })
@@ -15428,13 +15439,7 @@ ${this.lastInitStack}` : "";
15428
15439
  add(controller.on("pause", () => suspend("pause" /* PAUSE */)));
15429
15440
  add(controller.on("seeking", () => suspend("seeking" /* SEEKING */)));
15430
15441
  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
- };
15442
+ return dispose;
15438
15443
  }
15439
15444
 
15440
15445
  // src/playback/error/InvalidSeekError.ts
@@ -24951,7 +24956,8 @@ ${this.lastInitStack}` : "";
24951
24956
  var vinylTestAssets = {
24952
24957
  prog: {
24953
24958
  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`
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`
24955
24961
  },
24956
24962
  dash: {
24957
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`,
@@ -24966,6 +24972,10 @@ ${this.lastInitStack}` : "";
24966
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`,
24967
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`,
24968
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`,
24969
24979
  // Ad-break assets (see DMTestAssetBuilder src/adBreaks): a ~3-min
24970
24980
  // labeled ABR ladder with five-language narration, full + forced
24971
24981
  // captions. Base content only — DASH ad-break scenarios are not
@@ -24985,6 +24995,10 @@ ${this.lastInitStack}` : "";
24985
24995
  live_static_video_60s_4s_mpegts: `https://assets.dev.vinyl.music.amazon.dev/hls/live_static_video_60s_4s_mpegts/main.m3u8`,
24986
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`,
24987
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`,
24988
25002
  // Ad-break assets: base content + HLS Interstitial scenario variants.
24989
25003
  // `*_av` = video+audio, `*_audio` = audio-only; each `__<scenario>`
24990
25004
  // injects a different ad-break configuration. See DMTestAssetBuilder
@@ -27745,7 +27759,6 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27745
27759
  makeBreak({
27746
27760
  startTime: 10,
27747
27761
  duration: 20,
27748
- // Null per-ad duration so the per-ad cap does not end it.
27749
27762
  ads: [
27750
27763
  {
27751
27764
  id: "a1",
@@ -27849,8 +27862,8 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27849
27862
  });
27850
27863
  });
27851
27864
  describe("ad duration and playout limits", () => {
27852
- it("advances to the next ad when one reaches its declared duration", async () => {
27853
- 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;
27854
27867
  const c = createController();
27855
27868
  await setContent(
27856
27869
  c,
@@ -27863,6 +27876,7 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27863
27876
  id: "a1",
27864
27877
  startTime: 0,
27865
27878
  duration: 5,
27879
+ // declared, server-rounded
27866
27880
  uri: "ad1.m3u8"
27867
27881
  },
27868
27882
  {
@@ -27881,7 +27895,51 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27881
27895
  playbackController.dispatch("playing", {});
27882
27896
  updateTime(6);
27883
27897
  await flush();
27884
- 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();
27885
27943
  });
27886
27944
  it("ends the whole break when the playout limit is reached, dropping remaining ads", async () => {
27887
27945
  var _a3;
@@ -27922,6 +27980,70 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
27922
27980
  expect(c.currentAdBreak).toBeNull();
27923
27981
  expect(resumes.at(-1)).toBe(0);
27924
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
+ });
27925
28047
  });
27926
28048
  describe("CUE=ONCE and replay", () => {
27927
28049
  it("replays a non-ONCE break after the playhead seeks back before it", async () => {
@@ -33203,10 +33325,38 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
33203
33325
  expect(onStallEntered).toHaveBeenCalledTimes(2);
33204
33326
  expect(onStallEnded).toHaveBeenCalledTimes(2);
33205
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
+ });
33206
33355
  it("stops reporting after disposal", async () => {
33207
33356
  monitor();
33208
33357
  host.dispatch("playing", {});
33209
33358
  stop();
33359
+ stop = void 0;
33210
33360
  await poll2(8);
33211
33361
  expect(onStallEntered).not.toHaveBeenCalled();
33212
33362
  });
@@ -55011,27 +55161,31 @@ x`);
55011
55161
  if (!supportsMse()) pending("MSE not supported");
55012
55162
  });
55013
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
+ }
55014
55186
  describe("when the play head freezes mid-playback", () => {
55015
55187
  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;
55188
+ const { player, mediaSource, stallEnteredSpy, stallEndedSpy } = await arrangeStall();
55035
55189
  const nextEnded = stallEndedSpy.next(
55036
55190
  10,
55037
55191
  "stallEnded after data buffered timed out after {timeout}s"
@@ -55040,7 +55194,62 @@ x`);
55040
55194
  const ended = await nextEnded;
55041
55195
  expect(ended.reason).toBe("playing");
55042
55196
  expect(stallEnteredSpy).toHaveBeenCalledTimes(1);
55197
+ expect(stallEndedSpy).toHaveBeenCalledTimes(1);
55043
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);
55044
55253
  });
55045
55254
  });
55046
55255
  });
@@ -55769,6 +55978,7 @@ x`);
55769
55978
  var ANCHOR_MS = Date.parse(ANCHOR_ISO);
55770
55979
  var CONTENT_ASSET = vinylTestAssets.hls.live_static_video_audio_60s_4s;
55771
55980
  var AD_ASSET = vinylTestAssets.hls.live_static_video_audio_60s_2s;
55981
+ var SHORT_AD_ASSET = vinylTestAssets.hls.edge_static_video_1s;
55772
55982
  function injectingManifestProvider(mainUrl, interstitials) {
55773
55983
  return async () => {
55774
55984
  const mainText = await (await fetch(mainUrl)).text();
@@ -56403,8 +56613,8 @@ x`);
56403
56613
  id: "postroll-1",
56404
56614
  startTime: 0,
56405
56615
  duration: 4,
56406
- // Short ad so the break ends quickly on its own.
56407
- 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 }],
56408
56618
  cue: "POST"
56409
56619
  }
56410
56620
  ])