Compare commits

...

4 Commits

Author SHA1 Message Date
Chris
850ba07223
Merge 793906bb68 into 1a75251ffb 2025-11-30 01:00:30 +10:00
Ryan Hass
1a75251ffb
Add yolov9 inference speeds for UHD 730 GPU. (#21090)
Some checks are pending
CI / AMD64 Extra 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
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
This adds the inference speeds measured on an i5-11400T with a UHD 730
GPU running at nominal temperatures.
2025-11-29 07:32:16 -06:00
Josh Hawkins
048475e750
API admin exemptions and route guard updates (#21094)
* update exempt paths and add missing guard to api endpoints

* admin only frigate+ submission
2025-11-29 07:30:04 -06:00
Chris Suich
793906bb68 feat: use persisted state for selected camera on settings page
Leverage the usePersistence() hook to store the selected camera so that
navigating away from the settings page and back will remember the
selected camera.
2025-11-15 11:26:59 -05:00
13 changed files with 81 additions and 14 deletions

View File

@ -159,7 +159,7 @@ Inference speeds vary greatly depending on the CPU or GPU used, some known examp
| Intel HD 530 | 15 - 35 ms | | | | Can only run one detector instance | | Intel HD 530 | 15 - 35 ms | | | | Can only run one detector instance |
| Intel HD 620 | 15 - 25 ms | | 320: ~ 35 ms | | | | Intel HD 620 | 15 - 25 ms | | 320: ~ 35 ms | | |
| Intel HD 630 | ~ 15 ms | | 320: ~ 30 ms | | | | Intel HD 630 | ~ 15 ms | | 320: ~ 30 ms | | |
| Intel UHD 730 | ~ 10 ms | | 320: ~ 19 ms 640: ~ 54 ms | | | | Intel UHD 730 | ~ 10 ms | t-320: 14ms s-320: 24ms t-640: 34ms s-640: 65ms | 320: ~ 19 ms 640: ~ 54 ms | | |
| Intel UHD 770 | ~ 15 ms | t-320: ~ 16 ms s-320: ~ 20 ms s-640: ~ 40 ms | 320: ~ 20 ms 640: ~ 46 ms | | | | Intel UHD 770 | ~ 15 ms | t-320: ~ 16 ms s-320: ~ 20 ms s-640: ~ 40 ms | 320: ~ 20 ms 640: ~ 46 ms | | |
| Intel N100 | ~ 15 ms | s-320: 30 ms | 320: ~ 25 ms | | Can only run one detector instance | | Intel N100 | ~ 15 ms | s-320: 30 ms | 320: ~ 25 ms | | Can only run one detector instance |
| Intel N150 | ~ 15 ms | t-320: 16 ms s-320: 24 ms | | | | | Intel N150 | ~ 15 ms | t-320: 16 ms s-320: 24 ms | | | |

View File

@ -62,8 +62,8 @@ def require_admin_by_default():
"/", "/",
"/version", "/version",
"/config/schema.json", "/config/schema.json",
"/metrics",
# Authenticated user endpoints (allow_any_authenticated) # Authenticated user endpoints (allow_any_authenticated)
"/metrics",
"/stats", "/stats",
"/stats/history", "/stats/history",
"/config", "/config",
@ -76,22 +76,28 @@ def require_admin_by_default():
"/recognized_license_plates", "/recognized_license_plates",
"/timeline", "/timeline",
"/timeline/hourly", "/timeline/hourly",
"/events/summary",
"/recordings/storage", "/recordings/storage",
"/recordings/summary", "/recordings/summary",
"/recordings/unavailable", "/recordings/unavailable",
"/go2rtc/streams", "/go2rtc/streams",
"/event_ids",
"/events",
"/exports",
} }
# Path prefixes that should be exempt (for paths with parameters) # Path prefixes that should be exempt (for paths with parameters)
EXEMPT_PREFIXES = ( EXEMPT_PREFIXES = (
"/logs/", # /logs/{service} "/logs/", # /logs/{service}
"/review", # /review, /review/{id}, /review_ids, /review/summary, etc. "/review", # /review, /review/{id}, /review/summary, /review_ids, etc.
"/reviews/", # /reviews/viewed, /reviews/delete "/reviews/", # /reviews/viewed, /reviews/delete
"/events/", # /events/{id}/thumbnail, etc. (camera-scoped) "/events/", # /events/{id}/thumbnail, /events/summary, etc. (camera-scoped)
"/export/", # /export/{camera}/start/..., /export/{id}/rename, /export/{id}
"/go2rtc/streams/", # /go2rtc/streams/{camera} "/go2rtc/streams/", # /go2rtc/streams/{camera}
"/users/", # /users/{username}/password (has own auth) "/users/", # /users/{username}/password (has own auth)
"/preview/", # /preview/{file}/thumbnail.jpg "/preview/", # /preview/{file}/thumbnail.jpg
"/exports/", # /exports/{export_id}
"/vod/", # /vod/{camera_name}/...
"/notifications/", # /notifications/pubkey, /notifications/register
) )
async def admin_checker(request: Request): async def admin_checker(request: Request):
@ -105,6 +111,24 @@ def require_admin_by_default():
if path.startswith(EXEMPT_PREFIXES): if path.startswith(EXEMPT_PREFIXES):
return return
# Dynamic camera path exemption:
# Any path whose first segment matches a configured camera name should
# bypass the global admin requirement. These endpoints enforce access
# via route-level dependencies (e.g. require_camera_access) to ensure
# per-camera authorization. This allows non-admin authenticated users
# (e.g. viewer role) to access camera-specific resources without
# needing admin privileges.
try:
if path.startswith("/"):
first_segment = path.split("/", 2)[1]
if (
first_segment
and first_segment in request.app.frigate_config.cameras
):
return
except Exception:
pass
# For all other paths, require admin role # For all other paths, require admin role
# Port 5000 (internal) requests have admin role set automatically # Port 5000 (internal) requests have admin role set automatically
role = request.headers.get("remote-role") role = request.headers.get("remote-role")
@ -113,7 +137,7 @@ def require_admin_by_default():
raise HTTPException( raise HTTPException(
status_code=403, status_code=403,
detail="Admin role required for this endpoint", detail="Access denied. A user with the admin role is required.",
) )
return admin_checker return admin_checker

