mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 23:29:50 +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:
@@ -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