mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-05 14:47:40 +03:00
Add dispatcher pipeline and drag-to-zoom calculation tests
Test the full MQTT payload parsing pipeline from dispatcher through to onvif command parameters, and verify the drag-to-zoom math including aspect ratio correction, boundary values, and reversed drag direction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2249217d93
commit
6d0a681dc0
@ -1,6 +1,24 @@
|
||||
from enum import Enum
|
||||
from unittest import TestCase, main
|
||||
|
||||
|
||||
class OnvifCommandEnum(str, Enum):
|
||||
"""Subset of commands needed for testing (avoids importing onvif module)."""
|
||||
|
||||
init = "init"
|
||||
move_down = "move_down"
|
||||
move_left = "move_left"
|
||||
move_relative = "move_relative"
|
||||
move_right = "move_right"
|
||||
move_up = "move_up"
|
||||
preset = "preset"
|
||||
stop = "stop"
|
||||
zoom_in = "zoom_in"
|
||||
zoom_out = "zoom_out"
|
||||
focus_in = "focus_in"
|
||||
focus_out = "focus_out"
|
||||
|
||||
|
||||
class TestMoveRelativeParsing(TestCase):
|
||||
"""Test the move_relative command parameter parsing logic."""
|
||||
|
||||
@ -36,5 +54,226 @@ class TestMoveRelativeParsing(TestCase):
|
||||
self.assertAlmostEqual(zoom, 0.5)
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def _parse_ptz_payload(self, payload: str):
|
||||
"""Replicate dispatcher parsing from _on_ptz_command."""
|
||||
preset = payload.lower()
|
||||
|
||||
if "preset" in preset:
|
||||
command = OnvifCommandEnum.preset
|
||||
param = preset[preset.index("_") + 1 :]
|
||||
elif "move_relative" in preset:
|
||||
command = OnvifCommandEnum.move_relative
|
||||
param = preset[preset.index("_") + 1 :]
|
||||
else:
|
||||
command = OnvifCommandEnum[preset]
|
||||
param = ""
|
||||
|
||||
return command, param
|
||||
|
||||
def test_move_relative_pan_tilt(self):
|
||||
command, param = self._parse_ptz_payload("move_relative_0.5_-0.3")
|
||||
self.assertEqual(command, OnvifCommandEnum.move_relative)
|
||||
self.assertEqual(param, "relative_0.5_-0.3")
|
||||
|
||||
def test_move_relative_pan_tilt_zoom(self):
|
||||
command, param = self._parse_ptz_payload("move_relative_0.5_-0.3_0.8")
|
||||
self.assertEqual(command, OnvifCommandEnum.move_relative)
|
||||
self.assertEqual(param, "relative_0.5_-0.3_0.8")
|
||||
|
||||
def test_simple_commands(self):
|
||||
for cmd_name in ["move_up", "move_down", "move_left", "move_right",
|
||||
"stop", "zoom_in", "zoom_out"]:
|
||||
command, param = self._parse_ptz_payload(cmd_name)
|
||||
self.assertEqual(command, OnvifCommandEnum[cmd_name])
|
||||
self.assertEqual(param, "")
|
||||
|
||||
def test_preset_command(self):
|
||||
command, param = self._parse_ptz_payload("preset_home")
|
||||
self.assertEqual(command, OnvifCommandEnum.preset)
|
||||
self.assertEqual(param, "home")
|
||||
|
||||
def test_full_pipeline_pan_tilt_only(self):
|
||||
"""Verify dispatcher param flows correctly into onvif parsing."""
|
||||
_, param = self._parse_ptz_payload("move_relative_0.5_-0.3")
|
||||
# Now parse as onvif.handle_command_async would
|
||||
parts = param.split("_")
|
||||
_, pan, tilt = parts[0], parts[1], parts[2]
|
||||
zoom = float(parts[3]) if len(parts) > 3 else 0
|
||||
self.assertAlmostEqual(float(pan), 0.5)
|
||||
self.assertAlmostEqual(float(tilt), -0.3)
|
||||
self.assertAlmostEqual(zoom, 0)
|
||||
|
||||
def test_full_pipeline_with_zoom(self):
|
||||
"""Verify dispatcher param flows correctly into onvif parsing with zoom."""
|
||||
_, param = self._parse_ptz_payload("move_relative_-0.2_0.7_0.65")
|
||||
parts = param.split("_")
|
||||
_, pan, tilt = parts[0], parts[1], parts[2]
|
||||
zoom = float(parts[3]) if len(parts) > 3 else 0
|
||||
self.assertAlmostEqual(float(pan), -0.2)
|
||||
self.assertAlmostEqual(float(tilt), 0.7)
|
||||
self.assertAlmostEqual(zoom, 0.65)
|
||||
|
||||
|
||||
class TestDragToZoomCalculation(TestCase):
|
||||
"""Test the drag-to-zoom math from LiveCameraView.
|
||||
|
||||
Replicates the zoom calculation logic from handleOverlayMouseUp:
|
||||
given a drag rectangle on the video overlay, compute the pan, tilt,
|
||||
and zoom values sent to the backend.
|
||||
"""
|
||||
|
||||
def _calculate_drag_zoom(self, drag_start, drag_end, rect_left, rect_top,
|
||||
rect_width, rect_height):
|
||||
"""Replicate the drag-to-zoom calculation from LiveCameraView."""
|
||||
x1 = min(drag_start[0], drag_end[0])
|
||||
y1 = min(drag_start[1], drag_end[1])
|
||||
x2 = max(drag_start[0], drag_end[0])
|
||||
y2 = max(drag_start[1], drag_end[1])
|
||||
|
||||
norm_x1 = (x1 - rect_left) / rect_width
|
||||
norm_y1 = (y1 - rect_top) / rect_height
|
||||
norm_x2 = (x2 - rect_left) / rect_width
|
||||
norm_y2 = (y2 - rect_top) / rect_height
|
||||
|
||||
box_w = norm_x2 - norm_x1
|
||||
box_h = norm_y2 - norm_y1
|
||||
|
||||
frame_aspect = rect_width / rect_height
|
||||
box_aspect = box_w / box_h
|
||||
if box_aspect > frame_aspect:
|
||||
box_h = box_w / frame_aspect
|
||||
else:
|
||||
box_w = box_h * frame_aspect
|
||||
|
||||
center_x = (norm_x1 + norm_x2) / 2
|
||||
center_y = (norm_y1 + norm_y2) / 2
|
||||
pan = (center_x - 0.5) * 2
|
||||
tilt = (0.5 - center_y) * 2
|
||||
|
||||
zoom = 1 - max(box_w, box_h)
|
||||
clamped_zoom = max(0, min(1, zoom))
|
||||
|
||||
return pan, tilt, clamped_zoom
|
||||
|
||||
def test_center_drag_half_frame(self):
|
||||
"""Drag a box covering the center 50% of a 2:1 frame.
|
||||
|
||||
Box is 500x250 = 0.5x0.5 normalized, but frame is 2:1.
|
||||
Aspect correction expands width: box_w = 0.5 * 2.0 = 1.0.
|
||||
zoom = 1 - max(1.0, 0.5) = 0.0.
|
||||
"""
|
||||
pan, tilt, zoom = self._calculate_drag_zoom(
|
||||
(250, 125), (750, 375), 0, 0, 1000, 500)
|
||||
self.assertAlmostEqual(pan, 0.0)
|
||||
self.assertAlmostEqual(tilt, 0.0)
|
||||
self.assertAlmostEqual(zoom, 0.0)
|
||||
|
||||
def test_center_drag_proportional_box(self):
|
||||
"""Drag a box matching the frame's 2:1 aspect ratio at 50% size."""
|
||||
# Frame 1000x500, box 500x125 at center = 0.5 x 0.25 normalized
|
||||
# box_aspect = 0.5/0.25 = 2.0 = frame_aspect, no expansion
|
||||
# zoom = 1 - max(0.5, 0.25) = 0.5
|
||||
pan, tilt, zoom = self._calculate_drag_zoom(
|
||||
(250, 187), (750, 312), 0, 0, 1000, 500)
|
||||
self.assertAlmostEqual(pan, 0.0)
|
||||
self.assertAlmostEqual(tilt, 0.002) # slight offset due to rounding
|
||||
self.assertAlmostEqual(zoom, 0.5)
|
||||
|
||||
def test_top_left_drag(self):
|
||||
"""Drag a box in the top-left quadrant of a 2:1 frame.
|
||||
|
||||
Box is 0.5x0.5 normalized, aspect correction expands to 1.0 wide.
|
||||
"""
|
||||
pan, tilt, zoom = self._calculate_drag_zoom(
|
||||
(0, 0), (500, 250), 0, 0, 1000, 500)
|
||||
self.assertAlmostEqual(pan, -0.5)
|
||||
self.assertAlmostEqual(tilt, 0.5)
|
||||
self.assertAlmostEqual(zoom, 0.0)
|
||||
|
||||
def test_bottom_right_drag(self):
|
||||
"""Drag a box in the bottom-right quadrant of a 2:1 frame."""
|
||||
pan, tilt, zoom = self._calculate_drag_zoom(
|
||||
(500, 250), (1000, 500), 0, 0, 1000, 500)
|
||||
self.assertAlmostEqual(pan, 0.5)
|
||||
self.assertAlmostEqual(tilt, -0.5)
|
||||
self.assertAlmostEqual(zoom, 0.0)
|
||||
|
||||
def test_full_frame_drag_no_zoom(self):
|
||||
"""Dragging the entire frame should produce zero zoom."""
|
||||
pan, tilt, zoom = self._calculate_drag_zoom(
|
||||
(0, 0), (1000, 500), 0, 0, 1000, 500)
|
||||
self.assertAlmostEqual(pan, 0.0)
|
||||
self.assertAlmostEqual(tilt, 0.0)
|
||||
self.assertAlmostEqual(zoom, 0.0)
|
||||
|
||||
def test_small_box_high_zoom(self):
|
||||
"""A small box should produce high zoom."""
|
||||
# 10% box at center of 1000x500 frame
|
||||
# box is 100x50px = 0.1 x 0.1 normalized
|
||||
# box_aspect = 1.0 < frame_aspect 2.0, expand width: 0.1 * 2.0 = 0.2
|
||||
# zoom = 1 - max(0.2, 0.1) = 0.8
|
||||
pan, tilt, zoom = self._calculate_drag_zoom(
|
||||
(450, 225), (550, 275), 0, 0, 1000, 500)
|
||||
self.assertAlmostEqual(pan, 0.0)
|
||||
self.assertAlmostEqual(tilt, 0.0)
|
||||
self.assertAlmostEqual(zoom, 0.8)
|
||||
|
||||
def test_aspect_ratio_correction_tall_box(self):
|
||||
"""A tall narrow box should be expanded to match frame aspect ratio."""
|
||||
# Frame is 1000x500 (2:1 aspect)
|
||||
# Drag a tall box: 100px wide x 250px tall at center
|
||||
pan, tilt, zoom = self._calculate_drag_zoom(
|
||||
(450, 125), (550, 375), 0, 0, 1000, 500)
|
||||
# Raw box: 0.1 wide x 0.5 tall
|
||||
# box_aspect = 0.1/0.5 = 0.2, frame_aspect = 2.0
|
||||
# box is taller -> expand width: box_w = 0.5 * 2.0 = 1.0
|
||||
# zoom = 1 - max(1.0, 0.5) = 0.0
|
||||
self.assertAlmostEqual(pan, 0.0)
|
||||
self.assertAlmostEqual(tilt, 0.0)
|
||||
self.assertAlmostEqual(zoom, 0.0)
|
||||
|
||||
def test_aspect_ratio_correction_wide_box(self):
|
||||
"""A wide short box should be expanded to match frame aspect ratio."""
|
||||
# Frame is 1000x500 (2:1 aspect)
|
||||
# Drag a wide box: 800px wide x 100px tall at center
|
||||
pan, tilt, zoom = self._calculate_drag_zoom(
|
||||
(100, 200), (900, 300), 0, 0, 1000, 500)
|
||||
# Raw box: 0.8 wide x 0.2 tall
|
||||
# box_aspect = 0.8/0.2 = 4.0 > frame_aspect 2.0
|
||||
# box is wider -> expand height: box_h = 0.8/2.0 = 0.4
|
||||
# zoom = 1 - max(0.8, 0.4) = 0.2
|
||||
self.assertAlmostEqual(pan, 0.0)
|
||||
self.assertAlmostEqual(tilt, 0.0)
|
||||
self.assertAlmostEqual(zoom, 0.2)
|
||||
|
||||
def test_offset_frame(self):
|
||||
"""Frame not at origin (simulating element offset on page)."""
|
||||
# Frame at (100, 50), size 800x400 (2:1 aspect)
|
||||
# Drag center half: (300, 150) to (700, 350)
|
||||
# Normalized: 0.5x0.5, aspect correction expands width to 1.0
|
||||
# zoom = 1 - 1.0 = 0.0
|
||||
pan, tilt, zoom = self._calculate_drag_zoom(
|
||||
(300, 150), (700, 350), 100, 50, 800, 400)
|
||||
self.assertAlmostEqual(pan, 0.0)
|
||||
self.assertAlmostEqual(tilt, 0.0)
|
||||
self.assertAlmostEqual(zoom, 0.0)
|
||||
|
||||
def test_reversed_drag_direction(self):
|
||||
"""Dragging bottom-right to top-left should give same result as top-left to bottom-right."""
|
||||
result1 = self._calculate_drag_zoom(
|
||||
(250, 125), (750, 375), 0, 0, 1000, 500)
|
||||
result2 = self._calculate_drag_zoom(
|
||||
(750, 375), (250, 125), 0, 0, 1000, 500)
|
||||
for a, b in zip(result1, result2):
|
||||
self.assertAlmostEqual(a, b)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(verbosity=2)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user