mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 13:34:13 +03:00
Compare commits
4 Commits
63c9be1f82
...
62e3887745
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62e3887745 | ||
|
|
8d71d8be4a | ||
|
|
1a75251ffb | ||
|
|
048475e750 |
@ -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 | | | |
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -124,45 +124,50 @@ def capture_frames(
|
|||||||
config_subscriber.check_for_updates()
|
config_subscriber.check_for_updates()
|
||||||
return config.enabled
|
return config.enabled
|
||||||
|
|
||||||
while not stop_event.is_set():
|
try:
|
||||||
if not get_enabled_state():
|
while not stop_event.is_set():
|
||||||
logger.debug(f"Stopping capture thread for disabled {config.name}")
|
if not get_enabled_state():
|
||||||
break
|
logger.debug(f"Stopping capture thread for disabled {config.name}")
|
||||||
|
|
||||||
fps.value = frame_rate.eps()
|
|
||||||
skipped_fps.value = skipped_eps.eps()
|
|
||||||
current_frame.value = datetime.now().timestamp()
|
|
||||||
frame_name = f"{config.name}_frame{frame_index}"
|
|
||||||
frame_buffer = frame_manager.write(frame_name)
|
|
||||||
try:
|
|
||||||
frame_buffer[:] = ffmpeg_process.stdout.read(frame_size)
|
|
||||||
except Exception:
|
|
||||||
# shutdown has been initiated
|
|
||||||
if stop_event.is_set():
|
|
||||||
break
|
break
|
||||||
|
|
||||||
logger.error(f"{config.name}: Unable to read frames from ffmpeg process.")
|
fps.value = frame_rate.eps()
|
||||||
|
skipped_fps.value = skipped_eps.eps()
|
||||||
|
current_frame.value = datetime.now().timestamp()
|
||||||
|
frame_name = f"{config.name}_frame{frame_index}"
|
||||||
|
frame_buffer = frame_manager.write(frame_name)
|
||||||
|
try:
|
||||||
|
frame_buffer[:] = ffmpeg_process.stdout.read(frame_size)
|
||||||
|
except Exception:
|
||||||
|
# shutdown has been initiated
|
||||||
|
if stop_event.is_set():
|
||||||
|
break
|
||||||
|
|
||||||
if ffmpeg_process.poll() is not None:
|
|
||||||
logger.error(
|
logger.error(
|
||||||
f"{config.name}: ffmpeg process is not running. exiting capture thread..."
|
f"{config.name}: Unable to read frames from ffmpeg process."
|
||||||
)
|
)
|
||||||
break
|
|
||||||
|
|
||||||
continue
|
if ffmpeg_process.poll() is not None:
|
||||||
|
logger.error(
|
||||||
|
f"{config.name}: ffmpeg process is not running. exiting capture thread..."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
frame_rate.update()
|
continue
|
||||||
|
|
||||||
# don't lock the queue to check, just try since it should rarely be full
|
frame_rate.update()
|
||||||
try:
|
|
||||||
# add to the queue
|
|
||||||
frame_queue.put((frame_name, current_frame.value), False)
|
|
||||||
frame_manager.close(frame_name)
|
|
||||||
except queue.Full:
|
|
||||||
# if the queue is full, skip this frame
|
|
||||||
skipped_eps.update()
|
|
||||||
|
|
||||||
frame_index = 0 if frame_index == shm_frame_count - 1 else frame_index + 1
|
# don't lock the queue to check, just try since it should rarely be full
|
||||||
|
try:
|
||||||
|
# add to the queue
|
||||||
|
frame_queue.put((frame_name, current_frame.value), False)
|
||||||
|
frame_manager.close(frame_name)
|
||||||
|
except queue.Full:
|
||||||
|
# if the queue is full, skip this frame
|
||||||
|
skipped_eps.update()
|
||||||
|
|
||||||
|
frame_index = 0 if frame_index == shm_frame_count - 1 else frame_index + 1
|
||||||
|
finally:
|
||||||
|
config_subscriber.stop()
|
||||||
|
|
||||||
|
|
||||||
class CameraWatchdog(threading.Thread):
|
class CameraWatchdog(threading.Thread):
|
||||||
@ -234,6 +239,16 @@ class CameraWatchdog(threading.Thread):
|
|||||||
else:
|
else:
|
||||||
self.ffmpeg_detect_process.wait()
|
self.ffmpeg_detect_process.wait()
|
||||||
|
|
||||||
|
# Wait for old capture thread to fully exit before starting a new one
|
||||||
|
if self.capture_thread is not None and self.capture_thread.is_alive():
|
||||||
|
self.logger.info("Waiting for capture thread to exit...")
|
||||||
|
self.capture_thread.join(timeout=5)
|
||||||
|
|
||||||
|
if self.capture_thread.is_alive():
|
||||||
|
self.logger.warning(
|
||||||
|
f"Capture thread for {self.config.name} did not exit in time"
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
"The following ffmpeg logs include the last 100 lines prior to exit."
|
"The following ffmpeg logs include the last 100 lines prior to exit."
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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