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
+30 -3
View File
@@ -1222,18 +1222,28 @@ export function initSandboxRuntimeModular(): void {
metadataBoundMedia.clear();
};
const isRenderMode = Boolean((window as Record<string, unknown>).__HF_EXPORT_RENDER_SEEK_CONFIG);
const mediaPreloader = createMediaPreloadManager();
const bindMediaMetadataListeners = () => {
if (state.tornDown) return;
const mediaEls = Array.from(document.querySelectorAll("video, audio")) as HTMLMediaElement[];
let newElementsBound = false;
for (const mediaEl of mediaEls) {
if (metadataBoundMedia.has(mediaEl)) continue;
metadataBoundMedia.add(mediaEl);
newElementsBound = true;
mediaEl.addEventListener("loadedmetadata", scheduleMetadataDurationHydration);
mediaEl.addEventListener("durationchange", scheduleMetadataDurationHydration);
}
// Eagerly preload media data so audio/video is buffered before the user
// clicks play. Without this, the first play() call fires on un-fetched
// media, producing silence or choppy audio until the browser caches it.
if (newElementsBound) {
mediaPreloader.refresh();
}
if (!mediaPreloader.isLazy() || isRenderMode) {
for (const mediaEl of mediaEls) {
if (mediaEl.preload !== "auto") {
mediaEl.preload = "auto";
}
@@ -1241,6 +1251,18 @@ export function initSandboxRuntimeModular(): void {
mediaEl.load();
}
}
} else {
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));
}
};
const syncMediaForCurrentState = () => {
@@ -1787,6 +1809,9 @@ export function initSandboxRuntimeModular(): void {
if (clock.isPlaying()) {
syncMediaForCurrentState();
if (mediaPreloader.isLazy() && transportTickCount % 10 === 0) {
mediaPreloader.sync(Math.max(0, state.currentTime || 0));
}
}
postState(false);
} finally {
@@ -1821,6 +1846,7 @@ export function initSandboxRuntimeModular(): void {
player.play = () => {
const tl = state.capturedTimeline;
if (!tl || clock.isPlaying()) return;
mediaPreloader.preloadAroundTime(Math.max(0, state.currentTime || 0));
const dur = getSafeTimelineDurationSeconds(tl, 0);
if (dur > 0) {
clock.setDuration(dur);
@@ -1890,6 +1916,7 @@ export function initSandboxRuntimeModular(): void {
Math.max(0, Number(timeSeconds) || 0),
state.canonicalFps,
);
mediaPreloader.preloadAroundTime(quantized);
webAudio.stopAll();
clock.detachAudioSource();
const wasPlaying = clock.isPlaying();