mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(core): retiming a parked scene in the preview takes effect without a reload (#6522)
* fix(core): retiming a parked scene in the preview takes effect without a reload * fix(core): an edit that leaves a parked scene without a start or length in seconds reloads instead
This commit is contained in:
@@ -81,6 +81,7 @@ interface Scene {
|
||||
hash: string;
|
||||
extraAttrs?: string;
|
||||
script?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
function preview(scenes: Scene[], shared = "s1", sharedMarkup = "") {
|
||||
@@ -92,11 +93,11 @@ function preview(scenes: Scene[], shared = "s1", sharedMarkup = "") {
|
||||
`<meta name="hf-scene-parts" content="${manifest}"><style>.shared{}</style>` +
|
||||
scenes.map((s) => `<style data-hf-scene="${s.id}">${s.css}</style>`).join("");
|
||||
const body =
|
||||
`<div data-composition-id="main" data-root="true" data-start="0" data-duration="${Math.max(6, ...scenes.map((s) => s.start + 2))}">${sharedMarkup}` +
|
||||
`<div data-composition-id="main" data-root="true" data-start="0" data-duration="${Math.max(6, ...scenes.map((s) => s.start + (s.duration ?? 2)))}">${sharedMarkup}` +
|
||||
scenes
|
||||
.map(
|
||||
(s) =>
|
||||
`<div data-composition-id="${s.id}" data-hf-scene="${s.id}" data-start="${s.start}" data-duration="2"${s.extraAttrs ?? ""}>${s.body}</div>`,
|
||||
`<div data-composition-id="${s.id}" data-hf-scene="${s.id}" data-start="${s.start}" data-duration="${s.duration ?? 2}"${s.extraAttrs ?? ""}>${s.body}</div>`,
|
||||
)
|
||||
.join("") +
|
||||
`</div>` +
|
||||
@@ -1949,6 +1950,22 @@ describe("the preview's scene window", () => {
|
||||
expect(sceneHost("s2").textContent).toBe("two, edited");
|
||||
});
|
||||
|
||||
it("retimes a parked scene an edit moves, for when it mounts and as it mounts", async () => {
|
||||
bootWindowed([S0, S1, S2], trackingRoot().root);
|
||||
await tick();
|
||||
// From 4 s to 1 s, and 3 s long: paused at 1.5 it plays, where before it was not even next.
|
||||
const moved: Scene = { ...S2, start: 1, duration: 3, hash: "h2b" };
|
||||
await window.__hfSwapScenes!(preview([S0, S1, moved]).html);
|
||||
expect(parked()).toEqual(["s2"]);
|
||||
await seekAndSettle(1.5);
|
||||
const host = sceneHost("s2");
|
||||
expect([
|
||||
host.textContent,
|
||||
host.getAttribute("data-start"),
|
||||
host.getAttribute("data-hf-authored-duration"),
|
||||
]).toEqual(["two", "1", "3"]);
|
||||
});
|
||||
|
||||
it("still mounts parked scenes after an edit to a mounted one", async () => {
|
||||
bootWindowed([S0, S1, S2], trackingRoot().root);
|
||||
await tick();
|
||||
@@ -1972,7 +1989,10 @@ describe("the preview's scene window", () => {
|
||||
body: '<audio src="/two.mp3" data-start="0"></audio>',
|
||||
hash: "h2c",
|
||||
};
|
||||
for (const edit of [fetching, untrimmed])
|
||||
// The window times a parked scene by its host's own seconds: a reference start or no length has none.
|
||||
const referenceStart: Scene = { ...S2, start: "s1" as unknown as number, hash: "h2d" };
|
||||
const noLength: Scene = { ...S2, duration: 0, hash: "h2e" };
|
||||
for (const edit of [fetching, untrimmed, referenceStart, noLength])
|
||||
await expect(window.__hfSwapScenes!(preview([S0, S1, edit]).html)).rejects.toThrow("s2");
|
||||
await seekAndSettle(5);
|
||||
expect(sceneHost("s2").textContent).toBe("two");
|
||||
|
||||
@@ -3,6 +3,7 @@ import { setPreviewRasterScale } from "./previewRasterHints";
|
||||
import { createFarSceneSkip } from "./farScenes";
|
||||
import {
|
||||
clipLengthsWritten,
|
||||
ownWindow,
|
||||
hostOf as sceneHostOf,
|
||||
isParked,
|
||||
parkedClips,
|
||||
@@ -4288,6 +4289,8 @@ export function initSandboxRuntimeModular(): void {
|
||||
if (refusal !== null) throw new Error(`scene ${name} cannot be swapped: ${refusal}`);
|
||||
if (!clipLengthsWritten(host))
|
||||
throw new Error(`scene ${name} cannot be swapped: a clip's length is not written`);
|
||||
if (!ownWindow(host))
|
||||
throw new Error(`scene ${name} cannot be swapped: its start or length is not in seconds`);
|
||||
const oldStyles = styles(partsOf(document, name));
|
||||
if (oldStyles.length !== styles(parts).length)
|
||||
throw new Error(`scene ${name} cannot be swapped: its styles moved`);
|
||||
@@ -4308,6 +4311,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
document
|
||||
.querySelector(`meta[name="${SCENE_PARTS_META}"]`)
|
||||
?.setAttribute("content", JSON.stringify(nextParts));
|
||||
windowScenes = windowedScenes();
|
||||
postTimeline();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SCENE_NO_SWAP_ATTR, SCENE_PART_ATTR, SCENE_PARTS_META } from "../sceneParts";
|
||||
import { STUDIO_PREVIEW_PARKED_SCENE_ATTR as PARKED } from "../studioPreviewMark";
|
||||
import { AUTHORED_DURATION_ATTR, AUTHORED_END_ATTR } from "./authoredTiming";
|
||||
import { parseElementDurationAttr, type ParkedTimelineScene } from "./timeline";
|
||||
import type { RuntimeTimelineClip } from "./types";
|
||||
|
||||
@@ -32,15 +33,19 @@ const parkable = (host: Element): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
/** A host's window when its own attributes give it in seconds; a reference start or no length gives none. */
|
||||
export function ownWindow(host: Element): { start: number; end: number } | null {
|
||||
const start = Number(host.getAttribute("data-start")?.trim() || Number.NaN);
|
||||
const duration = ownDuration(host);
|
||||
return Number.isFinite(start) && duration > 0 ? { start, end: start + duration } : null;
|
||||
}
|
||||
|
||||
function windowedHosts(): Array<WindowedScene & { host: Element }> {
|
||||
if (!document.querySelector(`meta[name="${SCENE_PARTS_META}"]`)) return [];
|
||||
return Array.from(document.querySelectorAll(`[${SCENE_PART_ATTR}]:not(style, script)`))
|
||||
.flatMap((host) => {
|
||||
const start = Number(host.getAttribute("data-start")?.trim() || Number.NaN);
|
||||
const duration = ownDuration(host);
|
||||
const name = host.getAttribute(SCENE_PART_ATTR)!;
|
||||
const windowed = Number.isFinite(start) && duration > 0 && parkable(host);
|
||||
return windowed ? [{ name, host, start, end: start + duration }] : [];
|
||||
const own = parkable(host) ? ownWindow(host) : null;
|
||||
return own ? [{ name: host.getAttribute(SCENE_PART_ATTR)!, host, ...own }] : [];
|
||||
})
|
||||
.sort((a, b) => a.start - b.start);
|
||||
}
|
||||
@@ -115,10 +120,38 @@ export function parkFarScenesAtBoot(time = 0): void {
|
||||
}
|
||||
}
|
||||
|
||||
// The runtime keeps a host's authored length under its own name, which an edit to the length replaces.
|
||||
const AUTHORED_COPY: Record<string, string> = {
|
||||
"data-duration": AUTHORED_DURATION_ATTR,
|
||||
"data-end": AUTHORED_END_ATTR,
|
||||
};
|
||||
|
||||
/** A parked scene is timed by its stand-in: it takes the host attributes an edit changed, keeping the runtime's. */
|
||||
function retimeParked(placeholder: Element, before: Element, after: Element): void {
|
||||
const changed = new Set(
|
||||
[...before.getAttributeNames(), ...after.getAttributeNames()].filter(
|
||||
(attr) => before.getAttribute(attr) !== after.getAttribute(attr),
|
||||
),
|
||||
);
|
||||
for (const attr of changed) {
|
||||
const value = after.getAttribute(attr);
|
||||
if (value === null) placeholder.removeAttribute(attr);
|
||||
else placeholder.setAttribute(attr, value);
|
||||
if (attr in AUTHORED_COPY) placeholder.removeAttribute(AUTHORED_COPY[attr]!);
|
||||
}
|
||||
}
|
||||
|
||||
/** Forgets what a swap replaced: the swapped scene's new markup is what a later mount puts back. */
|
||||
export function rememberScene(name: string, host: Element, scripts: Element[]): void {
|
||||
if (!pristine.has(name)) return;
|
||||
const old = pristine.get(name);
|
||||
if (!old) return;
|
||||
parkedClips.delete(name);
|
||||
const placeholder = hostOf(name);
|
||||
if (placeholder && isParked(placeholder)) {
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = old.html;
|
||||
retimeParked(placeholder, template.content.firstElementChild!, host);
|
||||
}
|
||||
pristine.set(name, {
|
||||
html: host.outerHTML,
|
||||
scripts: scripts.map((el) => el.cloneNode(true) as Element),
|
||||
|
||||
Reference in New Issue
Block a user