Compare commits

..

10 Commits

Author SHA1 Message Date
Josh Hawkins
71f1ed3cad update gemini docs 2026-01-17 19:58:19 -06:00
Josh Hawkins
bfe46d9f4a use export id for key instead of name 2026-01-17 16:18:43 -06:00
Josh Hawkins
4dd2567848 add test for percentage based zone filters 2026-01-17 15:33:07 -06:00
Josh Hawkins
208a83cf79 ensure all zone filters are converted to pixels
zone-level filters were never converted from percentage area to pixels. RuntimeFilterConfig was only applied to filters at the camera level, not zone.filters.

Fixes https://github.com/blakeblackshear/frigate/discussions/21694
2026-01-17 15:32:49 -06:00
Josh Hawkins
af942fb64e ensure users only see recognized plates from accessible cameras in explore 2026-01-17 14:56:18 -06:00
Josh Hawkins
c78d020cca i18n fix 2026-01-17 07:16:44 -06:00
Josh Hawkins
4fcf467507 add retry params to gemini 2026-01-16 10:08:11 -06:00
Josh Hawkins
dcff06906c lpr docs tweaks 2026-01-16 06:57:44 -06:00
Josh Hawkins
f428a64948 update copilot instructions 2026-01-15 20:42:24 -06:00
Josh Hawkins
32f6114573 misc triggers tweaks
i18n fixes
fix toaster color
fix clicking on labels selecting incorrect checkbox
2026-01-15 20:41:39 -06:00
5 changed files with 6 additions and 104 deletions

View File

@ -11,12 +11,6 @@ Cameras configured to output H.264 video and AAC audio will offer the most compa
- **Stream Viewing**: This stream will be rebroadcast as is to Home Assistant for viewing with the stream component. Setting this resolution too high will use significant bandwidth when viewing streams in Home Assistant, and they may not load reliably over slower connections. - **Stream Viewing**: This stream will be rebroadcast as is to Home Assistant for viewing with the stream component. Setting this resolution too high will use significant bandwidth when viewing streams in Home Assistant, and they may not load reliably over slower connections.
:::tip
For the best experience in Frigate's UI, configure your camera so that the detection and recording streams use the same aspect ratio. For example, if your main stream is 3840x2160 (16:9), set your substream to 640x360 (also 16:9) instead of 640x480 (4:3). While not strictly required, matching aspect ratios helps ensure seamless live stream display and preview/recordings playback.
:::
### Choosing a detect resolution ### Choosing a detect resolution
The ideal resolution for detection is one where the objects you want to detect fit inside the dimensions of the model used by Frigate (320x320). Frigate does not pass the entire camera frame to object detection. It will crop an area of motion from the full frame and look in that portion of the frame. If the area being inspected is larger than 320x320, Frigate must resize it before running object detection. Higher resolutions do not improve the detection accuracy because the additional detail is lost in the resize. Below you can see a reference for how large a 320x320 area is against common resolutions. The ideal resolution for detection is one where the objects you want to detect fit inside the dimensions of the model used by Frigate (320x320). Frigate does not pass the entire camera frame to object detection. It will crop an area of motion from the full frame and look in that portion of the frame. If the area being inspected is larger than 320x320, Frigate must resize it before running object detection. Higher resolutions do not improve the detection accuracy because the additional detail is lost in the resize. Below you can see a reference for how large a 320x320 area is against common resolutions.

View File

