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:
Miguel Ángel
2026-08-18 10:17:02 -04:00
committed by GitHub
parent 049f5618d7
commit afafca4b96
42 changed files with 2695 additions and 307 deletions
+9 -22
View File
@@ -7,6 +7,7 @@ import type {
import { stableClipId } from "./clipTree";
import { swallow } from "./diagnostics";
import { readElementPlaybackRate, readElementPlaybackStart } from "./media";
import { parseStrictFiniteTimingNumber, resolveNaturalMediaTimelineDuration } from "./playbackRate";
import { resolveCssStackingContextId } from "./stackingContext";
import { createRuntimeStartTimeResolver } from "./startResolver";
import { isSceneLikeCompositionId } from "../slideshow/index.js";
@@ -17,9 +18,7 @@ const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
const AUTHORED_END_ATTR = "data-hf-authored-end";
function parseNum(value: string | null | undefined): number | null {
if (value == null || value === "") return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
return parseStrictFiniteTimingNumber(value);
}
function parseElementDurationAttr(element: Element): number | null {
@@ -180,12 +179,8 @@ export function collectRuntimeTimelinePayload(params: {
if (declaredDuration != null && declaredDuration > 0) {
return declaredDuration;
}
const playbackStart =
parseNum(mediaEl.getAttribute("data-playback-start")) ??
parseNum(mediaEl.getAttribute("data-media-start")) ??
0;
if (Number.isFinite(mediaEl.duration) && mediaEl.duration > playbackStart) {
return Math.max(0, (mediaEl.duration - playbackStart) / readElementPlaybackRate(mediaEl));
if (Number.isFinite(mediaEl.duration)) {
return resolveNaturalMediaTimelineDuration(mediaEl, mediaEl.duration);
}
return null;
};
@@ -357,23 +352,15 @@ export function collectRuntimeTimelinePayload(params: {
);
const nodeCompositionId = node.getAttribute("data-composition-id");
let duration = parseElementDurationAttr(node);
if (
(duration == null || duration <= 0) &&
nodeCompositionId &&
nodeCompositionId !== rootCompositionId
) {
if (duration == null && nodeCompositionId && nodeCompositionId !== rootCompositionId) {
duration = resolveTimelineDurationSeconds(nodeCompositionId);
}
if ((duration == null || duration <= 0) && node instanceof HTMLMediaElement) {
const mediaStart =
parseNum(node.getAttribute("data-playback-start")) ??
parseNum(node.getAttribute("data-media-start")) ??
0;
if (Number.isFinite(node.duration) && node.duration > 0) {
duration = Math.max(0, node.duration - mediaStart);
if (duration == null && node instanceof HTMLMediaElement) {
if (Number.isFinite(node.duration)) {
duration = resolveNaturalMediaTimelineDuration(node, node.duration);
}
}
if (duration == null || duration <= 0) {
if (duration == null) {
const inheritedDuration = compositionContext.inheritedDuration;
if (inheritedDuration != null && inheritedDuration > 0) {
const inheritedStart = compositionContext.inheritedStart ?? 0;