mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-12 20:47:30 +00:00
fix: stabilize apple master timeline and playback
This commit is contained in:
@@ -282,9 +282,13 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
// CLI-specific routes (before shared API)
|
||||
app.get("/api/runtime.js", (c) => {
|
||||
const serve = async () => {
|
||||
const runtimeSource = existsSync(runtimePath)
|
||||
? readFileSync(runtimePath, "utf-8")
|
||||
: await loadRuntimeSourceFallback();
|
||||
// Prefer the runtime generated from the current core source over a
|
||||
// potentially stale copied artifact. This keeps local studio/preview
|
||||
// sessions aligned with source edits without requiring a manual
|
||||
// rebuild of the CLI runtime bundle first.
|
||||
const runtimeSource =
|
||||
(await loadRuntimeSourceFallback()) ??
|
||||
(existsSync(runtimePath) ? readFileSync(runtimePath, "utf-8") : null);
|
||||
if (!runtimeSource) return c.text("runtime not available", 404);
|
||||
return c.body(runtimeSource, 200, {
|
||||
"Content-Type": "text/javascript",
|
||||
|
||||
@@ -16,6 +16,9 @@ import { applyCaptionOverrides } from "./captionOverrides";
|
||||
import type { RuntimeDeterministicAdapter, RuntimeJson, RuntimeTimelineLike } from "./types";
|
||||
import type { PlayerAPI } from "../core.types";
|
||||
|
||||
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
||||
const AUTHORED_END_ATTR = "data-hf-authored-end";
|
||||
|
||||
export function initSandboxRuntimeModular(): void {
|
||||
const state = createRuntimeState();
|
||||
const runtimeWindow = window as Window & {
|
||||
@@ -237,7 +240,18 @@ export function initSandboxRuntimeModular(): void {
|
||||
// Preserve explicit root duration so timeline payload can distinguish
|
||||
// authored finite duration from loop-inflated timeline duration.
|
||||
if (rootEl && node === rootEl) continue;
|
||||
// Non-root compositions derive duration from timeline.
|
||||
// Preserve authored timing for reference-start resolution in Studio and
|
||||
// timeline payload generation. The runtime still strips the public attrs
|
||||
// so visibility/parity continues to derive from the live sub-timeline.
|
||||
const authoredDuration = node.getAttribute("data-duration");
|
||||
const authoredEnd = node.getAttribute("data-end");
|
||||
if (authoredDuration != null && !node.hasAttribute(AUTHORED_DURATION_ATTR)) {
|
||||
node.setAttribute(AUTHORED_DURATION_ATTR, authoredDuration);
|
||||
}
|
||||
if (authoredEnd != null && !node.hasAttribute(AUTHORED_END_ATTR)) {
|
||||
node.setAttribute(AUTHORED_END_ATTR, authoredEnd);
|
||||
}
|
||||
// Non-root compositions derive visible duration from timeline.
|
||||
// Strip both data-duration AND data-end so the visibility system
|
||||
// falls back to the GSAP timeline duration (parity with preview).
|
||||
node.removeAttribute("data-duration");
|
||||
|
||||
@@ -135,6 +135,112 @@ describe("createRuntimeStartTimeResolver", () => {
|
||||
expect(resolver.resolveStartForElement(after)).toBe(5);
|
||||
});
|
||||
|
||||
it("uses preserved authored duration when live composition duration was sanitized", () => {
|
||||
const slide1 = document.createElement("div");
|
||||
slide1.id = "slide-1";
|
||||
slide1.setAttribute("data-composition-id", "slide-1");
|
||||
slide1.setAttribute("data-start", "0");
|
||||
slide1.setAttribute("data-hf-authored-duration", "14");
|
||||
document.body.appendChild(slide1);
|
||||
|
||||
const slide2 = document.createElement("div");
|
||||
slide2.id = "slide-2";
|
||||
slide2.setAttribute("data-start", "slide-1");
|
||||
slide2.setAttribute("data-hf-authored-duration", "12");
|
||||
document.body.appendChild(slide2);
|
||||
|
||||
const slide3 = document.createElement("div");
|
||||
slide3.setAttribute("data-start", "slide-2");
|
||||
document.body.appendChild(slide3);
|
||||
|
||||
const resolver = createRuntimeStartTimeResolver({ includeAuthoredTimingAttrs: true });
|
||||
expect(resolver.resolveStartForElement(slide2)).toBe(14);
|
||||
expect(resolver.resolveStartForElement(slide3)).toBe(26);
|
||||
});
|
||||
|
||||
it("adds composition host offset for nested absolute starts", () => {
|
||||
const host = document.createElement("div");
|
||||
host.id = "slide-5";
|
||||
host.setAttribute("data-composition-id", "slide-video-agent");
|
||||
host.setAttribute("data-start", "54");
|
||||
host.setAttribute("data-duration", "45");
|
||||
document.body.appendChild(host);
|
||||
|
||||
const innerRoot = document.createElement("div");
|
||||
innerRoot.setAttribute("data-composition-id", "slide-video-agent");
|
||||
host.appendChild(innerRoot);
|
||||
|
||||
const video = document.createElement("video");
|
||||
video.setAttribute("data-start", "0");
|
||||
innerRoot.appendChild(video);
|
||||
|
||||
const resolver = createRuntimeStartTimeResolver({});
|
||||
expect(resolver.resolveStartForElement(video)).toBe(54);
|
||||
});
|
||||
|
||||
it("keeps nested references in the host composition timeline", () => {
|
||||
const host = document.createElement("div");
|
||||
host.id = "slide-5";
|
||||
host.setAttribute("data-composition-id", "slide-video-agent");
|
||||
host.setAttribute("data-start", "54");
|
||||
host.setAttribute("data-duration", "45");
|
||||
document.body.appendChild(host);
|
||||
|
||||
const innerRoot = document.createElement("div");
|
||||
innerRoot.setAttribute("data-composition-id", "slide-video-agent");
|
||||
host.appendChild(innerRoot);
|
||||
|
||||
const firstClip = document.createElement("div");
|
||||
firstClip.id = "bullet-reveal";
|
||||
firstClip.setAttribute("data-start", "1");
|
||||
firstClip.setAttribute("data-duration", "2");
|
||||
innerRoot.appendChild(firstClip);
|
||||
|
||||
const secondClip = document.createElement("div");
|
||||
secondClip.setAttribute("data-start", "bullet-reveal + 1");
|
||||
innerRoot.appendChild(secondClip);
|
||||
|
||||
const resolver = createRuntimeStartTimeResolver({});
|
||||
expect(resolver.resolveStartForElement(firstClip)).toBe(55);
|
||||
expect(resolver.resolveStartForElement(secondClip)).toBe(58);
|
||||
});
|
||||
|
||||
it("adds the nearest composition root start for nested absolute media in inlined compositions", () => {
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
document.body.appendChild(root);
|
||||
|
||||
const slide1 = document.createElement("div");
|
||||
slide1.id = "slide-1";
|
||||
slide1.setAttribute("data-composition-id", "slide-core-conviction");
|
||||
slide1.setAttribute("data-start", "0");
|
||||
slide1.setAttribute("data-hf-authored-duration", "14");
|
||||
root.appendChild(slide1);
|
||||
|
||||
const slide2 = document.createElement("div");
|
||||
slide2.id = "slide-2";
|
||||
slide2.setAttribute("data-composition-id", "slide-avatar-v");
|
||||
slide2.setAttribute("data-start", "slide-1");
|
||||
slide2.setAttribute("data-hf-authored-duration", "12");
|
||||
root.appendChild(slide2);
|
||||
|
||||
const slide3 = document.createElement("div");
|
||||
slide3.id = "slide-3";
|
||||
slide3.setAttribute("data-composition-id", "slide-translation");
|
||||
slide3.setAttribute("data-start", "slide-2");
|
||||
slide3.setAttribute("data-hf-authored-duration", "16");
|
||||
root.appendChild(slide3);
|
||||
|
||||
const video = document.createElement("video");
|
||||
video.setAttribute("data-start", "0");
|
||||
slide3.appendChild(video);
|
||||
|
||||
const resolver = createRuntimeStartTimeResolver({ includeAuthoredTimingAttrs: true });
|
||||
expect(resolver.resolveStartForElement(slide2)).toBe(14);
|
||||
expect(resolver.resolveStartForElement(slide3)).toBe(26);
|
||||
expect(resolver.resolveStartForElement(video)).toBe(26);
|
||||
});
|
||||
|
||||
it("returns fallback when reference target not found", () => {
|
||||
const el = document.createElement("div");
|
||||
el.setAttribute("data-start", "nonexistent");
|
||||
@@ -228,6 +334,26 @@ describe("createRuntimeStartTimeResolver", () => {
|
||||
expect(resolver.resolveDurationForElement(el)).toBe(5);
|
||||
});
|
||||
|
||||
it("resolves preserved authored duration when runtime stripped the public attr", () => {
|
||||
const el = document.createElement("div");
|
||||
el.setAttribute("data-composition-id", "comp-1");
|
||||
el.setAttribute("data-hf-authored-duration", "9");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const resolver = createRuntimeStartTimeResolver({ includeAuthoredTimingAttrs: true });
|
||||
expect(resolver.resolveDurationForElement(el)).toBe(9);
|
||||
});
|
||||
|
||||
it("ignores preserved authored duration by default", () => {
|
||||
const el = document.createElement("div");
|
||||
el.setAttribute("data-composition-id", "comp-1");
|
||||
el.setAttribute("data-hf-authored-duration", "9");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const resolver = createRuntimeStartTimeResolver({});
|
||||
expect(resolver.resolveDurationForElement(el)).toBeNull();
|
||||
});
|
||||
|
||||
it("caches duration results", () => {
|
||||
const el = document.createElement("div");
|
||||
el.setAttribute("data-duration", "4");
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { RuntimeTimelineLike } from "./types";
|
||||
|
||||
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
||||
const AUTHORED_END_ATTR = "data-hf-authored-end";
|
||||
|
||||
type ReferenceExpression =
|
||||
| {
|
||||
kind: "absolute";
|
||||
@@ -12,10 +15,27 @@ type ReferenceExpression =
|
||||
};
|
||||
|
||||
function parseNumeric(value: string | null | undefined): number | null {
|
||||
if (value == null || value === "") return null;
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function parseDurationAttr(element: Element): number | null {
|
||||
return parseNumeric(element.getAttribute("data-duration"));
|
||||
}
|
||||
|
||||
function parseEndAttr(element: Element): number | null {
|
||||
return parseNumeric(element.getAttribute("data-end"));
|
||||
}
|
||||
|
||||
function parseAuthoredDurationAttr(element: Element): number | null {
|
||||
return parseNumeric(element.getAttribute(AUTHORED_DURATION_ATTR));
|
||||
}
|
||||
|
||||
function parseAuthoredEndAttr(element: Element): number | null {
|
||||
return parseNumeric(element.getAttribute(AUTHORED_END_ATTR));
|
||||
}
|
||||
|
||||
function parseStartExpression(raw: string | null | undefined): ReferenceExpression | null {
|
||||
const normalized = (raw ?? "").trim();
|
||||
if (!normalized) return null;
|
||||
@@ -37,11 +57,13 @@ function parseStartExpression(raw: string | null | undefined): ReferenceExpressi
|
||||
|
||||
export function createRuntimeStartTimeResolver(params: {
|
||||
timelineRegistry?: Record<string, RuntimeTimelineLike | undefined>;
|
||||
includeAuthoredTimingAttrs?: boolean;
|
||||
}): {
|
||||
resolveStartForElement: (element: Element, fallback?: number) => number;
|
||||
resolveDurationForElement: (element: Element) => number | null;
|
||||
} {
|
||||
const timelineRegistry = params.timelineRegistry ?? {};
|
||||
const includeAuthoredTimingAttrs = params.includeAuthoredTimingAttrs ?? false;
|
||||
const startCache = new WeakMap<Element, number | null>();
|
||||
const durationCache = new WeakMap<Element, number | null>();
|
||||
const visiting = new Set<Element>();
|
||||
@@ -59,12 +81,16 @@ export function createRuntimeStartTimeResolver(params: {
|
||||
const cached = durationCache.get(element);
|
||||
if (cached !== undefined) return cached;
|
||||
let resolved: number | null = null;
|
||||
const durationAttr = parseNumeric(element.getAttribute("data-duration"));
|
||||
const durationAttr =
|
||||
parseDurationAttr(element) ??
|
||||
(includeAuthoredTimingAttrs ? parseAuthoredDurationAttr(element) : null);
|
||||
if (durationAttr != null && durationAttr > 0) {
|
||||
resolved = durationAttr;
|
||||
}
|
||||
if (resolved == null || resolved <= 0) {
|
||||
const endAttr = parseNumeric(element.getAttribute("data-end"));
|
||||
const endAttr =
|
||||
parseEndAttr(element) ??
|
||||
(includeAuthoredTimingAttrs ? parseAuthoredEndAttr(element) : null);
|
||||
if (endAttr != null) {
|
||||
const start = resolveStartForElementInternal(element, 0);
|
||||
const delta = endAttr - start;
|
||||
@@ -106,6 +132,17 @@ export function createRuntimeStartTimeResolver(params: {
|
||||
return null;
|
||||
};
|
||||
|
||||
const resolveHostOffsetForElement = (element: Element, fallback: number): number => {
|
||||
if (element.hasAttribute("data-composition-id")) {
|
||||
const parentComposition = element.parentElement?.closest("[data-composition-id]");
|
||||
if (!parentComposition) return 0;
|
||||
return resolveStartForElementInternal(parentComposition, fallback);
|
||||
}
|
||||
const compositionRoot = element.closest("[data-composition-id]");
|
||||
if (!compositionRoot) return 0;
|
||||
return resolveStartForElementInternal(compositionRoot, fallback);
|
||||
};
|
||||
|
||||
const resolveStartForElementInternal = (element: Element, fallback: number): number => {
|
||||
const cached = startCache.get(element);
|
||||
if (cached !== undefined) {
|
||||
@@ -141,8 +178,9 @@ export function createRuntimeStartTimeResolver(params: {
|
||||
}
|
||||
if (expression.kind === "absolute") {
|
||||
const absolute = Math.max(0, expression.value);
|
||||
startCache.set(element, absolute);
|
||||
return absolute;
|
||||
const resolved = Math.max(0, resolveHostOffsetForElement(element, fallback) + absolute);
|
||||
startCache.set(element, resolved);
|
||||
return resolved;
|
||||
}
|
||||
const target = findReferenceTarget(expression.refId);
|
||||
if (!target) {
|
||||
|
||||
@@ -311,6 +311,40 @@ describe("collectRuntimeTimelinePayload", () => {
|
||||
expect(sceneClip?.duration).toBe(8);
|
||||
});
|
||||
|
||||
it("keeps composition clips sequential when authored durations were preserved privately", () => {
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
document.body.appendChild(root);
|
||||
|
||||
const slide1 = document.createElement("div");
|
||||
slide1.id = "slide-1";
|
||||
slide1.setAttribute("data-composition-id", "slide-1");
|
||||
slide1.setAttribute("data-start", "0");
|
||||
slide1.setAttribute("data-hf-authored-duration", "14");
|
||||
root.appendChild(slide1);
|
||||
|
||||
const slide2 = document.createElement("div");
|
||||
slide2.id = "slide-2";
|
||||
slide2.setAttribute("data-composition-id", "slide-2");
|
||||
slide2.setAttribute("data-start", "slide-1");
|
||||
slide2.setAttribute("data-hf-authored-duration", "12");
|
||||
root.appendChild(slide2);
|
||||
|
||||
const slide3 = document.createElement("div");
|
||||
slide3.id = "slide-3";
|
||||
slide3.setAttribute("data-composition-id", "slide-3");
|
||||
slide3.setAttribute("data-start", "slide-2");
|
||||
slide3.setAttribute("data-hf-authored-duration", "16");
|
||||
root.appendChild(slide3);
|
||||
|
||||
const result = collectRuntimeTimelinePayload(defaultParams);
|
||||
const starts = Object.fromEntries(result.clips.map((clip) => [clip.id, clip.start]));
|
||||
expect(starts["slide-1"]).toBe(0);
|
||||
expect(starts["slide-2"]).toBe(14);
|
||||
expect(starts["slide-3"]).toBe(26);
|
||||
expect(result.durationInFrames).toBe(42 * 30);
|
||||
});
|
||||
|
||||
it("discovers GSAP-animated scene elements via timeline introspection", () => {
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
|
||||
@@ -6,11 +6,34 @@ import type {
|
||||
} from "./types";
|
||||
import { createRuntimeStartTimeResolver } from "./startResolver";
|
||||
|
||||
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
||||
const AUTHORED_END_ATTR = "data-hf-authored-end";
|
||||
|
||||
function parseNum(value: string | null | undefined): number | null {
|
||||
if (value == null || value === "") return null;
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function parseElementDurationAttr(element: Element): number | null {
|
||||
return (
|
||||
parseNum(element.getAttribute("data-duration")) ??
|
||||
parseNum(element.getAttribute(AUTHORED_DURATION_ATTR))
|
||||
);
|
||||
}
|
||||
|
||||
function parseElementEndAttr(element: Element): number | null {
|
||||
return (
|
||||
parseNum(element.getAttribute("data-end")) ?? parseNum(element.getAttribute(AUTHORED_END_ATTR))
|
||||
);
|
||||
}
|
||||
|
||||
function maxDefinedNumber(...values: Array<number | null>): number | null {
|
||||
const finite = values.filter((value): value is number => Number.isFinite(value ?? null));
|
||||
if (finite.length === 0) return null;
|
||||
return Math.max(...finite);
|
||||
}
|
||||
|
||||
/**
|
||||
* When multiple content kinds share the same track number, split them
|
||||
* onto separate tracks so the timeline UI shows distinct rows.
|
||||
@@ -97,6 +120,7 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
const timelineRegistry = runtimeWindow.__timelines ?? {};
|
||||
const startResolver = createRuntimeStartTimeResolver({
|
||||
timelineRegistry,
|
||||
includeAuthoredTimingAttrs: true,
|
||||
});
|
||||
const resolveTimelineDurationSeconds = (compositionId: string | null): number | null => {
|
||||
if (!compositionId) return null;
|
||||
@@ -189,13 +213,31 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
};
|
||||
|
||||
const root = document.querySelector("[data-composition-id]") as Element | null;
|
||||
const compositionNodes = Array.from(document.querySelectorAll("[data-composition-id]"));
|
||||
const rootCompositionId = root?.getAttribute("data-composition-id") ?? null;
|
||||
const rootCompositionStart = root ? startResolver.resolveStartForElement(root, 0) : 0;
|
||||
const mediaWindowEnd = resolveMediaWindowEndSeconds();
|
||||
const mediaWindowDuration =
|
||||
mediaWindowEnd != null ? Math.max(0, mediaWindowEnd - Math.max(0, rootCompositionStart)) : null;
|
||||
const rootDurationFromTimeline = resolveTimelineDurationSeconds(rootCompositionId);
|
||||
const rootDurationFromAttr = parseNum(root?.getAttribute("data-duration"));
|
||||
const rootDurationFromAttr = parseElementDurationAttr(root ?? document.body);
|
||||
const compositionWindowEnd = maxDefinedNumber(
|
||||
...compositionNodes
|
||||
.filter((node) => node !== root)
|
||||
.map((node) => {
|
||||
const start = startResolver.resolveStartForElement(node, 0);
|
||||
const duration =
|
||||
startResolver.resolveDurationForElement(node) ??
|
||||
resolveTimelineDurationSeconds(node.getAttribute("data-composition-id")) ??
|
||||
null;
|
||||
if (!Number.isFinite(start) || duration == null || duration <= 0) return null;
|
||||
return Math.max(0, start) + duration;
|
||||
}),
|
||||
);
|
||||
const compositionWindowDuration =
|
||||
compositionWindowEnd != null
|
||||
? Math.max(0, compositionWindowEnd - Math.max(0, rootCompositionStart))
|
||||
: null;
|
||||
const timelineDurationCandidate =
|
||||
typeof rootDurationFromTimeline === "number" &&
|
||||
Number.isFinite(rootDurationFromTimeline) &&
|
||||
@@ -214,17 +256,31 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
mediaWindowDuration > 0
|
||||
? mediaWindowDuration
|
||||
: null;
|
||||
const compositionWindowDurationCandidate =
|
||||
typeof compositionWindowDuration === "number" &&
|
||||
Number.isFinite(compositionWindowDuration) &&
|
||||
compositionWindowDuration > 0
|
||||
? compositionWindowDuration
|
||||
: null;
|
||||
const finiteWindowFloor = maxDefinedNumber(
|
||||
mediaWindowDurationCandidate,
|
||||
compositionWindowDurationCandidate,
|
||||
);
|
||||
const timelineLooksLoopInflated =
|
||||
timelineDurationCandidate != null &&
|
||||
mediaWindowDurationCandidate != null &&
|
||||
timelineDurationCandidate > mediaWindowDurationCandidate + 1;
|
||||
finiteWindowFloor != null &&
|
||||
timelineDurationCandidate > finiteWindowFloor + 1;
|
||||
// Prefer explicit authored root duration first.
|
||||
// If absent, guard against loop-inflated GSAP durations by trusting finite media window.
|
||||
const preferredRootDuration =
|
||||
attrDurationCandidate ??
|
||||
(timelineLooksLoopInflated
|
||||
? mediaWindowDurationCandidate
|
||||
: (timelineDurationCandidate ?? mediaWindowDurationCandidate));
|
||||
? finiteWindowFloor
|
||||
: maxDefinedNumber(
|
||||
timelineDurationCandidate,
|
||||
mediaWindowDurationCandidate,
|
||||
compositionWindowDurationCandidate,
|
||||
));
|
||||
const rootCompositionDuration =
|
||||
preferredRootDuration != null
|
||||
? Math.min(preferredRootDuration, params.maxTimelineDurationSeconds)
|
||||
@@ -242,7 +298,6 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
if (!Number.isFinite(start) || start >= timelineWindowEnd) return 0;
|
||||
return Math.max(0, Math.min(duration, timelineWindowEnd - start));
|
||||
};
|
||||
const compositionNodes = Array.from(document.querySelectorAll("[data-composition-id]"));
|
||||
const clips: RuntimeTimelineClip[] = [];
|
||||
const scenes: RuntimeTimelineScene[] = [];
|
||||
// Only collect elements that are explicitly part of the timeline:
|
||||
@@ -270,7 +325,7 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
compositionContext.inheritedStart ?? 0,
|
||||
);
|
||||
const nodeCompositionId = node.getAttribute("data-composition-id");
|
||||
let duration = parseNum(node.getAttribute("data-duration"));
|
||||
let duration = parseElementDurationAttr(node);
|
||||
if (
|
||||
(duration == null || duration <= 0) &&
|
||||
nodeCompositionId &&
|
||||
@@ -523,7 +578,14 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
const compositionId = compositionNode.getAttribute("data-composition-id");
|
||||
if (!compositionId || !isSceneLikeCompositionId(compositionId)) continue;
|
||||
const start = startResolver.resolveStartForElement(compositionNode, 0);
|
||||
const durationFromAttr = parseNum(compositionNode.getAttribute("data-duration"));
|
||||
let durationFromAttr = parseElementDurationAttr(compositionNode);
|
||||
if (
|
||||
(durationFromAttr == null || durationFromAttr <= 0) &&
|
||||
parseElementEndAttr(compositionNode) != null
|
||||
) {
|
||||
const end = parseElementEndAttr(compositionNode)!;
|
||||
durationFromAttr = Math.max(0, end - start);
|
||||
}
|
||||
const durationFromTimeline = resolveTimelineDurationSeconds(compositionId);
|
||||
const duration =
|
||||
durationFromAttr && durationFromAttr > 0 ? durationFromAttr : durationFromTimeline;
|
||||
|
||||
@@ -370,16 +370,17 @@ function devProjectApi(): Plugin {
|
||||
return _api;
|
||||
};
|
||||
|
||||
// Serve the local runtime IIFE so compositions don't depend on CDN
|
||||
// In dev, prefer the runtime built from source over a checked-in dist
|
||||
// artifact. Otherwise Studio can silently serve a stale runtime bundle
|
||||
// after source edits in packages/core, which makes browser behavior lag
|
||||
// behind the code under test until someone manually rebuilds core/dist.
|
||||
const runtimePath = resolve(__dirname, "../core/dist/hyperframe.runtime.iife.js");
|
||||
server.middlewares.use((req, res, next) => {
|
||||
if (req.url !== "/api/runtime.js") return next();
|
||||
const serve = async () => {
|
||||
let runtimeSource: string | null = null;
|
||||
if (existsSync(runtimePath)) {
|
||||
let runtimeSource = await loadRuntimeSourceForDev(server);
|
||||
if (!runtimeSource && existsSync(runtimePath)) {
|
||||
runtimeSource = readFileSync(runtimePath, "utf-8");
|
||||
} else {
|
||||
runtimeSource = await loadRuntimeSourceForDev(server);
|
||||
}
|
||||
|
||||
if (!runtimeSource) {
|
||||
|
||||
Reference in New Issue
Block a user