diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py index d22bb04a3..b8107af8d 100644 --- a/frigate/ptz/onvif.py +++ b/frigate/ptz/onvif.py @@ -295,6 +295,13 @@ class OnvifController: ): del move_request["Speed"]["Zoom"] except Exception as e: + if ( + self.config.cameras[camera_name].onvif.autotracking.zooming + == ZoomingModeEnum.relative + ): + self.config.cameras[ + camera_name + ].onvif.autotracking.zooming = ZoomingModeEnum.disabled logger.warning( f"Relative zoom not supported for {camera_name}: {e}" ) diff --git a/frigate/test/test_ptz_commands.py b/frigate/test/test_ptz_commands.py index 178f2be03..2828d254c 100644 --- a/frigate/test/test_ptz_commands.py +++ b/frigate/test/test_ptz_commands.py @@ -20,7 +20,12 @@ class OnvifCommandEnum(str, Enum): class TestMoveRelativeParsing(TestCase): - """Test the move_relative command parameter parsing logic.""" + """Test the move_relative command parameter parsing logic. + + NOTE: _parse_move_relative replicates logic from + OnvifController.handle_command_async (frigate/ptz/onvif.py). + If that parsing changes, this helper must be updated to match. + """ def _parse_move_relative(self, param: str): """Replicate the parsing logic from OnvifController.handle_command_async.""" @@ -57,8 +62,10 @@ class TestMoveRelativeParsing(TestCase): class TestDispatcherPtzParsing(TestCase): """Test the dispatcher's _on_ptz_command parsing pipeline. - Replicates the logic from FrigateDispatcher._on_ptz_command to verify - that MQTT payloads are correctly decomposed into command + param. + Replicates the logic from FrigateDispatcher._on_ptz_command + (frigate/comms/dispatcher.py) to verify that MQTT payloads are + correctly decomposed into command + param. + If that parsing changes, this helper must be updated to match. """ def _parse_ptz_payload(self, payload: str): diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index 0d4e880fe..63babb446 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -131,6 +131,9 @@ type LiveCameraViewProps = { toggleFullscreen: () => void; }; +/** Minimum drag distance (px) to distinguish a drag-to-zoom from a click-to-move. */ +const MIN_DRAG_DISTANCE = 20; + function getClientPos( e: React.MouseEvent | React.TouchEvent, ): { x: number; y: number } | null { @@ -293,8 +296,7 @@ export default function LiveCameraView({ const dx = Math.abs(pos.x - dragStart.x); const dy = Math.abs(pos.y - dragStart.y); - // Minimum drag distance of 20px to distinguish from click - if (dx < 20 && dy < 20) { + if (dx < MIN_DRAG_DISTANCE && dy < MIN_DRAG_DISTANCE) { // Click (not drag) — move to point without zoom const normalizedX = (pos.x - rect.left) / rect.width; const normalizedY = (pos.y - rect.top) / rect.height; @@ -352,7 +354,7 @@ export default function LiveCameraView({ if (!isDragging || !clickOverlayRef.current) return null; const dx = Math.abs(dragCurrent.x - dragStart.x); const dy = Math.abs(dragCurrent.y - dragStart.y); - if (dx < 20 && dy < 20) return null; // Don't show rectangle for small movements + if (dx < MIN_DRAG_DISTANCE && dy < MIN_DRAG_DISTANCE) return null; const rect = clickOverlayRef.current.getBoundingClientRect(); const x1 = Math.min(dragStart.x, dragCurrent.x) - rect.left; @@ -762,6 +764,10 @@ export default function LiveCameraView({ onMouseDown={handleOverlayMouseDown} onMouseMove={handleOverlayMouseMove} onMouseUp={handleOverlayMouseUp} + onMouseLeave={() => { + setDragStart(null); + setDragCurrent(null); + }} onTouchStart={handleOverlayMouseDown} onTouchMove={handleOverlayMouseMove} onTouchEnd={handleOverlayMouseUp}