View File

@ -70,6 +70,7 @@ router = APIRouter(tags=[Tags.events])
@router.get( @router.get(
"/events", "/events",
response_model=list[EventResponse], response_model=list[EventResponse],
dependencies=[Depends(allow_any_authenticated())],
summary="Get events", summary="Get events",
description="Returns a list of events.", description="Returns a list of events.",
) )
@ -344,6 +345,7 @@ def events(
@router.get( @router.get(
"/events/explore", "/events/explore",
response_model=list[EventResponse], response_model=list[EventResponse],
dependencies=[Depends(allow_any_authenticated())],
summary="Get summary of objects.", summary="Get summary of objects.",
description="""Gets a summary of objects from the database. description="""Gets a summary of objects from the database.
Returns a list of objects with a max of `limit` objects for each label. Returns a list of objects with a max of `limit` objects for each label.
@ -436,6 +438,7 @@ def events_explore(
@router.get( @router.get(
"/event_ids", "/event_ids",
response_model=list[EventResponse], response_model=list[EventResponse],
dependencies=[Depends(allow_any_authenticated())],
summary="Get events by ids.", summary="Get events by ids.",
description="""Gets events by a list of ids. description="""Gets events by a list of ids.
Returns a list of events. Returns a list of events.
@ -469,6 +472,7 @@ async def event_ids(ids: str, request: Request):
@router.get( @router.get(
"/events/search", "/events/search",
dependencies=[Depends(allow_any_authenticated())],
summary="Search events.", summary="Search events.",
description="""Searches for events in the database. description="""Searches for events in the database.
Returns a list of events. Returns a list of events.
@ -919,6 +923,7 @@ def events_summary(
@router.get( @router.get(
"/events/{event_id}", "/events/{event_id}",
response_model=EventResponse, response_model=EventResponse,
dependencies=[Depends(allow_any_authenticated())],
summary="Get event by id.", summary="Get event by id.",
description="Gets an event by its id.", description="Gets an event by its id.",
) )
@ -962,6 +967,7 @@ def set_retain(event_id: str):
@router.post( @router.post(
"/events/{event_id}/plus", "/events/{event_id}/plus",
response_model=EventUploadPlusResponse, response_model=EventUploadPlusResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Send event to Frigate+.", summary="Send event to Frigate+.",
description="""Sends an event to Frigate+. description="""Sends an event to Frigate+.
Returns a success message or an error if the event is not found. Returns a success message or an error if the event is not found.
@ -1102,6 +1108,7 @@ async def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = N
@router.put( @router.put(
"/events/{event_id}/false_positive", "/events/{event_id}/false_positive",
response_model=EventUploadPlusResponse, response_model=EventUploadPlusResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Submit false positive to Frigate+", summary="Submit false positive to Frigate+",
description="""Submit an event as a false positive to Frigate+. description="""Submit an event as a false positive to Frigate+.
This endpoint is the same as the standard Frigate+ submission endpoint, This endpoint is the same as the standard Frigate+ submission endpoint,

View File

@ -14,6 +14,7 @@ from peewee import DoesNotExist
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
from frigate.api.auth import ( from frigate.api.auth import (
allow_any_authenticated,
get_allowed_cameras_for_filter, get_allowed_cameras_for_filter,
require_camera_access, require_camera_access,
require_role, require_role,
@ -44,6 +45,7 @@ router = APIRouter(tags=[Tags.export])
@router.get( @router.get(
"/exports", "/exports",
response_model=ExportsResponse, response_model=ExportsResponse,
dependencies=[Depends(allow_any_authenticated())],
summary="Get exports", summary="Get exports",
description="""Gets all exports from the database for cameras the user has access to. description="""Gets all exports from the database for cameras the user has access to.
Returns a list of exports ordered by date (most recent first).""", Returns a list of exports ordered by date (most recent first).""",
@ -272,6 +274,7 @@ async def export_delete(event_id: str, request: Request):
@router.get( @router.get(
"/exports/{export_id}", "/exports/{export_id}",
response_model=ExportModel, response_model=ExportModel,
dependencies=[Depends(allow_any_authenticated())],
summary="Get a single export", summary="Get a single export",
description="""Gets a specific export by ID. The user must have access to the camera description="""Gets a specific export by ID. The user must have access to the camera
associated with the export.""", associated with the export.""",

View File

@ -945,6 +945,7 @@ async def vod_hour(
@router.get( @router.get(
"/vod/event/{event_id}", "/vod/event/{event_id}",
dependencies=[Depends(allow_any_authenticated())],
description="Returns an HLS playlist for the specified object. Append /master.m3u8 or /index.m3u8 for HLS playback.", description="Returns an HLS playlist for the specified object. Append /master.m3u8 or /index.m3u8 for HLS playback.",
) )
async def vod_event( async def vod_event(

View File

@ -5,11 +5,12 @@ import os
from typing import Any from typing import Any
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from fastapi import APIRouter, Request from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from peewee import DoesNotExist from peewee import DoesNotExist
from py_vapid import Vapid01, utils from py_vapid import Vapid01, utils
from frigate.api.auth import allow_any_authenticated
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
from frigate.const import CONFIG_DIR from frigate.const import CONFIG_DIR
from frigate.models import User from frigate.models import User
@ -21,6 +22,7 @@ router = APIRouter(tags=[Tags.notifications])
@router.get( @router.get(
"/notifications/pubkey", "/notifications/pubkey",
dependencies=[Depends(allow_any_authenticated())],
summary="Get VAPID public key", summary="Get VAPID public key",
description="""Gets the VAPID public key for the notifications. description="""Gets the VAPID public key for the notifications.
Returns the public key or an error if notifications are not enabled. Returns the public key or an error if notifications are not enabled.
@ -47,6 +49,7 @@ def get_vapid_pub_key(request: Request):
@router.post( @router.post(
"/notifications/register", "/notifications/register",
dependencies=[Depends(allow_any_authenticated())],
summary="Register notifications", summary="Register notifications",
description="""Registers a notifications subscription. description="""Registers a notifications subscription.
Returns a success message or an error if the subscription is not provided. Returns a success message or an error if the subscription is not provided.

View File

@ -577,7 +577,9 @@ def delete_reviews(body: ReviewModifyMultipleBody):
@router.get( @router.get(
"/review/activity/motion", response_model=list[ReviewActivityMotionResponse] "/review/activity/motion",
response_model=list[ReviewActivityMotionResponse],
dependencies=[Depends(allow_any_authenticated())],
) )
def motion_activity( def motion_activity(
params: ReviewActivityMotionQueryParams = Depends(), params: ReviewActivityMotionQueryParams = Depends(),
@ -739,6 +741,7 @@ async def set_not_reviewed(
@router.post( @router.post(
"/review/summarize/start/{start_ts}/end/{end_ts}", "/review/summarize/start/{start_ts}/end/{end_ts}",
dependencies=[Depends(allow_any_authenticated())],
description="Use GenAI to summarize review items over a period of time.", description="Use GenAI to summarize review items over a period of time.",
) )
def generate_review_summary(request: Request, start_ts: float, end_ts: float): def generate_review_summary(request: Request, start_ts: float, end_ts: float):

View File

@ -1299,7 +1299,8 @@ function ObjectDetailsTab({
</div> </div>
</div> </div>
{search.data.type === "object" && {isAdmin &&
search.data.type === "object" &&
config?.plus?.enabled && config?.plus?.enabled &&
search.end_time != undefined && search.end_time != undefined &&
search.has_snapshot && ( search.has_snapshot && (

View File

@ -38,6 +38,7 @@ import { isDesktop, isIOS, isMobileOnly, isSafari } from "react-device-detect";
import { useApiHost } from "@/api"; 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";
type TrackingDetailsProps = { type TrackingDetailsProps = {
className?: string; className?: string;
@ -777,6 +778,7 @@ function LifecycleIconRow({
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const isAdmin = useIsAdmin();
const aspectRatio = useMemo(() => { const aspectRatio = useMemo(() => {
if (!config) { if (!config) {
@ -993,7 +995,7 @@ function LifecycleIconRow({
<div className="ml-3 flex-shrink-0 px-1 text-right text-xs text-primary-variant"> <div className="ml-3 flex-shrink-0 px-1 text-right text-xs text-primary-variant">
<div className="flex flex-row items-center gap-3"> <div className="flex flex-row items-center gap-3">
<div className="whitespace-nowrap">{formattedEventTimestamp}</div> <div className="whitespace-nowrap">{formattedEventTimestamp}</div>
{(config?.plus?.enabled || item.data.box) && ( {((isAdmin && config?.plus?.enabled) || item.data.box) && (
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}> <DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger> <DropdownMenuTrigger>
<div className="rounded p-1 pr-2" role="button"> <div className="rounded p-1 pr-2" role="button">
@ -1002,7 +1004,7 @@ function LifecycleIconRow({
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuPortal> <DropdownMenuPortal>
<DropdownMenuContent> <DropdownMenuContent>
{config?.plus?.enabled && ( {isAdmin && config?.plus?.enabled && (
<DropdownMenuItem <DropdownMenuItem
className="cursor-pointer" className="cursor-pointer"
onSelect={async () => { onSelect={async () => {

View File

@ -20,6 +20,7 @@ import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator
import { baseUrl } from "@/api/baseUrl"; import { baseUrl } from "@/api/baseUrl";
import { getTranslatedLabel } from "@/utils/i18n"; import { getTranslatedLabel } from "@/utils/i18n";
import useImageLoaded from "@/hooks/use-image-loaded"; import useImageLoaded from "@/hooks/use-image-loaded";
import { useIsAdmin } from "@/hooks/use-is-admin";
export type FrigatePlusDialogProps = { export type FrigatePlusDialogProps = {
upload?: Event; upload?: Event;
@ -57,7 +58,9 @@ export function FrigatePlusDialog({
); );
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
const isAdmin = useIsAdmin();
const showCard = const showCard =
isAdmin &&
!!upload && !!upload &&
upload.data.type === "object" && upload.data.type === "object" &&
upload.plus_id !== "not_enabled" && upload.plus_id !== "not_enabled" &&

View File

@ -20,6 +20,7 @@ import { cn } from "@/lib/utils";
import { ASPECT_VERTICAL_LAYOUT, RecordingPlayerError } from "@/types/record"; import { ASPECT_VERTICAL_LAYOUT, RecordingPlayerError } from "@/types/record";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ObjectTrackOverlay from "@/components/overlay/ObjectTrackOverlay"; import ObjectTrackOverlay from "@/components/overlay/ObjectTrackOverlay";
import { useIsAdmin } from "@/hooks/use-is-admin";
// Android native hls does not seek correctly // Android native hls does not seek correctly
const USE_NATIVE_HLS = false; const USE_NATIVE_HLS = false;
@ -83,6 +84,7 @@ export default function HlsVideoPlayer({
}: HlsVideoPlayerProps) { }: HlsVideoPlayerProps) {
const { t } = useTranslation("components/player"); const { t } = useTranslation("components/player");
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const isAdmin = useIsAdmin();
// for detail stream context in History // for detail stream context in History
const currentTime = currentTimeOverride; const currentTime = currentTimeOverride;
@ -285,7 +287,7 @@ export default function HlsVideoPlayer({
volume: true, volume: true,
seek: true, seek: true,
playbackRate: true, playbackRate: true,
plusUpload: config?.plus?.enabled == true, plusUpload: isAdmin && config?.plus?.enabled == true,
fullscreen: supportsFullscreen, fullscreen: supportsFullscreen,
}} }}
setControlsOpen={setControlsOpen} setControlsOpen={setControlsOpen}

View File

@ -13,6 +13,7 @@ import { useTranslation } from "react-i18next";
import { Event } from "@/types/event"; import { Event } from "@/types/event";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { useState } from "react"; import { useState } from "react";
import { useIsAdmin } from "@/hooks/use-is-admin";
type EventMenuProps = { type EventMenuProps = {
event: Event; event: Event;
@ -35,6 +36,7 @@ export default function EventMenu({
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation("views/explore"); const { t } = useTranslation("views/explore");
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const isAdmin = useIsAdmin();
const handleObjectSelect = () => { const handleObjectSelect = () => {
if (isSelected) { if (isSelected) {
@ -85,7 +87,8 @@ export default function EventMenu({
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
{event.has_snapshot && {isAdmin &&
event.has_snapshot &&
event.plus_id == undefined && event.plus_id == undefined &&
event.data.type == "object" && event.data.type == "object" &&
config?.plus?.enabled && ( config?.plus?.enabled && (

View File

@ -37,6 +37,7 @@ import EnrichmentsSettingsView from "@/views/settings/EnrichmentsSettingsView";
import UiSettingsView from "@/views/settings/UiSettingsView"; import UiSettingsView from "@/views/settings/UiSettingsView";
import FrigatePlusSettingsView from "@/views/settings/FrigatePlusSettingsView"; import FrigatePlusSettingsView from "@/views/settings/FrigatePlusSettingsView";
import { useSearchEffect } from "@/hooks/use-overlay-state"; import { useSearchEffect } from "@/hooks/use-overlay-state";
import { usePersistence } from "@/hooks/use-persistence";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { useInitialCameraState } from "@/api/ws"; import { useInitialCameraState } from "@/api/ws";
import { useIsAdmin } from "@/hooks/use-is-admin"; import { useIsAdmin } from "@/hooks/use-is-admin";
@ -207,7 +208,21 @@ export default function Settings() {
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order); .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
}, [config]); }, [config]);
const [selectedCamera, setSelectedCamera] = useState<string>(""); const [persistedCamera, setPersistedCamera] = usePersistence(
"selectedCamera",
"",
);
const [selectedCamera, setSelectedCamera] = useState(persistedCamera);
useEffect(() => {
if (persistedCamera) {
setSelectedCamera(persistedCamera);
}
}, [persistedCamera]);
useEffect(() => {
if (selectedCamera) {
setPersistedCamera(selectedCamera);
}
}, [selectedCamera, setPersistedCamera]);
const { payload: allCameraStates } = useInitialCameraState( const { payload: allCameraStates } = useInitialCameraState(
cameras.length > 0 ? cameras[0].name : "", cameras.length > 0 ? cameras[0].name : "",