( rawHtml: string, projectDir: string, probeMediaDuration?: MediaDurationProber, )
| 30 | * @param probeMediaDuration - Optional callback to probe media duration (e.g., via ffprobe) |
| 31 | */ |
| 32 | export async function compileHtml( |
| 33 | rawHtml: string, |
| 34 | projectDir: string, |
| 35 | probeMediaDuration?: MediaDurationProber, |
| 36 | ): Promise<string> { |
| 37 | const { html: staticCompiled, unresolved } = compileTimingAttrs(rawHtml); |
| 38 | let html = staticCompiled; |
| 39 | |
| 40 | if (!probeMediaDuration) return html; |
| 41 | |
| 42 | // Phase 1: Resolve missing durations |
| 43 | const mediaUnresolved = unresolved.filter( |
| 44 | (el) => el.tagName === "video" || el.tagName === "audio", |
| 45 | ); |
| 46 | |
| 47 | if (mediaUnresolved.length > 0) { |
| 48 | const resolutions: ResolvedDuration[] = []; |
| 49 | |
| 50 | for (const el of mediaUnresolved) { |
| 51 | if (!el.src) continue; |
| 52 | const src = resolveMediaSrc(el.src, projectDir); |
| 53 | const fileDuration = await probeMediaDuration(src); |
| 54 | if (fileDuration <= 0) continue; |
| 55 | |
| 56 | const effectiveDuration = fileDuration - el.mediaStart; |
| 57 | resolutions.push({ |
| 58 | id: el.id, |
| 59 | duration: effectiveDuration > 0 ? effectiveDuration : fileDuration, |
| 60 | }); |
| 61 | } |
| 62 | |
| 63 | if (resolutions.length > 0) { |
| 64 | html = injectDurations(html, resolutions); |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | // Phase 2: Validate pre-resolved media — clamp data-duration to actual source duration |
| 69 | const preResolved = extractResolvedMedia(html); |
| 70 | const clampList: ResolvedDuration[] = []; |
| 71 | |
| 72 | for (const el of preResolved) { |
| 73 | if (!el.src) continue; |
| 74 | if (el.loop) continue; |
| 75 | const src = resolveMediaSrc(el.src, projectDir); |
| 76 | const fileDuration = await probeMediaDuration(src); |
| 77 | if (fileDuration <= 0) continue; |
| 78 | |
| 79 | const maxDuration = fileDuration - el.mediaStart; |
| 80 | if (maxDuration > 0 && shouldClampMediaDuration(el.duration, maxDuration)) { |
| 81 | clampList.push({ id: el.id, duration: maxDuration }); |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | if (clampList.length > 0) { |
| 86 | html = clampDurations(html, clampList); |
| 87 | } |
| 88 | |
| 89 | return html; |
no test coverage detected