fix(video): hold final frame through composition

This commit is contained in:
Miguel Ángel
2026-07-16 02:15:01 +00:00
parent 71d7be7921
commit 2e8f871bc8
15 changed files with 352 additions and 90 deletions
+10 -8
View File
@@ -18,7 +18,7 @@ import {
injectDurations,
extractResolvedMedia,
clampDurations,
shouldClampMediaDuration,
shouldClampResolvedMediaDuration,
CSS_URL_RE,
isNonRelativeUrl,
type ResolvedDuration,
@@ -478,7 +478,8 @@ async function compileHtmlFile(
let compiledHtml =
resolutions.length > 0 ? injectDurations(staticCompiled, resolutions) : staticCompiled;
// Phase 2: Validate pre-resolved media — clamp data-duration to actual source duration (parallel ffprobe)
// Phase 2: Bound authored audio to playable source (parallel ffprobe).
// Explicit video slots may outlive their source and hold the final frame.
const preResolved = extractResolvedMedia(compiledHtml);
const clampResults = await Promise.all(
preResolved
@@ -496,16 +497,17 @@ async function compileHtmlFile(
);
const clampList: ResolvedDuration[] = [];
for (const r of clampResults) {
if (r.maxDuration > 0 && shouldClampMediaDuration(r.duration, r.maxDuration)) {
if (
r.maxDuration > 0 &&
shouldClampResolvedMediaDuration(r.tagName, r.duration, r.maxDuration)
) {
clampList.push({ id: r.id, duration: r.maxDuration });
// This clip's `data-duration` is being silently shortened to its source.
// Surface it so the author can confirm the longer slot wasn't intended.
// ponytail: top-level only — sub-composition clips still get clamped (and
// videos still hold the last frame); thread `log` through
// parseSubCompositions to warn for them too.
const kind = r.tagName === "audio" ? "Audio" : "Video";
// ponytail: top-level only — sub-composition audio still gets clamped;
// thread `log` through parseSubCompositions to warn for it too.
log?.warn(
`[compile] ${kind} "${r.id}" (${r.src}) is ${r.maxDuration.toFixed(2)}s but its ` +
`[compile] Audio "${r.id}" (${r.src}) is ${r.maxDuration.toFixed(2)}s but its ` +
`data-duration is ${r.duration.toFixed(2)}s — the slot is shortened to the media ` +
`length. Set data-duration to ~${r.maxDuration.toFixed(2)}s, trim data-media-start, ` +
`or use a longer/looping source if that isn't intended.`,