Files
hyperframes/packages/core/src/runtime/state.ts
T
Miguel Ángel 7382fabab9 fix(core): consolidate external asset and dependency preservation (#2410)
## Summary

- preserve external SVG fragment references during bundling
- preserve external module scripts and serve `.mjs` with a JavaScript MIME type
- retain template-head stylesheets when mounting sub-compositions
- add regression coverage across compiler runtime and file-server paths

Consolidates and replaces #2390, #2297, and #2375.

## Verification

- core compiler/runtime tests: 89 passed
- producer file-server tests: 48 passed
- core, producer, engine, and CLI typechecks passed
- `git diff --check`
2026-07-14 21:55:51 -04:00

130 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { RuntimeDeterministicAdapter, RuntimeTimelineLike } from "./types";
import type { RuntimeMediaClip } from "./media";
import type { TransportClock } from "./clock";
export type RuntimeState = {
capturedTimeline: RuntimeTimelineLike | null;
isPlaying: boolean;
currentTime: number;
deterministicAdapters: RuntimeDeterministicAdapter[];
canonicalFps: number;
bridgeMuted: boolean;
bridgeVolume: number;
/**
* Internal mute of audible media output, owned by the audio-ownership
* protocol between the parent (`<hyperframes-player>`) and this runtime.
* Independent of `bridgeMuted` (the user's mute preference). When the
* parent takes over audible playback via parent-frame proxies, it sets
* this to `true` so the runtime keeps driving timed media for frame
* accuracy but produces no audio of its own.
*/
mediaOutputMuted: boolean;
/**
* Disable runtime ownership of native media elements. Slideshow presenter
* mode keeps the slide timeline paused while users interact with embedded
* media, so the runtime must not auto-play, auto-pause, seek, or volume-sync
* those native elements on every transport tick.
*/
nativeMediaSyncDisabled: boolean;
/**
* Disable the runtime's WebAudio replacement for native <audio> elements.
* Slideshow presenter/audience windows mirror native media element events
* across browsers, so muting those elements for WebAudio ownership breaks
* audible presenter playback and remote sync.
*/
webAudioMediaDisabled: boolean;
/**
* Latch so the `media-autoplay-blocked` outbound message is posted at most
* once per runtime session. The parent only needs the first signal — it
* takes over playback and further rejections are the same problem.
*/
mediaAutoplayBlockedPosted: boolean;
/**
* One-shot flag: force a hard media sync on the next tick. Set on
* play/pause/seek/rate transitions to immediately correct any
* accumulated sub-threshold drift from pause/play toggling.
* Consumed (reset to false) by `syncMediaForCurrentState`.
*/
mediaForceSyncNextTick: boolean;
playbackRate: number;
bridgeLastPostedFrame: number;
bridgeLastPostedAt: number;
bridgeLastPostedPlaying: boolean;
bridgeLastPostedMuted: boolean;
/**
* Max interval (ms) between outbound timeline samples on the parent-frame
* control bridge. The bridge posts on every changed frame, but also at
* least once per this interval so a paused/idle timeline still confirms
* its position to any listener.
*
* **Cross-reference (do not change in isolation)**: the parent-frame
* audio-mirror loop in `<hyperframes-player>` waits for
* `MIRROR_REQUIRED_CONSECUTIVE_DRIFT_SAMPLES` consecutive over-threshold
* samples before issuing a `currentTime` correction. The product of
* those two constants is the worst-case A/V re-sync latency:
*
* worst_case_correction_latency_ms
* ≈ MIRROR_REQUIRED_CONSECUTIVE_DRIFT_SAMPLES × bridgeMaxPostIntervalMs
*
* Today: `2 × 80 ms = 160 ms`, which sits comfortably under the
* perceptual A/V re-sync tolerance. If you raise this interval, audit
* `MIRROR_REQUIRED_CONSECUTIVE_DRIFT_SAMPLES` in
* `packages/player/src/hyperframes-player.ts` — leaving it at `2` will
* silently push correction latency past the tolerance budget.
*/
bridgeMaxPostIntervalMs: number;
controlBridgeHandler: ((event: MessageEvent) => void) | null;
beforeUnloadHandler: (() => void) | null;
injectedCompStyles: HTMLStyleElement[];
injectedCompScripts: HTMLScriptElement[];
injectedCompLinks: HTMLLinkElement[];
cachedTimedMediaEls: Array<HTMLVideoElement | HTMLAudioElement>;
cachedMediaClips: RuntimeMediaClip[];
cachedVideoClips: RuntimeMediaClip[];
cachedMediaTimelineDurationSeconds: number;
tornDown: boolean;
/**
* Single-clock transport. The sole time authority — GSAP is always
* paused and seeked to `clock.now()` on each rAF tick. Eliminates
* the two-clock drift problem described in issue #668.
*/
transportClock: TransportClock | null;
/** rAF ID for the single-clock tick loop. */
transportRafId: number | null;
};
export function createRuntimeState(): RuntimeState {
return {
capturedTimeline: null,
isPlaying: false,
currentTime: 0,
deterministicAdapters: [],
canonicalFps: 30,
bridgeMuted: false,
bridgeVolume: 1,
mediaOutputMuted: false,
nativeMediaSyncDisabled: false,
webAudioMediaDisabled: false,
mediaAutoplayBlockedPosted: false,
mediaForceSyncNextTick: false,
playbackRate: 1,
bridgeLastPostedFrame: -1,
bridgeLastPostedAt: 0,
bridgeLastPostedPlaying: false,
bridgeLastPostedMuted: false,
bridgeMaxPostIntervalMs: 80,
controlBridgeHandler: null,
beforeUnloadHandler: null,
injectedCompStyles: [],
injectedCompScripts: [],
injectedCompLinks: [],
cachedTimedMediaEls: [],
cachedMediaClips: [],
cachedVideoClips: [],
cachedMediaTimelineDurationSeconds: 0,
tornDown: false,
transportClock: null,
transportRafId: null,
};
}