Compare commits

...

6 Commits

Author SHA1 Message Date
GuoQing Liu
53b6f7018c
Merge 33048ebc01 into 1a75251ffb 2025-11-29 21:35:42 +08: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
ZhaiSoul
33048ebc01 docs: add license translation 2025-11-25 15:48:52 +00:00
ZhaiSoul
665c5c9ea6 style: Improve the styling of the Chinese document jump tips bar in dark mode 2025-11-25 15:11:26 +00:00
ZhaiSoul
5febd5e178 docs: update chinese readme 2025-11-25 15:08:39 +00:00
14 changed files with 118 additions and 42 deletions

View File

@ -1,28 +1,31 @@
<p align="center"> <p align="center">
<img align="center" alt="logo" src="docs/static/img/frigate.png"> <img align="center" alt="logo" src="docs/static/img/branding/frigate.png">
</p> </p>
# Frigate - 一个具有实时目标检测的本地NVR # Frigate NVR™ - 一个具有实时目标检测的本地 NVR
[English](https://github.com/blakeblackshear/frigate) | \[简体中文\] [English](https://github.com/blakeblackshear/frigate) | \[简体中文\]
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
<a href="https://hosted.weblate.org/engage/frigate-nvr/-/zh_Hans/"> <a href="https://hosted.weblate.org/engage/frigate-nvr/-/zh_Hans/">
<img src="https://hosted.weblate.org/widget/frigate-nvr/-/zh_Hans/svg-badge.svg" alt="翻译状态" /> <img src="https://hosted.weblate.org/widget/frigate-nvr/-/zh_Hans/svg-badge.svg" alt="翻译状态" />
</a> </a>
一个完整的本地网络视频录像机NVR专为[Home Assistant](https://www.home-assistant.io)设计具备AI物体检测功能。使用OpenCV和TensorFlow在本地为IP摄像头执行实时物体检测。 一个完整的本地网络视频录像机NVR专为[Home Assistant](https://www.home-assistant.io)设计,具备 AI 目标/物体检测功能。使用 OpenCV TensorFlow 在本地为 IP 摄像头执行实时物体检测。
强烈推荐使用GPU或者AI加速器例如[Google Coral加速器](https://coral.ai/products/) 或者 [Hailo](https://hailo.ai/)。它们的性能甚至超过目前的顶级CPU并且可以以极低的耗电实现更优的性能。 强烈推荐使用 GPU 或者 AI 加速器(例如[Google Coral 加速器](https://coral.ai/products/) 或者 [Hailo](https://hailo.ai/)等)。它们的运行效率远远高于现在的顶级 CPU并且功耗也极低。
- 通过[自定义组件](https://github.com/blakeblackshear/frigate-hass-integration)与Home Assistant紧密集成
- 设计上通过仅在必要时和必要地点寻找物体,最大限度地减少资源使用并最大化性能 - 通过[自定义组件](https://github.com/blakeblackshear/frigate-hass-integration)与 Home Assistant 紧密集成
- 设计上通过仅在必要时和必要地点寻找目标,最大限度地减少资源使用并最大化性能
- 大量利用多进程处理,强调实时性而非处理每一帧 - 大量利用多进程处理,强调实时性而非处理每一帧
- 使用非常低开销的运动检测来确定运行物体检测的位置 - 使用非常低开销的画面变动检测(也叫运动检测)来确定运行目标检测的位置
- 使用TensorFlow进行物体检测运行在单独的进程中以达到最大FPS - 使用 TensorFlow 进行目标检测,并运行在单独的进程中以达到最大 FPS
- 通过MQTT进行通信便于集成到其他系统中 - 通过 MQTT 进行通信,便于集成到其他系统中
- 根据检测到的物体设置保留时间进行视频录制 - 根据检测到的物体设置保留时间进行视频录制
- 24/7全天候录制 - 24/7 全天候录制
- 通过RTSP重新流传输以减少摄像头的连接数 - 通过 RTSP 重新流传输以减少摄像头的连接数
- 支持WebRTC和MSE实现低延迟的实时观看 - 支持 WebRTC MSE实现低延迟的实时观看
## 社区中文翻译文档 ## 社区中文翻译文档
@ -32,39 +35,55 @@
如果您想通过捐赠支持开发,请使用 [Github Sponsors](https://github.com/sponsors/blakeblackshear)。 如果您想通过捐赠支持开发,请使用 [Github Sponsors](https://github.com/sponsors/blakeblackshear)。
## 协议
本项目采用 **MIT 许可证**授权。
**代码部分**:本代码库中的源代码、配置文件和文档均遵循 [MIT 许可证](LICENSE)。您可以自由使用、修改和分发这些代码,但必须保留原始版权声明。
**商标部分**“Frigate”名称、“Frigate NVR”品牌以及 Frigate 的 Logo 为 **Frigate LLC 的商标****不在** MIT 许可证覆盖范围内。
有关品牌资产的规范使用详情,请参阅我们的[《商标政策》](TRADEMARK.md)。
## 截图 ## 截图
### 实时监控面板 ### 实时监控面板
<div> <div>
<img width="800" alt="实时监控面板" src="https://github.com/blakeblackshear/frigate/assets/569905/5e713cb9-9db5-41dc-947a-6937c3bc376e"> <img width="800" alt="实时监控面板" src="https://github.com/blakeblackshear/frigate/assets/569905/5e713cb9-9db5-41dc-947a-6937c3bc376e">
</div> </div>
### 简单的核查工作流程 ### 简单的核查工作流程
<div> <div>
<img width="800" alt="简单的审查工作流程" src="https://github.com/blakeblackshear/frigate/assets/569905/6fed96e8-3b18-40e5-9ddc-31e6f3c9f2ff"> <img width="800" alt="简单的审查工作流程" src="https://github.com/blakeblackshear/frigate/assets/569905/6fed96e8-3b18-40e5-9ddc-31e6f3c9f2ff">
</div> </div>
### 多摄像头可按时间轴查看 ### 多摄像头可按时间轴查看
<div> <div>
<img width="800" alt="多摄像头可按时间轴查看" src="https://github.com/blakeblackshear/frigate/assets/569905/d6788a15-0eeb-4427-a8d4-80b93cae3d74"> <img width="800" alt="多摄像头可按时间轴查看" src="https://github.com/blakeblackshear/frigate/assets/569905/d6788a15-0eeb-4427-a8d4-80b93cae3d74">
</div> </div>
### 内置遮罩和区域编辑器 ### 内置遮罩和区域编辑器
<div> <div>
<img width="800" alt="内置遮罩和区域编辑器" src="https://github.com/blakeblackshear/frigate/assets/569905/d7885fc3-bfe6-452f-b7d0-d957cb3e31f5"> <img width="800" alt="内置遮罩和区域编辑器" src="https://github.com/blakeblackshear/frigate/assets/569905/d7885fc3-bfe6-452f-b7d0-d957cb3e31f5">
</div> </div>
## 翻译 ## 翻译
我们使用 [Weblate](https://hosted.weblate.org/projects/frigate-nvr/) 平台提供翻译支持,欢迎参与进来一起完善。 我们使用 [Weblate](https://hosted.weblate.org/projects/frigate-nvr/) 平台提供翻译支持,欢迎参与进来一起完善。
## 非官方中文讨论社区 ## 非官方中文讨论社区
欢迎加入中文讨论QQ群[1043861059](https://qm.qq.com/q/7vQKsTmSz)
欢迎加入中文讨论 QQ 群:[1043861059](https://qm.qq.com/q/7vQKsTmSz)
Bilibilihttps://space.bilibili.com/3546894915602564 Bilibilihttps://space.bilibili.com/3546894915602564
## 中文社区赞助商 ## 中文社区赞助商
[![EdgeOne](https://edgeone.ai/media/34fe3a45-492d-4ea4-ae5d-ea1087ca7b4b.png)](https://edgeone.ai/zh?from=github) [![EdgeOne](https://edgeone.ai/media/34fe3a45-492d-4ea4-ae5d-ea1087ca7b4b.png)](https://edgeone.ai/zh?from=github)
本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助 本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助
---
**Copyright © 2025 Frigate LLC.**

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

@ -1,13 +1,18 @@
.alert { .alert {
padding: 12px; padding: 12px;
background: #fff8e6; background: #fff8e6;
border-bottom: 1px solid #ffd166; border-bottom: 1px solid #ffd166;
text-align: center; text-align: center;
font-size: 15px; font-size: 15px;
} }
.alert a { [data-theme="dark"] .alert {
color: #1890ff; background: #3b2f0b;
font-weight: 500; border-bottom: 1px solid #665c22;
margin-left: 6px; }
}
.alert a {
color: #1890ff;
font-weight: 500;
margin-left: 6px;
}

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 && (