mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-04 12:37:43 +03:00
chapter and thumbnail fixes (#23100)
- Skip null end_time when building export chapter metadata - Use plain seconds for export thumbnail ffmpeg seek
This commit is contained in:
parent
7ad233ef15
commit
5bc15d4aa9
@ -420,6 +420,7 @@ class RecordingExporter(threading.Thread):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
total_output = windows[-1][2] + (windows[-1][1] - windows[-1][0])
|
total_output = windows[-1][2] + (windows[-1][1] - windows[-1][0])
|
||||||
|
last_recorded_end = windows[-1][1]
|
||||||
|
|
||||||
def wall_to_output(t: float) -> float:
|
def wall_to_output(t: float) -> float:
|
||||||
t = max(float(self.start_time), min(float(self.end_time), t))
|
t = max(float(self.start_time), min(float(self.end_time), t))
|
||||||
@ -432,8 +433,18 @@ class RecordingExporter(threading.Thread):
|
|||||||
|
|
||||||
chapter_blocks: list[str] = []
|
chapter_blocks: list[str] = []
|
||||||
for review in review_rows:
|
for review in review_rows:
|
||||||
|
if review.start_time is None:
|
||||||
|
continue
|
||||||
|
# In-progress segments have a NULL end_time until the activity
|
||||||
|
# closes; clamp to the last recorded second so the chapter never
|
||||||
|
# extends past the actual video.
|
||||||
|
review_end = (
|
||||||
|
float(review.end_time)
|
||||||
|
if review.end_time is not None
|
||||||
|
else last_recorded_end
|
||||||
|
)
|
||||||
start_out = wall_to_output(float(review.start_time))
|
start_out = wall_to_output(float(review.start_time))
|
||||||
end_out = wall_to_output(float(review.end_time))
|
end_out = wall_to_output(review_end)
|
||||||
|
|
||||||
# Drop chapters that fall entirely in a recording gap, or are
|
# Drop chapters that fall entirely in a recording gap, or are
|
||||||
# too short to be navigable in a player.
|
# too short to be navigable in a player.
|
||||||
@ -516,16 +527,14 @@ class RecordingExporter(threading.Thread):
|
|||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
diff = self.start_time - preview.start_time
|
diff = max(0.0, float(self.start_time) - float(preview.start_time))
|
||||||
minutes = int(diff / 60)
|
|
||||||
seconds = int(diff % 60)
|
|
||||||
ffmpeg_cmd = [
|
ffmpeg_cmd = [
|
||||||
"/usr/lib/ffmpeg/7.0/bin/ffmpeg", # hardcode path for exports thumbnail due to missing libwebp support
|
"/usr/lib/ffmpeg/7.0/bin/ffmpeg", # hardcode path for exports thumbnail due to missing libwebp support
|
||||||
"-hide_banner",
|
"-hide_banner",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"warning",
|
"warning",
|
||||||
"-ss",
|
"-ss",
|
||||||
f"00:{minutes}:{seconds}",
|
f"{diff:.3f}",
|
||||||
"-i",
|
"-i",
|
||||||
preview.path,
|
preview.path,
|
||||||
"-frames",
|
"-frames",
|
||||||
|
|||||||
@ -499,5 +499,56 @@ class TestSchedulesCleanup(unittest.TestCase):
|
|||||||
assert job.id not in manager.jobs
|
assert job.id not in manager.jobs
|
||||||
|
|
||||||
|
|
||||||
|
class TestChapterMetadataInProgressReview(unittest.TestCase):
|
||||||
|
"""Regression: in-progress review segments have end_time=NULL until the
|
||||||
|
activity closes. The chapter builder must clamp the chapter end to the
|
||||||
|
last recorded second instead of crashing on float(None)."""
|
||||||
|
|
||||||
|
def _fake_select_returning(self, rows: list) -> MagicMock:
|
||||||
|
mock_query = MagicMock()
|
||||||
|
mock_query.where.return_value = mock_query
|
||||||
|
mock_query.order_by.return_value = mock_query
|
||||||
|
mock_query.iterator.return_value = iter(rows)
|
||||||
|
return mock_query
|
||||||
|
|
||||||
|
def test_in_progress_review_does_not_crash_and_clamps_to_last_recording(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
exporter = _make_exporter(end_minus_start=200)
|
||||||
|
# Recordings cover [1000, 1150]; export window is [1000, 1200] so
|
||||||
|
# the last recorded second is 1150 (a 50s gap at the tail).
|
||||||
|
recordings = [
|
||||||
|
MagicMock(start_time=1000.0, end_time=1150.0),
|
||||||
|
]
|
||||||
|
in_progress = MagicMock(
|
||||||
|
start_time=1100.0,
|
||||||
|
end_time=None,
|
||||||
|
severity="alert",
|
||||||
|
data={"objects": ["person"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
chapter_path = os.path.join(tmpdir, "chapters.txt")
|
||||||
|
exporter._chapter_metadata_path = lambda: chapter_path # type: ignore[method-assign]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"frigate.record.export.ReviewSegment.select",
|
||||||
|
return_value=self._fake_select_returning([in_progress]),
|
||||||
|
):
|
||||||
|
result = exporter._build_chapter_metadata_file(recordings)
|
||||||
|
|
||||||
|
assert result == chapter_path
|
||||||
|
with open(chapter_path) as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Output time is windows[-1][1] - windows[-1][0] = 150s.
|
||||||
|
# Review starts at wall=1100, output offset = 100s -> 100000ms.
|
||||||
|
# Clamped end = last_recorded_end (1150) -> output offset = 150s -> 150000ms.
|
||||||
|
assert "[CHAPTER]" in content
|
||||||
|
assert "START=100000" in content
|
||||||
|
assert "END=150000" in content
|
||||||
|
assert "title=Alert: person" in content
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user