feat(core): integrate media preloader into runtime and studio

Wire the MediaPreloadManager into init.ts:
- Detect render mode via __HF_EXPORT_RENDER_SEEK_CONFIG (keeps eager preload)
- Gate bindMediaMetadataListeners: lazy mode sets preload="metadata",
  eager mode keeps preload="auto" (unchanged for small compositions)
- Advance preload window in the timeline poll tick loop
- Call preloadAroundTime on seek for instant buffering at seek target

Studio Player.tsx: hasUnloadedAssets now skips elements with
preload!="auto" so deferred clips don't block the loading overlay.
This commit is contained in:
Miguel Ángel
2026-05-11 18:44:58 -07:00
parent 773c4261e2
commit 372da1cd28
+34 -7
View File
@@ -1222,24 +1222,46 @@ export function initSandboxRuntimeModular(): void {
metadataBoundMedia.clear(); metadataBoundMedia.clear();
}; };
const isRenderMode = Boolean((window as Record<string, unknown>).__HF_EXPORT_RENDER_SEEK_CONFIG);
const mediaPreloader = createMediaPreloadManager();
const bindMediaMetadataListeners = () => { const bindMediaMetadataListeners = () => {
if (state.tornDown) return; if (state.tornDown) return;
const mediaEls = Array.from(document.querySelectorAll("video, audio")) as HTMLMediaElement[]; const mediaEls = Array.from(document.querySelectorAll("video, audio")) as HTMLMediaElement[];
let newElementsBound = false;
for (const mediaEl of mediaEls) { for (const mediaEl of mediaEls) {
if (metadataBoundMedia.has(mediaEl)) continue; if (metadataBoundMedia.has(mediaEl)) continue;
metadataBoundMedia.add(mediaEl); metadataBoundMedia.add(mediaEl);
newElementsBound = true;
mediaEl.addEventListener("loadedmetadata", scheduleMetadataDurationHydration); mediaEl.addEventListener("loadedmetadata", scheduleMetadataDurationHydration);
mediaEl.addEventListener("durationchange", scheduleMetadataDurationHydration); mediaEl.addEventListener("durationchange", scheduleMetadataDurationHydration);
}
// Eagerly preload media data so audio/video is buffered before the user if (newElementsBound) {
// clicks play. Without this, the first play() call fires on un-fetched mediaPreloader.refresh();
// media, producing silence or choppy audio until the browser caches it. }
if (mediaEl.preload !== "auto") {
mediaEl.preload = "auto"; if (!mediaPreloader.isLazy() || isRenderMode) {
for (const mediaEl of mediaEls) {
if (mediaEl.preload !== "auto") {
mediaEl.preload = "auto";
}
if (mediaEl.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) {
mediaEl.load();
}
} }
if (mediaEl.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) { } else {
mediaEl.load(); for (const mediaEl of mediaEls) {
if (!mediaEl.hasAttribute("data-start")) continue;
if (mediaEl.preload === "auto" || mediaEl.preload === "") {
mediaEl.preload = "metadata";
}
if (mediaEl.readyState < HTMLMediaElement.HAVE_METADATA) {
mediaEl.load();
}
} }
mediaPreloader.preloadAroundTime(Math.max(0, state.currentTime || 0));
} }
}; };
@@ -1787,6 +1809,9 @@ export function initSandboxRuntimeModular(): void {
if (clock.isPlaying()) { if (clock.isPlaying()) {
syncMediaForCurrentState(); syncMediaForCurrentState();
if (mediaPreloader.isLazy() && transportTickCount % 10 === 0) {
mediaPreloader.sync(Math.max(0, state.currentTime || 0));
}
} }
postState(false); postState(false);
} finally { } finally {
@@ -1821,6 +1846,7 @@ export function initSandboxRuntimeModular(): void {
player.play = () => { player.play = () => {
const tl = state.capturedTimeline; const tl = state.capturedTimeline;
if (!tl || clock.isPlaying()) return; if (!tl || clock.isPlaying()) return;
mediaPreloader.preloadAroundTime(Math.max(0, state.currentTime || 0));
const dur = getSafeTimelineDurationSeconds(tl, 0); const dur = getSafeTimelineDurationSeconds(tl, 0);
if (dur > 0) { if (dur > 0) {
clock.setDuration(dur); clock.setDuration(dur);
@@ -1890,6 +1916,7 @@ export function initSandboxRuntimeModular(): void {
Math.max(0, Number(timeSeconds) || 0), Math.max(0, Number(timeSeconds) || 0),
state.canonicalFps, state.canonicalFps,
); );
mediaPreloader.preloadAroundTime(quantized);
webAudio.stopAll(); webAudio.stopAll();
clock.detachAudioSource(); clock.detachAudioSource();
const wasPlaying = clock.isPlaying(); const wasPlaying = clock.isPlaying();