Debug replay fixes (#23276)

* filter replay camera from camera selectors

* add face rec and lpr to replay configuration sheet

* add missing config topic subscriptions in embeddings maintainer

* pop replay camera from config object when stopping
This commit is contained in:
Josh Hawkins 2026-05-21 09:12:53 -05:00 committed by GitHub
parent 01c82d6921
commit 555ef89800
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 119 additions and 32 deletions

View File

@ -169,6 +169,7 @@ class DebugReplayManager:
CameraConfigUpdateTopic(CameraConfigUpdateEnum.remove, replay_name), CameraConfigUpdateTopic(CameraConfigUpdateEnum.remove, replay_name),
frigate_config.cameras[replay_name], frigate_config.cameras[replay_name],
) )
frigate_config.cameras.pop(replay_name, None)
if replay_name is not None: if replay_name is not None:
self._cleanup_db(replay_name) self._cleanup_db(replay_name)

View File

@ -98,10 +98,17 @@ class EmbeddingMaintainer(threading.Thread):
[ [
CameraConfigUpdateEnum.add, CameraConfigUpdateEnum.add,
CameraConfigUpdateEnum.remove, CameraConfigUpdateEnum.remove,
CameraConfigUpdateEnum.detect,
CameraConfigUpdateEnum.face_recognition,
CameraConfigUpdateEnum.ffmpeg,
CameraConfigUpdateEnum.lpr,
CameraConfigUpdateEnum.motion,
CameraConfigUpdateEnum.objects,
CameraConfigUpdateEnum.object_genai, CameraConfigUpdateEnum.object_genai,
CameraConfigUpdateEnum.review, CameraConfigUpdateEnum.review,
CameraConfigUpdateEnum.review_genai, CameraConfigUpdateEnum.review_genai,
CameraConfigUpdateEnum.semantic_search, CameraConfigUpdateEnum.semantic_search,
CameraConfigUpdateEnum.zones,
], ],
) )
self.enrichment_config_subscriber = ConfigSubscriber("config/") self.enrichment_config_subscriber = ConfigSubscriber("config/")

View File

