cursedbelt 5.101.0 → 5.103.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.
Files changed (72) hide show
  1. package/dist/react/engagement/beacon.js +3 -3
  2. package/dist/react/engagement/beacon.js.map +1 -1
  3. package/dist/react/engagement/beaconClient.d.ts +25 -2
  4. package/dist/react/engagement/beaconClient.d.ts.map +1 -1
  5. package/dist/react/engagement/beaconClient.js +105 -14
  6. package/dist/react/engagement/beaconClient.js.map +1 -1
  7. package/dist/react/engagement/index.d.ts +2 -1
  8. package/dist/react/engagement/index.d.ts.map +1 -1
  9. package/dist/react/engagement/index.js +4 -2
  10. package/dist/react/engagement/index.js.map +1 -1
  11. package/dist/react/engagement/playbackBeacon.d.ts +3 -0
  12. package/dist/react/engagement/playbackBeacon.d.ts.map +1 -0
  13. package/dist/react/engagement/playbackBeacon.js +27 -0
  14. package/dist/react/engagement/playbackBeacon.js.map +1 -0
  15. package/dist/react/media/MediaLightbox.js +1 -1
  16. package/dist/react/media/MediaLightbox.js.map +1 -1
  17. package/dist/react/media/VideoPlayer.d.ts +3 -1
  18. package/dist/react/media/VideoPlayer.d.ts.map +1 -1
  19. package/dist/react/media/VideoPlayer.js +62 -14
  20. package/dist/react/media/VideoPlayer.js.map +1 -1
  21. package/dist/react/media/VideoSlideContent.d.ts +12 -7
  22. package/dist/react/media/VideoSlideContent.d.ts.map +1 -1
  23. package/dist/react/media/VideoSlideContent.js +19 -9
  24. package/dist/react/media/VideoSlideContent.js.map +1 -1
  25. package/dist/react/media/mediaPhoto.d.ts +3 -0
  26. package/dist/react/media/mediaPhoto.d.ts.map +1 -1
  27. package/dist/react/media/mediaPhoto.js.map +1 -1
  28. package/dist/react/media/playbackWatch.d.ts +83 -0
  29. package/dist/react/media/playbackWatch.d.ts.map +1 -0
  30. package/dist/react/media/playbackWatch.js +167 -0
  31. package/dist/react/media/playbackWatch.js.map +1 -0
  32. package/dist/react/media/playerSource.d.ts +24 -0
  33. package/dist/react/media/playerSource.d.ts.map +1 -0
  34. package/dist/react/media/playerSource.js +22 -0
  35. package/dist/react/media/playerSource.js.map +1 -0
  36. package/dist/react/media/sourceFault.d.ts +5 -59
  37. package/dist/react/media/sourceFault.d.ts.map +1 -1
  38. package/dist/react/media/sourceFault.js +69 -2
  39. package/dist/react/media/sourceFault.js.map +1 -1
  40. package/dist/react/media/usePlaybackWatch.d.ts +14 -0
  41. package/dist/react/media/usePlaybackWatch.d.ts.map +1 -0
  42. package/dist/react/media/usePlaybackWatch.js +32 -0
  43. package/dist/react/media/usePlaybackWatch.js.map +1 -0
  44. package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
  45. package/dist/react/media-gallery/MediaGallery.js +21 -5
  46. package/dist/react/media-gallery/MediaGallery.js.map +1 -1
  47. package/dist/testing/designBaseline.d.ts +5 -1
  48. package/dist/testing/designBaseline.d.ts.map +1 -1
  49. package/dist/testing/designBaseline.js +27 -13
  50. package/dist/testing/designBaseline.js.map +1 -1
  51. package/package.json +2 -2
  52. package/src/react/engagement/beacon.spec.tsx +2 -1
  53. package/src/react/engagement/beacon.tsx +3 -3
  54. package/src/react/engagement/beaconClient.spec.ts +63 -6
  55. package/src/react/engagement/beaconClient.ts +119 -13
  56. package/src/react/engagement/index.ts +4 -1
  57. package/src/react/engagement/playbackBeacon.ts +27 -0
  58. package/src/react/media/MediaLightbox.tsx +2 -2
  59. package/src/react/media/VideoPlayer.spec.tsx +24 -0
  60. package/src/react/media/VideoPlayer.tsx +72 -12
  61. package/src/react/media/VideoSlideContent.spec.tsx +28 -0
  62. package/src/react/media/VideoSlideContent.tsx +49 -32
  63. package/src/react/media/mediaPhoto.ts +3 -0
  64. package/src/react/media/playbackWatch.spec.ts +196 -0
  65. package/src/react/media/playbackWatch.ts +218 -0
  66. package/src/react/media/playerSource.ts +30 -0
  67. package/src/react/media/sourceFault.spec.tsx +12 -0
  68. package/src/react/media/sourceFault.tsx +25 -1
  69. package/src/react/media/usePlaybackWatch.tsx +34 -0
  70. package/src/react/media-gallery/MediaGallery.tsx +32 -13
  71. package/src/testing/designBaseline.spec.ts +36 -0
  72. package/src/testing/designBaseline.ts +34 -15
