Auto-recover DXGI capture after duplication interface loss
BetterCam/DXcam engines now detect when the DXGI Desktop Duplication interface is lost (display mode change, sleep/wake, UAC prompt, etc.) and automatically reinitialize the camera with a 3-second cooldown between attempts, instead of error-looping indefinitely. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""BetterCam-based screen capture engine (Windows only, DXGI Desktop Duplication)."""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
@@ -15,6 +16,8 @@ from wled_controller.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
_REINIT_COOLDOWN = 3.0 # seconds between re-init attempts
|
||||
|
||||
|
||||
class BetterCamCaptureStream(CaptureStream):
|
||||
"""BetterCam capture stream for a specific display."""
|
||||
@@ -23,6 +26,7 @@ class BetterCamCaptureStream(CaptureStream):
|
||||
super().__init__(display_index, config)
|
||||
self._camera = None
|
||||
self._bettercam = None
|
||||
self._last_reinit: float = 0
|
||||
|
||||
def initialize(self) -> None:
|
||||
try:
|
||||
@@ -48,6 +52,7 @@ class BetterCamCaptureStream(CaptureStream):
|
||||
raise RuntimeError(f"Failed to create BetterCam camera for display {self.display_index}")
|
||||
|
||||
self._initialized = True
|
||||
self._last_reinit = time.monotonic()
|
||||
logger.info(f"BetterCam capture stream initialized (display={self.display_index})")
|
||||
|
||||
def cleanup(self) -> None:
|
||||
@@ -72,6 +77,24 @@ class BetterCamCaptureStream(CaptureStream):
|
||||
self._initialized = False
|
||||
logger.info(f"BetterCam capture stream cleaned up (display={self.display_index})")
|
||||
|
||||
def _try_reinit(self) -> bool:
|
||||
"""Attempt to reinitialize the DXGI capture after a failure.
|
||||
|
||||
Returns True if reinit succeeded, False if on cooldown or failed.
|
||||
"""
|
||||
now = time.monotonic()
|
||||
if now - self._last_reinit < _REINIT_COOLDOWN:
|
||||
return False
|
||||
self._last_reinit = now
|
||||
logger.warning(f"BetterCam: reinitializing camera for display {self.display_index}")
|
||||
try:
|
||||
self.cleanup()
|
||||
self.initialize()
|
||||
return True
|
||||
except Exception as reinit_err:
|
||||
logger.error(f"BetterCam reinit failed (display={self.display_index}): {reinit_err}")
|
||||
return False
|
||||
|
||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
||||
if not self._initialized:
|
||||
self.initialize()
|
||||
@@ -98,6 +121,10 @@ class BetterCamCaptureStream(CaptureStream):
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to capture display {self.display_index} with BetterCam: {e}")
|
||||
# DXGI duplication can be lost after display changes / sleep / UAC.
|
||||
# Attempt to recreate the camera so capture can recover.
|
||||
if self._try_reinit():
|
||||
return None # let the caller retry on next tick
|
||||
raise RuntimeError(f"Screen capture failed: {e}")
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""DXcam-based screen capture engine (Windows only, DXGI Desktop Duplication)."""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
@@ -15,6 +16,8 @@ from wled_controller.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
_REINIT_COOLDOWN = 3.0 # seconds between re-init attempts
|
||||
|
||||
|
||||
class DXcamCaptureStream(CaptureStream):
|
||||
"""DXcam capture stream for a specific display."""
|
||||
@@ -23,6 +26,7 @@ class DXcamCaptureStream(CaptureStream):
|
||||
super().__init__(display_index, config)
|
||||
self._camera = None
|
||||
self._dxcam = None
|
||||
self._last_reinit: float = 0
|
||||
|
||||
def initialize(self) -> None:
|
||||
try:
|
||||
@@ -48,6 +52,7 @@ class DXcamCaptureStream(CaptureStream):
|
||||
raise RuntimeError(f"Failed to create DXcam camera for display {self.display_index}")
|
||||
|
||||
self._initialized = True
|
||||
self._last_reinit = time.monotonic()
|
||||
logger.info(f"DXcam capture stream initialized (display={self.display_index})")
|
||||
|
||||
def cleanup(self) -> None:
|
||||
@@ -72,6 +77,24 @@ class DXcamCaptureStream(CaptureStream):
|
||||
self._initialized = False
|
||||
logger.info(f"DXcam capture stream cleaned up (display={self.display_index})")
|
||||
|
||||
def _try_reinit(self) -> bool:
|
||||
"""Attempt to reinitialize the DXGI capture after a failure.
|
||||
|
||||
Returns True if reinit succeeded, False if on cooldown or failed.
|
||||
"""
|
||||
now = time.monotonic()
|
||||
if now - self._last_reinit < _REINIT_COOLDOWN:
|
||||
return False
|
||||
self._last_reinit = now
|
||||
logger.warning(f"DXcam: reinitializing camera for display {self.display_index}")
|
||||
try:
|
||||
self.cleanup()
|
||||
self.initialize()
|
||||
return True
|
||||
except Exception as reinit_err:
|
||||
logger.error(f"DXcam reinit failed (display={self.display_index}): {reinit_err}")
|
||||
return False
|
||||
|
||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
||||
if not self._initialized:
|
||||
self.initialize()
|
||||
@@ -98,6 +121,10 @@ class DXcamCaptureStream(CaptureStream):
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to capture display {self.display_index} with DXcam: {e}")
|
||||
# DXGI duplication can be lost after display changes / sleep / UAC.
|
||||
# Attempt to recreate the camera so capture can recover.
|
||||
if self._try_reinit():
|
||||
return None # let the caller retry on next tick
|
||||
raise RuntimeError(f"Screen capture failed: {e}")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user