Add live preview streaming for capture tests via WebSocket
Replace blocking REST-based capture tests with WebSocket endpoints that stream intermediate frame thumbnails at ~100ms intervals, giving real-time visual feedback during capture. Preview resolution adapts dynamically to the client viewport size and device pixel ratio. - New shared helper (_test_helpers.py) with engine_factory pattern to avoid MSS thread-affinity issues - WS endpoints for stream, capture template, and PP template tests - Enhanced overlay spinner with live preview image and stats - Frontend _runTestViaWS shared helper replaces three REST test runners Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import time
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
@@ -365,6 +365,68 @@ def test_template(
|
||||
logger.error(f"Error cleaning up test stream: {e}")
|
||||
|
||||
|
||||
# ===== REAL-TIME CAPTURE TEMPLATE TEST WEBSOCKET =====
|
||||
|
||||
|
||||
@router.websocket("/api/v1/capture-templates/test/ws")
|
||||
async def test_template_ws(
|
||||
websocket: WebSocket,
|
||||
token: str = Query(""),
|
||||
):
|
||||
"""WebSocket for capture template test with intermediate frame previews.
|
||||
|
||||
Config is sent as the first client message (JSON with engine_type,
|
||||
engine_config, display_index, capture_duration).
|
||||
"""
|
||||
from wled_controller.api.routes._test_helpers import (
|
||||
authenticate_ws_token,
|
||||
stream_capture_test,
|
||||
)
|
||||
|
||||
if not authenticate_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
return
|
||||
|
||||
await websocket.accept()
|
||||
|
||||
# Read config from first client message
|
||||
try:
|
||||
config = await websocket.receive_json()
|
||||
except Exception as e:
|
||||
await websocket.send_json({"type": "error", "detail": f"Expected JSON config: {e}"})
|
||||
await websocket.close(code=4003)
|
||||
return
|
||||
|
||||
engine_type = config.get("engine_type", "")
|
||||
engine_config = config.get("engine_config", {})
|
||||
display_index = config.get("display_index", 0)
|
||||
duration = float(config.get("capture_duration", 5.0))
|
||||
pw = int(config.get("preview_width", 0)) or None
|
||||
|
||||
if engine_type not in EngineRegistry.get_available_engines():
|
||||
await websocket.send_json({"type": "error", "detail": f"Engine '{engine_type}' not available"})
|
||||
await websocket.close(code=4003)
|
||||
return
|
||||
|
||||
# Engine factory — creates + initializes engine inside the capture thread
|
||||
# to avoid thread-affinity issues (e.g. MSS uses thread-local state)
|
||||
def engine_factory():
|
||||
s = EngineRegistry.create_stream(engine_type, display_index, engine_config)
|
||||
s.initialize()
|
||||
return s
|
||||
|
||||
logger.info(f"Capture template test WS connected ({engine_type}, display {display_index}, {duration}s)")
|
||||
|
||||
try:
|
||||
await stream_capture_test(websocket, engine_factory, duration, preview_width=pw)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Capture template test WS error: {e}")
|
||||
finally:
|
||||
logger.info("Capture template test WS disconnected")
|
||||
|
||||
|
||||
# ===== FILTER TYPE ENDPOINTS =====
|
||||
|
||||
@router.get("/api/v1/filters", response_model=FilterTypeListResponse, tags=["Filters"])
|
||||
|
||||
Reference in New Issue
Block a user