mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 01:56:04 +00:00
* fix(engine): resolve relative data-start references in video-frame extraction <video data-start="intro"> (a relative reference to another clip's end) is resolved by the browser runtime but parseVideoElements/parseImageElements did a raw parseFloat, yielding NaN start/end. The FrameLookupTable active-window checks (start <= t <= end) are then always false, so the clip is never injected and composites BLANK in the final render — while lint/validate/inspect/snapshot and the live preview all look fine. The docs' Relative Timing section teaches exactly this pattern on <video>. Share the pure reference-syntax parser (parseStartExpression) out of the runtime resolver into @hyperframes/core, and resolve references in the extractor against the linkedom document it already holds: a reference resolves to the target clip's resolved start + its duration (data-duration or data-end) + offset, mirroring the runtime. Cycle-guarded; an unknown target or unknown duration falls back to the target's start / 0 (never NaN), matching runtime semantics. Natural-media-duration-only targets aren't known at parse time (same limit as the runtime's fallback). parseImageElements gets the same fix. Runtime resolver behavior is unchanged (its 25-case suite still passes). * chore: re-trigger CI to refresh a stuck CodeQL aggregate check
48 lines
2.0 KiB
TypeScript
48 lines
2.0 KiB
TypeScript
/**
|
|
* Pure parser for the `data-start` timing expression grammar, shared by the
|
|
* browser runtime resolver (`createRuntimeStartTimeResolver`) and the Node-side
|
|
* video-frame extractor (`parseVideoElements`) so both agree on exactly what a
|
|
* relative reference means. No DOM/browser dependencies — safe to import in
|
|
* Node.
|
|
*
|
|
* Grammar (matches the docs' "Relative Timing" section):
|
|
* - `"12.5"` -> absolute seconds
|
|
* - `"intro"` -> start when clip `intro` ends
|
|
* - `"intro + 2"` -> 2s after `intro` ends
|
|
* - `"intro - 0.5"` -> 0.5s before `intro` ends (overlap)
|
|
*/
|
|
|
|
export type ReferenceExpression =
|
|
| { kind: "absolute"; value: number }
|
|
| { kind: "reference"; refId: string; offset: number };
|
|
|
|
/** Parse a value to a finite number, or `null` if it isn't one. */
|
|
export function parseNumeric(value: string | null | undefined): number | null {
|
|
if (value == null || value === "") return null;
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
|
|
/**
|
|
* Parse a raw `data-start` value into an absolute time or a clip reference.
|
|
* Returns `null` when the value is empty or not a recognized expression.
|
|
*/
|
|
export function parseStartExpression(raw: string | null | undefined): ReferenceExpression | null {
|
|
const normalized = (raw ?? "").trim();
|
|
if (!normalized) return null;
|
|
const absolute = parseNumeric(normalized);
|
|
if (absolute != null) {
|
|
return { kind: "absolute", value: absolute };
|
|
}
|
|
const referenceMatch = normalized.match(/^([A-Za-z0-9_.:-]+)(?:\s*([+-])\s*([0-9]*\.?[0-9]+))?$/);
|
|
if (!referenceMatch) return null;
|
|
const refId = (referenceMatch[1] ?? "").trim();
|
|
if (!refId) return null;
|
|
const sign = referenceMatch[2] ?? "+";
|
|
const offsetRaw = referenceMatch[3] ?? "0";
|
|
const parsedOffset = Number.parseFloat(offsetRaw);
|
|
const offsetMagnitude = Number.isFinite(parsedOffset) ? Math.max(0, parsedOffset) : 0;
|
|
const offset = sign === "-" ? -offsetMagnitude : offsetMagnitude;
|
|
return { kind: "reference", refId, offset };
|
|
}
|