mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
feat: make creator media edits render-safe (#3322)
* feat: make creator media edits render-safe * fix: align media playback timing * docs: add creator editing recipes * docs: expand creator editing guidance * fix: unify media source offsets * fix: scale natural media duration * fix: preserve natural media zero spans * fix: align compiled natural media timing * test: classify compiler media test as integration * fix: drop inactive media windows * fix: unify literal timing parsing * fix: keep browser media parsing serializable * fix: keep page timing readers strict * fix: close remaining preview timing gaps * fix(core): preserve Studio voice pitch at playback speed * chore: keep creator contract source-neutral
This commit is contained in:
@@ -19,6 +19,23 @@ it("source contains no raw NUL bytes", () => {
|
||||
});
|
||||
|
||||
describe("compileTimingAttrs", () => {
|
||||
it.each(["", " ", "0s", "0abc", "0px", "-1s", "Infinity", "NaN"])(
|
||||
"does not partially parse invalid literal data-duration=%j",
|
||||
(duration) => {
|
||||
const html = `<video id="v1" src="a.mp4" data-start="2" data-duration="${duration}">`;
|
||||
const { html: compiled } = compileTimingAttrs(html);
|
||||
|
||||
expect(compiled).not.toContain("data-end=");
|
||||
},
|
||||
);
|
||||
|
||||
it("uses Number semantics for hexadecimal literal timing", () => {
|
||||
const { html: compiled } = compileTimingAttrs(
|
||||
'<video id="v1" src="a.mp4" data-start="2" data-duration="0x10">',
|
||||
);
|
||||
expect(compiled).toContain('data-end="18"');
|
||||
});
|
||||
|
||||
it("adds data-end when data-start and data-duration are present on a video", () => {
|
||||
const html = '<video id="v1" src="a.mp4" data-start="2" data-duration="5">';
|
||||
const { html: compiled, unresolved } = compileTimingAttrs(html);
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
* and call injectDurations() to complete the compilation.
|
||||
*/
|
||||
|
||||
import {
|
||||
parseStrictFiniteTimingNumber,
|
||||
readElementPlaybackRate,
|
||||
readMediaStart,
|
||||
} from "../runtime/playbackRate.js";
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface UnresolvedElement {
|
||||
@@ -25,6 +31,7 @@ export interface UnresolvedElement {
|
||||
end?: number;
|
||||
duration?: number;
|
||||
mediaStart: number;
|
||||
playbackRate: number;
|
||||
compositionSrc?: string;
|
||||
}
|
||||
|
||||
@@ -40,6 +47,7 @@ export interface ResolvedMediaElement {
|
||||
start: number;
|
||||
duration: number;
|
||||
mediaStart: number;
|
||||
playbackRate: number;
|
||||
loop: boolean;
|
||||
}
|
||||
|
||||
@@ -92,6 +100,11 @@ function injectAttr(tag: string, attr: string, value: string): string {
|
||||
return tag.replace(/>$/, ` ${attr}="${value}">`);
|
||||
}
|
||||
|
||||
function setAttr(tag: string, attr: string, value: string): string {
|
||||
if (!hasAttr(tag, attr)) return injectAttr(tag, attr, value);
|
||||
return tag.replace(new RegExp(`(${attr}=["'])[^"']*(["'])`), `$1${value}$2`);
|
||||
}
|
||||
|
||||
// Real media/timing elements never live inside comments, <script>, or <style>.
|
||||
// The tag regexes below aren't comment-aware, so a comment that merely mentions
|
||||
// `<video>`/`<audio>` gets rewritten as if it were a real element (issue #1938).
|
||||
@@ -137,14 +150,16 @@ function compileTag(
|
||||
startStr = "0";
|
||||
}
|
||||
const start = parseFloat(startStr);
|
||||
const mediaStartStr = getAttr(result, "data-media-start");
|
||||
const mediaStart = mediaStartStr ? parseFloat(mediaStartStr) : 0;
|
||||
const attrReader = { getAttribute: (name: string) => getAttr(result, name) };
|
||||
const mediaStart = readMediaStart(attrReader);
|
||||
const playbackRate = readElementPlaybackRate(attrReader);
|
||||
|
||||
// 1. Compute data-end from data-start + data-duration
|
||||
if (!hasAttr(result, "data-end")) {
|
||||
const durationStr = getAttr(result, "data-duration");
|
||||
if (durationStr !== null) {
|
||||
const end = start + parseFloat(durationStr);
|
||||
const duration = parseStrictFiniteTimingNumber(durationStr);
|
||||
if (duration != null) {
|
||||
const end = start + duration;
|
||||
result = injectAttr(result, "data-end", String(end));
|
||||
} else if (id) {
|
||||
// No data-duration: mark as unresolved so caller can provide it
|
||||
@@ -154,6 +169,7 @@ function compileTag(
|
||||
src: getAttr(result, "src") ?? undefined,
|
||||
start,
|
||||
mediaStart,
|
||||
playbackRate,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -215,6 +231,7 @@ export function compileTimingAttrs(html: string): CompilationResult {
|
||||
tagName: "div",
|
||||
start: startStr ? parseFloat(startStr) : 0,
|
||||
mediaStart: 0,
|
||||
playbackRate: 1,
|
||||
compositionSrc: compositionSrc ?? undefined,
|
||||
});
|
||||
}
|
||||
@@ -241,8 +258,8 @@ export function injectDurations(html: string, resolutions: ResolvedDuration[]):
|
||||
let result = tag;
|
||||
|
||||
// Add data-duration if missing
|
||||
if (!hasAttr(result, "data-duration")) {
|
||||
result = injectAttr(result, "data-duration", String(duration));
|
||||
if (parseStrictFiniteTimingNumber(getAttr(result, "data-duration")) == null) {
|
||||
result = setAttr(result, "data-duration", String(duration));
|
||||
}
|
||||
|
||||
// Add data-end if missing
|
||||
@@ -279,12 +296,12 @@ export function extractResolvedMedia(html: string): ResolvedMediaElement[] {
|
||||
const durationStr = getAttr(tag, "data-duration");
|
||||
if (!id || durationStr === null) continue;
|
||||
|
||||
const duration = parseFloat(durationStr);
|
||||
if (!Number.isFinite(duration) || duration <= 0) continue;
|
||||
const duration = parseStrictFiniteTimingNumber(durationStr);
|
||||
if (duration == null || duration <= 0) continue;
|
||||
|
||||
const isVideo = /^<video/i.test(tag);
|
||||
const startStr = getAttr(tag, "data-start");
|
||||
const mediaStartStr = getAttr(tag, "data-media-start");
|
||||
const attrReader = { getAttribute: (name: string) => getAttr(tag, name) };
|
||||
|
||||
resolved.push({
|
||||
id,
|
||||
@@ -292,7 +309,8 @@ export function extractResolvedMedia(html: string): ResolvedMediaElement[] {
|
||||
src: getAttr(tag, "src") ?? undefined,
|
||||
start: startStr !== null ? parseFloat(startStr) : 0,
|
||||
duration,
|
||||
mediaStart: mediaStartStr ? parseFloat(mediaStartStr) : 0,
|
||||
mediaStart: readMediaStart(attrReader),
|
||||
playbackRate: readElementPlaybackRate(attrReader),
|
||||
loop: hasAttr(tag, "loop"),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user