mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
style: apply oxfmt baseline formatting across all source files (#25)
## Summary - Run `oxfmt .` across the entire codebase to establish formatted baseline - 299 files changed — mechanical formatting only, no logic changes - Double quotes, semicolons, 2-space indent, trailing commas, 100 print width Part 3/4 of [VA-851](https://linear.app/heygen/issue/VA-851/pre-migration-configure-eslint-prettier-and-conventional-commits) ## Test plan - [x] `pnpm format:check` — all 426 files pass - [x] `pnpm -r typecheck` — all packages pass - [x] `pnpm build` — all packages build - [x] All 348 tests pass
This commit is contained in:
@@ -21,7 +21,11 @@ function createLottieWebAnim(opts?: { totalFrames?: number; frameRate?: number }
|
||||
};
|
||||
}
|
||||
|
||||
function createDotLottiePlayer(opts?: { totalFrames?: number; frameRate?: number; duration?: number }) {
|
||||
function createDotLottiePlayer(opts?: {
|
||||
totalFrames?: number;
|
||||
frameRate?: number;
|
||||
duration?: number;
|
||||
}) {
|
||||
return {
|
||||
play: vi.fn(),
|
||||
pause: vi.fn(),
|
||||
|
||||
@@ -156,7 +156,11 @@ export function createLottieAdapter(): RuntimeDeterministicAdapter {
|
||||
// ── Type guards ────────────────────────────────────────────────────────────────
|
||||
|
||||
function isLottieWebAnimation(anim: unknown): anim is LottieWebAnimation {
|
||||
return typeof anim === "object" && anim !== null && typeof (anim as LottieWebAnimation).goToAndStop === "function";
|
||||
return (
|
||||
typeof anim === "object" &&
|
||||
anim !== null &&
|
||||
typeof (anim as LottieWebAnimation).goToAndStop === "function"
|
||||
);
|
||||
}
|
||||
|
||||
function isDotLottiePlayer(anim: unknown): anim is DotLottiePlayer {
|
||||
|
||||
@@ -54,7 +54,9 @@ describe("waapi adapter", () => {
|
||||
|
||||
it("handles animation that throws on pause", () => {
|
||||
const mockAnim = {
|
||||
pause: vi.fn(() => { throw new Error("invalid state"); }),
|
||||
pause: vi.fn(() => {
|
||||
throw new Error("invalid state");
|
||||
}),
|
||||
currentTime: 0,
|
||||
};
|
||||
(document as any).getAnimations = vi.fn(() => [mockAnim]);
|
||||
|
||||
@@ -86,18 +86,22 @@ describe("installRuntimeControlBridge", () => {
|
||||
it("ignores messages from wrong source", () => {
|
||||
const deps = createMockDeps();
|
||||
const handler = installRuntimeControlBridge(deps);
|
||||
handler(new MessageEvent("message", {
|
||||
data: { source: "other", type: "control", action: "play" },
|
||||
}));
|
||||
handler(
|
||||
new MessageEvent("message", {
|
||||
data: { source: "other", type: "control", action: "play" },
|
||||
}),
|
||||
);
|
||||
expect(deps.onPlay).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores messages with wrong type", () => {
|
||||
const deps = createMockDeps();
|
||||
const handler = installRuntimeControlBridge(deps);
|
||||
handler(new MessageEvent("message", {
|
||||
data: { source: "hf-parent", type: "state", action: "play" },
|
||||
}));
|
||||
handler(
|
||||
new MessageEvent("message", {
|
||||
data: { source: "hf-parent", type: "state", action: "play" },
|
||||
}),
|
||||
);
|
||||
expect(deps.onPlay).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -112,7 +116,7 @@ describe("installRuntimeControlBridge", () => {
|
||||
const deps = createMockDeps();
|
||||
const handler = installRuntimeControlBridge(deps);
|
||||
expect(() =>
|
||||
handler(makeControlMessage("flash-elements", { selectors: [".test"], duration: 500 }))
|
||||
handler(makeControlMessage("flash-elements", { selectors: [".test"], duration: 500 })),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,9 +44,7 @@ describe("loadExternalCompositions", () => {
|
||||
</body></html>
|
||||
`;
|
||||
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(compositionHtml, { status: 200 })
|
||||
);
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(compositionHtml, { status: 200 }));
|
||||
|
||||
await loadExternalCompositions({ ...defaultParams });
|
||||
|
||||
@@ -68,9 +66,7 @@ describe("loadExternalCompositions", () => {
|
||||
</body></html>
|
||||
`;
|
||||
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(compositionHtml, { status: 200 })
|
||||
);
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(compositionHtml, { status: 200 }));
|
||||
|
||||
const injectedStyles: HTMLStyleElement[] = [];
|
||||
await loadExternalCompositions({
|
||||
@@ -102,7 +98,7 @@ describe("loadExternalCompositions", () => {
|
||||
hostCompositionSrc: "https://example.com/broken.html",
|
||||
errorMessage: "Network error",
|
||||
}),
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -111,9 +107,7 @@ describe("loadExternalCompositions", () => {
|
||||
host.setAttribute("data-composition-src", "https://example.com/404.html");
|
||||
document.body.appendChild(host);
|
||||
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response("Not Found", { status: 404 })
|
||||
);
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("Not Found", { status: 404 }));
|
||||
|
||||
const onDiagnostic = vi.fn();
|
||||
await loadExternalCompositions({
|
||||
@@ -124,7 +118,7 @@ describe("loadExternalCompositions", () => {
|
||||
expect(onDiagnostic).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
code: "external_composition_load_failed",
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -165,9 +159,7 @@ describe("loadExternalCompositions", () => {
|
||||
document.body.appendChild(host);
|
||||
|
||||
const compositionHtml = `<html><body><p>New</p></body></html>`;
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(compositionHtml, { status: 200 })
|
||||
);
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(compositionHtml, { status: 200 }));
|
||||
|
||||
await loadExternalCompositions({ ...defaultParams });
|
||||
expect(host.querySelector("span")).toBeNull();
|
||||
@@ -186,9 +178,7 @@ describe("loadExternalCompositions", () => {
|
||||
</body></html>
|
||||
`;
|
||||
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(compositionHtml, { status: 200 })
|
||||
);
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(compositionHtml, { status: 200 }));
|
||||
|
||||
const injectedScripts: HTMLScriptElement[] = [];
|
||||
await loadExternalCompositions({
|
||||
|
||||
@@ -88,10 +88,13 @@ async function mountCompositionContent(params: {
|
||||
}): Promise<void> {
|
||||
let innerRoot: Element | null = null;
|
||||
if (params.hostCompositionId) {
|
||||
const candidateRoots = Array.from(params.sourceNode.querySelectorAll<Element>("[data-composition-id]"));
|
||||
const candidateRoots = Array.from(
|
||||
params.sourceNode.querySelectorAll<Element>("[data-composition-id]"),
|
||||
);
|
||||
innerRoot =
|
||||
candidateRoots.find((candidate) => candidate.getAttribute("data-composition-id") === params.hostCompositionId) ??
|
||||
null;
|
||||
candidateRoots.find(
|
||||
(candidate) => candidate.getAttribute("data-composition-id") === params.hostCompositionId,
|
||||
) ?? null;
|
||||
}
|
||||
const contentNode = innerRoot ?? params.sourceNode;
|
||||
|
||||
@@ -188,7 +191,9 @@ async function mountCompositionContent(params: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadExternalCompositions(params: LoadExternalCompositionsParams): Promise<void> {
|
||||
export async function loadExternalCompositions(
|
||||
params: LoadExternalCompositionsParams,
|
||||
): Promise<void> {
|
||||
const hosts = Array.from(document.querySelectorAll("[data-composition-src]"));
|
||||
if (hosts.length === 0) return;
|
||||
|
||||
@@ -207,7 +212,9 @@ export async function loadExternalCompositions(params: LoadExternalCompositionsP
|
||||
const hostCompositionId = host.getAttribute("data-composition-id");
|
||||
const localTemplate =
|
||||
hostCompositionId != null
|
||||
? document.querySelector<HTMLTemplateElement>(`template#${CSS.escape(hostCompositionId)}-template`)
|
||||
? document.querySelector<HTMLTemplateElement>(
|
||||
`template#${CSS.escape(hostCompositionId)}-template`,
|
||||
)
|
||||
: null;
|
||||
if (localTemplate) {
|
||||
await mountCompositionContent({
|
||||
@@ -234,7 +241,9 @@ export async function loadExternalCompositions(params: LoadExternalCompositionsP
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
const template =
|
||||
(hostCompositionId
|
||||
? doc.querySelector<HTMLTemplateElement>(`template#${CSS.escape(hostCompositionId)}-template`)
|
||||
? doc.querySelector<HTMLTemplateElement>(
|
||||
`template#${CSS.escape(hostCompositionId)}-template`,
|
||||
)
|
||||
: null) ?? doc.querySelector<HTMLTemplateElement>("template");
|
||||
const sourceNode = template ? template.content : doc.body;
|
||||
await mountCompositionContent({
|
||||
|
||||
@@ -36,7 +36,11 @@ export function initSandboxRuntimeModular(): void {
|
||||
const registerRuntimeCleanup = (callback: () => void) => {
|
||||
runtimeCleanupCallbacks.push(callback);
|
||||
};
|
||||
const postRuntimeDiagnosticOnce = (code: string, details: Record<string, RuntimeJson>, dedupeKey?: string) => {
|
||||
const postRuntimeDiagnosticOnce = (
|
||||
code: string,
|
||||
details: Record<string, RuntimeJson>,
|
||||
dedupeKey?: string,
|
||||
) => {
|
||||
const key = dedupeKey ?? `${code}:${JSON.stringify(details)}`;
|
||||
if (postedDiagnosticKeys.has(key)) {
|
||||
return;
|
||||
@@ -157,7 +161,10 @@ export function initSandboxRuntimeModular(): void {
|
||||
category: string;
|
||||
} => {
|
||||
const message = rawMessage.toLowerCase();
|
||||
if (message.includes("cannot read properties of null") || message.includes("cannot set properties of null")) {
|
||||
if (
|
||||
message.includes("cannot read properties of null") ||
|
||||
message.includes("cannot set properties of null")
|
||||
) {
|
||||
return { code: "runtime_null_dom_access", category: "dom-null-access" };
|
||||
}
|
||||
if (message.includes("failed to execute 'queryselector'")) {
|
||||
@@ -185,10 +192,13 @@ export function initSandboxRuntimeModular(): void {
|
||||
if (explicitRoot instanceof HTMLElement) {
|
||||
return explicitRoot;
|
||||
}
|
||||
const compositionNodes = Array.from(document.querySelectorAll("[data-composition-id]")) as HTMLElement[];
|
||||
const compositionNodes = Array.from(
|
||||
document.querySelectorAll("[data-composition-id]"),
|
||||
) as HTMLElement[];
|
||||
if (compositionNodes.length === 0) return null;
|
||||
return (
|
||||
compositionNodes.find((node) => !node.parentElement?.closest("[data-composition-id]")) ?? compositionNodes[0]
|
||||
compositionNodes.find((node) => !node.parentElement?.closest("[data-composition-id]")) ??
|
||||
compositionNodes[0]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -278,12 +288,18 @@ export function initSandboxRuntimeModular(): void {
|
||||
el.style.position = "absolute";
|
||||
}
|
||||
const hasExplicitVerticalAnchor =
|
||||
Boolean(el.style.top) || Boolean(el.style.bottom) || computed.top !== "auto" || computed.bottom !== "auto";
|
||||
Boolean(el.style.top) ||
|
||||
Boolean(el.style.bottom) ||
|
||||
computed.top !== "auto" ||
|
||||
computed.bottom !== "auto";
|
||||
if (!hasExplicitVerticalAnchor) {
|
||||
el.style.top = "0";
|
||||
}
|
||||
const hasExplicitHorizontalAnchor =
|
||||
Boolean(el.style.left) || Boolean(el.style.right) || computed.left !== "auto" || computed.right !== "auto";
|
||||
Boolean(el.style.left) ||
|
||||
Boolean(el.style.right) ||
|
||||
computed.left !== "auto" ||
|
||||
computed.right !== "auto";
|
||||
if (!hasExplicitHorizontalAnchor) {
|
||||
el.style.left = "0";
|
||||
}
|
||||
@@ -312,14 +328,20 @@ export function initSandboxRuntimeModular(): void {
|
||||
|
||||
const resolveStartForElement = (element: Element, fallback = 0): number => {
|
||||
const resolver = createRuntimeStartTimeResolver({
|
||||
timelineRegistry: (window.__timelines ?? {}) as Record<string, RuntimeTimelineLike | undefined>,
|
||||
timelineRegistry: (window.__timelines ?? {}) as Record<
|
||||
string,
|
||||
RuntimeTimelineLike | undefined
|
||||
>,
|
||||
});
|
||||
return resolver.resolveStartForElement(element, fallback);
|
||||
};
|
||||
|
||||
const resolveDurationForElement = (element: Element): number | null => {
|
||||
const resolver = createRuntimeStartTimeResolver({
|
||||
timelineRegistry: (window.__timelines ?? {}) as Record<string, RuntimeTimelineLike | undefined>,
|
||||
timelineRegistry: (window.__timelines ?? {}) as Record<
|
||||
string,
|
||||
RuntimeTimelineLike | undefined
|
||||
>,
|
||||
});
|
||||
return resolver.resolveDurationForElement(element);
|
||||
};
|
||||
@@ -399,13 +421,20 @@ export function initSandboxRuntimeModular(): void {
|
||||
if (!isUsableTimelineDuration(mediaDurationFloorSeconds)) {
|
||||
return MIN_VALID_TIMELINE_DURATION_SECONDS;
|
||||
}
|
||||
return Math.max(MIN_VALID_TIMELINE_DURATION_SECONDS, mediaDurationFloorSeconds * TIMELINE_FLOOR_COVERAGE_RATIO);
|
||||
return Math.max(
|
||||
MIN_VALID_TIMELINE_DURATION_SECONDS,
|
||||
mediaDurationFloorSeconds * TIMELINE_FLOOR_COVERAGE_RATIO,
|
||||
);
|
||||
};
|
||||
|
||||
const getSafeTimelineDurationSeconds = (timeline: RuntimeTimelineLike | null, fallback = 0): number => {
|
||||
const getSafeTimelineDurationSeconds = (
|
||||
timeline: RuntimeTimelineLike | null,
|
||||
fallback = 0,
|
||||
): number => {
|
||||
const timelineDuration = getTimelineDurationSeconds(timeline);
|
||||
const mediaFloor = resolveMediaDurationFloorSeconds();
|
||||
const fallbackDuration = Number.isFinite(fallback) && fallback > MIN_VALID_TIMELINE_DURATION_SECONDS ? fallback : 0;
|
||||
const fallbackDuration =
|
||||
Number.isFinite(fallback) && fallback > MIN_VALID_TIMELINE_DURATION_SECONDS ? fallback : 0;
|
||||
let safeDuration = 0;
|
||||
// Timeline is the source of truth for authored composition duration.
|
||||
if (isUsableTimelineDuration(timelineDuration)) {
|
||||
@@ -423,20 +452,30 @@ export function initSandboxRuntimeModular(): void {
|
||||
const timelines = (window.__timelines ?? {}) as Record<string, RuntimeTimelineLike | undefined>;
|
||||
const startResolver = createRuntimeStartTimeResolver({ timelineRegistry: timelines });
|
||||
const mediaDurationFloorSeconds = resolveMediaDurationFloorSeconds();
|
||||
const minCandidateDurationSeconds = resolveMinCandidateDurationSeconds(mediaDurationFloorSeconds);
|
||||
const minCandidateDurationSeconds =
|
||||
resolveMinCandidateDurationSeconds(mediaDurationFloorSeconds);
|
||||
const resolveCompositionStartSeconds = (compositionId: string): number => {
|
||||
const node = document.querySelector(`[data-composition-id="${CSS.escape(compositionId)}"]`) as Element | null;
|
||||
const node = document.querySelector(
|
||||
`[data-composition-id="${CSS.escape(compositionId)}"]`,
|
||||
) as Element | null;
|
||||
if (!node) return 0;
|
||||
return startResolver.resolveStartForElement(node, 0);
|
||||
};
|
||||
const createCompositeTimelineFromCandidates = (
|
||||
candidates: Array<{ compositionId: string; timeline: RuntimeTimelineLike; durationSeconds: number }>,
|
||||
candidates: Array<{
|
||||
compositionId: string;
|
||||
timeline: RuntimeTimelineLike;
|
||||
durationSeconds: number;
|
||||
}>,
|
||||
): RuntimeTimelineLike | null => {
|
||||
const gsapApi = window.gsap;
|
||||
if (!gsapApi || typeof gsapApi.timeline !== "function") return null;
|
||||
const compositeTimeline = gsapApi.timeline({ paused: true }) as RuntimeTimelineLike;
|
||||
for (const candidate of candidates) {
|
||||
compositeTimeline.add(candidate.timeline, resolveCompositionStartSeconds(candidate.compositionId));
|
||||
compositeTimeline.add(
|
||||
candidate.timeline,
|
||||
resolveCompositionStartSeconds(candidate.compositionId),
|
||||
);
|
||||
}
|
||||
return compositeTimeline;
|
||||
};
|
||||
@@ -469,7 +508,11 @@ export function initSandboxRuntimeModular(): void {
|
||||
};
|
||||
const addMissingChildCandidatesToRootTimeline = (
|
||||
rootTimeline: RuntimeTimelineLike,
|
||||
candidates: Array<{ compositionId: string; timeline: RuntimeTimelineLike; durationSeconds: number }>,
|
||||
candidates: Array<{
|
||||
compositionId: string;
|
||||
timeline: RuntimeTimelineLike;
|
||||
durationSeconds: number;
|
||||
}>,
|
||||
): string[] => {
|
||||
const rootWithChildren = rootTimeline as RuntimeTimelineLike & {
|
||||
getChildren?: (...args: unknown[]) => unknown[];
|
||||
@@ -509,7 +552,11 @@ export function initSandboxRuntimeModular(): void {
|
||||
if (!rootCompositionNode) return [];
|
||||
const seen = new Set<string>();
|
||||
const childNodes = Array.from(rootCompositionNode.querySelectorAll("[data-composition-id]"));
|
||||
const candidates: Array<{ compositionId: string; timeline: RuntimeTimelineLike; durationSeconds: number }> = [];
|
||||
const candidates: Array<{
|
||||
compositionId: string;
|
||||
timeline: RuntimeTimelineLike;
|
||||
durationSeconds: number;
|
||||
}> = [];
|
||||
for (const childNode of childNodes) {
|
||||
const childId = childNode.getAttribute("data-composition-id");
|
||||
if (!childId || childId === rootCompositionId) continue;
|
||||
@@ -517,7 +564,10 @@ export function initSandboxRuntimeModular(): void {
|
||||
seen.add(childId);
|
||||
const candidateTimeline = timelines[childId] ?? null;
|
||||
if (!candidateTimeline) continue;
|
||||
if (typeof candidateTimeline.play !== "function" || typeof candidateTimeline.pause !== "function") {
|
||||
if (
|
||||
typeof candidateTimeline.play !== "function" ||
|
||||
typeof candidateTimeline.pause !== "function"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const candidateDuration = getTimelineDurationSeconds(candidateTimeline);
|
||||
@@ -531,7 +581,11 @@ export function initSandboxRuntimeModular(): void {
|
||||
};
|
||||
const rootChildCandidates = collectRootChildCandidates();
|
||||
const ensureChildCandidatesActive = (
|
||||
candidates: Array<{ compositionId: string; timeline: RuntimeTimelineLike; durationSeconds: number }>,
|
||||
candidates: Array<{
|
||||
compositionId: string;
|
||||
timeline: RuntimeTimelineLike;
|
||||
durationSeconds: number;
|
||||
}>,
|
||||
): void => {
|
||||
for (const candidate of candidates) {
|
||||
const timelineWithPaused = candidate.timeline as RuntimeTimelineLike & {
|
||||
@@ -554,7 +608,12 @@ export function initSandboxRuntimeModular(): void {
|
||||
? addMissingChildCandidatesToRootTimeline(rootTimeline, rootChildCandidates)
|
||||
: [];
|
||||
// Mark children as bound so the polling loop stops re-resolving
|
||||
if (rootChildCandidates.length > 0 || !document.querySelector("[data-composition-id]:not([data-composition-id='" + rootCompositionId + "'])")) {
|
||||
if (
|
||||
rootChildCandidates.length > 0 ||
|
||||
!document.querySelector(
|
||||
"[data-composition-id]:not([data-composition-id='" + rootCompositionId + "'])",
|
||||
)
|
||||
) {
|
||||
childrenBound = true;
|
||||
}
|
||||
|
||||
@@ -564,7 +623,9 @@ export function initSandboxRuntimeModular(): void {
|
||||
try {
|
||||
const currentTime = rootTimeline.time();
|
||||
rootTimeline.seek(currentTime, false); // false = don't suppress events
|
||||
} catch { /* ignore */ }
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
const rootDurationSeconds = getTimelineDurationSeconds(rootTimeline);
|
||||
if (!isUsableTimelineDuration(rootDurationSeconds) && rootChildCandidates.length > 0) {
|
||||
@@ -592,7 +653,10 @@ export function initSandboxRuntimeModular(): void {
|
||||
},
|
||||
};
|
||||
}
|
||||
const durationFloorTimeline = createDurationFloorTimeline(mediaDurationFloorSeconds ?? 0, rootTimeline);
|
||||
const durationFloorTimeline = createDurationFloorTimeline(
|
||||
mediaDurationFloorSeconds ?? 0,
|
||||
rootTimeline,
|
||||
);
|
||||
const durationFloorSeconds = getTimelineDurationSeconds(durationFloorTimeline);
|
||||
if (durationFloorTimeline && isUsableTimelineDuration(durationFloorSeconds)) {
|
||||
return {
|
||||
@@ -616,7 +680,10 @@ export function initSandboxRuntimeModular(): void {
|
||||
}
|
||||
}
|
||||
if (!isUsableTimelineDuration(rootDurationSeconds) && rootChildCandidates.length === 0) {
|
||||
const durationFloorTimeline = createDurationFloorTimeline(mediaDurationFloorSeconds ?? 0, rootTimeline);
|
||||
const durationFloorTimeline = createDurationFloorTimeline(
|
||||
mediaDurationFloorSeconds ?? 0,
|
||||
rootTimeline,
|
||||
);
|
||||
const durationFloorSeconds = getTimelineDurationSeconds(durationFloorTimeline);
|
||||
if (durationFloorTimeline && isUsableTimelineDuration(durationFloorSeconds)) {
|
||||
return {
|
||||
@@ -750,9 +817,15 @@ export function initSandboxRuntimeModular(): void {
|
||||
const declaredHeight = Number(rootNode.getAttribute("data-height"));
|
||||
const computedStyle = window.getComputedStyle(rootNode);
|
||||
const hasDeclaredDimensions =
|
||||
Number.isFinite(declaredWidth) && declaredWidth > 0 && Number.isFinite(declaredHeight) && declaredHeight > 0;
|
||||
Number.isFinite(declaredWidth) &&
|
||||
declaredWidth > 0 &&
|
||||
Number.isFinite(declaredHeight) &&
|
||||
declaredHeight > 0;
|
||||
const looksCollapsed =
|
||||
rect.width <= 0 || rect.height <= 0 || rootNode.clientWidth <= 0 || rootNode.clientHeight <= 0;
|
||||
rect.width <= 0 ||
|
||||
rect.height <= 0 ||
|
||||
rootNode.clientWidth <= 0 ||
|
||||
rootNode.clientHeight <= 0;
|
||||
if (!hasDeclaredDimensions || !looksCollapsed) {
|
||||
return;
|
||||
}
|
||||
@@ -811,7 +884,10 @@ export function initSandboxRuntimeModular(): void {
|
||||
});
|
||||
};
|
||||
runtimeUnhandledRejectionListener = (event: PromiseRejectionEvent) => {
|
||||
const normalized = normalizeDiagnosticMessage(event.reason).slice(0, MAX_DIAGNOSTIC_MESSAGE_LENGTH);
|
||||
const normalized = normalizeDiagnosticMessage(event.reason).slice(
|
||||
0,
|
||||
MAX_DIAGNOSTIC_MESSAGE_LENGTH,
|
||||
);
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
@@ -831,22 +907,31 @@ export function initSandboxRuntimeModular(): void {
|
||||
};
|
||||
|
||||
const installAssetFailureDiagnostics = () => {
|
||||
const assetNodes = Array.from(document.querySelectorAll("img, video, audio, source, link[rel='stylesheet']"));
|
||||
const assetNodes = Array.from(
|
||||
document.querySelectorAll("img, video, audio, source, link[rel='stylesheet']"),
|
||||
);
|
||||
for (const node of assetNodes) {
|
||||
const onError = () => {
|
||||
if (!(node instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
const tagName = node.tagName.toLowerCase();
|
||||
const assetUrl = node.getAttribute("src") ?? node.getAttribute("href") ?? node.getAttribute("poster") ?? null;
|
||||
const diagnosticCode = tagName === "link" ? "runtime_stylesheet_load_failed" : "runtime_asset_load_failed";
|
||||
const assetUrl =
|
||||
node.getAttribute("src") ??
|
||||
node.getAttribute("href") ??
|
||||
node.getAttribute("poster") ??
|
||||
null;
|
||||
const diagnosticCode =
|
||||
tagName === "link" ? "runtime_stylesheet_load_failed" : "runtime_asset_load_failed";
|
||||
postRuntimeDiagnosticOnce(
|
||||
diagnosticCode,
|
||||
{
|
||||
tagName,
|
||||
assetUrl,
|
||||
currentSrc:
|
||||
node instanceof HTMLImageElement || node instanceof HTMLMediaElement ? node.currentSrc || null : null,
|
||||
node instanceof HTMLImageElement || node instanceof HTMLMediaElement
|
||||
? node.currentSrc || null
|
||||
: null,
|
||||
readyState: node instanceof HTMLMediaElement ? node.readyState : null,
|
||||
networkState: node instanceof HTMLMediaElement ? node.networkState : null,
|
||||
},
|
||||
@@ -890,7 +975,10 @@ export function initSandboxRuntimeModular(): void {
|
||||
});
|
||||
};
|
||||
|
||||
const rebindTimelineFromResolution = (resolution: TimelineResolution, reason: "loop_guard" | "manual"): boolean => {
|
||||
const rebindTimelineFromResolution = (
|
||||
resolution: TimelineResolution,
|
||||
reason: "loop_guard" | "manual",
|
||||
): boolean => {
|
||||
if (!resolution.timeline) return false;
|
||||
const previousTimeline = state.capturedTimeline;
|
||||
if (previousTimeline && previousTimeline === resolution.timeline) {
|
||||
@@ -940,7 +1028,9 @@ export function initSandboxRuntimeModular(): void {
|
||||
metadataRebindDebounceTimerId = null;
|
||||
const resolution = resolveRootTimelineFromDocument();
|
||||
if (!resolution.timeline) return;
|
||||
const hasResolvedMediaFloor = isUsableTimelineDuration(resolution.mediaDurationFloorSeconds ?? null);
|
||||
const hasResolvedMediaFloor = isUsableTimelineDuration(
|
||||
resolution.mediaDurationFloorSeconds ?? null,
|
||||
);
|
||||
if (!hasResolvedMediaFloor) return;
|
||||
if (!state.capturedTimeline) {
|
||||
if (bindRootTimelineIfAvailable()) {
|
||||
@@ -951,7 +1041,8 @@ export function initSandboxRuntimeModular(): void {
|
||||
}
|
||||
if (metadataRebindApplied) return;
|
||||
const currentDuration = getTimelineDurationSeconds(state.capturedTimeline);
|
||||
const nextDuration = resolution.selectedDurationSeconds ?? getTimelineDurationSeconds(resolution.timeline);
|
||||
const nextDuration =
|
||||
resolution.selectedDurationSeconds ?? getTimelineDurationSeconds(resolution.timeline);
|
||||
const isBetterCandidate =
|
||||
isUsableTimelineDuration(nextDuration) &&
|
||||
(!isUsableTimelineDuration(currentDuration) ||
|
||||
@@ -1005,7 +1096,8 @@ export function initSandboxRuntimeModular(): void {
|
||||
playing: state.isPlaying,
|
||||
playbackRate: state.playbackRate,
|
||||
});
|
||||
const rootCompId = document.querySelector("[data-composition-id]")?.getAttribute("data-composition-id") ?? null;
|
||||
const rootCompId =
|
||||
document.querySelector("[data-composition-id]")?.getAttribute("data-composition-id") ?? null;
|
||||
const visibilityNodes = Array.from(document.querySelectorAll("[data-start]"));
|
||||
for (const rawNode of visibilityNodes) {
|
||||
if (!(rawNode instanceof HTMLElement)) continue;
|
||||
@@ -1038,7 +1130,9 @@ export function initSandboxRuntimeModular(): void {
|
||||
if (compDur > 0) computedEnd = start + compDur;
|
||||
}
|
||||
}
|
||||
const isVisibleNow = state.currentTime >= start && (Number.isFinite(computedEnd) ? state.currentTime < computedEnd : true);
|
||||
const isVisibleNow =
|
||||
state.currentTime >= start &&
|
||||
(Number.isFinite(computedEnd) ? state.currentTime < computedEnd : true);
|
||||
rawNode.style.visibility = isVisibleNow ? "visible" : "hidden";
|
||||
}
|
||||
};
|
||||
@@ -1203,12 +1297,19 @@ export function initSandboxRuntimeModular(): void {
|
||||
initRuntimeAnalytics(postRuntimeMessage as (payload: unknown) => void);
|
||||
emitAnalyticsEvent("composition_loaded", {
|
||||
duration: player.getDuration(),
|
||||
compositionId: document.querySelector("[data-composition-id]")?.getAttribute("data-composition-id") ?? null,
|
||||
compositionId:
|
||||
document.querySelector("[data-composition-id]")?.getAttribute("data-composition-id") ?? null,
|
||||
});
|
||||
|
||||
state.controlBridgeHandler = installRuntimeControlBridge({
|
||||
onPlay: () => { player.play(); emitAnalyticsEvent("composition_played", { time: player.getTime() }); },
|
||||
onPause: () => { player.pause(); emitAnalyticsEvent("composition_paused", { time: player.getTime() }); },
|
||||
onPlay: () => {
|
||||
player.play();
|
||||
emitAnalyticsEvent("composition_played", { time: player.getTime() });
|
||||
},
|
||||
onPause: () => {
|
||||
player.pause();
|
||||
emitAnalyticsEvent("composition_paused", { time: player.getTime() });
|
||||
},
|
||||
onSeek: (frame, _seekMode) => {
|
||||
const time = Math.max(0, frame) / state.canonicalFps;
|
||||
player.seek(time);
|
||||
@@ -1280,7 +1381,9 @@ export function initSandboxRuntimeModular(): void {
|
||||
state.isPlaying &&
|
||||
state.capturedTimeline != null &&
|
||||
Math.max(0, state.currentTime || 0) < PLAY_REBIND_HOLD_SECONDS;
|
||||
const timelineBoundThisTick = shouldHoldRebindDuringEarlyPlay ? false : bindRootTimelineIfAvailable();
|
||||
const timelineBoundThisTick = shouldHoldRebindDuringEarlyPlay
|
||||
? false
|
||||
: bindRootTimelineIfAvailable();
|
||||
if (state.capturedTimeline && !player._timeline) {
|
||||
player._timeline = state.capturedTimeline;
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@ export type RuntimeMediaClip = {
|
||||
volume: number | null;
|
||||
};
|
||||
|
||||
export function refreshRuntimeMediaCache(params?: { resolveStartSeconds?: (element: Element) => number }): {
|
||||
export function refreshRuntimeMediaCache(params?: {
|
||||
resolveStartSeconds?: (element: Element) => number;
|
||||
}): {
|
||||
timedMediaEls: Array<HTMLVideoElement | HTMLAudioElement>;
|
||||
mediaClips: RuntimeMediaClip[];
|
||||
videoClips: RuntimeMediaClip[];
|
||||
maxMediaEnd: number;
|
||||
} {
|
||||
const mediaEls = Array.from(document.querySelectorAll("video[data-start], audio[data-start]")) as Array<
|
||||
HTMLVideoElement | HTMLAudioElement
|
||||
>;
|
||||
const mediaEls = Array.from(
|
||||
document.querySelectorAll("video[data-start], audio[data-start]"),
|
||||
) as Array<HTMLVideoElement | HTMLAudioElement>;
|
||||
const mediaClips: RuntimeMediaClip[] = [];
|
||||
const videoClips: RuntimeMediaClip[] = [];
|
||||
let maxMediaEnd = 0;
|
||||
@@ -24,12 +26,18 @@ export function refreshRuntimeMediaCache(params?: { resolveStartSeconds?: (eleme
|
||||
? params.resolveStartSeconds(el)
|
||||
: Number.parseFloat(el.dataset.start ?? "0");
|
||||
if (!Number.isFinite(start)) continue;
|
||||
const mediaStart = Number.parseFloat(el.dataset.playbackStart ?? el.dataset.mediaStart ?? "0") || 0;
|
||||
const mediaStart =
|
||||
Number.parseFloat(el.dataset.playbackStart ?? el.dataset.mediaStart ?? "0") || 0;
|
||||
let duration = Number.parseFloat(el.dataset.duration ?? "");
|
||||
if ((!Number.isFinite(duration) || duration <= 0) && Number.isFinite(el.duration) && el.duration > 0) {
|
||||
if (
|
||||
(!Number.isFinite(duration) || duration <= 0) &&
|
||||
Number.isFinite(el.duration) &&
|
||||
el.duration > 0
|
||||
) {
|
||||
duration = Math.max(0, el.duration - mediaStart);
|
||||
}
|
||||
const end = Number.isFinite(duration) && duration > 0 ? start + duration : Number.POSITIVE_INFINITY;
|
||||
const end =
|
||||
Number.isFinite(duration) && duration > 0 ? start + duration : Number.POSITIVE_INFINITY;
|
||||
const volumeRaw = Number.parseFloat(el.dataset.volume ?? "");
|
||||
const clip: RuntimeMediaClip = {
|
||||
el,
|
||||
@@ -56,7 +64,8 @@ export function syncRuntimeMedia(params: {
|
||||
const { el } = clip;
|
||||
if (!el.isConnected) continue;
|
||||
const relTime = params.timeSeconds - clip.start + clip.mediaStart;
|
||||
const isActive = params.timeSeconds >= clip.start && params.timeSeconds < clip.end && relTime >= 0;
|
||||
const isActive =
|
||||
params.timeSeconds >= clip.start && params.timeSeconds < clip.end && relTime >= 0;
|
||||
if (isActive) {
|
||||
if (clip.volume != null) el.volume = clip.volume;
|
||||
try {
|
||||
|
||||
@@ -8,7 +8,7 @@ function createMockPostMessage() {
|
||||
describe("createPickerModule", () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
document.head.querySelectorAll("style").forEach(s => s.remove());
|
||||
document.head.querySelectorAll("style").forEach((s) => s.remove());
|
||||
document.body.classList.remove("__hf-pick-active");
|
||||
});
|
||||
|
||||
@@ -33,15 +33,15 @@ describe("createPickerModule", () => {
|
||||
const picker = createPickerModule({ postMessage: createMockPostMessage() });
|
||||
picker.enablePickMode();
|
||||
const styles = document.head.querySelectorAll("style");
|
||||
const hasPickStyle = Array.from(styles).some(s =>
|
||||
s.textContent?.includes("__hf-pick-highlight")
|
||||
const hasPickStyle = Array.from(styles).some((s) =>
|
||||
s.textContent?.includes("__hf-pick-highlight"),
|
||||
);
|
||||
expect(hasPickStyle).toBe(true);
|
||||
|
||||
picker.disablePickMode();
|
||||
const stylesAfter = document.head.querySelectorAll("style");
|
||||
const hasPickStyleAfter = Array.from(stylesAfter).some(s =>
|
||||
s.textContent?.includes("__hf-pick-highlight")
|
||||
const hasPickStyleAfter = Array.from(stylesAfter).some((s) =>
|
||||
s.textContent?.includes("__hf-pick-highlight"),
|
||||
);
|
||||
expect(hasPickStyleAfter).toBe(false);
|
||||
});
|
||||
@@ -137,7 +137,7 @@ describe("createPickerModule", () => {
|
||||
expect.objectContaining({
|
||||
source: "hf-preview",
|
||||
type: "pick-mode-cancelled",
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -77,7 +77,8 @@ export function createPickerModule(deps: PickerModuleDeps): PickerModule {
|
||||
const trimLabel = (value: string, maxChars: number) =>
|
||||
value.length > maxChars ? `${value.slice(0, maxChars - 1)}…` : value;
|
||||
if (tag === "h1" || tag === "h2" || tag === "h3") return "Heading";
|
||||
if (tag === "p" || tag === "span" || tag === "div") return text.length > 0 ? trimLabel(text, 56) : "Text";
|
||||
if (tag === "p" || tag === "span" || tag === "div")
|
||||
return text.length > 0 ? trimLabel(text, 56) : "Text";
|
||||
if (tag === "img") return "Image";
|
||||
if (tag === "video") return "Video";
|
||||
if (tag === "audio") return "Audio";
|
||||
@@ -132,7 +133,11 @@ export function createPickerModule(deps: PickerModuleDeps): PickerModule {
|
||||
};
|
||||
}
|
||||
|
||||
function getPickInfosFromPoint(clientX: number, clientY: number, limit?: number): RuntimePickerElementInfo[] {
|
||||
function getPickInfosFromPoint(
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
limit?: number,
|
||||
): RuntimePickerElementInfo[] {
|
||||
return getPickCandidatesFromPoint(clientX, clientY, limit).map(extractElementInfo);
|
||||
}
|
||||
|
||||
@@ -217,7 +222,9 @@ export function createPickerModule(deps: PickerModuleDeps): PickerModule {
|
||||
getHovered: () => pickLastHoveredInfo,
|
||||
getSelected: () => pickLastSelectedInfo,
|
||||
getCandidatesAtPoint: (clientX, clientY, limit) =>
|
||||
Number.isFinite(clientX) && Number.isFinite(clientY) ? getPickInfosFromPoint(clientX, clientY, limit) : [],
|
||||
Number.isFinite(clientX) && Number.isFinite(clientY)
|
||||
? getPickInfosFromPoint(clientX, clientY, limit)
|
||||
: [],
|
||||
pickAtPoint: (clientX, clientY, index) => {
|
||||
if (!Number.isFinite(clientX) || !Number.isFinite(clientY)) return null;
|
||||
const infos = getPickInfosFromPoint(clientX, clientY, 8);
|
||||
@@ -240,12 +247,18 @@ export function createPickerModule(deps: PickerModuleDeps): PickerModule {
|
||||
const idx = Math.max(0, Math.min(infos.length - 1, Math.floor(Number(rawIndex))));
|
||||
const info = infos[idx];
|
||||
if (!info) continue;
|
||||
const duplicate = selected.some((item) => item.selector === info.selector && item.tagName === info.tagName);
|
||||
const duplicate = selected.some(
|
||||
(item) => item.selector === info.selector && item.tagName === info.tagName,
|
||||
);
|
||||
if (!duplicate) selected.push(info);
|
||||
}
|
||||
if (!selected.length) return [];
|
||||
setLastSelectedInfo(selected[0] ?? null);
|
||||
deps.postMessage({ source: "hf-preview", type: "element-picked-many", elementInfos: selected });
|
||||
deps.postMessage({
|
||||
source: "hf-preview",
|
||||
type: "element-picked-many",
|
||||
elementInfos: selected,
|
||||
});
|
||||
disablePickMode();
|
||||
return selected;
|
||||
},
|
||||
|
||||
@@ -5,14 +5,24 @@ import type { RuntimeTimelineLike } from "./types";
|
||||
function createMockTimeline(opts?: { time?: number; duration?: number }): RuntimeTimelineLike {
|
||||
const state = { time: opts?.time ?? 0, duration: opts?.duration ?? 10, paused: false };
|
||||
return {
|
||||
play: vi.fn(() => { state.paused = false; }),
|
||||
pause: vi.fn(() => { state.paused = true; }),
|
||||
seek: vi.fn((t: number) => { state.time = t; }),
|
||||
totalTime: vi.fn((t: number) => { state.time = t; }),
|
||||
play: vi.fn(() => {
|
||||
state.paused = false;
|
||||
}),
|
||||
pause: vi.fn(() => {
|
||||
state.paused = true;
|
||||
}),
|
||||
seek: vi.fn((t: number) => {
|
||||
state.time = t;
|
||||
}),
|
||||
totalTime: vi.fn((t: number) => {
|
||||
state.time = t;
|
||||
}),
|
||||
time: vi.fn(() => state.time),
|
||||
duration: vi.fn(() => state.duration),
|
||||
add: vi.fn(),
|
||||
paused: vi.fn((p?: boolean) => { if (p !== undefined) state.paused = p; }),
|
||||
paused: vi.fn((p?: boolean) => {
|
||||
if (p !== undefined) state.paused = p;
|
||||
}),
|
||||
timeScale: vi.fn(),
|
||||
set: vi.fn(),
|
||||
};
|
||||
@@ -25,9 +35,13 @@ function createMockDeps(timeline?: RuntimeTimelineLike | null) {
|
||||
getTimeline: vi.fn(() => timeline ?? null),
|
||||
setTimeline: vi.fn(),
|
||||
getIsPlaying: vi.fn(() => isPlaying),
|
||||
setIsPlaying: vi.fn((v: boolean) => { isPlaying = v; }),
|
||||
setIsPlaying: vi.fn((v: boolean) => {
|
||||
isPlaying = v;
|
||||
}),
|
||||
getPlaybackRate: vi.fn(() => playbackRate),
|
||||
setPlaybackRate: vi.fn((v: number) => { playbackRate = v; }),
|
||||
setPlaybackRate: vi.fn((v: number) => {
|
||||
playbackRate = v;
|
||||
}),
|
||||
getCanonicalFps: vi.fn(() => 30),
|
||||
onSyncMedia: vi.fn(),
|
||||
onStatePost: vi.fn(),
|
||||
|
||||
@@ -40,7 +40,10 @@ export function createRuntimePlayer(deps: PlayerDeps): RuntimePlayer {
|
||||
play: () => {
|
||||
const timeline = deps.getTimeline();
|
||||
if (!timeline || deps.getIsPlaying()) return;
|
||||
const safeDuration = Math.max(0, Number(deps.getSafeDuration?.() ?? timeline.duration() ?? 0) || 0);
|
||||
const safeDuration = Math.max(
|
||||
0,
|
||||
Number(deps.getSafeDuration?.() ?? timeline.duration() ?? 0) || 0,
|
||||
);
|
||||
if (safeDuration > 0) {
|
||||
const currentTime = Math.max(0, Number(timeline.time()) || 0);
|
||||
if (currentTime >= safeDuration) {
|
||||
@@ -87,7 +90,11 @@ export function createRuntimePlayer(deps: PlayerDeps): RuntimePlayer {
|
||||
renderSeek: (timeSeconds: number) => {
|
||||
const timeline = deps.getTimeline();
|
||||
if (!timeline) return;
|
||||
const quantized = seekTimelineDeterministically(timeline, timeSeconds, deps.getCanonicalFps());
|
||||
const quantized = seekTimelineDeterministically(
|
||||
timeline,
|
||||
timeSeconds,
|
||||
deps.getCanonicalFps(),
|
||||
);
|
||||
deps.onDeterministicSeek(quantized);
|
||||
deps.setIsPlaying(false);
|
||||
deps.onSyncMedia(quantized, false);
|
||||
|
||||
@@ -7,8 +7,7 @@ beforeAll(() => {
|
||||
(globalThis as any).CSS = {};
|
||||
}
|
||||
if (typeof CSS.escape !== "function") {
|
||||
CSS.escape = (value: string) =>
|
||||
value.replace(/([^\w-])/g, "\\$1");
|
||||
CSS.escape = (value: string) => value.replace(/([^\w-])/g, "\\$1");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -191,7 +190,16 @@ describe("createRuntimeStartTimeResolver", () => {
|
||||
el.setAttribute("data-composition-id", "comp-1");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const mockTimeline = { duration: () => 12, time: () => 0, play: () => {}, pause: () => {}, seek: () => {}, add: () => {}, paused: () => {}, set: () => {} };
|
||||
const mockTimeline = {
|
||||
duration: () => 12,
|
||||
time: () => 0,
|
||||
play: () => {},
|
||||
pause: () => {},
|
||||
seek: () => {},
|
||||
add: () => {},
|
||||
paused: () => {},
|
||||
set: () => {},
|
||||
};
|
||||
const resolver = createRuntimeStartTimeResolver({
|
||||
timelineRegistry: { "comp-1": mockTimeline as any },
|
||||
});
|
||||
@@ -204,7 +212,16 @@ describe("createRuntimeStartTimeResolver", () => {
|
||||
el.setAttribute("data-duration", "5");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const mockTimeline = { duration: () => 12, time: () => 0, play: () => {}, pause: () => {}, seek: () => {}, add: () => {}, paused: () => {}, set: () => {} };
|
||||
const mockTimeline = {
|
||||
duration: () => 12,
|
||||
time: () => 0,
|
||||
play: () => {},
|
||||
pause: () => {},
|
||||
seek: () => {},
|
||||
add: () => {},
|
||||
paused: () => {},
|
||||
set: () => {},
|
||||
};
|
||||
const resolver = createRuntimeStartTimeResolver({
|
||||
timelineRegistry: { "comp-1": mockTimeline as any },
|
||||
});
|
||||
|
||||
@@ -49,7 +49,10 @@ export function createRuntimeStartTimeResolver(params: {
|
||||
const findReferenceTarget = (refId: string): Element | null => {
|
||||
const byId = document.getElementById(refId);
|
||||
if (byId) return byId;
|
||||
return (document.querySelector(`[data-composition-id="${CSS.escape(refId)}"]`) as Element | null) ?? null;
|
||||
return (
|
||||
(document.querySelector(`[data-composition-id="${CSS.escape(refId)}"]`) as Element | null) ??
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
const resolveDurationForElement = (element: Element): number | null => {
|
||||
|
||||
@@ -194,7 +194,10 @@ describe("collectRuntimeTimelinePayload", () => {
|
||||
clip.setAttribute("data-duration", "5000");
|
||||
root.appendChild(clip);
|
||||
|
||||
const result = collectRuntimeTimelinePayload({ canonicalFps: 30, maxTimelineDurationSeconds: 60 });
|
||||
const result = collectRuntimeTimelinePayload({
|
||||
canonicalFps: 30,
|
||||
maxTimelineDurationSeconds: 60,
|
||||
});
|
||||
expect(result.durationInFrames).toBeLessThanOrEqual(60 * 30);
|
||||
});
|
||||
|
||||
@@ -263,13 +266,31 @@ describe("collectRuntimeTimelinePayload", () => {
|
||||
root.appendChild(comp);
|
||||
|
||||
(window as any).__timelines = {
|
||||
"main": { duration: () => 15, time: () => 0, play: () => {}, pause: () => {}, seek: () => {}, add: () => {}, paused: () => {}, set: () => {} },
|
||||
"scene-1": { duration: () => 8, time: () => 0, play: () => {}, pause: () => {}, seek: () => {}, add: () => {}, paused: () => {}, set: () => {} },
|
||||
main: {
|
||||
duration: () => 15,
|
||||
time: () => 0,
|
||||
play: () => {},
|
||||
pause: () => {},
|
||||
seek: () => {},
|
||||
add: () => {},
|
||||
paused: () => {},
|
||||
set: () => {},
|
||||
},
|
||||
"scene-1": {
|
||||
duration: () => 8,
|
||||
time: () => 0,
|
||||
play: () => {},
|
||||
pause: () => {},
|
||||
seek: () => {},
|
||||
add: () => {},
|
||||
paused: () => {},
|
||||
set: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = collectRuntimeTimelinePayload(defaultParams);
|
||||
// scene-1 should get duration 8 from timeline registry
|
||||
const sceneClip = result.clips.find(c => c.compositionId === "scene-1");
|
||||
const sceneClip = result.clips.find((c) => c.compositionId === "scene-1");
|
||||
expect(sceneClip).toBeDefined();
|
||||
expect(sceneClip?.duration).toBe(8);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { RuntimeTimelineClip, RuntimeTimelineMessage, RuntimeTimelineScene, RuntimeTimelineLike } from "./types";
|
||||
import type {
|
||||
RuntimeTimelineClip,
|
||||
RuntimeTimelineMessage,
|
||||
RuntimeTimelineScene,
|
||||
RuntimeTimelineLike,
|
||||
} from "./types";
|
||||
import { createRuntimeStartTimeResolver } from "./startResolver";
|
||||
|
||||
function parseNum(value: string | null | undefined): number | null {
|
||||
@@ -50,22 +55,26 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const resolveMediaElementDurationSeconds = (mediaEl: HTMLVideoElement | HTMLAudioElement): number | null => {
|
||||
const resolveMediaElementDurationSeconds = (
|
||||
mediaEl: HTMLVideoElement | HTMLAudioElement,
|
||||
): number | null => {
|
||||
const declaredDuration = parseNum(mediaEl.getAttribute("data-duration"));
|
||||
if (declaredDuration != null && declaredDuration > 0) {
|
||||
return declaredDuration;
|
||||
}
|
||||
const playbackStart =
|
||||
parseNum(mediaEl.getAttribute("data-playback-start")) ?? parseNum(mediaEl.getAttribute("data-media-start")) ?? 0;
|
||||
parseNum(mediaEl.getAttribute("data-playback-start")) ??
|
||||
parseNum(mediaEl.getAttribute("data-media-start")) ??
|
||||
0;
|
||||
if (Number.isFinite(mediaEl.duration) && mediaEl.duration > playbackStart) {
|
||||
return Math.max(0, mediaEl.duration - playbackStart);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const resolveMediaWindowEndSeconds = (): number | null => {
|
||||
const mediaNodes = Array.from(document.querySelectorAll("video[data-start], audio[data-start]")) as Array<
|
||||
HTMLVideoElement | HTMLAudioElement
|
||||
>;
|
||||
const mediaNodes = Array.from(
|
||||
document.querySelectorAll("video[data-start], audio[data-start]"),
|
||||
) as Array<HTMLVideoElement | HTMLAudioElement>;
|
||||
if (mediaNodes.length === 0) return null;
|
||||
let maxWindowEndSeconds = 0;
|
||||
for (const mediaNode of mediaNodes) {
|
||||
@@ -137,11 +146,15 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
? rootDurationFromTimeline
|
||||
: null;
|
||||
const attrDurationCandidate =
|
||||
typeof rootDurationFromAttr === "number" && Number.isFinite(rootDurationFromAttr) && rootDurationFromAttr > 0
|
||||
typeof rootDurationFromAttr === "number" &&
|
||||
Number.isFinite(rootDurationFromAttr) &&
|
||||
rootDurationFromAttr > 0
|
||||
? rootDurationFromAttr
|
||||
: null;
|
||||
const mediaWindowDurationCandidate =
|
||||
typeof mediaWindowDuration === "number" && Number.isFinite(mediaWindowDuration) && mediaWindowDuration > 0
|
||||
typeof mediaWindowDuration === "number" &&
|
||||
Number.isFinite(mediaWindowDuration) &&
|
||||
mediaWindowDuration > 0
|
||||
? mediaWindowDuration
|
||||
: null;
|
||||
const timelineLooksLoopInflated =
|
||||
@@ -156,8 +169,11 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
? mediaWindowDurationCandidate
|
||||
: (timelineDurationCandidate ?? mediaWindowDurationCandidate));
|
||||
const rootCompositionDuration =
|
||||
preferredRootDuration != null ? Math.min(preferredRootDuration, params.maxTimelineDurationSeconds) : null;
|
||||
const rootCompositionEnd = rootCompositionDuration != null ? rootCompositionStart + rootCompositionDuration : null;
|
||||
preferredRootDuration != null
|
||||
? Math.min(preferredRootDuration, params.maxTimelineDurationSeconds)
|
||||
: null;
|
||||
const rootCompositionEnd =
|
||||
rootCompositionDuration != null ? rootCompositionStart + rootCompositionDuration : null;
|
||||
const timelineWindowEnd =
|
||||
rootCompositionEnd ??
|
||||
(typeof mediaWindowEnd === "number" && Number.isFinite(mediaWindowEnd) && mediaWindowEnd > 0
|
||||
@@ -177,17 +193,27 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
for (let i = 0; i < nodes.length; i += 1) {
|
||||
const node = nodes[i];
|
||||
if (node === root) continue;
|
||||
if (["SCRIPT", "STYLE", "LINK", "META", "TEMPLATE", "NOSCRIPT"].includes(node.tagName)) continue;
|
||||
if (["SCRIPT", "STYLE", "LINK", "META", "TEMPLATE", "NOSCRIPT"].includes(node.tagName))
|
||||
continue;
|
||||
const compositionContext = resolveNearestCompositionContext(node, root);
|
||||
const start = startResolver.resolveStartForElement(node, compositionContext.inheritedStart ?? 0);
|
||||
const start = startResolver.resolveStartForElement(
|
||||
node,
|
||||
compositionContext.inheritedStart ?? 0,
|
||||
);
|
||||
const nodeCompositionId = node.getAttribute("data-composition-id");
|
||||
let duration = parseNum(node.getAttribute("data-duration"));
|
||||
if ((duration == null || duration <= 0) && nodeCompositionId && nodeCompositionId !== rootCompositionId) {
|
||||
if (
|
||||
(duration == null || duration <= 0) &&
|
||||
nodeCompositionId &&
|
||||
nodeCompositionId !== rootCompositionId
|
||||
) {
|
||||
duration = resolveTimelineDurationSeconds(nodeCompositionId);
|
||||
}
|
||||
if ((duration == null || duration <= 0) && node instanceof HTMLMediaElement) {
|
||||
const mediaStart =
|
||||
parseNum(node.getAttribute("data-playback-start")) ?? parseNum(node.getAttribute("data-media-start")) ?? 0;
|
||||
parseNum(node.getAttribute("data-playback-start")) ??
|
||||
parseNum(node.getAttribute("data-media-start")) ??
|
||||
0;
|
||||
if (Number.isFinite(node.duration) && node.duration > 0) {
|
||||
duration = Math.max(0, node.duration - mediaStart);
|
||||
}
|
||||
@@ -228,7 +254,10 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
start,
|
||||
duration,
|
||||
track:
|
||||
Number.parseInt(node.getAttribute("data-track-index") ?? node.getAttribute("data-track") ?? String(i), 10) || 0,
|
||||
Number.parseInt(
|
||||
node.getAttribute("data-track-index") ?? node.getAttribute("data-track") ?? String(i),
|
||||
10,
|
||||
) || 0,
|
||||
kind,
|
||||
tagName: tag,
|
||||
compositionId: node.getAttribute("data-composition-id"),
|
||||
@@ -250,7 +279,8 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
const start = startResolver.resolveStartForElement(compositionNode, 0);
|
||||
const durationFromAttr = parseNum(compositionNode.getAttribute("data-duration"));
|
||||
const durationFromTimeline = resolveTimelineDurationSeconds(compositionId);
|
||||
const duration = durationFromAttr && durationFromAttr > 0 ? durationFromAttr : durationFromTimeline;
|
||||
const duration =
|
||||
durationFromAttr && durationFromAttr > 0 ? durationFromAttr : durationFromTimeline;
|
||||
if (duration == null || duration <= 0) continue;
|
||||
const clampedDuration = clampDurationToRootWindow(start, duration);
|
||||
if (clampedDuration <= 0) continue;
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
export type RuntimeJson = string | number | boolean | null | RuntimeJson[] | { [key: string]: RuntimeJson };
|
||||
export type RuntimeJson =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| RuntimeJson[]
|
||||
| { [key: string]: RuntimeJson };
|
||||
|
||||
export type RuntimeBridgeControlAction =
|
||||
| "play"
|
||||
|
||||
Reference in New Issue
Block a user