mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(core): extend media start fix to all consumers, guard auto-start
Narrow the raw data-start read to media elements without data-hf-auto-start (explicitly authored global coordinates). Elements with auto-injected data-start="0" remain composition-local via the resolver. Apply consistently across all three consumers: - visibility loop (init.ts) - refreshRuntimeMediaCache start/duration (init.ts) - resolveMediaWindowEndSeconds (timeline.ts) Add regression test for auto-injected data-start="0" inside a late-starting host to prove it doesn't regress.
This commit is contained in:
@@ -539,6 +539,63 @@ describe("initSandboxRuntimeModular", () => {
|
||||
expect(pipVideo.style.visibility).toBe("hidden");
|
||||
});
|
||||
|
||||
it("shows auto-injected video at host time, not at t=0", () => {
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
root.setAttribute("data-root", "true");
|
||||
root.setAttribute("data-start", "0");
|
||||
root.setAttribute("data-width", "1920");
|
||||
root.setAttribute("data-height", "1080");
|
||||
document.body.appendChild(root);
|
||||
|
||||
const host = document.createElement("div");
|
||||
host.setAttribute("data-composition-id", "intro");
|
||||
host.setAttribute("data-start", "10");
|
||||
host.setAttribute("data-duration", "5");
|
||||
root.appendChild(host);
|
||||
|
||||
const innerRoot = document.createElement("div");
|
||||
innerRoot.setAttribute("data-composition-id", "intro");
|
||||
host.appendChild(innerRoot);
|
||||
|
||||
const video = document.createElement("video");
|
||||
video.setAttribute("data-start", "0");
|
||||
video.setAttribute("data-hf-auto-start", "");
|
||||
video.setAttribute("data-duration", "5");
|
||||
Object.defineProperty(video, "paused", { value: true, configurable: true });
|
||||
Object.defineProperty(video, "readyState", { value: 0, configurable: true });
|
||||
Object.defineProperty(video, "currentTime", {
|
||||
value: 0,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
video.load = () => {};
|
||||
innerRoot.appendChild(video);
|
||||
|
||||
(window as Window & { __timelines?: Record<string, RuntimeTimelineLike> }).__timelines = {
|
||||
main: createMockTimeline(30),
|
||||
intro: createMockTimeline(5),
|
||||
};
|
||||
|
||||
initSandboxRuntimeModular();
|
||||
|
||||
const player = (
|
||||
window as Window & {
|
||||
__player?: { seek: (timeSeconds: number) => void };
|
||||
}
|
||||
).__player;
|
||||
expect(player).toBeDefined();
|
||||
|
||||
player?.seek(12);
|
||||
expect(video.style.visibility).toBe("visible");
|
||||
|
||||
player?.seek(5);
|
||||
expect(video.style.visibility).toBe("hidden");
|
||||
|
||||
player?.seek(16);
|
||||
expect(video.style.visibility).toBe("hidden");
|
||||
});
|
||||
|
||||
it("plays scheduled child timelines without a captured root timeline when audio has failed", () => {
|
||||
const raf = createManualRaf();
|
||||
vi.spyOn(performance, "now").mockImplementation(() => raf.now());
|
||||
|
||||
@@ -1278,6 +1278,9 @@ export function initSandboxRuntimeModular(): void {
|
||||
element.hasAttribute("data-start") ||
|
||||
Boolean(resolveMediaCompositionContext(element).compositionRoot),
|
||||
resolveStartSeconds: (element) => {
|
||||
if (!element.hasAttribute("data-hf-auto-start") && element.hasAttribute("data-start")) {
|
||||
return Math.max(0, Number(element.getAttribute("data-start") ?? 0) || 0);
|
||||
}
|
||||
const context = resolveMediaCompositionContext(
|
||||
element as HTMLVideoElement | HTMLAudioElement,
|
||||
);
|
||||
@@ -1285,7 +1288,10 @@ export function initSandboxRuntimeModular(): void {
|
||||
},
|
||||
resolveDurationSeconds: (element) => {
|
||||
const context = resolveMediaCompositionContext(element);
|
||||
const start = resolveStartForElement(element, context.inheritedStart ?? 0);
|
||||
const start =
|
||||
!element.hasAttribute("data-hf-auto-start") && element.hasAttribute("data-start")
|
||||
? Math.max(0, Number(element.getAttribute("data-start") ?? 0) || 0)
|
||||
: resolveStartForElement(element, context.inheritedStart ?? 0);
|
||||
const mediaStart =
|
||||
Number.parseFloat(element.dataset.playbackStart ?? element.dataset.mediaStart ?? "0") ||
|
||||
0;
|
||||
@@ -1329,13 +1335,14 @@ export function initSandboxRuntimeModular(): void {
|
||||
const tag = rawNode.tagName.toLowerCase();
|
||||
if (tag === "script" || tag === "style" || tag === "link" || tag === "meta") continue;
|
||||
|
||||
// For media elements (video/audio) data-start is authored in global (composition-root)
|
||||
// time — the same contract used by the render pipeline's discoverMediaFromBrowser which
|
||||
// reads the raw attribute directly. Calling resolveStartForElement would add the nearest
|
||||
// ancestor composition's start a second time, creating a double-offset that keeps the
|
||||
// element permanently hidden when its host composition does not start at t=0.
|
||||
const isMediaElement = tag === "video" || tag === "audio";
|
||||
const start = isMediaElement
|
||||
// Media elements with explicitly authored data-start (no data-hf-auto-start
|
||||
// marker) use global coordinates — matching the render pipeline's
|
||||
// discoverMediaFromBrowser. resolveStartForElement would add the host
|
||||
// composition's offset a second time. Auto-injected data-start="0"
|
||||
// (data-hf-auto-start present) is composition-local and needs the resolver.
|
||||
const isGlobalMediaStart =
|
||||
(tag === "video" || tag === "audio") && !rawNode.hasAttribute("data-hf-auto-start");
|
||||
const start = isGlobalMediaStart
|
||||
? Math.max(0, Number(rawNode.getAttribute("data-start") ?? 0) || 0)
|
||||
: resolveStartForElement(rawNode, 0);
|
||||
let duration = resolveDurationForElement(rawNode);
|
||||
|
||||
@@ -220,7 +220,9 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
if (mediaNodes.length === 0) return null;
|
||||
let maxWindowEndSeconds = 0;
|
||||
for (const mediaNode of mediaNodes) {
|
||||
const start = startResolver.resolveStartForElement(mediaNode, 0);
|
||||
const start = !mediaNode.hasAttribute("data-hf-auto-start")
|
||||
? Math.max(0, Number(mediaNode.getAttribute("data-start") ?? 0) || 0)
|
||||
: startResolver.resolveStartForElement(mediaNode, 0);
|
||||
if (!Number.isFinite(start)) continue;
|
||||
const duration = resolveMediaElementDurationSeconds(mediaNode);
|
||||
if (duration == null || duration <= 0) continue;
|
||||
|
||||
Reference in New Issue
Block a user