mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-03 02:21:13 +03:00
Miscellaneous fixes (#23610)
Some checks are pending
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
Some checks are pending
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
* Handle back seeking going to previous clip * scope /recordings/unavailable query to the caller's allowed cameras * listen for config updates in activity manager * don't set search after awaited request Intentionally do NOT setSearch() to mark the open event submitted. This runs after the awaited request, by which point the user may have closed the dialog; re-setting the parent's selected event would resurrect it and the force-open effect would reopen it (see #23599). The local "submitted" state covers the open card, and mutate() updates the events cache so the grid and any future open reflect the result. * fix ruff #23201 removed pathlib import but for some reason it's just now causing ruff to fail --------- Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
parent
ea131e1663
commit
9b02c7318d
@ -3,6 +3,7 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
@ -269,12 +269,12 @@ async def no_recordings(
|
||||
cameras = params.cameras
|
||||
if cameras != "all":
|
||||
requested = set(unquote(cameras).split(","))
|
||||
filtered = requested.intersection(allowed_cameras)
|
||||
if not filtered:
|
||||
return JSONResponse(content=[])
|
||||
cameras = ",".join(filtered)
|
||||
camera_list = list(requested.intersection(allowed_cameras))
|
||||
else:
|
||||
cameras = allowed_cameras
|
||||
camera_list = list(allowed_cameras)
|
||||
|
||||
if not camera_list:
|
||||
return JSONResponse(content=[])
|
||||
|
||||
before = params.before or datetime.datetime.now().timestamp()
|
||||
after = (
|
||||
@ -283,12 +283,10 @@ async def no_recordings(
|
||||
)
|
||||
scale = params.scale
|
||||
|
||||
clauses = [(Recordings.end_time >= after) & (Recordings.start_time <= before)]
|
||||
if cameras != "all":
|
||||
camera_list = cameras.split(",")
|
||||
clauses.append((Recordings.camera << camera_list))
|
||||
else:
|
||||
camera_list = allowed_cameras
|
||||
clauses = [
|
||||
(Recordings.end_time >= after) & (Recordings.start_time <= before),
|
||||
(Recordings.camera << camera_list),
|
||||
]
|
||||
|
||||
# Get recording start times
|
||||
data: list[Recordings] = (
|
||||
|
||||
@ -13,6 +13,10 @@ from frigate.comms.event_metadata_updater import (
|
||||
EventMetadataTypeEnum,
|
||||
)
|
||||
from frigate.config import CameraConfig, FrigateConfig
|
||||
from frigate.config.camera.updater import (
|
||||
CameraConfigUpdateEnum,
|
||||
CameraConfigUpdateSubscriber,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -29,6 +33,11 @@ class CameraActivityManager:
|
||||
self.zone_all_object_counts: dict[str, Counter] = {}
|
||||
self.zone_active_object_counts: dict[str, Counter] = {}
|
||||
self.all_zone_labels: dict[str, set[str]] = {}
|
||||
self.config_subscriber = CameraConfigUpdateSubscriber(
|
||||
config,
|
||||
config.cameras,
|
||||
[CameraConfigUpdateEnum.zones, CameraConfigUpdateEnum.objects],
|
||||
)
|
||||
|
||||
for camera_config in config.cameras.values():
|
||||
if not camera_config.enabled_in_config:
|
||||
@ -56,7 +65,40 @@ class CameraActivityManager:
|
||||
else camera_config.objects.track
|
||||
)
|
||||
|
||||
def __rebuild_zone_labels(self) -> None:
|
||||
"""Rebuild zone label tracking after a runtime zones/objects change."""
|
||||
new_zone_labels: dict[str, set[str]] = {}
|
||||
|
||||
for camera_config in self.config.cameras.values():
|
||||
if not camera_config.enabled_in_config or camera_config.name is None:
|
||||
continue
|
||||
|
||||
for zone, zone_config in camera_config.zones.items():
|
||||
new_zone_labels.setdefault(zone, set()).update(
|
||||
zone_config.objects
|
||||
if zone_config.objects
|
||||
else camera_config.objects.track
|
||||
)
|
||||
|
||||
# drop counters for zones that no longer exist
|
||||
for zone in list(self.zone_all_object_counts.keys()):
|
||||
if zone not in new_zone_labels:
|
||||
self.zone_all_object_counts.pop(zone, None)
|
||||
self.zone_active_object_counts.pop(zone, None)
|
||||
|
||||
# ensure counters exist for new zones so the first count is published
|
||||
for zone in new_zone_labels:
|
||||
self.zone_all_object_counts.setdefault(zone, Counter())
|
||||
self.zone_active_object_counts.setdefault(zone, Counter())
|
||||
|
||||
self.all_zone_labels = new_zone_labels
|
||||
|
||||
def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None:
|
||||
updated_topics = self.config_subscriber.check_for_updates()
|
||||
|
||||
if "zones" in updated_topics or "objects" in updated_topics:
|
||||
self.__rebuild_zone_labels()
|
||||
|
||||
all_objects: list[dict[str, Any]] = []
|
||||
|
||||
for camera in new_activity.keys():
|
||||
@ -161,6 +203,9 @@ class CameraActivityManager:
|
||||
self.publish(f"{camera}/all", sum(list(all_objects.values())))
|
||||
self.publish(f"{camera}/all/active", sum(list(active_objects.values())))
|
||||
|
||||
def stop(self) -> None:
|
||||
self.config_subscriber.stop()
|
||||
|
||||
|
||||
class AudioActivityManager:
|
||||
def __init__(
|
||||
|
||||
@ -397,6 +397,8 @@ class Dispatcher:
|
||||
comm.publish(topic, payload, retain)
|
||||
|
||||
def stop(self) -> None:
|
||||
self.camera_activity.stop()
|
||||
|
||||
for comm in self.comms:
|
||||
comm.stop()
|
||||
|
||||
|
||||
@ -475,3 +475,55 @@ class TestHttpMedia(BaseTestHttp):
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
def test_recordings_unavailable_cameras_all_scopes_to_allowed_cameras(self):
|
||||
"""cameras=all must not error and must only consider allowed cameras.
|
||||
|
||||
allowed_cameras is mocked to ["front_door"]. A back_door recording that
|
||||
would otherwise fill the gap must be ignored, and the request must not
|
||||
500 the way it did when cameras was reassigned to a list.
|
||||
"""
|
||||
with AuthTestClient(self.app) as client:
|
||||
# front_door has a 20s gap (1010-1030).
|
||||
Recordings.insert(
|
||||
id="front_a",
|
||||
path="/media/recordings/front_a.mp4",
|
||||
camera="front_door",
|
||||
start_time=1000,
|
||||
end_time=1010,
|
||||
duration=10,
|
||||
motion=0,
|
||||
).execute()
|
||||
Recordings.insert(
|
||||
id="front_b",
|
||||
path="/media/recordings/front_b.mp4",
|
||||
camera="front_door",
|
||||
start_time=1030,
|
||||
end_time=1040,
|
||||
duration=10,
|
||||
motion=0,
|
||||
).execute()
|
||||
# back_door is not in allowed_cameras; its full-window coverage must
|
||||
# not mask the front_door gap.
|
||||
Recordings.insert(
|
||||
id="back_a",
|
||||
path="/media/recordings/back_a.mp4",
|
||||
camera="back_door",
|
||||
start_time=1000,
|
||||
end_time=1040,
|
||||
duration=40,
|
||||
motion=0,
|
||||
).execute()
|
||||
|
||||
response = client.get(
|
||||
"/recordings/unavailable",
|
||||
params={
|
||||
"after": 1000,
|
||||
"before": 1040,
|
||||
"scale": 5,
|
||||
"cameras": "all",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"start_time": 1010, "end_time": 1030}]
|
||||
|
||||
@ -1263,7 +1263,6 @@ function ObjectDetailsTab({
|
||||
}
|
||||
|
||||
setState("submitted");
|
||||
setSearch({ ...search, plus_id: "new_upload" });
|
||||
mutate(
|
||||
(key) => isEventsKey(key),
|
||||
(currentData: SearchResult[][] | SearchResult[] | undefined) =>
|
||||
@ -1286,7 +1285,7 @@ function ObjectDetailsTab({
|
||||
);
|
||||
}
|
||||
},
|
||||
[search, mutate, mapSearchResults, setSearch, isEventsKey, t],
|
||||
[search, mutate, mapSearchResults, isEventsKey, t],
|
||||
);
|
||||
|
||||
const popoverContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@ -47,6 +47,7 @@ type HlsVideoPlayerProps = {
|
||||
frigateControls?: boolean;
|
||||
inpointOffset?: number;
|
||||
onClipEnded?: (currentTime: number) => void;
|
||||
onClipPrevious?: (diff: number) => void;
|
||||
onPlayerLoaded?: () => void;
|
||||
onTimeUpdate?: (time: number) => void;
|
||||
onPlaying?: () => void;
|
||||
@ -74,6 +75,7 @@ export default function HlsVideoPlayer({
|
||||
frigateControls = true,
|
||||
inpointOffset = 0,
|
||||
onClipEnded,
|
||||
onClipPrevious,
|
||||
onPlayerLoaded,
|
||||
onTimeUpdate,
|
||||
onPlaying,
|
||||
@ -339,11 +341,17 @@ export default function HlsVideoPlayer({
|
||||
onSeek={(diff) => {
|
||||
const currentTime = videoRef.current?.currentTime;
|
||||
|
||||
if (!videoRef.current || !currentTime) {
|
||||
if (!videoRef.current || currentTime == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
videoRef.current.currentTime = Math.max(0, currentTime + diff);
|
||||
const newTime = currentTime + diff;
|
||||
|
||||
if (newTime < 0 && onClipPrevious) {
|
||||
onClipPrevious(diff);
|
||||
} else {
|
||||
videoRef.current.currentTime = Math.max(0, newTime);
|
||||
}
|
||||
}}
|
||||
onSetPlaybackRate={(rate) => {
|
||||
setPlaybackRate(rate, true);
|
||||
|
||||
@ -49,6 +49,7 @@ type DynamicVideoPlayerProps = {
|
||||
onControllerReady: (controller: DynamicVideoController) => void;
|
||||
onTimestampUpdate?: (timestamp: number) => void;
|
||||
onClipEnded?: () => void;
|
||||
onClipPrevious?: (diff: number) => void;
|
||||
onSeekToTime?: (timestamp: number, play?: boolean) => void;
|
||||
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
||||
toggleFullscreen: () => void;
|
||||
@ -68,6 +69,7 @@ export default function DynamicVideoPlayer({
|
||||
onControllerReady,
|
||||
onTimestampUpdate,
|
||||
onClipEnded,
|
||||
onClipPrevious,
|
||||
onSeekToTime,
|
||||
setFullResolution,
|
||||
toggleFullscreen,
|
||||
@ -343,6 +345,7 @@ export default function DynamicVideoPlayer({
|
||||
onTimeUpdate={onTimeUpdate}
|
||||
onPlayerLoaded={onPlayerLoaded}
|
||||
onClipEnded={onValidateClipEnd}
|
||||
onClipPrevious={onClipPrevious}
|
||||
onSeekToTime={(timestamp, play) => {
|
||||
if (onSeekToTime) {
|
||||
onSeekToTime(timestamp, play);
|
||||
|
||||
@ -336,6 +336,16 @@ export function RecordingView({
|
||||
[currentTimeRange, updateSelectedSegment],
|
||||
);
|
||||
|
||||
const onClipPrevious = useCallback(
|
||||
(diff: number) => {
|
||||
manuallySetCurrentTime(
|
||||
currentTime + diff,
|
||||
mainControllerRef.current?.isPlaying() ?? false,
|
||||
);
|
||||
},
|
||||
[currentTime, manuallySetCurrentTime],
|
||||
);
|
||||
|
||||
const onShareReviewLink = useCallback(
|
||||
(timestamp: number) => {
|
||||
const reviewUrl = createRecordingReviewUrl(location.pathname, {
|
||||
@ -902,6 +912,7 @@ export function RecordingView({
|
||||
);
|
||||
}}
|
||||
onClipEnded={onClipEnded}
|
||||
onClipPrevious={onClipPrevious}
|
||||
onSeekToTime={manuallySetCurrentTime}
|
||||
onControllerReady={(controller) => {
|
||||
mainControllerRef.current = controller;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user