Merge branch 'blakeblackshear:dev' into dev

This commit is contained in:
Wei Theng 2024-12-20 07:18:45 +00:00 committed by GitHub
commit beff5708d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 78 additions and 34 deletions

View File

@ -41,6 +41,7 @@ cameras:
... ...
onvif: onvif:
# Required: host of the camera being connected to. # Required: host of the camera being connected to.
# NOTE: HTTP is assumed by default; HTTPS is supported if you specify the scheme, ex: "https://0.0.0.0".
host: 0.0.0.0 host: 0.0.0.0
# Optional: ONVIF port for device (default: shown below). # Optional: ONVIF port for device (default: shown below).
port: 8000 port: 8000
@ -49,6 +50,8 @@ cameras:
user: admin user: admin
# Optional: password for login. # Optional: password for login.
password: admin password: admin
# Optional: Skip TLS verification from the ONVIF server (default: shown below)
tls_insecure: False
# Optional: PTZ camera object autotracking. Keeps a moving object in # Optional: PTZ camera object autotracking. Keeps a moving object in
# the center of the frame by automatically moving the PTZ camera. # the center of the frame by automatically moving the PTZ camera.
autotracking: autotracking:

View File

@ -694,6 +694,7 @@ cameras:
# to enable PTZ controls. # to enable PTZ controls.
onvif: onvif:
# Required: host of the camera being connected to. # Required: host of the camera being connected to.
# NOTE: HTTP is assumed by default; HTTPS is supported if you specify the scheme, ex: "https://0.0.0.0".
host: 0.0.0.0 host: 0.0.0.0
# Optional: ONVIF port for device (default: shown below). # Optional: ONVIF port for device (default: shown below).
port: 8000 port: 8000
@ -702,6 +703,8 @@ cameras:
user: admin user: admin
# Optional: password for login. # Optional: password for login.
password: admin password: admin
# Optional: Skip TLS verification from the ONVIF server (default: shown below)
tls_insecure: False
# Optional: Ignores time synchronization mismatches between the camera and the server during authentication. # Optional: Ignores time synchronization mismatches between the camera and the server during authentication.
# Using NTP on both ends is recommended and this should only be set to True in a "safe" environment due to the security risk it represents. # Using NTP on both ends is recommended and this should only be set to True in a "safe" environment due to the security risk it represents.
ignore_time_mismatch: False ignore_time_mismatch: False

View File

@ -74,6 +74,7 @@ class OnvifConfig(FrigateBaseModel):
port: int = Field(default=8000, title="Onvif Port") port: int = Field(default=8000, title="Onvif Port")
user: Optional[EnvString] = Field(default=None, title="Onvif Username") user: Optional[EnvString] = Field(default=None, title="Onvif Username")
password: Optional[EnvString] = Field(default=None, title="Onvif Password") password: Optional[EnvString] = Field(default=None, title="Onvif Password")
tls_insecure: bool = Field(default=False, title="Onvif Disable TLS verification")
autotracking: PtzAutotrackConfig = Field( autotracking: PtzAutotrackConfig = Field(
default_factory=PtzAutotrackConfig, default_factory=PtzAutotrackConfig,
title="PTZ auto tracking config.", title="PTZ auto tracking config.",

View File

@ -4,6 +4,7 @@ from typing import Optional
from pydantic import Field from pydantic import Field
from frigate.const import MAX_PRE_CAPTURE from frigate.const import MAX_PRE_CAPTURE
from frigate.review.types import SeverityEnum
from ..base import FrigateBaseModel from ..base import FrigateBaseModel
@ -101,3 +102,15 @@ class RecordConfig(FrigateBaseModel):
self.alerts.pre_capture, self.alerts.pre_capture,
self.detections.pre_capture, self.detections.pre_capture,
) )
def get_review_pre_capture(self, severity: SeverityEnum) -> int:
if severity == SeverityEnum.alert:
return self.alerts.pre_capture
else:
return self.detections.pre_capture
def get_review_post_capture(self, severity: SeverityEnum) -> int:
if severity == SeverityEnum.alert:
return self.alerts.post_capture
else:
return self.detections.post_capture

