MCPcopy Create free account
hub / github.com/heygen-com/hyperframes / compileHtmlFile

Function compileHtmlFile

packages/producer/src/services/htmlCompiler.ts:340–428  ·  view source on GitHub ↗

* Compile a single HTML file: static pass + ffprobe for unresolved media. * Returns compiled HTML and any unresolved composition elements that need browser resolution.

(
  html: string,
  baseDir: string,
  downloadDir: string,
  log?: ProducerLogger,
)

Source from the content-addressed store, hash-verified

338 * Returns compiled HTML and any unresolved composition elements that need browser resolution.
339 */
340async function compileHtmlFile(
341 html: string,
342 baseDir: string,
343 downloadDir: string,
344 log?: ProducerLogger,
345): Promise<{ html: string; unresolvedCompositions: UnresolvedElement[] }> {
346 const { html: staticCompiled, unresolved } = compileTimingAttrs(html);
347
348 const mediaUnresolved = unresolved.filter(
349 (el) => (el.tagName === "video" || el.tagName === "audio") && el.src,
350 );
351
352 const unresolvedCompositions = unresolved.filter((el) => el.tagName === "div");
353
354 // Phase 1: Resolve missing durations (parallel ffprobe)
355 const resolvedResults = await Promise.all(
356 mediaUnresolved.map((el) =>
357 resolveMediaDuration(el.src!, el.mediaStart, baseDir, downloadDir, el.tagName).then(
358 ({ duration }) => ({ id: el.id, duration }),
359 ),
360 ),
361 );
362 const resolutions: ResolvedDuration[] = resolvedResults.filter((r) => r.duration > 0);
363
364 let compiledHtml =
365 resolutions.length > 0 ? injectDurations(staticCompiled, resolutions) : staticCompiled;
366
367 // Phase 2: Validate pre-resolved media — clamp data-duration to actual source duration (parallel ffprobe)
368 const preResolved = extractResolvedMedia(compiledHtml);
369 const clampResults = await Promise.all(
370 preResolved
371 .filter((el) => !!el.src && !el.loop)
372 .map(async (el) => {
373 const { duration: maxDuration } = await resolveMediaDuration(
374 el.src!,
375 el.mediaStart,
376 baseDir,
377 downloadDir,
378 el.tagName,
379 );
380 return { id: el.id, tagName: el.tagName, duration: el.duration, maxDuration, src: el.src! };
381 }),
382 );
383 const clampList: ResolvedDuration[] = [];
384 for (const r of clampResults) {
385 if (r.maxDuration > 0 && shouldClampMediaDuration(r.duration, r.maxDuration)) {
386 clampList.push({ id: r.id, duration: r.maxDuration });
387 // This clip's `data-duration` is being silently shortened to its source.
388 // Surface it so the author can confirm the longer slot wasn't intended.
389 // ponytail: top-level only — sub-composition clips still get clamped (and
390 // videos still hold the last frame); thread `log` through
391 // parseSubCompositions to warn for them too.
392 const kind = r.tagName === "audio" ? "Audio" : "Video";
393 log?.warn(
394 `[compile] ${kind} "${r.id}" (${r.src}) is ${r.maxDuration.toFixed(2)}s but its ` +
395 `data-duration is ${r.duration.toFixed(2)}s — the slot is shortened to the media ` +
396 `length. Set data-duration to ~${r.maxDuration.toFixed(2)}s, trim data-media-start, ` +
397 `or use a longer/looping source if that isn't intended.`,

Callers 2

parseSubCompositionsFunction · 0.85
compileForRenderFunction · 0.85

Calls 7

compileTimingAttrsFunction · 0.90
injectDurationsFunction · 0.90
extractResolvedMediaFunction · 0.90
shouldClampMediaDurationFunction · 0.90
clampDurationsFunction · 0.90
resolveMediaDurationFunction · 0.85
warnMethod · 0.80

Tested by

no test coverage detected