mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-11-01 03:10:33 +03:00
Compare commits
2
Commits
f8a666b44e
...
a57b00fa5d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a57b00fa5d | ||
|
|
9b02c7318d |
@@ -3,6 +3,7 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from ruamel.yaml import YAML
|
from ruamel.yaml import YAML
|
||||||
|
|||||||
+9
-11
@@ -269,12 +269,12 @@ async def no_recordings(
|
|||||||
cameras = params.cameras
|
cameras = params.cameras
|
||||||
if cameras != "all":
|
if cameras != "all":
|
||||||
requested = set(unquote(cameras).split(","))
|
requested = set(unquote(cameras).split(","))
|
||||||
filtered = requested.intersection(allowed_cameras)
|
camera_list = list(requested.intersection(allowed_cameras))
|
||||||
if not filtered:
|
|
||||||
return JSONResponse(content=[])
|
|
||||||
cameras = ",".join(filtered)
|
|
||||||
else:
|
else:
|
||||||
cameras = allowed_cameras
|
camera_list = list(allowed_cameras)
|
||||||
|
|
||||||
|
if not camera_list:
|
||||||
|
return JSONResponse(content=[])
|
||||||
|
|
||||||
before = params.before or datetime.datetime.now().timestamp()
|
before = params.before or datetime.datetime.now().timestamp()
|
||||||
after = (
|
after = (
|
||||||
@@ -283,12 +283,10 @@ async def no_recordings(
|
|||||||
)
|
)
|
||||||
scale = params.scale
|
scale = params.scale
|
||||||
|
|
||||||
clauses = [(Recordings.end_time >= after) & (Recordings.start_time <= before)]
|
clauses = [
|
||||||
if cameras != "all":
|
(Recordings.end_time >= after) & (Recordings.start_time <= before),
|
||||||
camera_list = cameras.split(",")
|
(Recordings.camera << camera_list),
|
||||||
clauses.append((Recordings.camera << camera_list))
|
]
|
||||||
else:
|
|
||||||
camera_list = allowed_cameras
|
|
||||||
|
|
||||||
# Get recording start times
|
# Get recording start times
|
||||||
data: list[Recordings] = (
|
data: list[Recordings] = (
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ from frigate.comms.event_metadata_updater import (
|
|||||||
EventMetadataTypeEnum,
|
EventMetadataTypeEnum,
|
||||||
)
|
)
|
||||||
from frigate.config import CameraConfig, FrigateConfig
|
from frigate.config import CameraConfig, FrigateConfig
|
||||||
|
from frigate.config.camera.updater import (
|
||||||
|
CameraConfigUpdateEnum,
|
||||||
|
CameraConfigUpdateSubscriber,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -29,6 +33,11 @@ class CameraActivityManager:
|
|||||||
self.zone_all_object_counts: dict[str, Counter] = {}
|
self.zone_all_object_counts: dict[str, Counter] = {}
|
||||||
self.zone_active_object_counts: dict[str, Counter] = {}
|
self.zone_active_object_counts: dict[str, Counter] = {}
|
||||||
self.all_zone_labels: dict[str, set[str]] = {}
|
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():
|
for camera_config in config.cameras.values():
|
||||||
if not camera_config.enabled_in_config:
|
if not camera_config.enabled_in_config:
|
||||||
@@ -56,7 +65,40 @@ class CameraActivityManager:
|
|||||||
else camera_config.objects.track
|
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:
|
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]] = []
|
all_objects: list[dict[str, Any]] = []
|
||||||
|
|
||||||
for camera in new_activity.keys():
|
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", sum(list(all_objects.values())))
|
||||||
self.publish(f"{camera}/all/active", sum(list(active_objects.values())))
|
self.publish(f"{camera}/all/active", sum(list(active_objects.values())))
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
self.config_subscriber.stop()
|
||||||
|
|
||||||
|
|
||||||
class AudioActivityManager:
|
class AudioActivityManager:
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
@@ -397,6 +397,8 @@ class Dispatcher:
|
|||||||
comm.publish(topic, payload, retain)
|
comm.publish(topic, payload, retain)
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
|
self.camera_activity.stop()
|
||||||
|
|
||||||
for comm in self.comms:
|
for comm in self.comms:
|
||||||
comm.stop()
|
comm.stop()
|
||||||
|
|
||||||
|
|||||||
@@ -475,3 +475,55 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == []
|
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");
|
setState("submitted");
|
||||||
setSearch({ ...search, plus_id: "new_upload" });
|
|
||||||
mutate(
|
mutate(
|
||||||
(key) => isEventsKey(key),
|
(key) => isEventsKey(key),
|
||||||
(currentData: SearchResult[][] | SearchResult[] | undefined) =>
|
(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);
|
const popoverContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ type HlsVideoPlayerProps = {
|
|||||||
frigateControls?: boolean;
|
frigateControls?: boolean;
|
||||||
inpointOffset?: number;
|
inpointOffset?: number;
|
||||||
onClipEnded?: (currentTime: number) => void;
|
onClipEnded?: (currentTime: number) => void;
|
||||||
|
onClipPrevious?: (diff: number) => void;
|
||||||
onPlayerLoaded?: () => void;
|
onPlayerLoaded?: () => void;
|
||||||
onTimeUpdate?: (time: number) => void;
|
onTimeUpdate?: (time: number) => void;
|
||||||
onPlaying?: () => void;
|
onPlaying?: () => void;
|
||||||
@@ -74,6 +75,7 @@ export default function HlsVideoPlayer({
|
|||||||
frigateControls = true,
|
frigateControls = true,
|
||||||
inpointOffset = 0,
|
inpointOffset = 0,
|
||||||
onClipEnded,
|
onClipEnded,
|
||||||
|
onClipPrevious,
|
||||||
onPlayerLoaded,
|
onPlayerLoaded,
|
||||||
onTimeUpdate,
|
onTimeUpdate,
|
||||||
onPlaying,
|
onPlaying,
|
||||||
@@ -339,11 +341,17 @@ export default function HlsVideoPlayer({
|
|||||||
onSeek={(diff) => {
|
onSeek={(diff) => {
|
||||||
const currentTime = videoRef.current?.currentTime;
|
const currentTime = videoRef.current?.currentTime;
|
||||||
|
|
||||||
if (!videoRef.current || !currentTime) {
|
if (!videoRef.current || currentTime == undefined) {
|
||||||
return;
|
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) => {
|
onSetPlaybackRate={(rate) => {
|
||||||
setPlaybackRate(rate, true);
|
setPlaybackRate(rate, true);
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ type DynamicVideoPlayerProps = {
|
|||||||
onControllerReady: (controller: DynamicVideoController) => void;
|
onControllerReady: (controller: DynamicVideoController) => void;
|
||||||
onTimestampUpdate?: (timestamp: number) => void;
|
onTimestampUpdate?: (timestamp: number) => void;
|
||||||
onClipEnded?: () => void;
|
onClipEnded?: () => void;
|
||||||
|
onClipPrevious?: (diff: number) => void;
|
||||||
onSeekToTime?: (timestamp: number, play?: boolean) => void;
|
onSeekToTime?: (timestamp: number, play?: boolean) => void;
|
||||||
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
||||||
toggleFullscreen: () => void;
|
toggleFullscreen: () => void;
|
||||||
@@ -68,6 +69,7 @@ export default function DynamicVideoPlayer({
|
|||||||
onControllerReady,
|
onControllerReady,
|
||||||
onTimestampUpdate,
|
onTimestampUpdate,
|
||||||
onClipEnded,
|
onClipEnded,
|
||||||
|
onClipPrevious,
|
||||||
onSeekToTime,
|
onSeekToTime,
|
||||||
setFullResolution,
|
setFullResolution,
|
||||||
toggleFullscreen,
|
toggleFullscreen,
|
||||||
@@ -343,6 +345,7 @@ export default function DynamicVideoPlayer({
|
|||||||
onTimeUpdate={onTimeUpdate}
|
onTimeUpdate={onTimeUpdate}
|
||||||
onPlayerLoaded={onPlayerLoaded}
|
onPlayerLoaded={onPlayerLoaded}
|
||||||
onClipEnded={onValidateClipEnd}
|
onClipEnded={onValidateClipEnd}
|
||||||
|
onClipPrevious={onClipPrevious}
|
||||||
onSeekToTime={(timestamp, play) => {
|
onSeekToTime={(timestamp, play) => {
|
||||||
if (onSeekToTime) {
|
if (onSeekToTime) {
|
||||||
onSeekToTime(timestamp, play);
|
onSeekToTime(timestamp, play);
|
||||||
|
|||||||
@@ -336,6 +336,16 @@ export function RecordingView({
|
|||||||
[currentTimeRange, updateSelectedSegment],
|
[currentTimeRange, updateSelectedSegment],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onClipPrevious = useCallback(
|
||||||
|
(diff: number) => {
|
||||||
|
manuallySetCurrentTime(
|
||||||
|
currentTime + diff,
|
||||||
|
mainControllerRef.current?.isPlaying() ?? false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[currentTime, manuallySetCurrentTime],
|
||||||
|
);
|
||||||
|
|
||||||
const onShareReviewLink = useCallback(
|
const onShareReviewLink = useCallback(
|
||||||
(timestamp: number) => {
|
(timestamp: number) => {
|
||||||
const reviewUrl = createRecordingReviewUrl(location.pathname, {
|
const reviewUrl = createRecordingReviewUrl(location.pathname, {
|
||||||
@@ -902,6 +912,7 @@ export function RecordingView({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onClipEnded={onClipEnded}
|
onClipEnded={onClipEnded}
|
||||||
|
onClipPrevious={onClipPrevious}
|
||||||
onSeekToTime={manuallySetCurrentTime}
|
onSeekToTime={manuallySetCurrentTime}
|
||||||
onControllerReady={(controller) => {
|
onControllerReady={(controller) => {
|
||||||
mainControllerRef.current = controller;
|
mainControllerRef.current = controller;
|
||||||
|
|||||||
Reference in New Issue
Block a user