@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
@@ -7,6 +7,15 @@ import type { AdsProvider } from './AdsProvider';
7
7
  * The % playhead start tolerance for playback progress when emitting ad progress events.
8
8
  */
9
9
  export declare const AD_START_TOLERANCE = 0.05;
10
+ /**
11
+ * When a break's X-PLAYOUT-LIMIT budget is reached, an ad whose real media end is
12
+ * within this many seconds is allowed to finish on its own `ended` rather than
13
+ * being cut. The limit is a coarse, often whole-second budget while the media's
14
+ * true length is fractional, so without this grace an ad effectively as long as
15
+ * its budget would be clipped a fraction short. The tolerance is measured against
16
+ * the playback-reported (media) duration, not the ad's declared duration.
17
+ */
18
+ export declare const PLAYOUT_LIMIT_TOLERANCE = 1;
10
19
  export interface AdControllerImplDeps {
11
20
  readonly playbackController: ReadonlyPlaybackController;
12
21
  }
@@ -129,8 +138,15 @@ export declare class AdControllerImpl extends EventHostImpl<AdEventMap> implemen
129
138
  private emitAdProgressEvents;
130
139
  private emitTimeUpdate;
131
140
  /**
132
- * Ads breaks can have a playout limit that may cut off ad playback.
133
- * End the ad if that limit is reached.
141
+ * Enforces the break-level playout limit (X-PLAYOUT-LIMIT): a budget shared
142
+ * across all of the break's ads. When cumulative playout reaches it, the
143
+ * whole break ends, dropping any ads that have not started.
144
+ *
145
+ * This is deliberately NOT a per-ad cap. Each ad asset is its own source and
146
+ * ends on its media `ended` (see the 'ended' handler), which is what advances
147
+ * a multi-ad break. Ending an ad at its declared per-ad DURATION instead would
148
+ * truncate the real asset whenever a packager under-declares it — e.g. rounds
149
+ * a 6.732s asset down to a whole-second 6 — cutting off the fractional tail.
134
150
  */
135
151
  private enforcePlayoutLimits;
136
152
  private setPendingBreaks;