@@ -35,10 +35,10 @@ export function useEngagementBeacon(place, options = {}) {
35
35
  * may and may not carry.
36
36
  */
37
37
  export function useEngagementAction(action, options = {}) {
38
- const { basePath, fetchImpl, sessionPath, assumeSignedIn, place } = options;
38
+ const { basePath, fetchImpl, sessionPath, assumeSignedIn, place, beaconImpl } = options;
39
39
  return useCallback(() => {
40
- void reportAction(action, { basePath, fetchImpl, sessionPath, assumeSignedIn, place });
41
- }, [action, basePath, fetchImpl, sessionPath, assumeSignedIn, place]);
40
+ void reportAction(action, { basePath, fetchImpl, sessionPath, assumeSignedIn, place, beaconImpl });
41
+ }, [action, basePath, fetchImpl, sessionPath, assumeSignedIn, place, beaconImpl]);
42
42
  }
43
43
  /**
44
44
  * Report wherever the browser IS, and keep reporting as it moves.
@@ -1 +1 @@
1
- {"version":3,"file":"beacon.js","sourceRoot":"","sources":["../../../src/react/engagement/beacon.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC/C,OAAO,EAGN,YAAY,EACZ,WAAW,EACX,mBAAmB,GACnB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAoB,EAAE,UAA8B,EAAE;IACzF,iGAAiG;IACjG,+FAA+F;IAC/F,kFAAkF;IAClF,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IACrE,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,yEAAyE;QACzE,6CAA6C;QAC7C,KAAK,WAAW,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,CAAC;IAC/E,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,UAA+B,EAAE;IACpF,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAC5E,OAAO,WAAW,CAAC,GAAG,EAAE;QACvB,KAAK,YAAY,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAC;IACxF,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,CAAC,CAAC,CAAC;AACvE,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA8B,EAAE;IACjE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IACrE,0EAA0E;IAC1E,uEAAuE;IACvE,uEAAuE;IACvE,SAAS,CACR,GAAG,EAAE,CAAC,mBAAmB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,EAC/E,CAAC,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,CAAC,CAClD,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,gEAAgE;AAChE,MAAM,UAAU,gBAAgB,CAAC,KAA4B;IAC5D,iBAAiB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACtE,OAAO,IAAI,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"beacon.js","sourceRoot":"","sources":["../../../src/react/engagement/beacon.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC/C,OAAO,EAGN,YAAY,EACZ,WAAW,EACX,mBAAmB,GACnB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAoB,EAAE,UAA8B,EAAE;IACzF,iGAAiG;IACjG,+FAA+F;IAC/F,kFAAkF;IAClF,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IACrE,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,yEAAyE;QACzE,6CAA6C;QAC7C,KAAK,WAAW,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,CAAC;IAC/E,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,UAA+B,EAAE;IACpF,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IACxF,OAAO,WAAW,CAAC,GAAG,EAAE;QACvB,KAAK,YAAY,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;IACpG,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC;AACnF,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA8B,EAAE;IACjE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IACrE,0EAA0E;IAC1E,uEAAuE;IACvE,uEAAuE;IACvE,SAAS,CACR,GAAG,EAAE,CAAC,mBAAmB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,EAC/E,CAAC,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,CAAC,CAClD,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,gEAAgE;AAChE,MAAM,UAAU,gBAAgB,CAAC,KAA4B;IAC5D,iBAAiB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACtE,OAAO,IAAI,CAAC;AACb,CAAC"}
@@ -85,7 +85,7 @@ export interface ReportPlaceOptions {
85
85
  * not sending it in the first place means user text does not travel at all.
86
86
  */
87
87
  export declare function currentLocationPlace(): string;
88
- /** Reset the de-duplication memory. Tests, and a sign-out. */
88
+ /** Reset the de-duplication memory, and drop any queued action unsent. Tests, and a sign-out. */
89
89
  export declare function resetEngagementBeacon(): void;
90
90
  /**
91
91
  * Report an arrival at `place`. Returns whether anything was sent.
@@ -100,10 +100,27 @@ export interface ReportActionOptions extends ReportPlaceOptions {
100
100
  * against the manifest exactly as it resolves a view, so the default is right for every app.
101
101
  */
102
102
  place?: string;
103
+ /** Injected in tests; defaults to `navigator.sendBeacon`. The page-leave flush sends through it. */
104
+ beaconImpl?: (url: string, body: Blob) => boolean;
103
105
  }
106
+ /** How long the action queue waits for quiet before it sends (forge task 4034). */
107
+ export declare const ACTION_QUIET_MS = 5000;
108
+ /** The longest an action waits however busy the page is, so a page that crashes loses 30 s at most. */
109
+ export declare const ACTION_MAX_WAIT_MS = 30000;
110
+ /** One POST carries at most this many: the recorder's `MAX_ACTION_BATCH` (cursedbelt-server). */
111
+ export declare const ACTION_BATCH_MAX = 100;
112
+ /**
113
+ * Send what is queued, now: ONE POST per mount. A queue of one posts `{ place, action }` as
114
+ * `reportAction` always has, so a recorder older than the batch reads it; more posts
115
+ * `{ actions }`, and a recorder that refuses the batch (an older one answers 400) gets them one by
116
+ * one instead. `leaving` sends through `sendBeacon`, which outlives the page. Never throws.
117
+ */
118
+ export declare function flushEngagementActions(options?: {
119
+ leaving?: boolean;
120
+ }): Promise<void>;
104
121
  /**
105
122
  * Report that a FEATURE was used — `reportAction("search")` — beside the place it was used on
106
- * (forge task 3470). Returns whether anything was sent.
123
+ * (forge task 3470). Returns whether it was queued to send.
107
124
  *
108
125
  * The owner's question is "what features do they use and ignore"; a page view cannot answer it,
109
126
  * because one page holds several features. So the app names the action at the press, from the
@@ -117,6 +134,12 @@ export interface ReportActionOptions extends ReportPlaceOptions {
117
134
  * · the same silence: session-probed (the probe's answer is shared with `reportPlace`, one per
118
135
  * page load), fire-and-forget, never throws, never logs.
119
136
  *
137
+ * 🔴 BATCHED (forge task 4034): an action is queued, and the queue goes as ONE request after
138
+ * {@link ACTION_QUIET_MS} of quiet, {@link ACTION_MAX_WAIT_MS} after its first action at most, at
139
+ * {@link ACTION_BATCH_MAX}, and when the page is left ({@link flushEngagementActions}). So a
140
+ * feature the owner adds to the record costs no request per click — his words: *"batching some
141
+ * interactions with debounces will lower traffic"*.
142
+ *
120
143
  * NOT de-duplicated, unlike `reportPlace`: three plays are three uses. Only call it from apps
121
144
  * whose recorder was mounted WITH `actions` — elsewhere `/action` is a 404 in the console.
122
145
  */
