diff --git a/.env.example b/.env.example index 0f1ce1b..3bf6412 100644 --- a/.env.example +++ b/.env.example @@ -57,9 +57,10 @@ TTS_SPEECH_MODEL=mimo-v2.5-tts MOCK_IMAGE=false # ---- 5b. Image proxy (Cloudflare Worker, OPTIONAL) ----------------- -# Leave BOTH blank (the default) and the browser fetches images directly -# from the provider — exactly as the app worked before this proxy existed. -# You are completely unaffected; skip this whole section. +# Leave NEXT_PUBLIC_IMAGE_PROXY_URL blank (the default) and the browser +# fetches images directly from the provider — exactly as the app worked +# before this proxy existed. The ALLOWED_HOSTS value below is inert until +# a proxy URL is set, so you're completely unaffected; skip this section. # # Why you might want it: Chrome's direct fetch of im.runware.ai is unreliable # on some networks (ERR_QUIC_PROTOCOL_ERROR mid-stream → partial bytes → diff --git a/app/play/page.tsx b/app/play/page.tsx index bd834e2..5991db7 100644 --- a/app/play/page.tsx +++ b/app/play/page.tsx @@ -73,18 +73,22 @@ const IMAGE_PRELOAD_TIMEOUT_MS = 20000; function preloadImage(url: string): Promise { return new Promise((resolve) => { const img = new Image(); - const done = () => resolve(); - const timer = setTimeout(done, IMAGE_PRELOAD_TIMEOUT_MS); - img.onload = () => { + let timer: ReturnType; + // Single exit: clear the timeout and resolve. resolve() is idempotent, so + // whichever path fires first (load+decode, error, timeout) wins. + const done = () => { clearTimeout(timer); + resolve(); + }; + // Armed across BOTH network load and decode, so a hung decode still + // resolves quietly — better a broken than a stuck play loop. + timer = setTimeout(done, IMAGE_PRELOAD_TIMEOUT_MS); + img.onload = () => { // .decode() forces the bitmap to be fully decoded before we proceed — // without it, a slow decode could still cause a flash on first paint. img.decode().then(done, done); }; - img.onerror = () => { - clearTimeout(timer); - done(); - }; + img.onerror = done; img.src = url; }); }