mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-19 21:22:50 +03:00
With rw_timeout, the HLS demuxer skipped a timed-out segment and exited cleanly, so a run reported success with a hole in it, and when the keyframe probe and the decode lost different segments the equal frame counts paired frames with the wrong timestamps. The keyframe path now requires the probe to cover the whole run without a gap over 5 s, a decode whose stderr shows a skipped segment fails the run, and a cadence decode more than 5 s short of the run's duration fails it too, so these runs land in segments_with_errors. The keyframe probe polls should_stop and is killed on cancel, probe_video_dimensions gets the read timeout, and a user's own cancelled job no longer blocks their next search while it still counts toward the global cap.
86 lines
3.0 KiB
Python
86 lines
3.0 KiB
Python
"""Tests for motion search run decoding and truncation checks."""
|
|
|
|
import threading
|
|
import unittest
|
|
from types import SimpleNamespace
|
|
from unittest import mock
|
|
|
|
import numpy as np
|
|
|
|
from frigate.jobs.motion_search import MotionSearchJob, MotionSearchRunner
|
|
|
|
# Two contiguous 10 s segments, so the run's stream is 20 s long.
|
|
RUN = [
|
|
SimpleNamespace(start_time=1000.0, end_time=1010.0),
|
|
SimpleNamespace(start_time=1010.0, end_time=1020.0),
|
|
]
|
|
FRAME = np.zeros((2, 2), dtype=np.uint8)
|
|
|
|
|
|
class TestProcessRun(unittest.TestCase):
|
|
def setUp(self):
|
|
requestor = mock.patch("frigate.jobs.motion_search.InterProcessRequestor")
|
|
requestor.start()
|
|
self.addCleanup(requestor.stop)
|
|
|
|
self.runner = MotionSearchRunner(
|
|
MotionSearchJob(camera="cam"), mock.Mock(), threading.Event()
|
|
)
|
|
self.runner.fps_rate = 2.0
|
|
self.runner._detect_with_progress = mock.Mock(return_value=[])
|
|
|
|
def _process(self, kf_pts, keyframe_frames, cadence_frames):
|
|
def frames(*args, skip_nonkey, fps_rate):
|
|
count = keyframe_frames if skip_nonkey else cadence_frames
|
|
return iter([FRAME] * count)
|
|
|
|
with (
|
|
mock.patch(
|
|
"frigate.jobs.motion_search.probe_vod_keyframe_pts",
|
|
return_value=kf_pts,
|
|
),
|
|
mock.patch(
|
|
"frigate.jobs.motion_search.iter_vod_frames", side_effect=frames
|
|
) as decode,
|
|
):
|
|
result = self.runner._process_run(RUN)
|
|
return result, [c.kwargs["skip_nonkey"] for c in decode.call_args_list]
|
|
|
|
def test_complete_keyframe_probe_pairs_timestamps(self):
|
|
pts = [0.5, 2.5, 4.5, 6.5, 8.5, 10.5, 12.5, 14.5, 16.5, 18.5]
|
|
(_, frames), modes = self._process(pts, len(pts), 0)
|
|
|
|
self.assertEqual(frames, len(pts))
|
|
self.assertEqual(modes, [True])
|
|
ts_fn = self.runner._detect_with_progress.call_args.args[1]
|
|
self.assertAlmostEqual(ts_fn(5), 1010.5)
|
|
|
|
def test_keyframe_probe_gap_falls_back_to_cadence(self):
|
|
# Probe lost the second segment; an equal decode count must not be trusted.
|
|
pts = [0.5, 2.5, 4.5, 6.5, 8.5]
|
|
(_, frames), modes = self._process(pts, len(pts), 40)
|
|
|
|
self.assertEqual(modes, [False])
|
|
self.assertEqual(frames, 40)
|
|
|
|
def test_cadence_shortfall_raises(self):
|
|
self.runner.use_keyframe = False
|
|
with self.assertRaises(RuntimeError):
|
|
self._process([], 0, 20)
|
|
|
|
def test_zero_frames_raises(self):
|
|
self.runner.use_keyframe = False
|
|
with self.assertRaises(RuntimeError):
|
|
self._process([], 0, 0)
|
|
|
|
def test_small_cadence_drift_is_tolerated(self):
|
|
self.runner.use_keyframe = False
|
|
(_, frames), _ = self._process([], 0, 38)
|
|
self.assertEqual(frames, 38)
|
|
|
|
def test_shortfall_after_stop_does_not_raise(self):
|
|
self.runner.use_keyframe = False
|
|
self.runner.cancel_event.set()
|
|
(_, frames), _ = self._process([], 0, 3)
|
|
self.assertEqual(frames, 3)
|