@@ -1 +1 @@
1
- {"version":3,"file":"beaconClient.d.ts","sourceRoot":"","sources":["../../../src/react/engagement/beaconClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AAEtD;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,sBAAsB,CAAC;AAEhD,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAaD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAI7C;AAYD,8DAA8D;AAC9D,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AA6CD;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,kBAAuB,GAC9B,OAAO,CAAC,OAAO,CAAC,CAgClB;AAED,MAAM,WAAW,mBAAoB,SAAQ,kBAAkB;IAC9D;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,OAAO,CAAC,CAsBtG;AAED,8DAA8D;AAC9D,MAAM,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC;AAIpC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAehF"}
1
+ {"version":3,"file":"beaconClient.d.ts","sourceRoot":"","sources":["../../../src/react/engagement/beaconClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AAEtD;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,sBAAsB,CAAC;AAEhD,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAaD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAI7C;AAYD,iGAAiG;AACjG,wBAAgB,qBAAqB,IAAI,IAAI,CAQ5C;AA6CD;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,kBAAuB,GAC9B,OAAO,CAAC,OAAO,CAAC,CAgClB;AAED,MAAM,WAAW,mBAAoB,SAAQ,kBAAkB;IAC9D;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oGAAoG;IACpG,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC;CAClD;AAED,mFAAmF;AACnF,eAAO,MAAM,eAAe,OAAQ,CAAC;AACrC,uGAAuG;AACvG,eAAO,MAAM,kBAAkB,QAAS,CAAC;AACzC,iGAAiG;AACjG,eAAO,MAAM,gBAAgB,MAAM,CAAC;AA8CpC;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/F;AAuBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,OAAO,CAAC,CAwBtG;AAED,8DAA8D;AAC9D,MAAM,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC;AAIpC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAehF"}
@@ -92,10 +92,17 @@ export function currentLocationPlace() {
92
92
  * dozens.
93
93
  */
94
94
  let sessionAnswer = null;
95
- /** Reset the de-duplication memory. Tests, and a sign-out. */
95
+ /** Reset the de-duplication memory, and drop any queued action unsent. Tests, and a sign-out. */
96
96
  export function resetEngagementBeacon() {
97
97
  lastReported = null;
98
98
  sessionAnswer = null;
99
+ for (const queue of queues.values()) {
100
+ if (queue.quiet)
101
+ clearTimeout(queue.quiet);
102
+ if (queue.due)
103
+ clearTimeout(queue.due);
104
+ }
105
+ queues.clear();
99
106
  }
100
107
  /**
101
108
  * Read an answer to its end, and drop it.
@@ -183,9 +190,85 @@ export async function reportPlace(place, options = {}) {
183
190
  return false;
184
191
  }
185
192
  }
193
+ /** How long the action queue waits for quiet before it sends (forge task 4034). */
194
+ export const ACTION_QUIET_MS = 5_000;
195
+ /** The longest an action waits however busy the page is, so a page that crashes loses 30 s at most. */
196
+ export const ACTION_MAX_WAIT_MS = 30_000;
197
+ /** One POST carries at most this many: the recorder's `MAX_ACTION_BATCH` (cursedbelt-server). */
198
+ export const ACTION_BATCH_MAX = 100;
199
+ const queues = new Map();
200
+ /** The window the page-leave listeners sit on, so a second one is never added (tests swap it). */
201
+ let leaveTarget = null;
202
+ function defaultBeacon() {
203
+ const nav = globalThis.navigator;
204
+ return typeof nav?.sendBeacon === "function" ? nav.sendBeacon.bind(nav) : undefined;
205
+ }
206
+ /**
207
+ * Flush on the way out: `pagehide` (a close, a navigation, the bfcache) and a tab going hidden,
208
+ * which is the last moment a phone gives a page it is about to kill. A timer cannot fire after
209
+ * either, so this is what delivers the actions of a page closed inside its quiet window.
210
+ */
211
+ function listenForLeave() {
212
+ const win = globalThis.window;
213
+ if (!win || typeof win.addEventListener !== "function" || leaveTarget === win)
214
+ return;
215
+ leaveTarget = win;
216
+ const leave = () => {
217
+ void flushEngagementActions({ leaving: true });
218
+ };
219
+ win.addEventListener("pagehide", leave);
220
+ const doc = globalThis.document;
221
+ doc?.addEventListener?.("visibilitychange", () => {
222
+ if (doc.visibilityState === "hidden")
223
+ leave();
224
+ });
225
+ }
226
+ /**
227
+ * Send what is queued, now: ONE POST per mount. A queue of one posts `{ place, action }` as
228
+ * `reportAction` always has, so a recorder older than the batch reads it; more posts
229
+ * `{ actions }`, and a recorder that refuses the batch (an older one answers 400) gets them one by
230
+ * one instead. `leaving` sends through `sendBeacon`, which outlives the page. Never throws.
231
+ */
232
+ export async function flushEngagementActions(options = {}) {
233
+ const sends = [];
234
+ for (const [basePath, queue] of queues) {
235
+ if (queue.quiet)
236
+ clearTimeout(queue.quiet);
237
+ if (queue.due)
238
+ clearTimeout(queue.due);
239
+ queues.delete(basePath);
240
+ for (let i = 0; i < queue.actions.length; i += ACTION_BATCH_MAX) {
241
+ sends.push(sendActions(`${basePath}/action`, queue.actions.slice(i, i + ACTION_BATCH_MAX), queue, options.leaving === true));
242
+ }
243
+ }
244
+ await Promise.all(sends);
245
+ }
246
+ async function sendActions(url, actions, queue, leaving) {
247
+ const [only] = actions;
248
+ const body = JSON.stringify(actions.length === 1 && only ? { place: only.place, action: only.action } : { actions });
249
+ try {
250
+ if (leaving && queue.beaconImpl?.(url, new Blob([body], { type: "application/json" })))
251
+ return;
252
+ const res = await queue.fetchImpl(url, {
253
+ method: "POST",
254
+ headers: { "content-type": "application/json" },
255
+ credentials: "same-origin",
256
+ body,
257
+ keepalive: true,
258
+ });
259
+ await drain(res);
260
+ if (res.status === 400 && actions.length > 1) {
261
+ for (const a of actions)
262
+ await sendActions(url, [a], queue, false);
263
+ }
264
+ }
265
+ catch {
266
+ // See the header: telemetry never becomes the user's problem.
267
+ }
268
+ }
186
269
  /**
187
270
  * Report that a FEATURE was used — `reportAction("search")` — beside the place it was used on
188
- * (forge task 3470). Returns whether anything was sent.
271
+ * (forge task 3470). Returns whether it was queued to send.
189
272
  *
190
273
  * The owner's question is "what features do they use and ignore"; a page view cannot answer it,
191
274
  * because one page holds several features. So the app names the action at the press, from the
@@ -199,6 +282,12 @@ export async function reportPlace(place, options = {}) {
199
282
  * · the same silence: session-probed (the probe's answer is shared with `reportPlace`, one per
200
283
  * page load), fire-and-forget, never throws, never logs.
201
284
  *
285
+ * 🔴 BATCHED (forge task 4034): an action is queued, and the queue goes as ONE request after
286
+ * {@link ACTION_QUIET_MS} of quiet, {@link ACTION_MAX_WAIT_MS} after its first action at most, at
287
+ * {@link ACTION_BATCH_MAX}, and when the page is left ({@link flushEngagementActions}). So a
288
+ * feature the owner adds to the record costs no request per click — his words: *"batching some
289
+ * interactions with debounces will lower traffic"*.
290
+ *
202
291
  * NOT de-duplicated, unlike `reportPlace`: three plays are three uses. Only call it from apps
203
292
  * whose recorder was mounted WITH `actions` — elsewhere `/action` is a 404 in the console.
204
293
  */
@@ -214,20 +303,22 @@ export async function reportAction(action, options = {}) {
214
303
  return false;
215
304
  }
216
305
  const place = options.place ?? (typeof window === "undefined" ? "/" : currentLocationPlace());
217
- try {
218
- const res = await doFetch(`${options.basePath ?? ENGAGEMENT_BASE_PATH}/action`, {
219
- method: "POST",
220
- headers: { "content-type": "application/json" },
221
- credentials: "same-origin",
222
- body: JSON.stringify({ place, action }),
223
- keepalive: true,
224
- });
225
- await drain(res);
306
+ const basePath = options.basePath ?? ENGAGEMENT_BASE_PATH;
307
+ const queue = queues.get(basePath) ?? { actions: [], fetchImpl: doFetch, beaconImpl: undefined, quiet: null, due: null };
308
+ queue.fetchImpl = doFetch;
309
+ queue.beaconImpl = options.beaconImpl ?? defaultBeacon();
310
+ queue.actions.push({ place, action, at: Date.now() });
311
+ queues.set(basePath, queue);
312
+ listenForLeave();
313
+ if (queue.actions.length >= ACTION_BATCH_MAX) {
314
+ void flushEngagementActions();
226
315
  return true;
227
316
  }
228
- catch {
229
- return false;
230
- }
317
+ if (queue.quiet)
318
+ clearTimeout(queue.quiet);
319
+ queue.quiet = setTimeout(() => void flushEngagementActions(), ACTION_QUIET_MS);
320
+ queue.due ??= setTimeout(() => void flushEngagementActions(), ACTION_MAX_WAIT_MS);
321
+ return true;
231
322
  }
232
323
  let running = null;
233
324
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"beaconClient.js","sourceRoot":"","sources":["../../../src/react/engagement/beaconClient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"beaconClient.js","sourceRoot":"","sources":["../../../src/react/engagement/beaconClient.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
- export { ENGAGEMENT_BASE_PATH, type ReportActionOptions, type ReportPlaceOptions, reportAction, reportPlace, resetEngagementBeacon, SESSION_PATH, type StopBeacon, startLocationBeacon, currentLocationPlace, } from './beaconClient.js';
1
+ export { ENGAGEMENT_BASE_PATH, flushEngagementActions, type ReportActionOptions, type ReportPlaceOptions, reportAction, reportPlace, resetEngagementBeacon, SESSION_PATH, type StopBeacon, startLocationBeacon, currentLocationPlace, } from './beaconClient.js';
2
+ export { startPlaybackBeacon } from './playbackBeacon.js';
2
3
  export { EngagementBeacon, useEngagementAction, useEngagementBeacon, useLocationBeacon } from './beacon.js';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/engagement/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,YAAY,EACZ,WAAW,EACX,qBAAqB,EACrB,YAAY,EACZ,KAAK,UAAU,EACf,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/engagement/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACN,oBAAoB,EACpB,sBAAsB,EACtB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,YAAY,EACZ,WAAW,EACX,qBAAqB,EACrB,YAAY,EACZ,KAAK,UAAU,EACf,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
@@ -1,5 +1,7 @@
1
1
  // `cursedbelt/react/engagement-beacon` — the engagement beacon: `reportPlace`, `reportAction` and
2
- // `startLocationBeacon` (plain JS, no React) and the React hooks over them.
3
- export { ENGAGEMENT_BASE_PATH, reportAction, reportPlace, resetEngagementBeacon, SESSION_PATH, startLocationBeacon, currentLocationPlace, } from './beaconClient.js';
2
+ // `startLocationBeacon` (plain JS, no React) and the React hooks over them,
3
+ // and `startPlaybackBeacon`, which turns on the player's load-time reports.
4
+ export { ENGAGEMENT_BASE_PATH, flushEngagementActions, reportAction, reportPlace, resetEngagementBeacon, SESSION_PATH, startLocationBeacon, currentLocationPlace, } from './beaconClient.js';
5
+ export { startPlaybackBeacon } from './playbackBeacon.js';
4
6
  export { EngagementBeacon, useEngagementAction, useEngagementBeacon, useLocationBeacon } from './beacon.js';
5
7
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/engagement/index.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,4EAA4E;AAC5E,OAAO,EACN,oBAAoB,EAGpB,YAAY,EACZ,WAAW,EACX,qBAAqB,EACrB,YAAY,EAEZ,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/engagement/index.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,4EAA4E;AAC5E,4EAA4E;AAC5E,OAAO,EACN,oBAAoB,EACpB,sBAAsB,EAGtB,YAAY,EACZ,WAAW,EACX,qBAAqB,EACrB,YAAY,EAEZ,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare const PLAYBACK_TELEMETRY_PATH = "/api/playback-telemetry";
2
+ export declare function startPlaybackBeacon(path?: string): () => void;
3
+ //# sourceMappingURL=playbackBeacon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playbackBeacon.d.ts","sourceRoot":"","sources":["../../../src/react/engagement/playbackBeacon.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,uBAAuB,4BAA4B,CAAC;AAEjE,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,MAAgC,GAAG,MAAM,IAAI,CActF"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `startPlaybackBeacon` — send what `media/playbackWatch.ts` measures to the app's own API.
3
+ *
4
+ * A POST while the page is alive, and `sendBeacon` when it may not be: a report that a person gave
5
+ * up on a spinner is written as they leave, and a plain `fetch` there is canceled with the page.
6
+ * Same-origin, so the session cookie says whose it is and the body never has to. Failure is silence
7
+ * (`beaconClient.ts`): telemetry never becomes the viewer's problem.
8
+ */
9
+ import { setPlaybackReporter } from '../media/playbackWatch.js';
10
+ export const PLAYBACK_TELEMETRY_PATH = '/api/playback-telemetry';
11
+ export function startPlaybackBeacon(path = PLAYBACK_TELEMETRY_PATH) {
12
+ const report = (playback, leaving) => {
13
+ const body = JSON.stringify(playback);
14
+ if (leaving && navigator.sendBeacon?.(path, new Blob([body], { type: 'application/json' })))
15
+ return;
16
+ void fetch(path, {
17
+ method: 'POST',
18
+ credentials: 'same-origin',
19
+ keepalive: true,
20
+ headers: { 'content-type': 'application/json' },
21
+ body,
22
+ }).catch(() => undefined);
23
+ };
24
+ setPlaybackReporter(report);
25
+ return () => setPlaybackReporter(null);
26
+ }
27
+ //# sourceMappingURL=playbackBeacon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playbackBeacon.js","sourceRoot":"","sources":["../../../src/react/engagement/playbackBeacon.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAA8C,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAE5G,MAAM,CAAC,MAAM,uBAAuB,GAAG,yBAAyB,CAAC;AAEjE,MAAM,UAAU,mBAAmB,CAAC,OAAe,uBAAuB;IACxE,MAAM,MAAM,GAAqB,CAAC,QAAwB,EAAE,OAAO,EAAE,EAAE;QACrE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QACtC,IAAI,OAAO,IAAI,SAAS,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,CAAC;YAAE,OAAO;QACpG,KAAK,KAAK,CAAC,IAAI,EAAE;YACf,MAAM,EAAE,MAAM;YACd,WAAW,EAAE,aAAa;YAC1B,SAAS,EAAE,IAAI;YACf,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI;SACL,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC,CAAC;IACF,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,OAAO,GAAG,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;AACzC,CAAC"}
@@ -18,7 +18,7 @@ import { VideoSlideContent } from './VideoSlideContent.js';
18
18
  */
19
19
  export function MediaLightbox({ open, index, slides, close, onView, ...rest }) {
20
20
  return (_jsx(Lightbox, { ...rest, open: open, index: index, close: close, slides: slides, plugins: [Thumbnails, Zoom, Captions, Counter], carousel: { finite: true }, thumbnails: { position: 'bottom', showToggle: true }, on: { view: ({ index: i }) => onView?.(i) }, render: {
21
- slide: ({ slide }) => slide.type === 'video' ? _jsx(VideoSlideContent, { slide: slide }) : undefined,
21
+ slide: ({ slide, offset }) => slide.type === 'video' ? _jsx(VideoSlideContent, { active: offset === 0, slide: slide }) : undefined,
22
22
  } }));
23
23
  }
24
24
  //# sourceMappingURL=MediaLightbox.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MediaLightbox.js","sourceRoot":"","sources":["../../../src/react/media/MediaLightbox.tsx"],"names":[],"mappings":";AAAA,OAAO,uCAAuC,CAAC;AAC/C,OAAO,iDAAiD,CAAC;AACzD,OAAO,gDAAgD,CAAC;AACxD,OAAO,mDAAmD,CAAC;AAC3D,OAAO,QAAQ,EAAE,EAAc,MAAM,4BAA4B,CAAC;AAClE,OAAO,QAAQ,MAAM,6CAA6C,CAAC;AACnE,OAAO,OAAO,MAAM,4CAA4C,CAAC;AACjE,OAAO,UAAU,MAAM,+CAA+C,CAAC;AACvE,OAAO,IAAI,MAAM,yCAAyC,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAW3D;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAsB;IAC/F,OAAO,CACL,KAAC,QAAQ,OACH,IAAI,EACR,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,CAAC,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,EAC9C,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAC1B,UAAU,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,EACpD,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAC3C,MAAM,EAAE;YACN,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CACnB,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAAC,iBAAiB,IAAC,KAAK,EAAE,KAAK,GAAI,CAAC,CAAC,CAAC,SAAS;SAC3E,GACD,CACH,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"MediaLightbox.js","sourceRoot":"","sources":["../../../src/react/media/MediaLightbox.tsx"],"names":[],"mappings":";AAAA,OAAO,uCAAuC,CAAC;AAC/C,OAAO,iDAAiD,CAAC;AACzD,OAAO,gDAAgD,CAAC;AACxD,OAAO,mDAAmD,CAAC;AAC3D,OAAO,QAAQ,EAAE,EAAc,MAAM,4BAA4B,CAAC;AAClE,OAAO,QAAQ,MAAM,6CAA6C,CAAC;AACnE,OAAO,OAAO,MAAM,4CAA4C,CAAC;AACjE,OAAO,UAAU,MAAM,+CAA+C,CAAC;AACvE,OAAO,IAAI,MAAM,yCAAyC,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAW3D;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAsB;IAC/F,OAAO,CACL,KAAC,QAAQ,OACH,IAAI,EACR,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,CAAC,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,EAC9C,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAC1B,UAAU,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,EACpD,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAC3C,MAAM,EAAE;YACN,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAC3B,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAAC,iBAAiB,IAAC,MAAM,EAAE,MAAM,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,GAAI,CAAC,CAAC,CAAC,SAAS;SACjG,GACD,CACH,CAAC;AACJ,CAAC"}
@@ -55,6 +55,8 @@ export interface VideoPlayerProps {
55
55
  * two optional fields, so every existing source still satisfies this unchanged.
56
56
  */
57
57
  video: PlayerVideo;
58
+ /** Where this player is, in the load-time reports (`playbackWatch.ts`). */
59
+ surface?: string;
58
60
  /**
59
61
  * Start playing on mount. **Defaults to `true`** — see {@link usePrefersReducedMotion} and
60
62
  * the block on {@link AUTOPLAY_DEFAULT}.
@@ -400,7 +402,7 @@ export declare function useLiveScrub(hostRef: RefObject<HTMLElement | null>, pla
400
402
  */
401
403
  export declare function preferBundledHls(provider: MediaProviderAdapter | null): void;
402
404
  export declare function usePrefersReducedMotion(): boolean;
403
- export declare function VideoPlayer({ video, autoPlay, muted, volumeKey, className, aspectRatio, onEnded, loop, liveScrub, smallLayoutWhen, onStep, canStep, fullscreenOnDoubleClick, fillWindow, onFillWindowChange, ...rest }: VideoPlayerProps): import("react").JSX.Element;
405
+ export declare function VideoPlayer({ video, autoPlay, muted, volumeKey, className, aspectRatio, onEnded, loop, liveScrub, smallLayoutWhen, onStep, canStep, fullscreenOnDoubleClick, fillWindow, onFillWindowChange, surface, ...rest }: VideoPlayerProps): import("react").JSX.Element;
404
406
  /**
405
407
  * The transport state for a video surface: a thin progress bar pinned across the bottom edge
406
408
  * while derivatives build (indeterminate shimmer until real progress arrives), plus whatever
@@ -1 +1 @@
1
- {"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../../../src/react/media/VideoPlayer.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../../../src/react/media/VideoPlayer.tsx"],"names":[],"mappings":"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"}
@@ -6,8 +6,10 @@ import { Gesture, isHLSProvider, MediaPlayer, MediaProvider, Poster, Track, useM
6
6
  import { DefaultVideoLayout, defaultLayoutIcons, } from '@vidstack/react/player/layouts/default';
7
7
  import { useEffect, useMemo, useRef, useState, } from 'react';
8
8
  import { PLAYER_VOLUME_KEY, readSessionVolume, writeSessionVolume, } from '../media-gallery/playbackPreferences.js';
9
- import { hlsErrorIsAnswer, probeSourceFault, SourceFaultOverlay, } from './sourceFault.js';
9
+ import { hlsErrorIsAnswer, probeSourceFault, SourceFaultOverlay, hlsErrorName, } from './sourceFault.js';
10
10
  import { BarButton, mergeSlots, sectionSlots } from './controlBar.js';
11
+ import { playerSource } from './playerSource.js';
12
+ import { usePlaybackWatch } from './usePlaybackWatch.js';
11
13
  import { takePlayhead } from './playheadHandoff.js';
12
14
  import { isSurfaceControl } from './surfaceGesture.js';
13
15
  import { chapterSpans } from './videoChapters.js';
@@ -362,7 +364,7 @@ export function usePrefersReducedMotion() {
362
364
  });
363
365
  return reduced;
364
366
  }
365
- export function VideoPlayer({ video, autoPlay, muted, volumeKey = PLAYER_VOLUME_KEY, className, aspectRatio = DEFAULT_VIDEO_ASPECT_RATIO, onEnded, loop, liveScrub = true, smallLayoutWhen = isSmallVideoLayout, onStep, canStep = true, fullscreenOnDoubleClick = true, fillWindow = false, onFillWindowChange, ...rest }) {
367
+ export function VideoPlayer({ video, autoPlay, muted, volumeKey = PLAYER_VOLUME_KEY, className, aspectRatio = DEFAULT_VIDEO_ASPECT_RATIO, onEnded, loop, liveScrub = true, smallLayoutWhen = isSmallVideoLayout, onStep, canStep = true, fullscreenOnDoubleClick = true, fillWindow = false, onFillWindowChange, surface = 'player', ...rest }) {
366
368
  /*
367
369
  * 🔴 What the SOURCES have answered, and why a player needs to hold that at all.
368
370
  *
@@ -382,10 +384,38 @@ export function VideoPlayer({ video, autoPlay, muted, volumeKey = PLAYER_VOLUME_
382
384
  */
383
385
  const [ladderAnswered, setLadderAnswered] = useState(false);
384
386
  const [fault, setFault] = useState(null);
385
- // Prefer the adaptive HLS master; fall back to the progressive Range source.
386
- const src = video.hlsUrl && !ladderAnswered
387
- ? { src: video.hlsUrl, type: 'application/x-mpegurl' }
388
- : video.streamUrl;
387
+ /** Bumped by the fault's Try again: a fresh element, and the ladder gets its turn back. */
388
+ const [attempt, setAttempt] = useState(0);
389
+ // Memoized: a new object is a new source to Vidstack, and that restarts a load on every render.
390
+ const { hlsUrl, streamUrl } = video;
391
+ const { src, onLadder, progressive } = useMemo(() => playerSource({ hlsUrl, streamUrl }, ladderAnswered), [hlsUrl, streamUrl, ladderAnswered]);
392
+ const watch = usePlaybackWatch(video.id, surface);
393
+ useEffect(() => {
394
+ watch.current?.source(onLadder ? 'ladder' : progressive ? 'progressive' : 'none');
395
+ }, [watch, onLadder, progressive]);
396
+ /*
397
+ * 🔴 A typed source is a `<source>` child, and when one fails the `error` fires on the `<source>`,
398
+ * never on the `<video>`. Vidstack listens on the `<video>`, so a 404 there was silence and a
399
+ * spinner for ever (forge task 4062, reproduced in `e2e/playbackSources.browser.test.ts`). `error`
400
+ * does not bubble but it does capture, so the host hears it.
401
+ */
402
+ useEffect(() => {
403
+ const host = hostRef.current;
404
+ if (!host)
405
+ return;
406
+ const onSourceError = (event) => {
407
+ if (event.target instanceof HTMLSourceElement)
408
+ sourceFailed.current();
409
+ };
410
+ host.addEventListener('error', onSourceError, true);
411
+ return () => host.removeEventListener('error', onSourceError, true);
412
+ }, []);
413
+ const giveUp = (url) => {
414
+ void probeSourceFault(url).then((found) => {
415
+ setFault(found);
416
+ watch.current?.failed(`${found.kind}${found.status ? `:${found.status}` : ''}`);
417
+ });
418
+ };
389
419
  // See AUTOPLAY_DEFAULT. `muted` follows `autoPlay` because an unmuted autoplay is refused
390
420
  // by every browser and the refusal LOOKS like the spinner this batch removed — unless he
391
421
  // has already set a level on this surface this session, which is what `session` carries.
@@ -411,6 +441,11 @@ export function VideoPlayer({ video, autoPlay, muted, volumeKey = PLAYER_VOLUME_
411
441
  const applied = useRef({ volume: startVolume, muted: shouldMute });
412
442
  /** A mute the BROWSER forced on us — see `onAutoPlayFail` below. Never his choice, so never stored. */
413
443
  const forcedMute = useRef(false);
444
+ const sourceFailed = useRef(() => { });
445
+ sourceFailed.current = () => {
446
+ watch.current?.note('media:source');
447
+ giveUp(progressive);
448
+ };
414
449
  const status = video.processingStatus;
415
450
  // 🔴 `playbackDegraded` is its own reason to draw, not a processing state — a video that
416
451
  // optimized perfectly can still be served off the progressive copy because the ladder probe
@@ -425,19 +460,26 @@ export function VideoPlayer({ video, autoPlay, muted, volumeKey = PLAYER_VOLUME_
425
460
  ? {
426
461
  onDoubleClick: (event) => toggleFullscreenOnSurfaceDoubleClick(playerRef.current, event.target),
427
462
  }
428
- : {}), children: [_jsxs(MediaPlayer, { ref: playerRef, src: src,
463
+ : {}), children: [_jsxs(MediaPlayer, { ref: playerRef, ...(src ? { src } : {}),
429
464
  // 🔴 The hls.js engine is served from this origin, not from jsdelivr. See
430
465
  // `preferBundledHls` — without it an adaptive ladder depends on a third-party CDN
431
466
  // and fails as a permanent spinner when that CDN is unreachable.
432
467
  onProviderChange: (provider) => {
433
468
  preferBundledHls(provider);
434
- bindVidstackQuality(provider, qualityStore, typeof src !== 'string');
469
+ bindVidstackQuality(provider, qualityStore, onLadder);
435
470
  },
436
471
  /* See {@link hlsErrorIsAnswer}. Typed `unknown` on purpose: `hls.js` is an OPTIONAL
437
472
  peer, so its `ErrorData` must not become a static type dependency of this file. */
438
473
  onHlsError: (detail) => {
439
- if (hlsErrorIsAnswer(detail))
474
+ const error = hlsErrorName(detail);
475
+ if (error)
476
+ watch.current?.note(error);
477
+ if (!hlsErrorIsAnswer(detail))
478
+ return;
479
+ if (progressive)
440
480
  setLadderAnswered(true);
481
+ else
482
+ giveUp(video.hlsUrl);
441
483
  },
442
484
  /* The element itself gave up, and what that means depends on what was playing.
443
485
  With a LADDER in play it is the case Vidstack has no answer for at all — a browser
@@ -448,12 +490,13 @@ export function VideoPlayer({ video, autoPlay, muted, volumeKey = PLAYER_VOLUME_
448
490
  same call `hlsSource.ts` makes when its recovery budget is spent, and a playing film
449
491
  beats a dead one. On the progressive copy there is no turn left, so the URL is
450
492
  probed for what it actually said and the picture says it. */
451
- onError: () => {
452
- if (video.hlsUrl && !ladderAnswered) {
493
+ onError: (detail) => {
494
+ watch.current?.note(`media:${detail?.code ?? 'unknown'}`);
495
+ if (onLadder && progressive) {
453
496
  setLadderAnswered(true);
454
497
  return;
455
498
  }
456
- void probeSourceFault(video.streamUrl).then(setFault);
499
+ giveUp(progressive ?? video.hlsUrl);
457
500
  }, title: video.name, poster: video.posterUrl, crossOrigin: true, playsInline: true, autoPlay: shouldAutoPlay, muted: shouldMute,
458
501
  // 🔴 The level he set this session, restored on the element — see `resolveAutoplay`
459
502
  // and `SessionVolume`. Both values are constant for the life of this mount, so
@@ -493,12 +536,13 @@ export function VideoPlayer({ video, autoPlay, muted, volumeKey = PLAYER_VOLUME_
493
536
  // Load eagerly rather than on-visible: the watch surface can size to a
494
537
  // zero-height box during iOS Safari's container-query resolution, so a
495
538
  // `visible` gate never fired and the player sat on a perpetual spinner.
496
- load: 'eager', viewType: 'video', aspectRatio: aspectRatio, className: className, style: { width: '100%', height: '100%', maxHeight: '100%' }, loop: loop, onEnded: onEnded,
539
+ load: 'eager', viewType: 'video', aspectRatio: aspectRatio, className: className, style: { width: '100%', height: '100%', maxHeight: '100%' }, loop: loop, onEnded: onEnded, onWaiting: () => watch.current?.stalled(), onPlaying: () => watch.current?.resumed(),
497
540
  /* 🔴 Where a surface that just CLOSED left this video — the focus viewer's ‹ Back, for
498
541
  one. See `playheadHandoff.ts`. On `can-play`, because a seek before the element can
499
542
  play is dropped, and taking it consumes it: a later load of the same element (the
500
543
  ladder handing over to the progressive copy) must not jump back there again. */
501
544
  onCanPlay: () => {
545
+ watch.current?.ready();
502
546
  const at = takePlayhead(video.id);
503
547
  const player = playerRef.current;
504
548
  if (at !== null && player)
@@ -512,7 +556,11 @@ export function VideoPlayer({ video, autoPlay, muted, volumeKey = PLAYER_VOLUME_
512
556
  // "many options from very slow to very fast": 0.1 is frame-crawling, which is the
513
557
  // speed you actually want for studying a moment, and 4 is for skimming a long
514
558
  // clip. The default five steps cover neither end.
515
- playbackRates: PLAYBACK_RATES, ...(slots ? { slots } : {}) })] }), showProcessing ? (_jsx(ProcessingIndicator, { status: status, progress: video.processingProgress, playbackDegraded: video.playbackDegraded })) : null, fault ? _jsx(SourceFaultOverlay, { fault: fault, name: video.name }) : null] }));
559
+ playbackRates: PLAYBACK_RATES, ...(slots ? { slots } : {}) })] }, attempt), showProcessing ? (_jsx(ProcessingIndicator, { status: status, progress: video.processingProgress, playbackDegraded: video.playbackDegraded })) : null, fault ? (_jsx(SourceFaultOverlay, { fault: fault, name: video.name, onRetry: () => {
560
+ setFault(null);
561
+ setLadderAnswered(false);
562
+ setAttempt((n) => n + 1);
563
+ } })) : null] }));
516
564
  }
517
565
  /**
518
566
  * The named sections, as the `kind='chapters'` text track Vidstack's default layout is already