diff --git a/packages/core/src/compiler/timingCompiler.test.ts b/packages/core/src/compiler/timingCompiler.test.ts
index b9ec7bcc02..30f5bbc01c 100644
--- a/packages/core/src/compiler/timingCompiler.test.ts
+++ b/packages/core/src/compiler/timingCompiler.test.ts
@@ -468,3 +468,29 @@ describe("shouldClampResolvedMediaDuration", () => {
expect(shouldClampResolvedMediaDuration("audio", 5, 1)).toBe(true);
});
});
+
+describe("rate lane in compiled media", () => {
+ const lane = {
+ version: 1,
+ lanes: [
+ {
+ target: "rate",
+ points: [
+ { t: 0, v: 1 },
+ { t: 2, v: 3 },
+ ],
+ },
+ ],
+ };
+ const encodings = [
+ ["single-quoted JSON", `data-automation='${JSON.stringify(lane)}'`],
+ ["entity-encoded JSON", `data-automation="${JSON.stringify(lane).replace(/"/g, """)}"`],
+ ];
+
+ it.each(encodings)("hands a %s rate lane to the unresolved element", (_name, attr) => {
+ const { unresolved } = compileTimingAttrs(
+ ``,
+ );
+ expect(unresolved[0]?.playbackRate).toMatchObject({ target: "rate" });
+ });
+});
diff --git a/packages/core/src/compiler/timingCompiler.ts b/packages/core/src/compiler/timingCompiler.ts
index 30c0525cc3..2874a45190 100644
--- a/packages/core/src/compiler/timingCompiler.ts
+++ b/packages/core/src/compiler/timingCompiler.ts
@@ -21,9 +21,10 @@
import { parseNumeric } from "@hyperframes/parsers/composition-contract";
import {
parseStrictFiniteTimingNumber,
- readElementPlaybackRate,
+ readElementRateSpec,
readMediaStart,
} from "../runtime/playbackRate.js";
+import type { RateSpec } from "../speedRamp.js";
// ── Types ────────────────────────────────────────────────────────────────
export interface UnresolvedElement {
@@ -34,7 +35,7 @@ export interface UnresolvedElement {
end?: number;
duration?: number;
mediaStart: number;
- playbackRate: number;
+ playbackRate: RateSpec;
compositionSrc?: string;
}
@@ -50,7 +51,7 @@ export interface ResolvedMediaElement {
start: number;
duration: number;
mediaStart: number;
- playbackRate: number;
+ playbackRate: RateSpec;
loop: boolean;
}
@@ -91,8 +92,25 @@ function getAttr(tag: string, attr: string): string | null {
// made compileTag believe a Studio-stamped `data-hf-id`-only element already
// had an `id`, so it skipped its `hf-video-N` injection — leaving the element
// with no real `el.id`, which the render pipeline keys off of (blank wash).
- const match = tag.match(new RegExp(`(? {
+ return {
+ getAttribute: (name) => {
+ const raw = getAttr(tag, name);
+ return raw && name === "data-automation"
+ ? raw
+ .replace(/"/g, '"')
+ .replace(/'/g, "'")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/&/g, "&")
+ : raw;
+ },
+ };
}
function hasAttr(tag: string, attr: string): boolean {
@@ -228,9 +246,9 @@ function compileTag(
startStr = "0";
}
const start = parseNumeric(startStr);
- const attrReader = { getAttribute: (name: string) => getAttr(result, name) };
+ const attrReader = tagAttrReader(result);
const mediaStart = readMediaStart(attrReader);
- const playbackRate = readElementPlaybackRate(attrReader);
+ const playbackRate = readElementRateSpec(attrReader);
// 1. Compute data-end from data-start + data-duration. Skip relative id-refs.
if (!hasAttr(result, "data-end")) {
@@ -374,7 +392,7 @@ export function extractResolvedMedia(html: string): ResolvedMediaElement[] {
const isVideo = /^