From 7806fd570c04f8ab7fcc00b1e5d46eab7bd98197 Mon Sep 17 00:00:00 2001 From: dazzatronus Date: Mon, 26 Oct 2026 21:45:55 +1100 Subject: [PATCH] fix: capture html5 clips animated only by their markup --- src/components/canvas/players/html5-player.ts | 10 +++++- tests/html5-player.test.ts | 34 +++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/components/canvas/players/html5-player.ts b/src/components/canvas/players/html5-player.ts index 86097065..9e71ad7e 100644 --- a/src/components/canvas/players/html5-player.ts +++ b/src/components/canvas/players/html5-player.ts @@ -164,6 +164,7 @@ export class Html5Player extends Player { const { asset, captureFps } = this; const { frameCount } = computeHtml5FrameCount({ clipLengthSeconds: this.getLength(), + htmlContent: asset.html, jsContent: asset.js, cssContent: asset.css, fps: captureFps @@ -440,6 +441,7 @@ export class Html5Player extends Player { const { frameCount } = computeHtml5FrameCount({ clipLengthSeconds: this.getLength(), + htmlContent: this.asset.html, jsContent: this.asset.js, cssContent: this.asset.css, fps: this.captureFps @@ -692,7 +694,13 @@ export class Html5Player extends Player { const fps = cached?.fps ?? this.captureFps; const frameCount = cached?.pngs.length ?? - computeHtml5FrameCount({ clipLengthSeconds: this.getLength(), jsContent: this.asset.js, cssContent: this.asset.css, fps }).frameCount; + computeHtml5FrameCount({ + clipLengthSeconds: this.getLength(), + htmlContent: this.asset.html, + jsContent: this.asset.js, + cssContent: this.asset.css, + fps + }).frameCount; // Seeking to the bake's frame time makes a still match the frame playback shows there. const idx = Math.min(Math.max(0, Math.floor(seconds * fps)), frameCount - 1); const blob = cached ? cached.pngs[idx] : await this.captureStillFrame(idx / fps, cacheKey, stale); diff --git a/tests/html5-player.test.ts b/tests/html5-player.test.ts index 522587b9..fc1833b3 100644 --- a/tests/html5-player.test.ts +++ b/tests/html5-player.test.ts @@ -58,15 +58,16 @@ jest.mock("@canvas/players/player", () => ({ jest.mock("@shotstack/shotstack-canvas", () => ({ Html5AssetSchema: { safeParse: (data: unknown) => ({ success: true, data }) }, composeHtml5IframeSrcdoc: jest.fn(() => "Updated"), - computeHtml5FrameCount: ({ clipLengthSeconds, fps }: { clipLengthSeconds: number; fps: number }) => ({ + computeHtml5FrameCount: jest.fn(({ clipLengthSeconds, fps }: { clipLengthSeconds: number; fps: number }) => ({ frameCount: Math.ceil(clipLengthSeconds * fps) - }), + })), findHtml5IngestUrls: jest.fn(() => []), html5ClipLongestEdge: jest.fn(() => 1280), inlineHtml5IngestAssets: jest.fn() })); const canvasLib = jest.requireMock("@shotstack/shotstack-canvas") as { composeHtml5IframeSrcdoc: jest.Mock; + computeHtml5FrameCount: jest.Mock; findHtml5IngestUrls: jest.Mock; html5ClipLongestEdge: jest.Mock; inlineHtml5IngestAssets: jest.Mock; @@ -280,6 +281,24 @@ it("ignores an older iframe timeout while newer content is loading", async () => expect(harness.contentContainer.children).toEqual([expect.objectContaining({ kind: "loading" })]); }); +// A page animated only by a style block in its markup has frames to capture, as it does in the render. +it("counts frames from the page's markup when keying and capturing it", async () => { + const { player, harness, iframe } = createPlayer(); + Object.assign(player, { getLength: () => 1 }); + jest.spyOn(harness, "seekHarness").mockImplementation(() => { + throw new Error("stop after first seek"); + }); + + await harness.hashAsset(); + const capture = harness.captureFrames(); + await until(() => iframe.srcdoc === "Updated"); + iframe.dispatchEvent(new Event("load")); + await expect(capture).rejects.toThrow("stop after first seek"); + + const counted = canvasLib.computeHtml5FrameCount.mock.calls.map(([args]) => args.htmlContent); + expect(counted).toEqual(["
Background
", "
Background
"]); +}); + it("captures from a freshly loaded document rather than the live-edited one", async () => { const { player, harness, iframe } = createPlayer(); Object.assign(player, { getLength: () => 1 }); @@ -647,6 +666,17 @@ it("renders a still from its own page without waiting for the clip's capture", a expect(loading.visible).toBe(true); }); +it("counts frames from the page's markup when rendering a still", async () => { + const { player, harness } = createStillPlayer(); + jest.spyOn(harness, "captureFrames").mockImplementation(() => new Promise(() => {})); + harness.beginCapture(); + + await player.prepareStaticRender(); + player.endStaticRender(); + + expect(canvasLib.computeHtml5FrameCount).toHaveBeenCalledWith(expect.objectContaining({ htmlContent: "
Background
" })); +}); + it("takes a still from captured frames, in memory or cached, without rendering one", async () => { const { player, harness } = createStillPlayer(); Object.assign(player, { getPlaybackTime: () => 1 });