polite-media 0.3.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 (60) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +21 -0
  3. package/README.md +675 -0
  4. package/dist/coordinator.d.ts +231 -0
  5. package/dist/coordinator.d.ts.map +1 -0
  6. package/dist/coordinator.js +1017 -0
  7. package/dist/coordinator.js.map +1 -0
  8. package/dist/env.d.ts +30 -0
  9. package/dist/env.d.ts.map +1 -0
  10. package/dist/env.js +49 -0
  11. package/dist/env.js.map +1 -0
  12. package/dist/events.d.ts +70 -0
  13. package/dist/events.d.ts.map +1 -0
  14. package/dist/events.js +42 -0
  15. package/dist/events.js.map +1 -0
  16. package/dist/image.css +1 -0
  17. package/dist/image.d.ts +45 -0
  18. package/dist/image.d.ts.map +1 -0
  19. package/dist/image.js +127 -0
  20. package/dist/image.js.map +1 -0
  21. package/dist/layer.css +1 -0
  22. package/dist/reveal.d.ts +34 -0
  23. package/dist/reveal.d.ts.map +1 -0
  24. package/dist/reveal.js +72 -0
  25. package/dist/reveal.js.map +1 -0
  26. package/dist/sources.d.ts +22 -0
  27. package/dist/sources.d.ts.map +1 -0
  28. package/dist/sources.js +148 -0
  29. package/dist/sources.js.map +1 -0
  30. package/dist/targets.d.ts +20 -0
  31. package/dist/targets.d.ts.map +1 -0
  32. package/dist/targets.js +17 -0
  33. package/dist/targets.js.map +1 -0
  34. package/dist/video.css +1 -0
  35. package/dist/video.d.ts +15 -0
  36. package/dist/video.d.ts.map +1 -0
  37. package/dist/video.js +17 -0
  38. package/dist/video.js.map +1 -0
  39. package/dist/warm.d.ts +13 -0
  40. package/dist/warm.d.ts.map +1 -0
  41. package/dist/warm.js +12 -0
  42. package/dist/warm.js.map +1 -0
  43. package/dist/warming.d.ts +62 -0
  44. package/dist/warming.d.ts.map +1 -0
  45. package/dist/warming.js +136 -0
  46. package/dist/warming.js.map +1 -0
  47. package/package.json +96 -0
  48. package/src/coordinator.ts +1337 -0
  49. package/src/env.ts +56 -0
  50. package/src/events.ts +78 -0
  51. package/src/image.css +74 -0
  52. package/src/image.ts +160 -0
  53. package/src/layer.css +60 -0
  54. package/src/reveal.ts +75 -0
  55. package/src/sources.ts +164 -0
  56. package/src/targets.ts +27 -0
  57. package/src/video.css +74 -0
  58. package/src/video.ts +32 -0
  59. package/src/warm.ts +12 -0
  60. package/src/warming.ts +162 -0
