@hyperframes/player 0.6.0-alpha.9 → 0.6.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.
- package/dist/hyperframes-player.cjs +2 -2
- package/dist/hyperframes-player.cjs.map +1 -1
- package/dist/hyperframes-player.d.cts +45 -261
- package/dist/hyperframes-player.d.ts +45 -261
- package/dist/hyperframes-player.global.js +2 -2
- package/dist/hyperframes-player.global.js.map +1 -1
- package/dist/hyperframes-player.js +2 -2
- package/dist/hyperframes-player.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent-frame media proxy subsystem.
|
|
3
|
+
*
|
|
4
|
+
* Maintains mirror copies of the iframe's timed `<audio>`/`<video>` elements
|
|
5
|
+
* in the parent frame so that mobile browsers — which gate `el.play()` on user
|
|
6
|
+
* activation in the *same* frame — can still produce audible output via proxies
|
|
7
|
+
* the parent controls directly.
|
|
8
|
+
*
|
|
9
|
+
* See the class-level JSDoc on `HyperframesPlayer` for the full ownership model.
|
|
10
|
+
*/
|
|
11
|
+
interface ProxyEntry {
|
|
12
|
+
el: HTMLMediaElement;
|
|
13
|
+
start: number;
|
|
14
|
+
duration: number;
|
|
15
|
+
/**
|
|
16
|
+
* Count of consecutive steady-state samples in which the proxy's
|
|
17
|
+
* `currentTime` was found drifted beyond `MIRROR_DRIFT_THRESHOLD_SECONDS`.
|
|
18
|
+
* Reset on every in-threshold sample. A write is only issued once this
|
|
19
|
+
* reaches `MIRROR_REQUIRED_CONSECUTIVE_DRIFT_SAMPLES`, absorbing
|
|
20
|
+
* single-sample jitter without thrashing.
|
|
21
|
+
*/
|
|
22
|
+
driftSamples: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ShaderLoadingMode = "composition" | "player" | "none";
|
|
26
|
+
|
|
1
27
|
interface ControlsCallbacks {
|
|
2
28
|
onPlay: () => void;
|
|
3
29
|
onPause: () => void;
|
|
@@ -15,7 +41,6 @@ interface ControlsOptions {
|
|
|
15
41
|
declare function formatSpeed(speed: number): string;
|
|
16
42
|
declare function formatTime(seconds: number): string;
|
|
17
43
|
|
|
18
|
-
type ShaderLoadingMode = "composition" | "player" | "none";
|
|
19
44
|
declare class HyperframesPlayer extends HTMLElement {
|
|
20
45
|
static get observedAttributes(): string[];
|
|
21
46
|
private shadow;
|
|
@@ -24,124 +49,31 @@ declare class HyperframesPlayer extends HTMLElement {
|
|
|
24
49
|
private posterEl;
|
|
25
50
|
private controlsApi;
|
|
26
51
|
private resizeObserver;
|
|
27
|
-
private
|
|
28
|
-
private
|
|
29
|
-
private shaderLoaderTitleEl;
|
|
30
|
-
private shaderLoaderDetailEl;
|
|
31
|
-
private shaderLoaderTransitionValueEl;
|
|
32
|
-
private shaderLoaderFrameLabelEl;
|
|
33
|
-
private shaderLoaderFrameValueEl;
|
|
34
|
-
private shaderLoaderFrameRowEl;
|
|
35
|
-
private shaderLoaderHideTimeout;
|
|
52
|
+
private shaderLoader;
|
|
53
|
+
private probe;
|
|
36
54
|
private _ready;
|
|
37
|
-
private _duration;
|
|
38
55
|
private _currentTime;
|
|
56
|
+
private _duration;
|
|
39
57
|
private _paused;
|
|
58
|
+
private _lastUpdateMs;
|
|
40
59
|
private _volume;
|
|
41
60
|
private _compositionWidth;
|
|
42
61
|
private _compositionHeight;
|
|
43
|
-
private _probeInterval;
|
|
44
|
-
private _lastUpdateMs;
|
|
45
62
|
private _directTimelineAdapter;
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
* Parent-frame audio/video proxies, preloaded mirror copies of the iframe's
|
|
49
|
-
* timed media. They exist as a fallback for environments that block iframe
|
|
50
|
-
* `.play()` — mobile browsers require the user gesture to originate in the
|
|
51
|
-
* same frame as the media element, and postMessage doesn't transfer user
|
|
52
|
-
* activation (User Activation v2). The runtime inside the iframe signals
|
|
53
|
-
* `media-autoplay-blocked` the first time a play() attempt rejects with
|
|
54
|
-
* `NotAllowedError`; receiving that message flips `_audioOwner` to `parent`
|
|
55
|
-
* and these proxies start driving audible output while the iframe keeps
|
|
56
|
-
* advancing timed media silently for frame-accurate state.
|
|
57
|
-
*
|
|
58
|
-
* Preloading at iframe-load time (rather than lazily on promotion) keeps
|
|
59
|
-
* the audible audio cut-in tight when the promotion fires mid-playback.
|
|
60
|
-
*/
|
|
61
|
-
private _parentMedia;
|
|
62
|
-
/**
|
|
63
|
-
* Who owns audible playback right now.
|
|
64
|
-
*
|
|
65
|
-
* - `runtime` (default): the iframe's runtime drives timed media; parent
|
|
66
|
-
* proxies stay paused and silent. This is the correct path on desktop,
|
|
67
|
-
* in same-frame embeds, and anywhere the iframe has user activation.
|
|
68
|
-
* - `parent`: parent-frame proxies drive audible output; the iframe keeps
|
|
69
|
-
* syncing timed media but at `muted = true` (orthogonal to author/user
|
|
70
|
-
* volume settings). Entered only in response to an actual autoplay
|
|
71
|
-
* rejection from the runtime — we don't guess device class.
|
|
72
|
-
*
|
|
73
|
-
* The transition is one-way per session; once autoplay is known to be
|
|
74
|
-
* gated, there's no benefit to attempting the iframe path again.
|
|
75
|
-
*/
|
|
76
|
-
private _audioOwner;
|
|
77
|
-
/**
|
|
78
|
-
* Watches the iframe document for sub-composition media added after
|
|
79
|
-
* initial setup. Disconnected on iframe reload (fresh iframe = fresh
|
|
80
|
-
* observer against the new document).
|
|
81
|
-
*/
|
|
82
|
-
private _mediaObserver?;
|
|
83
|
-
/**
|
|
84
|
-
* One-shot latch for `playbackerror`. Without it, under parent ownership
|
|
85
|
-
* where the parent frame itself lacks activation, every paused→playing
|
|
86
|
-
* transition in the iframe state loop would re-fire `play()` (and its
|
|
87
|
-
* rejection) on each proxy — spamming host subscribers through a whole
|
|
88
|
-
* playback session. Mirrors the `mediaAutoplayBlockedPosted` latch on the
|
|
89
|
-
* runtime side. Cleared on `_onIframeLoad` alongside the owner reset, so
|
|
90
|
-
* a fresh composition gets a fresh shot at surfacing the error.
|
|
91
|
-
*/
|
|
92
|
-
private _playbackErrorPosted;
|
|
63
|
+
private _directTimelineClock;
|
|
64
|
+
private _media;
|
|
93
65
|
constructor();
|
|
94
66
|
connectedCallback(): void;
|
|
95
67
|
disconnectedCallback(): void;
|
|
96
68
|
attributeChangedCallback(name: string, _old: string | null, val: string | null): void;
|
|
97
69
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* timeline tools (e.g. `@hyperframes/studio`) that need to inspect
|
|
102
|
-
* the composition's DOM or read its `__player` / `__timelines`
|
|
103
|
-
* runtime objects.
|
|
104
|
-
*
|
|
105
|
-
* **Common pitfall:** the iframe lives inside the player's Shadow DOM.
|
|
106
|
-
* Passing the `<hyperframes-player>` element itself to code that expects
|
|
107
|
-
* an `<iframe>` will silently break — `.contentWindow` returns `null`.
|
|
108
|
-
* Always extract `iframeElement` first:
|
|
109
|
-
*
|
|
110
|
-
* ```ts
|
|
111
|
-
* // ❌ Wrong — element ref doesn't expose contentWindow
|
|
112
|
-
* iframeRef.current = playerRef.current;
|
|
113
|
-
*
|
|
114
|
-
* // ✓ Right — bridge the actual iframe
|
|
115
|
-
* iframeRef.current = playerRef.current.iframeElement;
|
|
116
|
-
* ```
|
|
70
|
+
* The inner `<iframe>` rendering the composition. Use this when integrating
|
|
71
|
+
* with tools that need `contentWindow` — `.contentWindow` on the
|
|
72
|
+
* `<hyperframes-player>` element itself returns `null` (Shadow DOM).
|
|
117
73
|
*/
|
|
118
74
|
get iframeElement(): HTMLIFrameElement;
|
|
119
75
|
play(): void;
|
|
120
76
|
pause(): void;
|
|
121
|
-
/**
|
|
122
|
-
* Move playback to `timeInSeconds`.
|
|
123
|
-
*
|
|
124
|
-
* Two transports, with different precision semantics — read this before
|
|
125
|
-
* writing assertions against `seek` from outside the player:
|
|
126
|
-
*
|
|
127
|
-
* - **Same-origin (sync) path** — when the runtime's `window.__player.seek`
|
|
128
|
-
* is reachable, we call it directly. `timeInSeconds` is forwarded
|
|
129
|
-
* *verbatim* (no rounding), so a same-origin scrub of `seek(7.3333)`
|
|
130
|
-
* lands the runtime at `7.3333 s` — sub-frame precision relative to
|
|
131
|
-
* `DEFAULT_FPS` (30). Studio scrub UIs that need fractional-frame
|
|
132
|
-
* alignment (e.g. waveform scrubbing on long-duration audio) get the
|
|
133
|
-
* exact requested time.
|
|
134
|
-
* - **Cross-origin (postMessage) path** — when same-origin access throws
|
|
135
|
-
* or `__player.seek` is missing, we fall back to the postMessage bridge.
|
|
136
|
-
* The wire protocol carries integer frames (`frame: Math.round(t × FPS)`),
|
|
137
|
-
* so cross-origin embeds are *frame-quantized* and `seek(7.3333)` lands
|
|
138
|
-
* at `Math.round(7.3333 × 30) / 30 ≈ 7.3333…` (same value here, but for
|
|
139
|
-
* most fractional inputs you'll see a snap to the nearest 1/30 s).
|
|
140
|
-
*
|
|
141
|
-
* `this._currentTime` always reflects the *requested* `timeInSeconds`
|
|
142
|
-
* regardless of transport, so the controls UI shows the un-quantized value
|
|
143
|
-
* either way; the asymmetry only affects what the runtime actually paints.
|
|
144
|
-
*/
|
|
145
77
|
seek(timeInSeconds: number): void;
|
|
146
78
|
get currentTime(): number;
|
|
147
79
|
set currentTime(t: number);
|
|
@@ -161,172 +93,24 @@ declare class HyperframesPlayer extends HTMLElement {
|
|
|
161
93
|
get loop(): boolean;
|
|
162
94
|
set loop(l: boolean);
|
|
163
95
|
private _sendControl;
|
|
164
|
-
private _shaderCaptureScaleParam;
|
|
165
|
-
private _shaderLoadingMode;
|
|
166
|
-
private _prepareSrc;
|
|
167
|
-
private _prepareSrcdoc;
|
|
168
96
|
private _reloadShaderOptions;
|
|
169
|
-
private _createShaderLoader;
|
|
170
|
-
private _showShaderLoader;
|
|
171
|
-
private _hideShaderLoader;
|
|
172
|
-
private _scheduleShaderLoaderHideCleanup;
|
|
173
|
-
private _resetShaderLoader;
|
|
174
|
-
private _updateShaderLoader;
|
|
175
|
-
/**
|
|
176
|
-
* Reach into the runtime's `window.__player.seek` directly, skipping the
|
|
177
|
-
* postMessage hop. Same-origin only — cross-origin embeds throw a
|
|
178
|
-
* `SecurityError` on `contentWindow` property access, which we catch and
|
|
179
|
-
* report as a no-op so the caller can transparently fall back to the
|
|
180
|
-
* postMessage bridge. Returns `true` only when the runtime accepted the
|
|
181
|
-
* call (`__player.seek` exists, is callable, and didn't throw).
|
|
182
|
-
*
|
|
183
|
-
* Studio has used this access path privately via `iframe.contentWindow.__player`
|
|
184
|
-
* (see `useTimelinePlayer.ts`); this helper just formalizes the same
|
|
185
|
-
* detection inside the player so external scrub UIs get the same
|
|
186
|
-
* single-task latency. The runtime-side `seek` is the same wrapped
|
|
187
|
-
* function the postMessage handler calls (`installRuntimeControlBridge`
|
|
188
|
-
* routes through `player.seek`), so `markExplicitSeek()` and downstream
|
|
189
|
-
* runtime state stay identical between the two paths.
|
|
190
|
-
*/
|
|
191
97
|
private _trySyncSeek;
|
|
98
|
+
private _withDirectTimeline;
|
|
192
99
|
private _tryDirectTimelineSeek;
|
|
193
100
|
private _tryDirectTimelinePlay;
|
|
194
101
|
private _tryDirectTimelinePause;
|
|
195
|
-
private _startDirectTimelineClock;
|
|
196
|
-
private _stopDirectTimelineClock;
|
|
197
|
-
private _resolveDirectTimelineAdapter;
|
|
198
|
-
private _resolveDirectTimelineAdapterFromWindow;
|
|
199
|
-
private _hasRuntimeBridge;
|
|
200
|
-
private _resolvePlaybackDurationAdapter;
|
|
201
|
-
private _isControlsClick;
|
|
202
102
|
private _onMessage;
|
|
203
|
-
private
|
|
103
|
+
private _onProbeReady;
|
|
104
|
+
private _rescale;
|
|
204
105
|
private _onIframeLoad;
|
|
205
|
-
/** Inject the HyperFrames runtime into the iframe if not already present. */
|
|
206
|
-
private _injectRuntime;
|
|
207
|
-
private _updateScale;
|
|
208
106
|
private _setupControls;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
* — ITU-R BT.1359 puts A/V offset perceptibility at roughly ±45 ms, so
|
|
217
|
-
* anything looser risks audible lip-sync drift on talking-head content
|
|
218
|
-
* (a core use case). The re-seek cost at this tightness is a handful of
|
|
219
|
-
* extra `currentTime` writes per second; the media element's own buffer
|
|
220
|
-
* smooths them out without visible rebuffer on the mirror path.
|
|
221
|
-
*/
|
|
222
|
-
private static readonly MIRROR_DRIFT_THRESHOLD_SECONDS;
|
|
223
|
-
/**
|
|
224
|
-
* How many *consecutive* over-threshold steady-state samples we wait for
|
|
225
|
-
* before issuing a `currentTime` write. A value of 2 means a single
|
|
226
|
-
* spike (one slow bridge tick, one tab-throttled rAF batch, one GC pause)
|
|
227
|
-
* is absorbed without a seek; sustained drift still corrects on the very
|
|
228
|
-
* next tick after the threshold is crossed twice in a row.
|
|
229
|
-
*
|
|
230
|
-
* **Coupling with the timeline-control bridge** — read before changing:
|
|
231
|
-
* worst_case_correction_latency_ms
|
|
232
|
-
* ≈ MIRROR_REQUIRED_CONSECUTIVE_DRIFT_SAMPLES × bridgeMaxPostIntervalMs
|
|
233
|
-
*
|
|
234
|
-
* `bridgeMaxPostIntervalMs` (currently `80`) lives at
|
|
235
|
-
* `packages/core/src/runtime/state.ts` (field on `RuntimeState`). At
|
|
236
|
-
* today's values, worst-case is `2 × 80 ms = 160 ms` — still well under
|
|
237
|
-
* the human shot-change tolerance for A/V re-sync. If you bump bridge
|
|
238
|
-
* cadence (raising `bridgeMaxPostIntervalMs`) you may need to drop this
|
|
239
|
-
* constant to `1` to keep the product under ~150 ms; if you tighten
|
|
240
|
-
* cadence you can raise this to absorb more jitter without perceptual
|
|
241
|
-
* cost. There is a back-reference in `state.ts` next to
|
|
242
|
-
* `bridgeMaxPostIntervalMs` so a change to either side surfaces the
|
|
243
|
-
* coupling.
|
|
244
|
-
*/
|
|
245
|
-
private static readonly MIRROR_REQUIRED_CONSECUTIVE_DRIFT_SAMPLES;
|
|
246
|
-
/**
|
|
247
|
-
* Mirror parent-proxy `currentTime` to the iframe timeline. Defaults to
|
|
248
|
-
* the *coalesced* path: a single over-threshold sample is treated as
|
|
249
|
-
* jitter and merely increments a per-proxy counter; the actual seek only
|
|
250
|
-
* fires once `MIRROR_REQUIRED_CONSECUTIVE_DRIFT_SAMPLES` consecutive
|
|
251
|
-
* samples agree. Pass `{ force: true }` for one-shot alignment moments
|
|
252
|
-
* (audio-ownership promotion, brand-new proxy initialization) where we
|
|
253
|
-
* cannot tolerate even ~80 ms of misaligned audible playback.
|
|
254
|
-
*
|
|
255
|
-
* The counter is also reset on any in-threshold sample and on any
|
|
256
|
-
* out-of-range timeline position, so a proxy that drops back into a
|
|
257
|
-
* scene later starts fresh rather than carrying stale samples from the
|
|
258
|
-
* last time it was active.
|
|
259
|
-
*/
|
|
260
|
-
private _mirrorParentMediaTime;
|
|
261
|
-
/**
|
|
262
|
-
* Take ownership of audible playback. Fired in response to the runtime's
|
|
263
|
-
* `media-autoplay-blocked` signal — the iframe has lost the autoplay lottery
|
|
264
|
-
* and will never produce audio without a fresh gesture inside itself.
|
|
265
|
-
*
|
|
266
|
-
* Effects, in order:
|
|
267
|
-
* 1. Ask the runtime to mute its own media output via the bridge. The
|
|
268
|
-
* runtime then keeps advancing timed media for frame-accurate state
|
|
269
|
-
* but produces no sound of its own, freeing us to be the single
|
|
270
|
-
* audible source without racing a volume-reassert loop.
|
|
271
|
-
* 2. Align every parent proxy's currentTime to the iframe's timeline so
|
|
272
|
-
* the cut-over is imperceptible.
|
|
273
|
-
* 3. If the player is currently playing, start the proxies.
|
|
274
|
-
*
|
|
275
|
-
* Idempotent: repeat calls are a no-op.
|
|
276
|
-
*/
|
|
277
|
-
private _promoteToParentProxy;
|
|
278
|
-
/**
|
|
279
|
-
* Create a parent-frame media element, configure it, and start preloading.
|
|
280
|
-
* Returns the newly-created proxy entry, or `null` if one already exists for
|
|
281
|
-
* this src (dedup) — callers that need to act on the new element should
|
|
282
|
-
* branch on the return value rather than inferring via `_parentMedia.length`.
|
|
283
|
-
*/
|
|
284
|
-
private _createParentMedia;
|
|
285
|
-
/**
|
|
286
|
-
* Set up a single parent-frame audio from an explicit URL (via `audio-src`).
|
|
287
|
-
* Convenience for the common single-narration case — starts preloading
|
|
288
|
-
* immediately without waiting for the iframe to load.
|
|
289
|
-
*/
|
|
290
|
-
private _setupParentAudioFromUrl;
|
|
291
|
-
/**
|
|
292
|
-
* Mirror every timed iframe media element (`audio[data-start]`,
|
|
293
|
-
* `video[data-start]`) into a parent-frame proxy. The proxies preload at
|
|
294
|
-
* iframe-ready time so the cut-over to parent ownership — should the
|
|
295
|
-
* runtime's autoplay attempt later reject — is instantaneous.
|
|
296
|
-
*
|
|
297
|
-
* Under runtime ownership (the default) these proxies stay paused and
|
|
298
|
-
* inert; the iframe is the audible source. Ownership flips only in
|
|
299
|
-
* response to a real `media-autoplay-blocked` message from the runtime.
|
|
300
|
-
*
|
|
301
|
-
* Also installs a MutationObserver so that media added to the iframe
|
|
302
|
-
* *after* the initial scan (sub-composition activation is the common
|
|
303
|
-
* case) gets a proxy on the fly. Without this, under parent ownership
|
|
304
|
-
* late-added `<audio data-start>` would be silenced by the runtime
|
|
305
|
-
* (`outputMuted` sticks per-tick) but have no parent-frame counterpart
|
|
306
|
-
* to play — a silent hole in the audio track.
|
|
307
|
-
*/
|
|
308
|
-
private _setupParentMedia;
|
|
309
|
-
/**
|
|
310
|
-
* Create a parent-frame proxy mirroring a single iframe media element.
|
|
311
|
-
* Extracted so both the initial scan and the MutationObserver path use
|
|
312
|
-
* identical URL-resolution and attribute parsing.
|
|
313
|
-
*/
|
|
314
|
-
private _adoptIframeMedia;
|
|
315
|
-
/**
|
|
316
|
-
* Watch the iframe document for subtree additions of timed media so
|
|
317
|
-
* sub-composition activation (late-attached `<audio data-start>`) grows
|
|
318
|
-
* the parent-proxy set automatically. Disconnected on iframe reload via
|
|
319
|
-
* `_teardownMediaObserver`.
|
|
320
|
-
*/
|
|
321
|
-
private _observeDynamicMedia;
|
|
322
|
-
private _teardownMediaObserver;
|
|
323
|
-
/**
|
|
324
|
-
* Inverse of `_adoptIframeMedia`: drop the parent proxy mirroring a removed
|
|
325
|
-
* iframe media element. Resolves the src identically so matching is exact,
|
|
326
|
-
* then pauses, clears the src (frees the decoder), and splices it out.
|
|
327
|
-
*/
|
|
328
|
-
private _detachIframeMedia;
|
|
329
|
-
private _hidePoster;
|
|
107
|
+
get _audioOwner(): "parent" | "runtime";
|
|
108
|
+
get _parentMedia(): ProxyEntry[];
|
|
109
|
+
_mirrorParentMediaTime(t: number, opts?: {
|
|
110
|
+
force?: boolean;
|
|
111
|
+
}): void;
|
|
112
|
+
_promoteToParentProxy(): void;
|
|
113
|
+
_observeDynamicMedia(doc: Document): void;
|
|
330
114
|
}
|
|
331
115
|
|
|
332
116
|
export { type ControlsCallbacks, type ControlsOptions, HyperframesPlayer, SPEED_PRESETS, type ShaderLoadingMode, formatSpeed, formatTime };
|