From 7c5fd1ddbc314bc27add6710f63cb1d38a5534ea Mon Sep 17 00:00:00 2001 From: dazzatronus Date: Wed, 28 Oct 2026 09:14:47 +1100 Subject: [PATCH] perf: inline html5 images at the size a scaled bake draws them --- src/components/canvas/players/html5-player.ts | 12 +++++++----- tests/html5-player.test.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/canvas/players/html5-player.ts b/src/components/canvas/players/html5-player.ts index b1158fe6..9158d52a 100644 --- a/src/components/canvas/players/html5-player.ts +++ b/src/components/canvas/players/html5-player.ts @@ -291,9 +291,10 @@ export class Html5Player extends Player { // Captured frames are SVG images, which can't load remote files, so ingest files are inlined as data URIs. // Callers await this only when the asset has ingest URLs, so other assets compose without yielding. - private async composeWithIngestFiles(asset: Html5Asset): Promise { + // Every frame decodes its inlined images again, so a scaled bake inlines images sized for its scale. + private async composeWithIngestFiles(asset: Html5Asset, scale: number = 1): Promise { const longestEdge = html5ClipLongestEdge(this.clipConfiguration, this.edit.getResolvedEdit()?.output); - return composeHtml5IframeSrcdoc(await inlineHtml5IngestAssets(asset, longestEdge)); + return composeHtml5IframeSrcdoc(await inlineHtml5IngestAssets(asset, longestEdge * scale)); } private async mountIframe(asset: Html5Asset): Promise { @@ -442,7 +443,7 @@ export class Html5Player extends Player { // An earlier capture may have seeked this iframe, and a seeked animation can leave state // a fresh page lacks (GSAP writes start values inline once a tween has // rendered). The backend captures from a fresh page, so capture does too. - if (!(await this.loadFreshPage(this.iframe, stale))) return null; + if (!(await this.loadFreshPage(this.iframe, stale, scale))) return null; const { frameCount } = computeHtml5FrameCount({ clipLengthSeconds: this.getLength(), @@ -535,8 +536,9 @@ export class Html5Player extends Player { return true; } - private async loadFreshPage(iframe: HTMLIFrameElement, stale: () => boolean): Promise { - const srcdoc = findHtml5IngestUrls(this.asset).length > 0 ? await this.composeWithIngestFiles(this.asset) : composeHtml5IframeSrcdoc(this.asset); + private async loadFreshPage(iframe: HTMLIFrameElement, stale: () => boolean, scale: number = 1): Promise { + const srcdoc = + findHtml5IngestUrls(this.asset).length > 0 ? await this.composeWithIngestFiles(this.asset, scale) : composeHtml5IframeSrcdoc(this.asset); if (stale()) return false; iframe.srcdoc = srcdoc; // eslint-disable-line no-param-reassign -- reloading the iframe it is given is this helper's job await waitForIframeLoad(iframe, undefined, true); diff --git a/tests/html5-player.test.ts b/tests/html5-player.test.ts index 94696d51..16857fa6 100644 --- a/tests/html5-player.test.ts +++ b/tests/html5-player.test.ts @@ -938,6 +938,20 @@ it("keys each bake by its scale", async () => { expect(jest.requireMock("@canvas/players/html5-cache").html5CachePut).toHaveBeenCalledWith("original@0.5", expect.anything()); }); +it("inlines ingest files sized for the bake's scale, and for full size in a still", async () => { + const { player, harness } = createStillPlayer(); + shownAt(harness, 0.46); + canvasLib.findHtml5IngestUrls.mockReturnValue([INGEST_URL]); + canvasLib.inlineHtml5IngestAssets.mockImplementation(async asset => asset); + + await harness.runCapture(); + expect(canvasLib.inlineHtml5IngestAssets).toHaveBeenLastCalledWith(expect.objectContaining({ type: "html5" }), 640); + + await player.prepareStaticRender(); + player.endStaticRender(); + expect(canvasLib.inlineHtml5IngestAssets).toHaveBeenLastCalledWith(expect.objectContaining({ type: "html5" }), 1280); +}); + it("keeps stills and captures at full resolution while a scaled bake exists", async () => { const { player, harness } = createStillPlayer(); Object.assign(harness, { capturedFrames: [new Blob(["baked 0"])], capturedHash: "original@0.5", capturedScale: 0.5 });