@amazon/vinyl 4.0.1-next.20261017024550 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,8 +3,8 @@ import type { ReadonlyPlaybackController } from './ReadonlyPlaybackController';
3
3
  import { StallEndedReason } from './ReadonlyPlaybackController';
4
4
  export interface StallMonitorOptions {
5
5
  /**
6
- * Seconds the play head must be frozen (no `timeUpdate`) while playing before a stall is
7
- * reported. Default: 1
6
+ * Seconds the play head must be frozen (`currentTime` not advancing) while playing before a
7
+ * stall is reported. Default: 1
8
8
  */
9
9
  readonly stallThreshold?: number;
10
10
  /**
@@ -13,7 +13,7 @@ export interface StallMonitorOptions {
13
13
  readonly pollInterval?: number;
14
14
  }
15
15
  export interface StallEndedInfo {
16
- /** Time of the last `timeUpdate` observed before the play head froze. */
16
+ /** Time of the last progress observed before the play head froze. */
17
17
  readonly started: Timestamp;
18
18
  /** Time the stall ended. */
19
19
  readonly ended: Timestamp;
@@ -30,14 +30,19 @@ export interface StallMonitorCallbacks {
30
30
  }
31
31
  /**
32
32
  * Watches a {@link ReadonlyPlaybackController} for stalls while playing: the play head freezing
33
- * (no `timeUpdate`) for longer than {@link StallMonitorOptions.stallThreshold} seconds.
33
+ * (`currentTime` not advancing) for longer than {@link StallMonitorOptions.stallThreshold} seconds.
34
+ *
35
+ * Progress is observed both from `timeUpdate` events and by reading `currentTime` on each poll. A
36
+ * late `timeUpdate` alone is not a stall: when the main thread is busy (a long task, a GC pause) or
37
+ * event delivery is delayed, the play head keeps advancing while no `timeUpdate` reaches script,
38
+ * and the poll sees that advance rather than reporting a freeze.
34
39
  *
35
40
  * Detection only runs once playback has actually been observed (a `playing` event) and is
36
41
  * suspended on `pause`/`seeking`/`emptied` until playback is observed again — so a stall is never
37
42
  * reported during initial loading or while awaiting a seek. A detected stall ends when the play
38
43
  * head advances again (`playing`) or playback stops for another reason (`pause`/`seeking`/
39
- * `emptied`); {@link StallEndedInfo.duration} is measured from the last `timeUpdate` before the
40
- * freeze, so it reflects the full frozen time rather than only the time past the threshold.
44
+ * `emptied`); {@link StallEndedInfo.duration} is measured from the last observed progress before
45
+ * the freeze, so it reflects the full frozen time rather than only the time past the threshold.
41
46
  *
42
47
  * Returns a function that stops the monitor.
43
48
  */
@@ -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;AASD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAC9B,UAAU,EAAE,0BAA0B,EACtC,SAAS,EAAE,qBAAqB,EAChC,OAAO,GAAE,mBAAwB,GAClC,MAAM,IAAI,CA+EZ"}
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,qEAAqE;IACrE,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;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAC9B,UAAU,EAAE,0BAA0B,EACtC,SAAS,EAAE,qBAAqB,EAChC,OAAO,GAAE,mBAAwB,GAClC,MAAM,IAAI,CA8FZ"}
@@ -12,24 +12,29 @@ import { StallEndedReason } from './ReadonlyPlaybackController';
12
12
  const PROGRESS_EPSILON = 1e-6;
13
13
  /**
14
14
  * Watches a {@link ReadonlyPlaybackController} for stalls while playing: the play head freezing
15
- * (no `timeUpdate`) for longer than {@link StallMonitorOptions.stallThreshold} seconds.
15
+ * (`currentTime` not advancing) for longer than {@link StallMonitorOptions.stallThreshold} seconds.
16
+ *
17
+ * Progress is observed both from `timeUpdate` events and by reading `currentTime` on each poll. A
18
+ * late `timeUpdate` alone is not a stall: when the main thread is busy (a long task, a GC pause) or
19
+ * event delivery is delayed, the play head keeps advancing while no `timeUpdate` reaches script,
20
+ * and the poll sees that advance rather than reporting a freeze.
16
21
  *
17
22
  * Detection only runs once playback has actually been observed (a `playing` event) and is
18
23
  * suspended on `pause`/`seeking`/`emptied` until playback is observed again — so a stall is never
19
24
  * reported during initial loading or while awaiting a seek. A detected stall ends when the play
20
25
  * head advances again (`playing`) or playback stops for another reason (`pause`/`seeking`/
21
- * `emptied`); {@link StallEndedInfo.duration} is measured from the last `timeUpdate` before the
22
- * freeze, so it reflects the full frozen time rather than only the time past the threshold.
26
+ * `emptied`); {@link StallEndedInfo.duration} is measured from the last observed progress before
27
+ * the freeze, so it reflects the full frozen time rather than only the time past the threshold.
23
28
  *
24
29
  * Returns a function that stops the monitor.
25
30
  */
26
31
  export function createStallMonitor(controller, callbacks, options = {}) {
27
32
  const stallThresholdMs = (options.stallThreshold ?? 1) * 1000;
28
33
  const pollIntervalMs = (options.pollInterval ?? 0.25) * 1000;
29
- // Only detect once playback has been observed; gates out initial load and post-seek buffering.
30
- let observedPlaying = false;
31
- // Wall-clock time of the last `timeUpdate` — the reference start of any freeze.
34
+ // Wall-clock time of the last observed progress — the reference start of any freeze.
32
35
  let lastProgressAt = Date.now();
36
+ // Play head position at the last observed progress.
37
+ let lastProgressPosition = controller.currentTime;
33
38
  let inStall = false;
34
39
  let pollId = null;
35
40
  const { add, dispose } = createDisposer();
@@ -52,11 +57,26 @@ export function createStallMonitor(controller, callbacks, options = {}) {
52
57
  pollId = null;
53
58
  };
54
59
  add(stopPoll);
60
+ const markProgress = () => {
61
+ lastProgressAt = Date.now();
62
+ lastProgressPosition = controller.currentTime;
63
+ };
55
64
  const startPoll = () => {
56
65
  if (pollId != null)
57
66
  return;
67
+ // Polls only while playback is observed — started on `playing`, stopped by `suspend` —
68
+ // which gates out initial load and post-seek buffering.
58
69
  pollId = setInterval(() => {
59
- if (inStall || !observedPlaying)
70
+ // The play head advanced without a `timeUpdate` reaching us yet (e.g. a long task
71
+ // delayed it past this poll): that's progress, not a freeze.
72
+ if (controller.currentTime - lastProgressPosition >
73
+ PROGRESS_EPSILON) {
74
+ if (inStall)
75
+ endStall(StallEndedReason.PLAYING);
76
+ markProgress();
77
+ return;
78
+ }
79
+ if (inStall)
60
80
  return;
61
81
  if (Date.now() - lastProgressAt >= stallThresholdMs) {
62
82
  inStall = true;
@@ -68,7 +88,6 @@ export function createStallMonitor(controller, callbacks, options = {}) {
68
88
  // playback is observed again.
69
89
  const suspend = (reason) => {
70
90
  endStall(reason);
71
- observedPlaying = false;
72
91
  stopPoll();
73
92
  };
74
93
  add(controller.on('playing', () => {
@@ -78,8 +97,7 @@ export function createStallMonitor(controller, callbacks, options = {}) {
78
97
  // Whether resuming or freshly starting, the play head is advancing again, so restart the
79
98
  // freeze clock. Without this the poll could immediately re-detect a stall from the stale
80
99
  // pre-freeze timestamp before the first post-resume `timeUpdate` resets it.
81
- lastProgressAt = Date.now();
82
- observedPlaying = true;
100
+ markProgress();
83
101
  startPoll();
84
102
  }));
85
103
  add(controller.on('timeUpdate', ({ previous, current }) => {
@@ -90,7 +108,7 @@ export function createStallMonitor(controller, callbacks, options = {}) {
90
108
  // The play head advanced: close any open stall, then reset the freeze clock.
91
109
  if (inStall)
92
110
  endStall(StallEndedReason.PLAYING);
93
- lastProgressAt = Date.now();
111
+ markProgress();
94
112
  }));
95
113
  add(controller.on('pause', () => suspend(StallEndedReason.PAUSE)));
96
114
  add(controller.on('seeking', () => suspend(StallEndedReason.SEEKING)));