View File

@ -6,6 +6,7 @@ from importlib.util import find_spec
from pathlib import Path from pathlib import Path
import numpy import numpy
import requests
from onvif import ONVIFCamera, ONVIFError from onvif import ONVIFCamera, ONVIFError
from zeep.exceptions import Fault, TransportError from zeep.exceptions import Fault, TransportError
from zeep.transports import Transport from zeep.transports import Transport
@ -48,7 +49,11 @@ class OnvifController:
if cam.onvif.host: if cam.onvif.host:
try: try:
transport = Transport(timeout=10, operation_timeout=10) session = requests.Session()
session.verify = not cam.onvif.tls_insecure
transport = Transport(
timeout=10, operation_timeout=10, session=session
)
self.cams[cam_name] = { self.cams[cam_name] = {
"onvif": ONVIFCamera( "onvif": ONVIFCamera(
cam.onvif.host, cam.onvif.host,
@ -558,6 +563,7 @@ class OnvifController:
if not self._init_onvif(camera_name): if not self._init_onvif(camera_name):
return return
try:
if command == OnvifCommandEnum.init: if command == OnvifCommandEnum.init:
# already init # already init
return return
@ -569,11 +575,14 @@ class OnvifController:
_, pan, tilt = param.split("_") _, pan, tilt = param.split("_")
self._move_relative(camera_name, float(pan), float(tilt), 0, 1) self._move_relative(camera_name, float(pan), float(tilt), 0, 1)
elif ( elif (
command == OnvifCommandEnum.zoom_in or command == OnvifCommandEnum.zoom_out command == OnvifCommandEnum.zoom_in
or command == OnvifCommandEnum.zoom_out
): ):
self._zoom(camera_name, command) self._zoom(camera_name, command)
else: else:
self._move(camera_name, command) self._move(camera_name, command)
except ONVIFError as e:
logger.error(f"Unable to handle onvif command: {e}")
def get_camera_info(self, camera_name: str) -> dict[str, any]: def get_camera_info(self, camera_name: str) -> dict[str, any]:
if camera_name not in self.cams.keys(): if camera_name not in self.cams.keys():

View File

@ -29,6 +29,7 @@ from frigate.const import (
RECORD_DIR, RECORD_DIR,
) )
from frigate.models import Recordings, ReviewSegment from frigate.models import Recordings, ReviewSegment
from frigate.review.types import SeverityEnum
from frigate.util.services import get_video_properties from frigate.util.services import get_video_properties
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -194,6 +195,7 @@ class RecordingMaintainer(threading.Thread):
ReviewSegment.select( ReviewSegment.select(
ReviewSegment.start_time, ReviewSegment.start_time,
ReviewSegment.end_time, ReviewSegment.end_time,
ReviewSegment.severity,
ReviewSegment.data, ReviewSegment.data,
) )
.where( .where(
@ -219,11 +221,15 @@ class RecordingMaintainer(threading.Thread):
[r for r in recordings_to_insert if r is not None], [r for r in recordings_to_insert if r is not None],
) )
def drop_segment(self, cache_path: str) -> None:
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
async def validate_and_move_segment( async def validate_and_move_segment(
self, camera: str, reviews: list[ReviewSegment], recording: dict[str, any] self, camera: str, reviews: list[ReviewSegment], recording: dict[str, any]
) -> None: ) -> None:
cache_path = recording["cache_path"] cache_path: str = recording["cache_path"]
start_time = recording["start_time"] start_time: datetime.datetime = recording["start_time"]
record_config = self.config.cameras[camera].record record_config = self.config.cameras[camera].record
# Just delete files if recordings are turned off # Just delete files if recordings are turned off
@ -231,8 +237,7 @@ class RecordingMaintainer(threading.Thread):
camera not in self.config.cameras camera not in self.config.cameras
or not self.config.cameras[camera].record.enabled or not self.config.cameras[camera].record.enabled
): ):
Path(cache_path).unlink(missing_ok=True) self.drop_segment(cache_path)
self.end_time_cache.pop(cache_path, None)
return return
if cache_path in self.end_time_cache: if cache_path in self.end_time_cache:
@ -260,24 +265,34 @@ class RecordingMaintainer(threading.Thread):
return return
# if cached file's start_time is earlier than the retain days for the camera # if cached file's start_time is earlier than the retain days for the camera
# meaning continuous recording is not enabled
if start_time <= ( if start_time <= (
datetime.datetime.now().astimezone(datetime.timezone.utc) datetime.datetime.now().astimezone(datetime.timezone.utc)
- datetime.timedelta(days=self.config.cameras[camera].record.retain.days) - datetime.timedelta(days=self.config.cameras[camera].record.retain.days)
): ):
# if the cached segment overlaps with the events: # if the cached segment overlaps with the review items:
overlaps = False overlaps = False
for review in reviews: for review in reviews:
# if the event starts in the future, stop checking events severity = SeverityEnum[review.severity]
# if the review item starts in the future, stop checking review items
# and remove this segment # and remove this segment
if review.start_time > end_time.timestamp(): if (
review.start_time - record_config.get_review_pre_capture(severity)
) > end_time.timestamp():
overlaps = False overlaps = False
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
break break
# if the event is in progress or ends after the recording starts, keep it # if the review item is in progress or ends after the recording starts, keep it
# and stop looking at events # and stop looking at review items
if review.end_time is None or review.end_time >= start_time.timestamp(): if (
review.end_time is None
or (
review.end_time
+ record_config.get_review_post_capture(severity)
)
>= start_time.timestamp()
):
overlaps = True overlaps = True
break break
@ -296,7 +311,7 @@ class RecordingMaintainer(threading.Thread):
cache_path, cache_path,
record_mode, record_mode,
) )
# if it doesn't overlap with an event, go ahead and drop the segment # if it doesn't overlap with an review item, go ahead and drop the segment
# if it ends more than the configured pre_capture for the camera # if it ends more than the configured pre_capture for the camera
else: else:
camera_info = self.object_recordings_info[camera] camera_info = self.object_recordings_info[camera]
@ -307,9 +322,9 @@ class RecordingMaintainer(threading.Thread):
most_recently_processed_frame_time - record_config.event_pre_capture most_recently_processed_frame_time - record_config.event_pre_capture
).astimezone(datetime.timezone.utc) ).astimezone(datetime.timezone.utc)
if end_time < retain_cutoff: if end_time < retain_cutoff:
Path(cache_path).unlink(missing_ok=True) self.drop_segment(cache_path)
self.end_time_cache.pop(cache_path, None)
# else retain days includes this segment # else retain days includes this segment
# meaning continuous recording is enabled
else: else:
# assume that empty means the relevant recording info has not been received yet # assume that empty means the relevant recording info has not been received yet
camera_info = self.object_recordings_info[camera] camera_info = self.object_recordings_info[camera]
@ -390,8 +405,7 @@ class RecordingMaintainer(threading.Thread):
# check if the segment shouldn't be stored # check if the segment shouldn't be stored
if segment_info.should_discard_segment(store_mode): if segment_info.should_discard_segment(store_mode):
Path(cache_path).unlink(missing_ok=True) self.drop_segment(cache_path)
self.end_time_cache.pop(cache_path, None)
return return
# directory will be in utc due to start_time being in utc # directory will be in utc due to start_time being in utc

View File

@ -142,6 +142,7 @@ export interface CameraConfig {
password: string | null; password: string | null;
port: number; port: number;
user: string | null; user: string | null;
tls_insecure: boolean;
}; };
record: { record: {
enabled: boolean; enabled: boolean;