mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(core): parent-frame proxy bypass, data-preload-eager opt-out, configurable threshold
- Player: _adoptIframeMedia now skips media with preload="metadata" or "none", preventing parent-frame proxies from bypassing the preloader. MutationObserver extended to watch preload attribute changes so proxies are created just-in-time when the preloader promotes a clip. - init.ts: lazy-mode demotion loop skips elements with data-preload-eager, letting power users keep specific clips eagerly buffered. - mediaPreloader: reads window.__HF_LAZY_PRELOAD_THRESHOLD as an override, falling back to the default 6.
This commit is contained in:
@@ -1263,6 +1263,10 @@ export function initSandboxRuntimeModular(): void {
|
||||
// timed clips and would never promote them back.
|
||||
for (const mediaEl of mediaEls) {
|
||||
if (!mediaEl.hasAttribute("data-start")) continue;
|
||||
// Power-user opt-out: data-preload-eager keeps a clip eagerly buffered
|
||||
// even under lazy mode, useful when a specific clip must be instantly
|
||||
// available regardless of playhead proximity.
|
||||
if (mediaEl.hasAttribute("data-preload-eager")) continue;
|
||||
if (mediaEl.preload === "auto" || mediaEl.preload === "") {
|
||||
mediaEl.preload = "metadata";
|
||||
// Kick off the metadata fetch explicitly — some browsers (Chrome Lite
|
||||
|
||||
@@ -322,4 +322,37 @@ describe("createMediaPreloadManager", () => {
|
||||
|
||||
expect(onActivation).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("respects window.__HF_LAZY_PRELOAD_THRESHOLD override", () => {
|
||||
elements = Array.from({ length: 4 }, (_, i) =>
|
||||
mockMediaElement({ start: String(i * 5), duration: "5" }),
|
||||
);
|
||||
setupDOM(elements);
|
||||
|
||||
// 4 elements is below the default threshold (6) but at our custom one
|
||||
(window as Record<string, unknown>).__HF_LAZY_PRELOAD_THRESHOLD = 4;
|
||||
|
||||
const manager = createMediaPreloadManager();
|
||||
manager.refresh();
|
||||
|
||||
expect(manager.isLazy()).toBe(true);
|
||||
|
||||
// Clean up
|
||||
delete (window as Record<string, unknown>).__HF_LAZY_PRELOAD_THRESHOLD;
|
||||
});
|
||||
|
||||
it("falls back to default threshold when __HF_LAZY_PRELOAD_THRESHOLD is not set", () => {
|
||||
elements = Array.from({ length: 4 }, (_, i) =>
|
||||
mockMediaElement({ start: String(i * 5), duration: "5" }),
|
||||
);
|
||||
setupDOM(elements);
|
||||
|
||||
// Ensure it's not set
|
||||
delete (window as Record<string, unknown>).__HF_LAZY_PRELOAD_THRESHOLD;
|
||||
|
||||
const manager = createMediaPreloadManager();
|
||||
manager.refresh();
|
||||
|
||||
expect(manager.isLazy()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,7 +38,11 @@ export function createMediaPreloadManager(options?: {
|
||||
function refresh(): void {
|
||||
const cache = refreshRuntimeMediaCache(options);
|
||||
clips = cache.mediaClips;
|
||||
lazy = clips.length >= LAZY_THRESHOLD;
|
||||
const configuredThreshold =
|
||||
typeof (window as Record<string, unknown>).__HF_LAZY_PRELOAD_THRESHOLD === "number"
|
||||
? ((window as Record<string, unknown>).__HF_LAZY_PRELOAD_THRESHOLD as number)
|
||||
: LAZY_THRESHOLD;
|
||||
lazy = clips.length >= configuredThreshold;
|
||||
if (lazy && !activationEmitted) {
|
||||
activationEmitted = true;
|
||||
options?.onActivation?.(clips.length);
|
||||
|
||||
@@ -548,8 +548,15 @@ describe("HyperframesPlayer media MutationObserver scoping", () => {
|
||||
expect(observedTargets).not.toContain(fakeDoc.body);
|
||||
// Subtree is still required — sub-composition media can be deeply nested
|
||||
// inside the host (e.g. wrapper div around the `<audio>`).
|
||||
// Attribute observation on "preload" is required so the player creates
|
||||
// parent proxies just-in-time when the preloader promotes a clip.
|
||||
for (const call of observeSpy.mock.calls) {
|
||||
expect(call[1]).toEqual({ childList: true, subtree: true });
|
||||
expect(call[1]).toEqual({
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["preload"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1498,6 +1498,14 @@ class HyperframesPlayer extends HTMLElement {
|
||||
* identical URL-resolution and attribute parsing.
|
||||
*/
|
||||
private _adoptIframeMedia(iframeEl: HTMLMediaElement): void {
|
||||
// Respect the preloader's demotion: if the iframe element has been set to
|
||||
// metadata-only or none, creating a parent proxy with preload="auto" would
|
||||
// bypass the lazy preloader and eagerly buffer the clip. Skip it — the
|
||||
// MutationObserver in _observeDynamicMedia watches for preload attribute
|
||||
// changes and will create the proxy just-in-time when the preloader
|
||||
// promotes the clip.
|
||||
if (iframeEl.preload === "metadata" || iframeEl.preload === "none") return;
|
||||
|
||||
const rawSrc =
|
||||
iframeEl.getAttribute("src") || iframeEl.querySelector("source")?.getAttribute("src");
|
||||
if (!rawSrc) return;
|
||||
@@ -1544,6 +1552,22 @@ class HyperframesPlayer extends HTMLElement {
|
||||
if (typeof MutationObserver === "undefined" || !doc.body) return;
|
||||
const obs = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
// Attribute mutations: the preloader promotes a clip by changing its
|
||||
// preload attribute from "metadata" to "auto". When that happens, the
|
||||
// early-return guard in _adoptIframeMedia no longer blocks, so we can
|
||||
// create the parent proxy just-in-time.
|
||||
if (m.type === "attributes" && m.attributeName === "preload") {
|
||||
const target = m.target;
|
||||
if (
|
||||
target instanceof HTMLMediaElement &&
|
||||
target.matches("audio[data-start], video[data-start]") &&
|
||||
target.preload === "auto"
|
||||
) {
|
||||
this._adoptIframeMedia(target);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const added of m.addedNodes) {
|
||||
if (!(added instanceof Element)) continue;
|
||||
// Handle both the node itself and any timed media nested inside
|
||||
@@ -1578,13 +1602,19 @@ class HyperframesPlayer extends HTMLElement {
|
||||
}
|
||||
}
|
||||
});
|
||||
const observeOpts: MutationObserverInit = {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["preload"],
|
||||
};
|
||||
const hosts = doc.querySelectorAll("[data-composition-id]");
|
||||
if (hosts.length > 0) {
|
||||
for (const host of hosts) {
|
||||
obs.observe(host, { childList: true, subtree: true });
|
||||
obs.observe(host, observeOpts);
|
||||
}
|
||||
} else {
|
||||
obs.observe(doc.body, { childList: true, subtree: true });
|
||||
obs.observe(doc.body, observeOpts);
|
||||
}
|
||||
this._mediaObserver = obs;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user