From 81b275979b08382d7a4bf09e1c9485d96e3afbd5 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Fri, 20 Mar 2026 14:29:51 +0300 Subject: [PATCH] Fix stale frame in API CSS preview when source is inactive Server: send initial frame immediately after metadata for api_input sources so the client gets the current buffer (fallback color if inactive) instead of never receiving a frame when push_generation stays at 0. Frontend: clear all test modal canvases on open to prevent stale pixels from previous sessions. Reset FPS timestamps after metadata so the initial bootstrap frame isn't counted in the FPS chart. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../wled_controller/api/routes/color_strip_sources.py | 8 ++++++++ .../static/js/features/color-strips.ts | 11 ++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/server/src/wled_controller/api/routes/color_strip_sources.py b/server/src/wled_controller/api/routes/color_strip_sources.py index 57c3e02..3485c6b 100644 --- a/server/src/wled_controller/api/routes/color_strip_sources.py +++ b/server/src/wled_controller/api/routes/color_strip_sources.py @@ -890,6 +890,14 @@ async def test_color_strip_ws( meta["layer_infos"] = layer_infos await websocket.send_text(_json.dumps(meta)) + # For api_input: send the current buffer immediately so the client + # gets a frame right away (fallback color if inactive) rather than + # leaving the canvas blank/stale until external data arrives. + if is_api_input: + initial_colors = stream.get_latest_colors() + if initial_colors is not None: + await websocket.send_bytes(initial_colors.tobytes()) + # For picture sources, grab the live stream for frame preview _frame_live = None if is_picture and hasattr(stream, 'live_stream'): diff --git a/server/src/wled_controller/static/js/features/color-strips.ts b/server/src/wled_controller/static/js/features/color-strips.ts index 997dcb3..642c478 100644 --- a/server/src/wled_controller/static/js/features/color-strips.ts +++ b/server/src/wled_controller/static/js/features/color-strips.ts @@ -2563,10 +2563,15 @@ function _openTestModal(sourceId: string) { modal.onclick = (e) => { if (e.target === modal) closeTestCssSourceModal(); }; _cssTestSourceId = sourceId; - // Reset views + // Reset views and clear stale canvas content (document.getElementById('css-test-strip-view') as HTMLElement).style.display = 'none'; (document.getElementById('css-test-rect-view') as HTMLElement).style.display = 'none'; (document.getElementById('css-test-layers-view') as HTMLElement).style.display = 'none'; + // Clear all test canvases to prevent stale frames from previous sessions + modal.querySelectorAll('canvas').forEach(c => { + const ctx = c.getContext('2d'); + if (ctx) ctx.clearRect(0, 0, c.width, c.height); + }); (document.getElementById('css-test-led-group') as HTMLElement).style.display = ''; // Input source selector: shown for both CSS test and CSPT test, hidden for api_input const csptGroup = document.getElementById('css-test-cspt-input-group') as HTMLElement | null; @@ -2660,6 +2665,10 @@ function _cssTestConnect(sourceId: string, ledCount: number, fps?: number) { const isPicture = _cssTestMeta.edges && _cssTestMeta.edges.length > 0; _cssTestIsComposite = _cssTestMeta.layers && _cssTestMeta.layers.length > 0; + // Reset FPS timestamps so the initial bootstrap frame + // (sent right after metadata for api_input) isn't counted + if (_cssTestIsApiInput) _cssTestFpsTimestamps = []; + // Show correct view (document.getElementById('css-test-strip-view') as HTMLElement).style.display = (isPicture || _cssTestIsComposite) ? 'none' : ''; (document.getElementById('css-test-rect-view') as HTMLElement).style.display = isPicture ? '' : 'none';