* Catch error when regex is invalid

* Fix i18n label

* Mobile camera drawer i18n fixes

* additional frame cache debug logs

* Add Romanian

* Fix exports thumbnail path

* Improve clip buffer and remove outdated comments

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
Nicolas Mowen 2025-06-20 16:39:47 -06:00 committed by GitHub
parent 4ff81d5877
commit 8a9ebe9292
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 45 additions and 23 deletions

View File

@ -598,7 +598,7 @@ def recording_clip(
if clip.start_time < start_ts: if clip.start_time < start_ts:
file.write(f"inpoint {int(start_ts - clip.start_time)}\n") file.write(f"inpoint {int(start_ts - clip.start_time)}\n")
# if this is the ending clip and end trim is enabled, add an outpoint # if this is the ending clip, add an outpoint
if clip.end_time > end_ts: if clip.end_time > end_ts:
file.write(f"outpoint {int(end_ts - clip.start_time)}\n") file.write(f"outpoint {int(end_ts - clip.start_time)}\n")

View File

@ -350,6 +350,7 @@ class CameraState:
removed_obj = tracked_objects[id] removed_obj = tracked_objects[id]
if "end_time" not in removed_obj.obj_data: if "end_time" not in removed_obj.obj_data:
removed_obj.obj_data["end_time"] = frame_time removed_obj.obj_data["end_time"] = frame_time
logger.debug(f"{self.name}: end callback for object {id}")
for c in self.callbacks["end"]: for c in self.callbacks["end"]:
c(self.name, removed_obj, frame_name) c(self.name, removed_obj, frame_name)
@ -441,6 +442,17 @@ class CameraState:
for t in self.frame_cache.keys() for t in self.frame_cache.keys()
if t not in current_thumb_frames and t not in current_best_frames if t not in current_thumb_frames and t not in current_best_frames
] ]
if len(thumb_frames_to_delete) > 0:
logger.debug(f"{self.name}: Current frame cache contents:")
for k, v in self.frame_cache.items():
logger.debug(f" frame time: {k}, object id: {v['object_id']}")
for obj_id, obj in tracked_objects.items():
thumb_time = (
obj.thumbnail_data["frame_time"] if obj.thumbnail_data else None
)
logger.debug(
f"{self.name}: Tracked object {obj_id} thumbnail frame_time: {thumb_time}"
)
for t in thumb_frames_to_delete: for t in thumb_frames_to_delete:
object_id = self.frame_cache[t].get("object_id", "unknown") object_id = self.frame_cache[t].get("object_id", "unknown")
logger.debug(f"{self.name}: Deleting {t} from frame cache for {object_id}") logger.debug(f"{self.name}: Deleting {t} from frame cache for {object_id}")

View File

@ -1500,18 +1500,24 @@ class LicensePlateProcessingMixin:
# Determine subLabel based on known plates, use regex matching # Determine subLabel based on known plates, use regex matching
# Default to the detected plate, use label name if there's a match # Default to the detected plate, use label name if there's a match
sub_label = next( try:
( sub_label = next(
label (
for label, plates in self.lpr_config.known_plates.items() label
if any( for label, plates in self.lpr_config.known_plates.items()
re.match(f"^{plate}$", top_plate) if any(
or distance(plate, top_plate) <= self.lpr_config.match_distance re.match(f"^{plate}$", top_plate)
for plate in plates or distance(plate, top_plate) <= self.lpr_config.match_distance
) for plate in plates
), )
None, ),
) None,
)
except re.error:
logger.error(
f"{camera}: Invalid regex in known plates configuration: {self.lpr_config.known_plates}"
)
sub_label = None
# If it's a known plate, publish to sub_label # If it's a known plate, publish to sub_label
if sub_label is not None: if sub_label is not None:

View File

@ -126,7 +126,7 @@ class RecordingExporter(threading.Thread):
minutes = int(diff / 60) minutes = int(diff / 60)
seconds = int(diff % 60) seconds = int(diff % 60)
ffmpeg_cmd = [ ffmpeg_cmd = [
"7.0", "/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",

View File

@ -48,7 +48,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { ReviewSegment } from "@/types/review"; import { REVIEW_PADDING, ReviewSegment } from "@/types/review";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import Chip from "@/components/indicators/Chip"; import Chip from "@/components/indicators/Chip";
import { capitalizeAll } from "@/utils/stringUtil"; import { capitalizeAll } from "@/utils/stringUtil";
@ -1229,11 +1229,14 @@ export function VideoTab({ search }: VideoTabProps) {
const { data: reviewItem } = useSWR<ReviewSegment>([ const { data: reviewItem } = useSWR<ReviewSegment>([
`review/event/${search.id}`, `review/event/${search.id}`,
]); ]);
const endTime = useMemo(() => search.end_time ?? Date.now() / 1000, [search]);
// subtract 2 seconds from start_time to account for keyframes and any differences in the record/detect streams const clipTimeRange = useMemo(() => {
// to help the start of the event from not being completely cut off const startTime = search.start_time - REVIEW_PADDING;
const source = `${baseUrl}vod/${search.camera}/start/${search.start_time - 2}/end/${endTime}/index.m3u8`; const endTime = (search.end_time ?? Date.now() / 1000) + REVIEW_PADDING;
return `start/${startTime}/end/${endTime}`;
}, [search]);
const source = `${baseUrl}vod/${search.camera}/${clipTimeRange}/index.m3u8`;
return ( return (
<> <>
@ -1272,7 +1275,7 @@ export function VideoTab({ search }: VideoTabProps) {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<a <a
download download
href={`${baseUrl}api/${search.camera}/start/${search.start_time}/end/${endTime}/clip.mp4?trim=end`} href={`${baseUrl}api/${search.camera}/${clipTimeRange}/clip.mp4`}
> >
<Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"> <Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500">
<FaDownload className="size-4 text-white" /> <FaDownload className="size-4 text-white" />

View File

@ -6,6 +6,7 @@ export const supportedLanguageKeys = [
"de", "de",
"it", "it",
"ca", "ca",
"ro",
"nl", "nl",
"nb-NO", "nb-NO",
"zh-CN", "zh-CN",

View File

@ -1578,7 +1578,7 @@ function FrigateCameraFeatures({
<div className="mt-3 flex flex-col gap-5"> <div className="mt-3 flex flex-col gap-5">
{!isRestreamed && ( {!isRestreamed && (
<div className="flex flex-col gap-2 p-2"> <div className="flex flex-col gap-2 p-2">
<Label>{t("streaming.title", { ns: "components/dialog" })}</Label> <Label>{t("stream.title")}</Label>
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground"> <div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<LuX className="size-4 text-danger" /> <LuX className="size-4 text-danger" />
<div> <div>
@ -1596,7 +1596,7 @@ function FrigateCameraFeatures({
</div> </div>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-80 text-xs"> <PopoverContent className="w-80 text-xs">
{t("streaming.restreaming.desc", { {t("streaming.restreaming.desc.title", {
ns: "components/dialog", ns: "components/dialog",
})} })}
<div className="mt-2 flex items-center text-primary"> <div className="mt-2 flex items-center text-primary">
@ -1606,7 +1606,7 @@ function FrigateCameraFeatures({
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline" className="inline"
> >
{t("streaming.restreaming.readTheDocumentation", { {t("streaming.restreaming.desc.readTheDocumentation", {
ns: "components/dialog", ns: "components/dialog",
})} })}
<LuExternalLink className="ml-2 inline-flex size-3" /> <LuExternalLink className="ml-2 inline-flex size-3" />