@ -14,6 +14,7 @@ import Konva from "konva";
import { useResizeObserver } from "@/hooks/resize-observer"; import { useResizeObserver } from "@/hooks/resize-observer";
import { useApiHost } from "@/api"; import { useApiHost } from "@/api";
import { resolveCameraName } from "@/hooks/use-camera-friendly-name"; import { resolveCameraName } from "@/hooks/use-camera-friendly-name";
import { isReplayCamera } from "@/utils/cameraUtil";
import Heading from "@/components/ui/heading"; import Heading from "@/components/ui/heading";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -67,6 +68,7 @@ export default function Step2StateArea({
([name, cam]) => ([name, cam]) =>
cam.enabled && cam.enabled &&
cam.enabled_in_config && cam.enabled_in_config &&
!isReplayCamera(name) &&
!selectedCameraNames.includes(name), !selectedCameraNames.includes(name),
) )
.map(([name]) => ({ .map(([name]) => ({

View File

@ -57,6 +57,7 @@ import isEqual from "lodash/isEqual";
import set from "lodash/set"; import set from "lodash/set";
import type { ConfigSectionData, JsonObject } from "@/types/configForm"; import type { ConfigSectionData, JsonObject } from "@/types/configForm";
import { sanitizeSectionData } from "@/utils/configUtil"; import { sanitizeSectionData } from "@/utils/configUtil";
import { isReplayCamera } from "@/utils/cameraUtil";
import type { SectionRendererProps } from "./registry"; import type { SectionRendererProps } from "./registry";
const NOTIFICATION_SERVICE_WORKER = "/notifications-worker.js"; const NOTIFICATION_SERVICE_WORKER = "/notifications-worker.js";
@ -94,7 +95,7 @@ export default function NotificationsSettingsExtras({
return Object.values(config.cameras) return Object.values(config.cameras)
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order) .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order)
.filter((c) => c.enabled_in_config); .filter((c) => c.enabled_in_config && !isReplayCamera(c.name));
}, [config]); }, [config]);
const notificationCameras = useMemo(() => { const notificationCameras = useMemo(() => {
@ -106,6 +107,7 @@ export default function NotificationsSettingsExtras({
.filter( .filter(
(conf) => (conf) =>
conf.enabled_in_config && conf.enabled_in_config &&
!isReplayCamera(conf.name) &&
conf.notifications && conf.notifications &&
conf.notifications.enabled_in_config, conf.notifications.enabled_in_config,
) )
@ -359,6 +361,7 @@ export default function NotificationsSettingsExtras({
Object.values(config.cameras).some( Object.values(config.cameras).some(
(c) => (c) =>
c.enabled_in_config && c.enabled_in_config &&
!isReplayCamera(c.name) &&
c.notifications && c.notifications &&
c.notifications.enabled_in_config, c.notifications.enabled_in_config,
), ),

View File

@ -26,6 +26,7 @@ import {
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { CameraNameLabel } from "../camera/FriendlyNameLabel"; import { CameraNameLabel } from "../camera/FriendlyNameLabel";
import { isReplayCamera } from "@/utils/cameraUtil";
import { isDesktop, isMobile } from "react-device-detect"; import { isDesktop, isMobile } from "react-device-detect";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import {
@ -52,7 +53,9 @@ export default function CreateRoleDialog({
const { t } = useTranslation(["views/settings"]); const { t } = useTranslation(["views/settings"]);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const cameras = Object.keys(config.cameras || {}); const cameras = Object.keys(config.cameras || {}).filter(
(name) => !isReplayCamera(name),
);
const existingRoles = Object.keys(config.auth?.roles || {}); const existingRoles = Object.keys(config.auth?.roles || {});

View File

@ -25,6 +25,7 @@ import {
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
import { isReplayCamera } from "@/utils/cameraUtil";
type EditRoleCamerasOverlayProps = { type EditRoleCamerasOverlayProps = {
show: boolean; show: boolean;
@ -46,7 +47,9 @@ export default function EditRoleCamerasDialog({
const { t } = useTranslation(["views/settings"]); const { t } = useTranslation(["views/settings"]);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const cameras = Object.keys(config.cameras || {}); const cameras = Object.keys(config.cameras || {}).filter(
(name) => !isReplayCamera(name),
);
const formSchema = z.object({ const formSchema = z.object({
cameras: z cameras: z

View File

@ -54,6 +54,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import { Textarea } from "../ui/textarea"; import { Textarea } from "../ui/textarea";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useIsAdmin } from "@/hooks/use-is-admin"; import { useIsAdmin } from "@/hooks/use-is-admin";
import { isReplayCamera } from "@/utils/cameraUtil";
const EXPORT_OPTIONS = [ const EXPORT_OPTIONS = [
"1", "1",
@ -448,7 +449,9 @@ export function ExportContent({
); );
const cameraActivities = useMemo<CameraActivity[]>(() => { const cameraActivities = useMemo<CameraActivity[]>(() => {
const allCameraIds = Object.keys(config?.cameras ?? {}); const allCameraIds = Object.keys(config?.cameras ?? {}).filter(
(name) => !isReplayCamera(name),
);
const byCamera = new Map<string, Event[]>(); const byCamera = new Map<string, Event[]>();
events?.forEach((event) => { events?.forEach((event) => {

View File

@ -13,6 +13,7 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { isReplayCamera } from "@/utils/cameraUtil";
import { useTimezone } from "@/hooks/use-date-utils"; import { useTimezone } from "@/hooks/use-date-utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { LuX } from "react-icons/lu"; import { LuX } from "react-icons/lu";
@ -36,11 +37,16 @@ export default function ObjectPathPlotter() {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const eventsPerPage = 20; const eventsPerPage = 20;
const cameraNames = useMemo(() => {
if (!config) return [];
return Object.keys(config.cameras).filter((name) => !isReplayCamera(name));
}, [config]);
useEffect(() => { useEffect(() => {
if (config && !selectedCamera) { if (cameraNames.length > 0 && !selectedCamera) {
setSelectedCamera(Object.keys(config.cameras)[0]); setSelectedCamera(cameraNames[0]);
} }
}, [config, selectedCamera]); }, [cameraNames, selectedCamera]);
const searchQuery = useMemo(() => { const searchQuery = useMemo(() => {
if (!selectedCamera) return null; if (!selectedCamera) return null;
@ -143,12 +149,11 @@ export default function ObjectPathPlotter() {
<SelectValue placeholder="Select camera" /> <SelectValue placeholder="Select camera" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{config && {cameraNames.map((cameraName) => (
Object.keys(config.cameras).map((cameraName) => ( <SelectItem key={cameraName} value={cameraName}>
<SelectItem key={cameraName} value={cameraName}> {cameraName}
{cameraName} </SelectItem>
</SelectItem> ))}
))}
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={timeRange} onValueChange={setTimeRange}> <Select value={timeRange} onValueChange={setTimeRange}>

View File

@ -18,6 +18,7 @@ import {
} from "@/utils/configUtil"; } from "@/utils/configUtil";
import { extractSectionSchema } from "@/hooks/use-config-schema"; import { extractSectionSchema } from "@/hooks/use-config-schema";
import { applySchemaDefaults } from "@/lib/config-schema"; import { applySchemaDefaults } from "@/lib/config-schema";
import { isReplayCamera } from "@/utils/cameraUtil";
const INTERNAL_FIELD_SUFFIXES = ["enabled_in_config", "raw_mask"]; const INTERNAL_FIELD_SUFFIXES = ["enabled_in_config", "raw_mask"];
@ -602,9 +603,13 @@ function getEffectiveGlobalBaseline(
return normalizeConfigValue(defaults as JsonValue); return normalizeConfigValue(defaults as JsonValue);
} }
} }
const cameraSectionValues = Object.keys(config.cameras ?? {}).map((name) => const cameraSectionValues = Object.keys(config.cameras ?? {})
normalizeConfigValue(getBaseCameraSectionValue(config, name, sectionPath)), .filter((name) => !isReplayCamera(name))
); .map((name) =>
normalizeConfigValue(
getBaseCameraSectionValue(config, name, sectionPath),
),
);
return deriveSyntheticGlobalValue(cameraSectionValues, compareFields); return deriveSyntheticGlobalValue(cameraSectionValues, compareFields);
} }
@ -684,7 +689,9 @@ export function useCamerasOverridingSection(
const sectionMeta = OVERRIDABLE_SECTIONS.find((s) => s.key === sectionPath); const sectionMeta = OVERRIDABLE_SECTIONS.find((s) => s.key === sectionPath);
const compareFields = sectionMeta?.compareFields; const compareFields = sectionMeta?.compareFields;
const cameraNames = Object.keys(config.cameras); const cameraNames = Object.keys(config.cameras).filter(
(name) => !isReplayCamera(name),
);
const cameraSectionValues = cameraNames.map((name) => const cameraSectionValues = cameraNames.map((name) =>
normalizeConfigValue( normalizeConfigValue(
getBaseCameraSectionValue(config, name, sectionPath), getBaseCameraSectionValue(config, name, sectionPath),

View File

@ -1,6 +1,7 @@
import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import useSWR from "swr"; import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { isReplayCamera } from "@/utils/cameraUtil";
/** /**
* Returns true if the current user has access to all cameras. * Returns true if the current user has access to all cameras.
@ -16,7 +17,7 @@ export function useHasFullCameraAccess() {
if (!config?.cameras) return false; if (!config?.cameras) return false;
const enabledCameraNames = Object.entries(config.cameras) const enabledCameraNames = Object.entries(config.cameras)
.filter(([, cam]) => cam.enabled_in_config) .filter(([name, cam]) => cam.enabled_in_config && !isReplayCamera(name))
.map(([name]) => name); .map(([name]) => name);
return ( return (

View File

@ -637,7 +637,7 @@ export default function Events() {
} }
setStartTime(recording.startTime); setStartTime(recording.startTime);
const allCameras = reviewFilter?.cameras ?? Object.keys(config.cameras); const allCameras = reviewFilter?.cameras ?? allowedCameras;
return { return {
camera: recording.camera, camera: recording.camera,

View File

@ -378,6 +378,34 @@ export default function Replay() {
showTitle showTitle
showOverrideIndicator={false} showOverrideIndicator={false}
/> />
{config?.face_recognition?.enabled && (
<ConfigSectionTemplate
sectionKey="face_recognition"
level="replay"
cameraName={status.replay_camera ?? undefined}
skipSave
noStickyButtons
requiresRestart={false}
collapsible
defaultCollapsed={false}
showTitle
showOverrideIndicator={false}
/>
)}
{config?.lpr?.enabled && (
<ConfigSectionTemplate
sectionKey="lpr"
level="replay"
cameraName={status.replay_camera ?? undefined}
skipSave
noStickyButtons
requiresRestart={false}
collapsible
defaultCollapsed={false}
showTitle
showOverrideIndicator={false}
/>
)}
</div> </div>
)} )}
</> </>

View File

@ -100,6 +100,7 @@ import {
} from "@/utils/configUtil"; } from "@/utils/configUtil";
import type { ProfileState, ProfilesApiResponse } from "@/types/profile"; import type { ProfileState, ProfilesApiResponse } from "@/types/profile";
import { getProfileColor } from "@/utils/profileColors"; import { getProfileColor } from "@/utils/profileColors";
import { isReplayCamera } from "@/utils/cameraUtil";
import { ProfileSectionDropdown } from "@/components/settings/ProfileSectionDropdown"; import { ProfileSectionDropdown } from "@/components/settings/ProfileSectionDropdown";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import RestartDialog from "@/components/overlay/dialog/RestartDialog"; import RestartDialog from "@/components/overlay/dialog/RestartDialog";
@ -661,7 +662,12 @@ export default function Settings() {
} }
return Object.values(config.cameras) return Object.values(config.cameras)
.filter((conf) => conf.ui.dashboard && conf.enabled_in_config) .filter(
(conf) =>
conf.ui.dashboard &&
conf.enabled_in_config &&
!isReplayCamera(conf.name),
)
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order); .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
}, [config]); }, [config]);

View File

@ -32,6 +32,7 @@ import {
ZoomLevel, ZoomLevel,
} from "@/types/review"; } from "@/types/review";
import { getChunkedTimeRange } from "@/utils/timelineUtil"; import { getChunkedTimeRange } from "@/utils/timelineUtil";
import { isReplayCamera } from "@/utils/cameraUtil";
import { getEndOfDayTimestamp } from "@/utils/dateUtil"; import { getEndOfDayTimestamp } from "@/utils/dateUtil";
import axios from "axios"; import axios from "axios";
import { import {
@ -1015,12 +1016,14 @@ function MotionReview({
let cameras; let cameras;
if (!filter || !filter.cameras) { if (!filter || !filter.cameras) {
cameras = Object.values(config.cameras); cameras = Object.values(config.cameras).filter(
(cam) => !isReplayCamera(cam.name),
);
} else { } else {
const filteredCams = filter.cameras; const filteredCams = filter.cameras;
cameras = Object.values(config.cameras).filter((cam) => cameras = Object.values(config.cameras).filter(
filteredCams.includes(cam.name), (cam) => filteredCams.includes(cam.name) && !isReplayCamera(cam.name),
); );
} }

View File

@ -44,6 +44,7 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import type { ProfileState } from "@/types/profile"; import type { ProfileState } from "@/types/profile";
import { getProfileColor } from "@/utils/profileColors"; import { getProfileColor } from "@/utils/profileColors";
import { isReplayCamera } from "@/utils/cameraUtil";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import {
Select, Select,
@ -87,7 +88,10 @@ export default function CameraManagementView({
const enabledCameras = useMemo(() => { const enabledCameras = useMemo(() => {
if (config) { if (config) {
return Object.keys(config.cameras) return Object.keys(config.cameras)
.filter((camera) => config.cameras[camera].enabled_in_config) .filter(
(camera) =>
config.cameras[camera].enabled_in_config && !isReplayCamera(camera),
)
.sort((a, b) => { .sort((a, b) => {
const orderA = config.cameras[a].ui?.order ?? 0; const orderA = config.cameras[a].ui?.order ?? 0;
const orderB = config.cameras[b].ui?.order ?? 0; const orderB = config.cameras[b].ui?.order ?? 0;
@ -180,7 +184,11 @@ export default function CameraManagementView({
const disabledCameras = useMemo(() => { const disabledCameras = useMemo(() => {
if (config) { if (config) {
return Object.keys(config.cameras) return Object.keys(config.cameras)
.filter((camera) => !config.cameras[camera].enabled_in_config) .filter(
(camera) =>
!config.cameras[camera].enabled_in_config &&
!isReplayCamera(camera),
)
.sort(); .sort();
} }
return []; return [];
@ -188,7 +196,9 @@ export default function CameraManagementView({
const allCameras = useMemo(() => { const allCameras = useMemo(() => {
if (config) { if (config) {
return Object.keys(config.cameras).sort(); return Object.keys(config.cameras)
.filter((camera) => !isReplayCamera(camera))
.sort();
} }
return []; return [];
}, [config]); }, [config]);

View File

@ -16,6 +16,7 @@ import FrigatePlusCurrentModelSummary from "@/views/settings/components/FrigateP
import { useDocDomain } from "@/hooks/use-doc-domain"; import { useDocDomain } from "@/hooks/use-doc-domain";
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { isReplayCamera } from "@/utils/cameraUtil";
import type { SettingsPageProps } from "@/views/settings/SingleSectionPage"; import type { SettingsPageProps } from "@/views/settings/SingleSectionPage";
export default function FrigatePlusSettingsView(_props: SettingsPageProps) { export default function FrigatePlusSettingsView(_props: SettingsPageProps) {
@ -139,8 +140,9 @@ export default function FrigatePlusSettingsView(_props: SettingsPageProps) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{Object.entries(config.cameras).map( {Object.entries(config.cameras)
([name, camera]) => ( .filter(([name]) => !isReplayCamera(name))
.map(([name, camera]) => (
<tr <tr
key={name} key={name}
className="border-b border-secondary" className="border-b border-secondary"
@ -156,8 +158,7 @@ export default function FrigatePlusSettingsView(_props: SettingsPageProps) {
)} )}
</td> </td>
</tr> </tr>
), ))}
)}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -19,6 +19,7 @@ import type { JsonObject } from "@/types/configForm";
import type { ProfileState, ProfilesApiResponse } from "@/types/profile"; import type { ProfileState, ProfilesApiResponse } from "@/types/profile";
import { getProfileColor } from "@/utils/profileColors"; import { getProfileColor } from "@/utils/profileColors";
import { PROFILE_ELIGIBLE_SECTIONS } from "@/utils/configUtil"; import { PROFILE_ELIGIBLE_SECTIONS } from "@/utils/configUtil";
import { isReplayCamera } from "@/utils/cameraUtil";
import { resolveCameraName } from "@/hooks/use-camera-friendly-name"; import { resolveCameraName } from "@/hooks/use-camera-friendly-name";
import { useDocDomain } from "@/hooks/use-doc-domain"; import { useDocDomain } from "@/hooks/use-doc-domain";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -145,7 +146,9 @@ export default function ProfilesView({
if (!config || allProfileNames.length === 0) return {}; if (!config || allProfileNames.length === 0) return {};
const data: Record<string, Record<string, string[]>> = {}; const data: Record<string, Record<string, string[]>> = {};
const cameras = Object.keys(config.cameras).sort(); const cameras = Object.keys(config.cameras)
.filter((name) => !isReplayCamera(name))
.sort();
for (const profile of allProfileNames) { for (const profile of allProfileNames) {
data[profile] = {}; data[profile] = {};

View File

@ -25,6 +25,7 @@ import useSWR from "swr";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
import { resolveCameraName } from "@/hooks/use-camera-friendly-name"; import { resolveCameraName } from "@/hooks/use-camera-friendly-name";
import { isReplayCamera } from "@/utils/cameraUtil";
type CameraMetricsProps = { type CameraMetricsProps = {
lastUpdated: number; lastUpdated: number;
@ -316,7 +317,7 @@ export default function CameraMetrics({
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{config && {config &&
Object.values(config.cameras).map((camera) => { Object.values(config.cameras).map((camera) => {
if (camera.enabled) { if (camera.enabled && !isReplayCamera(camera.name)) {
return ( return (
<Fragment key={camera.name}> <Fragment key={camera.name}>
{probeCameraName == camera.name && ( {probeCameraName == camera.name && (