KC test uses shared LiveStreamManager, tree-nav dropdown, KC card badge fix
- KC test WS now acquires from LiveStreamManager instead of creating its own DXGI duplicator, eliminating capture contention with running LED targets - Tree-nav refactored to compact dropdown on click with outside-click dismiss (closes on click outside the trigger+panel, not just outside the container) - KC target card badge (e.g. "Daylight Cycle") no longer wastes empty space Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -849,59 +849,42 @@ async def test_kc_target_ws(
|
||||
await websocket.accept()
|
||||
logger.info(f"KC test WS connected for {target_id} (fps={fps})")
|
||||
|
||||
capture_stream = None
|
||||
# Use the shared LiveStreamManager so we share the capture stream with
|
||||
# running LED targets instead of creating a competing DXGI duplicator.
|
||||
live_stream_mgr = processor_manager_inst._live_stream_manager
|
||||
live_stream = None
|
||||
|
||||
try:
|
||||
live_stream = await asyncio.to_thread(
|
||||
live_stream_mgr.acquire, target.picture_source_id
|
||||
)
|
||||
logger.info(f"KC test WS acquired shared live stream for {target.picture_source_id}")
|
||||
|
||||
prev_frame_ref = None
|
||||
|
||||
while True:
|
||||
loop_start = time.monotonic()
|
||||
|
||||
pil_image = None
|
||||
capture_stream_local = None
|
||||
|
||||
try:
|
||||
import httpx
|
||||
# Reload chain each iteration for dynamic sources
|
||||
chain = source_store_inst.resolve_stream_chain(target.picture_source_id)
|
||||
raw_stream = chain["raw_stream"]
|
||||
capture = await asyncio.to_thread(live_stream.get_latest_frame)
|
||||
|
||||
if isinstance(raw_stream, StaticImagePictureSource):
|
||||
source = raw_stream.image_source
|
||||
if source.startswith(("http://", "https://")):
|
||||
async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client:
|
||||
resp = await client.get(source)
|
||||
resp.raise_for_status()
|
||||
pil_image = Image.open(io.BytesIO(resp.content)).convert("RGB")
|
||||
else:
|
||||
from pathlib import Path
|
||||
path = Path(source)
|
||||
if path.exists():
|
||||
pil_image = Image.open(path).convert("RGB")
|
||||
if capture is None or capture.image is None:
|
||||
await asyncio.sleep(frame_interval)
|
||||
continue
|
||||
|
||||
elif isinstance(raw_stream, ScreenCapturePictureSource):
|
||||
try:
|
||||
capture_tmpl = template_store_inst.get_template(raw_stream.capture_template_id)
|
||||
except ValueError:
|
||||
break
|
||||
|
||||
if capture_tmpl.engine_type not in EngineRegistry.get_available_engines():
|
||||
break
|
||||
|
||||
capture_stream_local = EngineRegistry.create_stream(
|
||||
capture_tmpl.engine_type, raw_stream.display_index, capture_tmpl.engine_config
|
||||
)
|
||||
capture_stream_local.initialize()
|
||||
screen_capture = capture_stream_local.capture_frame()
|
||||
if screen_capture is not None and isinstance(screen_capture.image, np.ndarray):
|
||||
pil_image = Image.fromarray(screen_capture.image)
|
||||
|
||||
else:
|
||||
# VideoCaptureSource or other — not directly supported in WS test
|
||||
break
|
||||
# Skip if same frame object (no new capture yet)
|
||||
if capture is prev_frame_ref:
|
||||
await asyncio.sleep(frame_interval * 0.5)
|
||||
continue
|
||||
prev_frame_ref = capture
|
||||
|
||||
pil_image = Image.fromarray(capture.image) if isinstance(capture.image, np.ndarray) else None
|
||||
if pil_image is None:
|
||||
await asyncio.sleep(frame_interval)
|
||||
continue
|
||||
|
||||
# Apply postprocessing
|
||||
# Apply postprocessing (if the source chain has PP templates)
|
||||
chain = source_store_inst.resolve_stream_chain(target.picture_source_id)
|
||||
pp_template_ids = chain.get("postprocessing_template_ids", [])
|
||||
if pp_template_ids and pp_template_store_inst:
|
||||
img_array = np.array(pil_image)
|
||||
@@ -920,7 +903,7 @@ async def test_kc_target_ws(
|
||||
img_array = result
|
||||
except ValueError:
|
||||
pass
|
||||
pil_image = Image.fromarray(img_array)
|
||||
pil_image = Image.fromarray(img_array)
|
||||
|
||||
# Extract colors
|
||||
img_array = np.array(pil_image)
|
||||
@@ -971,12 +954,6 @@ async def test_kc_target_ws(
|
||||
if isinstance(inner_e, WebSocketDisconnect):
|
||||
raise
|
||||
logger.warning(f"KC test WS frame error for {target_id}: {inner_e}")
|
||||
finally:
|
||||
if capture_stream_local:
|
||||
try:
|
||||
capture_stream_local.cleanup()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
elapsed = time.monotonic() - loop_start
|
||||
sleep_time = frame_interval - elapsed
|
||||
@@ -988,9 +965,11 @@ async def test_kc_target_ws(
|
||||
except Exception as e:
|
||||
logger.error(f"KC test WS error for {target_id}: {e}", exc_info=True)
|
||||
finally:
|
||||
if capture_stream:
|
||||
if live_stream is not None:
|
||||
try:
|
||||
capture_stream.cleanup()
|
||||
await asyncio.to_thread(
|
||||
live_stream_mgr.release, target.picture_source_id
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
logger.info(f"KC test WS closed for {target_id}")
|
||||
|
||||
Reference in New Issue
Block a user