mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 21:44:13 +03:00
Compare commits
6 Commits
e8e48b8ac0
...
53b6f7018c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53b6f7018c | ||
|
|
1a75251ffb | ||
|
|
048475e750 | ||
|
|
33048ebc01 | ||
|
|
665c5c9ea6 | ||
|
|
5febd5e178 |
53
README_CN.md
53
README_CN.md
@ -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) | \[简体中文\]
|
||||||
|
|
||||||
|
[](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)
|
||||||
|
|
||||||
Bilibili:https://space.bilibili.com/3546894915602564
|
Bilibili:https://space.bilibili.com/3546894915602564
|
||||||
|
|
||||||
|
|
||||||
## 中文社区赞助商
|
## 中文社区赞助商
|
||||||
|
|
||||||
[](https://edgeone.ai/zh?from=github)
|
[](https://edgeone.ai/zh?from=github)
|
||||||
本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助
|
本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Copyright © 2025 Frigate LLC.**
|
||||||
|
|||||||
@ -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 | | | |
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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.""",
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
@ -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" &&
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user