mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-06 14:52:47 +03:00
Miscellaneous fixes (0.18) (#24567)
CI / Assemble and push default build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
* run recording snapshot ffmpeg off the event loop The recording snapshot and Frigate+ recording submit endpoints called get_image_from_recording inline, which is a blocking ffmpeg subprocess with no timeout, so every other request stalled until the frame was decoded. Both calls now run in asyncio.to_thread. * fix camera info dialog overflowing on mobile * adjust test
This commit is contained in:
+13
-4
@@ -351,8 +351,13 @@ async def get_snapshot_from_recording(
|
|||||||
mime_type = "png" if format == "png" else "jpeg"
|
mime_type = "png" if format == "png" else "jpeg"
|
||||||
config: FrigateConfig = request.app.frigate_config
|
config: FrigateConfig = request.app.frigate_config
|
||||||
|
|
||||||
image_data = get_image_from_recording(
|
image_data = await asyncio.to_thread(
|
||||||
config.ffmpeg, recording.path, time_in_segment, codec, height
|
get_image_from_recording,
|
||||||
|
config.ffmpeg,
|
||||||
|
recording.path,
|
||||||
|
time_in_segment,
|
||||||
|
codec,
|
||||||
|
height,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not image_data:
|
if not image_data:
|
||||||
@@ -406,8 +411,12 @@ async def submit_recording_snapshot_to_plus(
|
|||||||
config: FrigateConfig = request.app.frigate_config
|
config: FrigateConfig = request.app.frigate_config
|
||||||
recording: Recordings = recording_query.get()
|
recording: Recordings = recording_query.get()
|
||||||
time_in_segment = frame_time - recording.start_time
|
time_in_segment = frame_time - recording.start_time
|
||||||
image_data = get_image_from_recording(
|
image_data = await asyncio.to_thread(
|
||||||
config.ffmpeg, recording.path, time_in_segment, "png"
|
get_image_from_recording,
|
||||||
|
config.ffmpeg,
|
||||||
|
recording.path,
|
||||||
|
time_in_segment,
|
||||||
|
"png",
|
||||||
)
|
)
|
||||||
|
|
||||||
if not image_data:
|
if not image_data:
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
"""Unit tests for recordings/media API endpoints."""
|
"""Unit tests for recordings/media API endpoints."""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
@@ -527,3 +530,90 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == [{"start_time": 1010, "end_time": 1030}]
|
assert response.json() == [{"start_time": 1010, "end_time": 1030}]
|
||||||
|
|
||||||
|
def _insert_recording(self, id: str, start_time: float, end_time: float) -> None:
|
||||||
|
Recordings.insert(
|
||||||
|
id=id,
|
||||||
|
path=f"/media/recordings/{id}.mp4",
|
||||||
|
camera="front_door",
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
duration=end_time - start_time,
|
||||||
|
motion=0,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
def test_recording_snapshot_does_not_block_event_loop(self):
|
||||||
|
self._insert_recording("snapshot", 1000, 1010)
|
||||||
|
started = threading.Event()
|
||||||
|
release = threading.Event()
|
||||||
|
finished = threading.Event()
|
||||||
|
|
||||||
|
def slow_image(*args):
|
||||||
|
started.set()
|
||||||
|
release.wait(timeout=5)
|
||||||
|
finished.set()
|
||||||
|
return b"jpeg"
|
||||||
|
|
||||||
|
with (
|
||||||
|
AuthTestClient(self.app) as client,
|
||||||
|
patch("frigate.api.media.get_image_from_recording", slow_image),
|
||||||
|
ThreadPoolExecutor(max_workers=1) as executor,
|
||||||
|
):
|
||||||
|
snapshot = executor.submit(
|
||||||
|
client.get, "/front_door/recordings/1005/snapshot.jpg"
|
||||||
|
)
|
||||||
|
self.assertTrue(started.wait(timeout=2))
|
||||||
|
version = client.get("/version")
|
||||||
|
blocked = finished.is_set()
|
||||||
|
release.set()
|
||||||
|
|
||||||
|
self.assertEqual(version.status_code, 200)
|
||||||
|
self.assertFalse(blocked)
|
||||||
|
self.assertEqual(snapshot.result().status_code, 200)
|
||||||
|
|
||||||
|
def test_recording_snapshot_hit_returns_image(self):
|
||||||
|
self._insert_recording("snapshot", 1000, 1010)
|
||||||
|
|
||||||
|
with (
|
||||||
|
AuthTestClient(self.app) as client,
|
||||||
|
patch("frigate.api.media.get_image_from_recording", return_value=b"jpeg"),
|
||||||
|
):
|
||||||
|
response = client.get("/front_door/recordings/1005/snapshot.jpg")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.headers["Content-Type"], "image/jpeg")
|
||||||
|
self.assertEqual(response.content, b"jpeg")
|
||||||
|
|
||||||
|
def test_recording_snapshot_misses_return_404(self):
|
||||||
|
self._insert_recording("snapshot", 1000, 1010)
|
||||||
|
|
||||||
|
with AuthTestClient(self.app) as client:
|
||||||
|
absent = client.get("/front_door/recordings/2000/snapshot.jpg")
|
||||||
|
rounded_miss = client.get("/front_door/recordings/1010.5/snapshot.jpg")
|
||||||
|
|
||||||
|
self.assertEqual(absent.status_code, 404)
|
||||||
|
self.assertEqual(rounded_miss.status_code, 404)
|
||||||
|
|
||||||
|
def test_plus_snapshot_miss_returns_404_without_image_lookup(self):
|
||||||
|
with (
|
||||||
|
AuthTestClient(self.app) as client,
|
||||||
|
patch("frigate.api.media.get_image_from_recording") as image,
|
||||||
|
):
|
||||||
|
response = client.post("/front_door/plus/1005")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
image.assert_not_called()
|
||||||
|
|
||||||
|
def test_plus_snapshot_hit_uploads_image(self):
|
||||||
|
self._insert_recording("snapshot", 1000, 1010)
|
||||||
|
|
||||||
|
with (
|
||||||
|
AuthTestClient(self.app) as client,
|
||||||
|
patch("frigate.api.media.get_image_from_recording", return_value=b"png"),
|
||||||
|
patch("frigate.api.media.cv2.imdecode", return_value="frame"),
|
||||||
|
patch.object(self.app.frigate_config.plus_api, "upload_image") as upload,
|
||||||
|
):
|
||||||
|
response = client.post("/front_door/plus/1005")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
upload.assert_called_once_with("frame", "front_door")
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export default function CameraInfoDialog({
|
|||||||
open={showCameraInfoDialog}
|
open={showCameraInfoDialog}
|
||||||
onOpenChange={setShowCameraInfoDialog}
|
onOpenChange={setShowCameraInfoDialog}
|
||||||
>
|
>
|
||||||
<DialogContent>
|
<DialogContent className="scrollbar-container max-h-[90dvh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="smart-capitalize">
|
<DialogTitle className="smart-capitalize">
|
||||||
{t("cameras.info.cameraProbeInfo", {
|
{t("cameras.info.cameraProbeInfo", {
|
||||||
|
|||||||
Reference in New Issue
Block a user