@@ -0,0 +1,231 @@
1
+ import { type Target } from './targets.js';
2
+ /**
3
+ * @module
4
+ * The single arbiter of what plays. Nothing outside this module calls `play()`
5
+ * or `pause()`, so "why is this video running" always has one answer.
6
+ *
7
+ * The `@module` tag matters: without it TypeScript attaches this preamble to the
8
+ * first declaration below, and a consumer hovering that symbol gets a sentence
9
+ * about internal call discipline instead of its own documentation.
10
+ */
11
+ /**
12
+ * Deliberately three cases rather than a number. These are the ones that exist,
13
+ * the type system does the checking a bare `number` would invite (`0.5`), and it
14
+ * avoids inventing an answer to which of several incumbents a rival displaces --
15
+ * the incumbent's hysteresis has clean semantics only for a single slot.
16
+ */
17
+ export type AtOnce = 0 | 1 | 'all';
18
+ /**
19
+ * How patient a video is about starting, as a genuine ladder: each value waits
20
+ * for everything the one before it did, and then something more.
21
+ *
22
+ * Buffering is deliberately not a fourth value here. It is a different question
23
+ * and lives on {@link ConfigureOptions.requireBuffered}, so the two compose.
24
+ */
25
+ export type StartWhen = 'visible' | 'page-loaded' | 'interaction';
26
+ /**
27
+ * Options for {@link configure}. Every field is optional; anything left out keeps
28
+ * its default.
29
+ *
30
+ * All-optional deliberately. As a fully required interface this could not be used
31
+ * for what a consumer naturally reaches for -- `const preset: ConfigureOptions =
32
+ * { atOnce: 0 }` -- and only the inline `configure({ ... })` form worked, via
33
+ * contextual typing.
34
+ */
35
+ export interface ConfigureOptions {
36
+ /**
37
+ * How far outside the viewport a video starts buffering, so it is ready by the
38
+ * time it arrives. `'200px'` on a feed is the difference between a card that
39
+ * plays as it lands and one that shows its poster first.
40
+ *
41
+ * Defaults to `'0px'`: buffering video a visitor may never scroll to is the
42
+ * opposite of what this package is for, and on a twelve-card grid it would
43
+ * fetch twelve files.
44
+ *
45
+ * This drives a second observer of its own, and deliberately does not touch
46
+ * the thresholds. `intersectionRatio` is measured against the root *including*
47
+ * the margin, so a single observer made every threshold mean less than it
48
+ * said: measured on a 368px card at a 50px margin, 25% on screen reported 0.39
49
+ * and `pauseBelow: 0.25` actually stopped the video at about 10% visible, with
50
+ * the error scaling by element height. The observer that decides playback
51
+ * keeps no margin, so a fraction is always the true visible fraction.
52
+ */
53
+ prefetchMargin?: string;
54
+ /**
55
+ * Which viewports count as small. Configurable because 767px is one project's
56
+ * breakpoint, not a fact about phones.
57
+ */
58
+ smallViewport?: string;
59
+ /**
60
+ * How many videos may run at once.
61
+ *
62
+ * - `'all'` lets every visible video play. A bento grid of cards whose content
63
+ * *is* the video is meant to move.
64
+ * - `1` gives one video the screen at a time, the rest holding their posters.
65
+ * A feed wants this: the eye has one subject, and the handover happens as
66
+ * the next card takes the slot.
67
+ * - `0` never starts a video, spending nothing on data or battery.
68
+ *
69
+ * Pass an object to split the answer by viewport, which is the default:
70
+ * `{ small: 1, large: 'all' }`. Phones have far less decode headroom than
71
+ * desktops -- three concurrent H.264 streams while compositing drops frames
72
+ * badly on real hardware -- so they arbitrate while a desktop does not.
73
+ * {@link ConfigureOptions.smallViewport} decides which side a viewport is on.
74
+ *
75
+ * No standards or platform source recommends any of these; the default is the
76
+ * one with device testing behind it.
77
+ */
78
+ atOnce?: AtOnce | {
79
+ small: AtOnce;
80
+ large: AtOnce;
81
+ };
82
+ /**
83
+ * Visible fraction, 0 to 1, at or below which a video stops.
84
+ *
85
+ * Defaults to `0.5`: a video runs while it is the thing you are looking at and
86
+ * stops once it is mostly gone. At `0` it stopped only when entirely off
87
+ * screen, so one hanging on by a sliver effectively never stopped.
88
+ *
89
+ * **This caps how tall a managed video can be.** `intersectionRatio` is a
90
+ * fraction of the *element*, and with no margin on the playback observer its
91
+ * ceiling is `viewport / height` -- so anything taller than twice the viewport
92
+ * can never reach `0.5` and would never play. Measured at a 953px viewport:
93
+ * 1.5x viewport height peaks at 0.667 and 3x at 0.333. Lower it for a tall
94
+ * video, or shorten the box.
95
+ */
96
+ pauseBelow?: number;
97
+ /**
98
+ * How patient a video is about starting. Each rung is strictly more patient
99
+ * than the last.
100
+ *
101
+ * - `'visible'` starts fetching the moment the video is on screen. Module
102
+ * scripts are deferred, so on a real page that lands inside the tail of the
103
+ * page's own loading and competes with it.
104
+ * - `'page-loaded'` waits for `window`'s `load` event first, so the video
105
+ * competes with nothing the page still needs. The default.
106
+ * - `'interaction'` additionally waits for the visitor: the first
107
+ * `pointerdown`, `keydown` or `scroll`.
108
+ *
109
+ * `'page-loaded'` is the default because a video that never plays reads as
110
+ * broken. It keeps the part that matters most: the fetch happens after `load`,
111
+ * so those bytes never compete with the page's own.
112
+ *
113
+ * **Reach for `'interaction'` when Largest Contentful Paint matters.** The
114
+ * browser stops updating LCP on "a tap, scroll, or keypress"
115
+ * (https://web.dev/articles/lcp), so a video revealed after that signal can
116
+ * never become the LCP element, and a synthetic audit, which never interacts,
117
+ * never starts it at all. The cost is a visitor who lands and never scrolls,
118
+ * taps or types: they see a still.
119
+ *
120
+ * Below the fold the choice barely matters, since a video down there cannot be
121
+ * seen without scrolling and scrolling is the interaction. It is a policy for
122
+ * whatever is on screen at load, which in practice means the hero.
123
+ *
124
+ * Two things compose with this rather than replacing it.
125
+ * {@link ConfigureOptions.requireBuffered} asks for data as well as patience,
126
+ * and `until` gates one video on your own promise while this is the policy for
127
+ * all of them. A video waits for every gate that applies to it.
128
+ */
129
+ startWhen?: StartWhen;
130
+ /**
131
+ * Hold playback until the video can play through without stalling.
132
+ *
133
+ * Separate from {@link ConfigureOptions.startWhen} because they answer
134
+ * different questions: that one is *when may it begin*, this one is *how much
135
+ * data first*. As a fourth `startWhen` value it competed with `'interaction'`,
136
+ * so "wait for the user, and also wait for the buffer" could not be said at
137
+ * all.
138
+ *
139
+ * Raises `preload` to `'auto'` when it prepares, which it has to: the markup
140
+ * contract says `preload="none"`, and a browser buffers nothing until playback
141
+ * is asked for, so waiting for `canplaythrough` without the promotion would
142
+ * wait forever.
143
+ */
144
+ requireBuffered?: boolean;
145
+ }
146
+ /**
147
+ * Call before the first `register`.
148
+ *
149
+ * `atOnce`, `startWhen` and `requireBuffered` are read on every reconcile, so
150
+ * they can be changed at any time and take effect on the next pass. The three
151
+ * keys in {@link CONSTRUCTION_TIME_KEYS} cannot, and throw if patched while
152
+ * videos are registered. Unregister everything first, or configure earlier.
153
+ */
154
+ export declare function configure(patch: ConfigureOptions): void;
155
+ export interface RegisterOptions {
156
+ /**
157
+ * Hold this video out of the arbiter's reach until the promise settles. A hero
158
+ * at scroll-top is reported visible in the observer's very first batch, so
159
+ * without a gate it starts before whatever the page is waiting on (a splash
160
+ * screen, a consent dialog) has finished.
161
+ */
162
+ until?: Promise<unknown>;
163
+ /**
164
+ * Element to observe instead of the video, for when the video is absolutely
165
+ * positioned inside a wrapper that carries the real layout box.
166
+ */
167
+ observe?: Element;
168
+ /**
169
+ * Override the page's {@link ConfigureOptions.startWhen} for this video.
170
+ *
171
+ * A page usually wants one policy, but not always: only a video that can be
172
+ * the LCP element needs the strictest gate, and holding a below-fold grid to
173
+ * the same rule buys nothing.
174
+ */
175
+ startWhen?: StartWhen;
176
+ }
177
+ export declare function reconcile(): void;
178
+ /**
179
+ * Starts managing a video: reveals it on its first genuinely painted frame,
180
+ * plays it only while it is visible, falls through its `<source>` list when one
181
+ * cannot be decoded, and stops it when a gate closes.
182
+ *
183
+ * The video and its poster must already share a box carrying `data-polite-media`
184
+ * in the authored markup, and the video should be `muted loop playsinline
185
+ * preload="none"`. Calling this twice on the same element is a no-op.
186
+ *
187
+ * @param video the element to manage
188
+ * @param options see {@link RegisterOptions}
189
+ */
190
+ export declare function register(video: HTMLVideoElement, options?: RegisterOptions): void;
191
+ /** Anything that names one or more videos. See {@link Target}. */
192
+ export type VideoTarget = Target<HTMLVideoElement>;
193
+ /**
194
+ * Registers every video a target names, so the common case is one line and
195
+ * matches `revealImages` on the image side rather than being a second idea.
196
+ *
197
+ * `observe` is deliberately not accepted. Each observed element maps to exactly
198
+ * one entry, so handing the same wrapper to several videos would silently
199
+ * discard all but the last. Anything needing it, or a different gate per video,
200
+ * goes through {@link register} one at a time.
201
+ *
202
+ * Idempotent, because `register` is: safe to call on every navigation of a
203
+ * client-side router, where module scripts do not re-run.
204
+ */
205
+ export declare function registerAll(target: VideoTarget, options?: Omit<RegisterOptions, 'observe'>): void;
206
+ /**
207
+ * Stops managing a video and releases everything it owned: the observer entry,
208
+ * any pending pause timer, its listeners, and the page-level listeners once it
209
+ * was the last one. Safe to call for a video that was never registered.
210
+ */
211
+ export declare function unregister(video: HTMLVideoElement): void;
212
+ export declare function pauseAll(): void;
213
+ /** Lets playback resume, undoing {@link pauseAll}. */
214
+ export declare function resumeAll(): void;
215
+ /**
216
+ * Releases every video, for a host tearing down the whole page. Configuration
217
+ * survives: it describes the page's setup rather than the videos currently on
218
+ * it, and a client-side router calling this per navigation would otherwise have
219
+ * its settings quietly reverted on the first swap. The once-per-page warnings do
220
+ * reset, because the markup they judge is about to be replaced.
221
+ */
222
+ export declare function unregisterAll(): void;
223
+ /** Internal reset for tests. Not exported from the package entry point. */
224
+ export declare function resetForTests(): void;
225
+ /** Internal view for tests. Not exported from the package entry point. */
226
+ export declare function inspect(): {
227
+ tracked: number;
228
+ observing: boolean;
229
+ lifecycle: boolean;
230
+ };
231
+ //# sourceMappingURL=coordinator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coordinator.d.ts","sourceRoot":"","sources":["../src/coordinator.ts"],"names":[],"mappings":"AAUA,OAAO,EAAkB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3D;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAEnC;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,aAAa,GAAG,aAAa,CAAC;AAElE;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;;;;;;;;;OAgBG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD;;;;;;;;;;;;;OAaG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AA4ED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAcvD;AAwDD,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACzB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAqhBD,wBAAgB,SAAS,IAAI,IAAI,CAsFhC;AAgLD;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,GAAE,eAAoB,GAAG,IAAI,CAoDrF;AAED,kEAAkE;AAClE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,WAAW,EACnB,OAAO,GAAE,IAAI,CAAC,eAAe,EAAE,SAAS,CAAM,GAC7C,IAAI,CAEN;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAuBxD;AAsDD,wBAAgB,QAAQ,IAAI,IAAI,CAG/B;AAED,sDAAsD;AACtD,wBAAgB,SAAS,IAAI,IAAI,CAGhC;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAOpC;AAED,2EAA2E;AAC3E,wBAAgB,aAAa,IAAI,IAAI,CAQpC;AAED,0EAA0E;AAC1E,wBAAgB,OAAO,IAAI;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAErF"}