From fb8b91c37600e4380f67df9b0a4123cf16dedf62 Mon Sep 17 00:00:00 2001 From: dazzatronus Date: Fri, 15 May 2026 17:00:13 +1000 Subject: [PATCH 1/2] fix: enhance HTML5 player with prewarm capture and asset update logic --- src/components/canvas/players/html5-player.ts | 57 ++++++++++++++++--- src/core/player-reconciler.ts | 19 +++++-- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/components/canvas/players/html5-player.ts b/src/components/canvas/players/html5-player.ts index bea35fd0..ea46e0b9 100644 --- a/src/components/canvas/players/html5-player.ts +++ b/src/components/canvas/players/html5-player.ts @@ -3,7 +3,13 @@ import type { Edit } from "@core/edit-session"; import { EditEvent } from "@core/events/edit-events"; import { type Size } from "@layouts/geometry"; import type { ResolvedClip } from "@schemas"; -import { Html5AssetSchema, composeHtml5IframeSrcdoc, type Html5Asset } from "@shotstack/shotstack-canvas"; +import { + Html5AssetSchema, + composeHtml5IframeSrcdoc, + computeHtml5FrameCount, + detectHtml5DurationWithRetry, + type Html5Asset +} from "@shotstack/shotstack-canvas"; import * as pixi from "pixi.js"; import { computeHtml5CacheKey, html5CacheGet, html5CachePut } from "./html5-cache"; @@ -214,6 +220,7 @@ export class Html5Player extends Player { } await this.mountIframe(validation.data); this.configureKeyframes(); + this.prewarmCapture(); } catch (error) { console.error("Failed to render html5 asset:", error instanceof Error ? `${error.message}\n${error.stack}` : error); this.createFallbackGraphic(); @@ -327,12 +334,20 @@ export class Html5Player extends Player { await yieldFrame(); if (stale()) return null; - const detectedSeconds = this.detectAnimationDuration(); - const clipLengthSeconds = this.getLength(); - const hasJs = !!this.asset.js?.trim(); - const isStatic = detectedSeconds === null && !hasJs; + const detectedDurationMs = await detectHtml5DurationWithRetry(() => { + const seconds = this.detectAnimationDuration(); + return seconds === null ? null : seconds * 1000; + }, stale); + if (stale()) return null; + + const { frameCount } = computeHtml5FrameCount({ + detectedDurationMs, + clipLengthSeconds: this.getLength(), + jsContent: this.asset.js, + cssContent: this.asset.css, + fps: this.captureFps + }); const fps = this.captureFps; - const frameCount = isStatic ? 1 : Math.max(1, Math.ceil(Math.min(detectedSeconds ?? clipLengthSeconds, clipLengthSeconds) * fps)); const W = this.renderedWidth; const H = this.renderedHeight; this.captureFramesTotal = frameCount; @@ -379,12 +394,13 @@ export class Html5Player extends Player { const styles = Array.from(doc.querySelectorAll("style")) .map(el => ``) .join(""); + const animationOverride = ``; const bodyClone = doc.body.cloneNode(true) as HTMLElement; bodyClone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); const existingStyle = bodyClone.getAttribute("style") ?? ""; bodyClone.setAttribute("style", `width:${width}px;height:${height}px;margin:0;overflow:hidden;${existingStyle}`); const bodyXml = new XMLSerializer().serializeToString(bodyClone); - return `${styles}${bodyXml}`; + return `${styles}${animationOverride}${bodyXml}`; } private async getDecodedFrame(idx: number): Promise { @@ -439,11 +455,11 @@ export class Html5Player extends Player { this.iframe.srcdoc = composeHtml5IframeSrcdoc(this.asset); try { await waitForIframeLoad(this.iframe, undefined, true); + this.prewarmCapture(); } catch (err) { console.warn("[Html5Player] reload iframe load failed:", err); this.emitCaptureFailed(err, "static-placeholder"); } - // Stay in stale mode (iframe live) until the user plays again. } private disposeCapturedFrames(): void { @@ -480,11 +496,36 @@ export class Html5Player extends Player { } } + /** + * Run capture in the background without changing UI mode. + */ + private prewarmCapture(): void { + if (this.disposed || !this.iframe) return; + if (this.capturedFrames || this.captureInFlight) return; + this.captureFrames().catch(err => { + console.warn("[Html5Player] prewarm capture failed:", err); + }); + } + private triggerCaptureIfNeeded(): void { if (this.hasTriggeredCapture) return; if (!this.iframe || !this.edit.isPlaying) return; if (this.mode !== "editing" && this.mode !== "stale") return; + if (this.capturedFrames && this.capturedHash === this.contentHash) { + const hashAtTrigger = this.contentHash; + this.hasTriggeredCapture = true; + this.transitionToPlayback() + .catch(err => { + console.warn("[Html5Player] transitionToPlayback failed:", err); + this.transitionToEditing(); + }) + .finally(() => { + if (this.contentHash !== hashAtTrigger) this.hasTriggeredCapture = false; + }); + return; + } + this.hasTriggeredCapture = true; this.transitionToCapturing(); const hashAtTrigger = this.contentHash; diff --git a/src/core/player-reconciler.ts b/src/core/player-reconciler.ts index 17bf1fc8..88accb0e 100644 --- a/src/core/player-reconciler.ts +++ b/src/core/player-reconciler.ts @@ -332,18 +332,25 @@ export class PlayerReconciler { } /** - * Update player's asset and trigger reload if src changed. + * Update player's asset and trigger reload if the asset content changed. */ private updateAsset(player: Player, newAsset: unknown): void { - const oldSrc = (player.clipConfiguration.asset as { src?: string })?.src; - const newSrc = (newAsset as { src?: string })?.src; + const oldAsset = player.clipConfiguration.asset; + const assetType = (newAsset as { type?: string })?.type; - // Update the asset // eslint-disable-next-line no-param-reassign -- Intentional player state update player.clipConfiguration.asset = newAsset as ResolvedClip["asset"]; - // If src changed, trigger async reload - if (oldSrc !== newSrc && player.reloadAsset) { + let needsReload: boolean; + if (assetType === "html5") { + needsReload = JSON.stringify(oldAsset) !== JSON.stringify(newAsset); + } else { + const oldSrc = (oldAsset as { src?: string })?.src; + const newSrc = (newAsset as { src?: string })?.src; + needsReload = oldSrc !== newSrc; + } + + if (needsReload && player.reloadAsset) { player .reloadAsset() .then(() => { From 484a37a13c133dbd5b739060632e740abdf59c04 Mon Sep 17 00:00:00 2001 From: dazzatronus Date: Fri, 15 May 2026 17:08:35 +1000 Subject: [PATCH 2/2] fix: update @shotstack/shotstack-canvas dependency to version 2.7.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 605e58ae..87b51240 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ }, "dependencies": { "@shotstack/schemas": "1.11.0", - "@shotstack/shotstack-canvas": "^2.7.2", + "@shotstack/shotstack-canvas": "^2.7.3", "howler": "^2.2.4", "mediabunny": "^1.11.2", "opentype.js": "^1.3.4",