From 39b588cbd0e196d1d1db14e959f837dc90cf7788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 16 Jul 2026 23:01:39 -0400 Subject: [PATCH] feat(core): swap undecodable video to its proxy at runtime (#2592) * feat(studio-server): serve H.264 proxies from the preview route Wires the codec manifest and the transcoder into the preview surface: the route negotiates a proxy via a query param and serves it through the existing range and ETag machinery, composition HTML carries a codec map for the runtime, and hostile assets pre-warm so a first play does not wait on a cold transcode. Exposes the three subpath exports the CLI surfaces consume upstack. Drops the TEMP fallow entry added with the transcoder: it has real importers now. * fix(studio-server): publish media proxy exports * fix(parsers): scan HTML comments linearly * feat(cli): let projects opt out of automatic proxying Adds media.autoProxy to hyperframes.json plus --proxy/--no-proxy flags, and forwards the resolved value into the studio and preview servers and the vite adapter. Lands before the runtime slice that turns auto-proxying on, so the switch exists before there is any behavior to switch off. * fix(cli): align media config schema * feat(core): swap undecodable video to its proxy at runtime Adds the browser-side half: before first load the runtime consults the injected codec map and swaps a hostile source to its proxy, and if a video still reports zero decodable width it rescues it reactively. An HEVC file carrying AAC fires no error event, so zero videoWidth, not the error event, is the reliable signal. Audio elements and alpha sources are never proxied, render mode never proxies, and each swap evicts the element's stale sync state and reports once. This completes the loop: auto-proxying is live for preview and studio from here. The opt-out (media.autoProxy, --no-proxy) shipped in the previous slice. --- packages/core/src/runtime/init.ts | 31 ++ packages/core/src/runtime/media.ts | 32 +- packages/core/src/runtime/mediaProxy.test.ts | 529 +++++++++++++++++++ packages/core/src/runtime/mediaProxy.ts | 368 +++++++++++++ 4 files changed, 956 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/runtime/mediaProxy.test.ts create mode 100644 packages/core/src/runtime/mediaProxy.ts diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index c6f606012..bac909fd6 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -24,6 +24,7 @@ import { resolveRuntimeMediaClipDuration, syncRuntimeMedia, } from "./media"; +import { handleErrorForProxy, handleMetadataForProxy, maybeProxyProactively } from "./mediaProxy"; import { probeAndCacheElementVolume, type VolumeKeyframe } from "./mediaVolumeEnvelope.js"; import { createPickerModule } from "./picker"; import { createRuntimePlayer, type RuntimePlayerTransport } from "./player"; @@ -1644,10 +1645,29 @@ export function initSandboxRuntimeModular(): void { }, METADATA_REBIND_DEBOUNCE_MS); }; + // Reactive/tertiary undecodable-media triggers (see mediaProxy.ts). Wrapped + // as event listeners here — rather than exported directly — because + // `addEventListener` hands the listener an `Event`, not the element; + // `event.currentTarget` recovers it. Bound/unbound alongside the metadata + // listeners below, reusing `metadataBoundMedia` as the once-per-element + // dedupe (no separate tracking set needed). + const onMediaLoadedMetadataForProxy = (event: Event) => { + if (event.currentTarget instanceof HTMLMediaElement) { + handleMetadataForProxy(event.currentTarget); + } + }; + const onMediaErrorForProxy = (event: Event) => { + if (event.currentTarget instanceof HTMLMediaElement) { + handleErrorForProxy(event.currentTarget); + } + }; + const unbindMediaMetadataListeners = () => { for (const mediaEl of metadataBoundMedia) { mediaEl.removeEventListener("loadedmetadata", scheduleMetadataDurationHydration); mediaEl.removeEventListener("durationchange", scheduleMetadataDurationHydration); + mediaEl.removeEventListener("loadedmetadata", onMediaLoadedMetadataForProxy); + mediaEl.removeEventListener("error", onMediaErrorForProxy); } metadataBoundMedia.clear(); }; @@ -1664,6 +1684,17 @@ export function initSandboxRuntimeModular(): void { } mediaEl.addEventListener("loadedmetadata", scheduleMetadataDurationHydration); mediaEl.addEventListener("durationchange", scheduleMetadataDurationHydration); + // Reactive (zero-videoWidth) + tertiary (error event) proxy-fallback + // triggers. Inert in render mode / when the codec map is absent / + // for