@ -97,7 +97,6 @@ class RecordingMaintainer(threading.Thread):
self.object_recordings_info: dict[str, list] = defaultdict(list) self.object_recordings_info: dict[str, list] = defaultdict(list)
self.audio_recordings_info: dict[str, list] = defaultdict(list) self.audio_recordings_info: dict[str, list] = defaultdict(list)
self.end_time_cache: dict[str, Tuple[datetime.datetime, float]] = {} self.end_time_cache: dict[str, Tuple[datetime.datetime, float]] = {}
self.unexpected_cache_files_logged: bool = False
async def move_files(self) -> None: async def move_files(self) -> None:
cache_files = [ cache_files = [
@ -113,14 +112,7 @@ class RecordingMaintainer(threading.Thread):
for cache in cache_files: for cache in cache_files:
cache_path = os.path.join(CACHE_DIR, cache) cache_path = os.path.join(CACHE_DIR, cache)
basename = os.path.splitext(cache)[0] basename = os.path.splitext(cache)[0]
try: camera, date = basename.rsplit("@", maxsplit=1)
camera, date = basename.rsplit("@", maxsplit=1)
except ValueError:
if not self.unexpected_cache_files_logged:
logger.warning("Skipping unexpected files in cache")
self.unexpected_cache_files_logged = True
continue
start_time = datetime.datetime.strptime( start_time = datetime.datetime.strptime(
date, CACHE_SEGMENT_FORMAT date, CACHE_SEGMENT_FORMAT
).astimezone(datetime.timezone.utc) ).astimezone(datetime.timezone.utc)
@ -172,13 +164,7 @@ class RecordingMaintainer(threading.Thread):
cache_path = os.path.join(CACHE_DIR, cache) cache_path = os.path.join(CACHE_DIR, cache)
basename = os.path.splitext(cache)[0] basename = os.path.splitext(cache)[0]
try: camera, date = basename.rsplit("@", maxsplit=1)
camera, date = basename.rsplit("@", maxsplit=1)
except ValueError:
if not self.unexpected_cache_files_logged:
logger.warning("Skipping unexpected files in cache")
self.unexpected_cache_files_logged = True
continue
# important that start_time is utc because recordings are stored and compared in utc # important that start_time is utc because recordings are stored and compared in utc
start_time = datetime.datetime.strptime( start_time = datetime.datetime.strptime(

View File

@ -1,66 +0,0 @@
import sys
import unittest
from unittest.mock import MagicMock, patch
# Mock complex imports before importing maintainer
sys.modules["frigate.comms.inter_process"] = MagicMock()
sys.modules["frigate.comms.detections_updater"] = MagicMock()
sys.modules["frigate.comms.recordings_updater"] = MagicMock()
sys.modules["frigate.config.camera.updater"] = MagicMock()
# Now import the class under test
from frigate.config import FrigateConfig # noqa: E402
from frigate.record.maintainer import RecordingMaintainer # noqa: E402
class TestMaintainer(unittest.IsolatedAsyncioTestCase):
async def test_move_files_survives_bad_filename(self):
config = MagicMock(spec=FrigateConfig)
config.cameras = {}
stop_event = MagicMock()
maintainer = RecordingMaintainer(config, stop_event)
# We need to mock end_time_cache to avoid key errors if logic proceeds
maintainer.end_time_cache = {}
# Mock filesystem
# One bad file, one good file
files = ["bad_filename.mp4", "camera@20210101000000+0000.mp4"]
with patch("os.listdir", return_value=files):
with patch("os.path.isfile", return_value=True):
with patch(
"frigate.record.maintainer.psutil.process_iter", return_value=[]
):
with patch("frigate.record.maintainer.logger.warning") as warn:
# Mock validate_and_move_segment to avoid further logic
maintainer.validate_and_move_segment = MagicMock()
try:
await maintainer.move_files()
except ValueError as e:
if "not enough values to unpack" in str(e):
self.fail("move_files() crashed on bad filename!")
raise e
except Exception:
# Ignore other errors (like DB connection) as we only care about the unpack crash
pass
# The bad filename is encountered in multiple loops, but should only warn once.
matching = [
c
for c in warn.call_args_list
if c.args
and isinstance(c.args[0], str)
and "Skipping unexpected files in cache" in c.args[0]
]
self.assertEqual(
1,
len(matching),
f"Expected a single warning for unexpected files, got {len(matching)}",
)
if __name__ == "__main__":
unittest.main()

View File

@ -13,7 +13,7 @@ import HlsVideoPlayer from "@/components/player/HlsVideoPlayer";
import { baseUrl } from "@/api/baseUrl"; import { baseUrl } from "@/api/baseUrl";
import { REVIEW_PADDING } from "@/types/review"; import { REVIEW_PADDING } from "@/types/review";
import { import {
ASPECT_PORTRAIT_LAYOUT, ASPECT_VERTICAL_LAYOUT,
ASPECT_WIDE_LAYOUT, ASPECT_WIDE_LAYOUT,
Recording, Recording,
} from "@/types/record"; } from "@/types/record";
@ -39,7 +39,6 @@ import { useApiHost } from "@/api";
import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator"; import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator";
import ObjectTrackOverlay from "../ObjectTrackOverlay"; import ObjectTrackOverlay from "../ObjectTrackOverlay";
import { useIsAdmin } from "@/hooks/use-is-admin"; import { useIsAdmin } from "@/hooks/use-is-admin";
import { VideoResolutionType } from "@/types/live";
type TrackingDetailsProps = { type TrackingDetailsProps = {
className?: string; className?: string;
@ -254,25 +253,16 @@ export function TrackingDetails({
const [timelineSize] = useResizeObserver(timelineContainerRef); const [timelineSize] = useResizeObserver(timelineContainerRef);
const [fullResolution, setFullResolution] = useState<VideoResolutionType>({
width: 0,
height: 0,
});
const aspectRatio = useMemo(() => { const aspectRatio = useMemo(() => {
if (!config) { if (!config) {
return 16 / 9; return 16 / 9;
} }
if (fullResolution.width && fullResolution.height) {
return fullResolution.width / fullResolution.height;
}
return ( return (
config.cameras[event.camera].detect.width / config.cameras[event.camera].detect.width /
config.cameras[event.camera].detect.height config.cameras[event.camera].detect.height
); );
}, [config, event, fullResolution]); }, [config, event]);
const label = event.sub_label const label = event.sub_label
? event.sub_label ? event.sub_label
@ -470,7 +460,7 @@ export function TrackingDetails({
return "normal"; return "normal";
} else if (aspectRatio > ASPECT_WIDE_LAYOUT) { } else if (aspectRatio > ASPECT_WIDE_LAYOUT) {
return "wide"; return "wide";
} else if (aspectRatio < ASPECT_PORTRAIT_LAYOUT) { } else if (aspectRatio < ASPECT_VERTICAL_LAYOUT) {
return "tall"; return "tall";
} else { } else {
return "normal"; return "normal";
@ -566,7 +556,6 @@ export function TrackingDetails({
onSeekToTime={handleSeekToTime} onSeekToTime={handleSeekToTime}
onUploadFrame={onUploadFrameToPlus} onUploadFrame={onUploadFrameToPlus}
onPlaying={() => setIsVideoLoading(false)} onPlaying={() => setIsVideoLoading(false)}
setFullResolution={setFullResolution}
isDetailMode={true} isDetailMode={true}
camera={event.camera} camera={event.camera}
currentTimeOverride={currentTime} currentTimeOverride={currentTime}
@ -634,7 +623,7 @@ export function TrackingDetails({
<div <div
className={cn( className={cn(
isDesktop && "justify-start overflow-hidden", isDesktop && "justify-start overflow-hidden",
aspectRatio > 1 && aspectRatio < ASPECT_PORTRAIT_LAYOUT aspectRatio > 1 && aspectRatio < 1.5
? "lg:basis-3/5" ? "lg:basis-3/5"
: "lg:basis-2/5", : "lg:basis-2/5",
)} )}

View File

@ -44,5 +44,4 @@ export type RecordingStartingPoint = {
export type RecordingPlayerError = "stalled" | "startup"; export type RecordingPlayerError = "stalled" | "startup";
export const ASPECT_VERTICAL_LAYOUT = 1.5; export const ASPECT_VERTICAL_LAYOUT = 1.5;
export const ASPECT_PORTRAIT_LAYOUT = 1.333;
export const ASPECT_WIDE_LAYOUT = 2; export const ASPECT_WIDE_LAYOUT = 2;