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:
@@ -7,7 +7,7 @@ import time
|
||||
import httpx
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import Response
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
@@ -467,3 +467,100 @@ async def test_picture_source(
|
||||
stream.cleanup()
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up test stream: {e}")
|
||||
|
||||
|
||||
# ===== REAL-TIME PICTURE SOURCE TEST WEBSOCKET =====
|
||||
|
||||
|
||||
@router.websocket("/api/v1/picture-sources/{stream_id}/test/ws")
|
||||
async def test_picture_source_ws(
|
||||
websocket: WebSocket,
|
||||
stream_id: str,
|
||||
token: str = Query(""),
|
||||
duration: float = Query(5.0),
|
||||
preview_width: int = Query(0),
|
||||
):
|
||||
"""WebSocket for picture source test with intermediate frame previews."""
|
||||
from wled_controller.api.routes._test_helpers import (
|
||||
authenticate_ws_token,
|
||||
stream_capture_test,
|
||||
)
|
||||
from wled_controller.api.dependencies import (
|
||||
get_picture_source_store as _get_ps_store,
|
||||
get_template_store as _get_t_store,
|
||||
get_pp_template_store as _get_pp_store,
|
||||
)
|
||||
|
||||
if not authenticate_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
return
|
||||
|
||||
store = _get_ps_store()
|
||||
template_store = _get_t_store()
|
||||
pp_store = _get_pp_store()
|
||||
|
||||
# Resolve stream chain
|
||||
try:
|
||||
chain = store.resolve_stream_chain(stream_id)
|
||||
except ValueError as e:
|
||||
await websocket.close(code=4004, reason=str(e))
|
||||
return
|
||||
|
||||
raw_stream = chain["raw_stream"]
|
||||
|
||||
# Static images don't benefit from streaming — reject gracefully
|
||||
if isinstance(raw_stream, StaticImagePictureSource):
|
||||
await websocket.close(code=4003, reason="Static image streams don't support live test")
|
||||
return
|
||||
|
||||
if not isinstance(raw_stream, ScreenCapturePictureSource):
|
||||
await websocket.close(code=4003, reason="Unsupported stream type for live test")
|
||||
return
|
||||
|
||||
# Create capture engine
|
||||
try:
|
||||
capture_template = template_store.get_template(raw_stream.capture_template_id)
|
||||
except ValueError as e:
|
||||
await websocket.close(code=4004, reason=str(e))
|
||||
return
|
||||
|
||||
if capture_template.engine_type not in EngineRegistry.get_available_engines():
|
||||
await websocket.close(code=4003, reason=f"Engine '{capture_template.engine_type}' not available")
|
||||
return
|
||||
|
||||
# Resolve postprocessing filters (if any)
|
||||
pp_filters = None
|
||||
pp_template_ids = chain.get("postprocessing_template_ids", [])
|
||||
if pp_template_ids:
|
||||
try:
|
||||
pp_template = pp_store.get_template(pp_template_ids[0])
|
||||
pp_filters = pp_store.resolve_filter_instances(pp_template.filters) or None
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Engine factory — creates + initializes engine inside the capture thread
|
||||
# to avoid thread-affinity issues (e.g. MSS uses thread-local state)
|
||||
_engine_type = capture_template.engine_type
|
||||
_display_index = raw_stream.display_index
|
||||
_engine_config = capture_template.engine_config
|
||||
|
||||
def engine_factory():
|
||||
s = EngineRegistry.create_stream(_engine_type, _display_index, _engine_config)
|
||||
s.initialize()
|
||||
return s
|
||||
|
||||
await websocket.accept()
|
||||
logger.info(f"Picture source test WS connected for {stream_id} ({duration}s)")
|
||||
|
||||
try:
|
||||
await stream_capture_test(
|
||||
websocket, engine_factory, duration,
|
||||
pp_filters=pp_filters,
|
||||
preview_width=preview_width or None,
|
||||
)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Picture source test WS error for {stream_id}: {e}")
|
||||
finally:
|
||||
logger.info(f"Picture source test WS disconnected for {stream_id}")
|
||||
|
||||
Reference in New Issue
Block a user