@@ -1 +1 @@
1
- {"version":3,"file":"AdControllerImpl.d.ts","sourceRoot":"","sources":["../../../../src/ad/AdControllerImpl.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AdControllerImpl.d.ts","sourceRoot":"","sources":["../../../../src/ad/AdControllerImpl.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,15 @@ import { AdError } from './AdError';
8
8
  * The % playhead start tolerance for playback progress when emitting ad progress events.
9
9
  */
10
10
  export const AD_START_TOLERANCE = 0.05;
11
+ /**
12
+ * When a break's X-PLAYOUT-LIMIT budget is reached, an ad whose real media end is
13
+ * within this many seconds is allowed to finish on its own `ended` rather than
14
+ * being cut. The limit is a coarse, often whole-second budget while the media's
15
+ * true length is fractional, so without this grace an ad effectively as long as
16
+ * its budget would be clipped a fraction short. The tolerance is measured against
17
+ * the playback-reported (media) duration, not the ad's declared duration.
18
+ */
19
+ export const PLAYOUT_LIMIT_TOLERANCE = 1;
11
20
  export const defaultAdControllerImplOptions = {
12
21
  adLoadTimeout: 15,
13
22
  preloadAheadTime: 10,
@@ -560,30 +569,43 @@ export class AdControllerImpl extends EventHostImpl {
560
569
  });
561
570
  }
562
571
  /**
563
- * Ads breaks can have a playout limit that may cut off ad playback.
564
- * End the ad if that limit is reached.
572
+ * Enforces the break-level playout limit (X-PLAYOUT-LIMIT): a budget shared
573
+ * across all of the break's ads. When cumulative playout reaches it, the
574
+ * whole break ends, dropping any ads that have not started.
575
+ *
576
+ * This is deliberately NOT a per-ad cap. Each ad asset is its own source and
577
+ * ends on its media `ended` (see the 'ended' handler), which is what advances
578
+ * a multi-ad break. Ending an ad at its declared per-ad DURATION instead would
579
+ * truncate the real asset whenever a packager under-declares it — e.g. rounds
580
+ * a 6.732s asset down to a whole-second 6 — cutting off the fractional tail.
565
581
  */
566
582
  enforcePlayoutLimits(adState) {
583
+ if (!adState.started)
584
+ return;
585
+ const { parentBreak } = adState;
586
+ // Enforce independent of a finite content duration so live ads with a
587
+ // declared playout limit are still bounded.
588
+ const limit = parentBreak.adBreak.playoutLimit;
589
+ if (limit == null)
590
+ return;
567
591
  const pC = this.deps.playbackController;
568
- const { ad, parentBreak } = adState;
569
- // Enforce playout limits independent of a finite content duration so
570
- // live ads with a declared playout limit are still bounded.
571
- if (adState.started) {
572
- const elapsed = Math.max(0, pC.currentTime - adState.timeStart);
573
- const adBreak = parentBreak.adBreak;
574
- const limit = adBreak.playoutLimit;
575
- if (limit != null &&
576
- parentBreak.playoutElapsed + elapsed >= limit) {
577
- // The break's total playout limit is reached: end the whole
578
- // break, dropping any ads that have not started.
579
- parentBreak.clear();
580
- this.endAd(adState);
581
- }
582
- else if (ad.duration != null && elapsed >= ad.duration) {
583
- // This ad reached its declared duration: advance to the next.
584
- this.endAd(adState);
585
- }
592
+ const elapsed = Math.max(0, pC.currentTime - adState.timeStart);
593
+ if (parentBreak.playoutElapsed + elapsed < limit)
594
+ return;
595
+ // Budget reached — but if the current ad is essentially at its natural
596
+ // media end (within PLAYOUT_LIMIT_TOLERANCE of the playback-reported
597
+ // duration), let it finish on its own `ended` rather than clipping the
598
+ // tail. A known finite, positive duration is required; an unknown (0) or
599
+ // non-finite duration (e.g. live) enforces the limit strictly.
600
+ if (isFinite(pC.duration) &&
601
+ pC.duration > 0 &&
602
+ pC.duration - pC.currentTime <= PLAYOUT_LIMIT_TOLERANCE) {
603
+ return;
586
604
  }
605
+ // The break's total playout limit is reached: end the whole break,
606
+ // dropping any ads that have not started.
607
+ parentBreak.clear();
608
+ this.endAd(adState);
587
609
  }
588
610
  setPendingBreaks(newPending) {
589
611
  this.pendingAdBreaks = newPending.filter((adBreak) => !this.isBreakSuppressed(adBreak));
@@ -1 +1 @@
1
- {"version":3,"file":"createStallMonitor.d.ts","sourceRoot":"","sources":["../../../../src/playback/createStallMonitor.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AAC9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAE/D,MAAM,WAAW,mBAAmB;IAChC;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAEhC;;OAEG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CACjC;AAED,MAAM,WAAW,cAAc;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAA;IAC3B,4BAA4B;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,2BAA2B;IAC3B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;CACpC;AAED,MAAM,WAAW,qBAAqB;IAClC,6CAA6C;IAC7C,QAAQ,CAAC,cAAc,EAAE,MAAM,IAAI,CAAA;IACnC,yCAAyC;IACzC,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAA;CACxD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAC9B,UAAU,EAAE,0BAA0B,EACtC,SAAS,EAAE,qBAAqB,EAChC,OAAO,GAAE,mBAAwB,GAClC,MAAM,IAAI,CA8EZ"}
1
+ {"version":3,"file":"createStallMonitor.d.ts","sourceRoot":"","sources":["../../../../src/playback/createStallMonitor.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AAC9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAE/D,MAAM,WAAW,mBAAmB;IAChC;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAEhC;;OAEG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CACjC;AAED,MAAM,WAAW,cAAc;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAA;IAC3B,4BAA4B;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,2BAA2B;IAC3B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;CACpC;AAED,MAAM,WAAW,qBAAqB;IAClC,6CAA6C;IAC7C,QAAQ,CAAC,cAAc,EAAE,MAAM,IAAI,CAAA;IACnC,yCAAyC;IACzC,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAA;CACxD;AASD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAC9B,UAAU,EAAE,0BAA0B,EACtC,SAAS,EAAE,qBAAqB,EAChC,OAAO,GAAE,mBAAwB,GAClC,MAAM,IAAI,CA+EZ"}
@@ -4,6 +4,12 @@
4
4
  */
5
5
  import { createDisposer } from '@amazon/vinyl-util';
6
6
  import { StallEndedReason } from './ReadonlyPlaybackController';
7
+ /**
8
+ * Minimum forward `currentTime` change (seconds) that counts as real progress. Guards the
9
+ * progress check against floating-point jitter so a negligible advance isn't mistaken for the
10
+ * play head moving.
11
+ */
12
+ const PROGRESS_EPSILON = 1e-6;
7
13
  /**
8
14
  * Watches a {@link ReadonlyPlaybackController} for stalls while playing: the play head freezing
9
15
  * (no `timeUpdate`) for longer than {@link StallMonitorOptions.stallThreshold} seconds.
@@ -26,6 +32,7 @@ export function createStallMonitor(controller, callbacks, options = {}) {
26
32
  let lastProgressAt = Date.now();
27
33
  let inStall = false;
28
34
  let pollId = null;
35
+ const { add, dispose } = createDisposer();
29
36
  const endStall = (reason) => {
30
37
  if (!inStall)
31
38
  return;
@@ -44,6 +51,7 @@ export function createStallMonitor(controller, callbacks, options = {}) {
44
51
  clearInterval(pollId);
45
52
  pollId = null;
46
53
  };
54
+ add(stopPoll);
47
55
  const startPoll = () => {
48
56
  if (pollId != null)
49
57
  return;
@@ -63,18 +71,22 @@ export function createStallMonitor(controller, callbacks, options = {}) {
63
71
  observedPlaying = false;
64
72
  stopPoll();
65
73
  };
66
- const { add, dispose } = createDisposer();
67
74
  add(controller.on('playing', () => {
68
- // Resuming from a stall closes it; otherwise this is a fresh start of playback, so
69
- // begin the freeze clock now (no `timeUpdate` has necessarily arrived yet).
75
+ // Resuming from a stall closes it (using the pre-resume freeze start for its duration).
70
76
  if (inStall)
71
77
  endStall(StallEndedReason.PLAYING);
72
- else
73
- lastProgressAt = Date.now();
78
+ // Whether resuming or freshly starting, the play head is advancing again, so restart the
79
+ // freeze clock. Without this the poll could immediately re-detect a stall from the stale
80
+ // pre-freeze timestamp before the first post-resume `timeUpdate` resets it.
81
+ lastProgressAt = Date.now();
74
82
  observedPlaying = true;
75
83
  startPoll();
76
84
  }));
77
- add(controller.on('timeUpdate', () => {
85
+ add(controller.on('timeUpdate', ({ previous, current }) => {
86
+ // Browsers emit `timeUpdate` while frozen (unchanged/backward `currentTime`); only a
87
+ // forward advance is progress, else a stall is ended and re-detected, doubling events.
88
+ if (previous != null && current - previous <= PROGRESS_EPSILON)
89
+ return;
78
90
  // The play head advanced: close any open stall, then reset the freeze clock.
79
91
  if (inStall)
80
92
  endStall(StallEndedReason.PLAYING);
@@ -83,12 +95,5 @@ export function createStallMonitor(controller, callbacks, options = {}) {
83
95
  add(controller.on('pause', () => suspend(StallEndedReason.PAUSE)));
84
96
  add(controller.on('seeking', () => suspend(StallEndedReason.SEEKING)));
85
97
  add(controller.on('emptied', () => suspend(StallEndedReason.EMPTIED)));
86
- let stopped = false;
87
- return () => {
88
- if (stopped)
89
- return;
90
- stopped = true;
91
- stopPoll();
92
- dispose();
93
- };
98
+ return dispose;
94
99
  }