diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index 2e06d7353..8625a63a8 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -71,22 +71,22 @@ ENV CCACHE_MAXSIZE 2G # Build libUSB without udev. Needed for Openvino NCS2 support WORKDIR /opt -RUN apt-get update && apt-get install -y unzip build-essential automake libtool ccache -RUN --mount=type=cache,target=/root/.ccache wget -q https://github.com/libusb/libusb/archive/v1.0.25.zip -O v1.0.25.zip && \ - unzip v1.0.25.zip && cd libusb-1.0.25 && \ +RUN apt-get update && apt-get install -y unzip build-essential automake libtool ccache pkg-config +RUN --mount=type=cache,target=/root/.ccache wget -q https://github.com/libusb/libusb/archive/v1.0.26.zip -O v1.0.26.zip && \ + unzip v1.0.26.zip && cd libusb-1.0.26 && \ ./bootstrap.sh && \ ./configure CC='ccache gcc' CCX='ccache g++' --disable-udev --enable-shared && \ make -j $(nproc --all) RUN apt-get update && \ apt-get install -y --no-install-recommends libusb-1.0-0-dev && \ rm -rf /var/lib/apt/lists/* -WORKDIR /opt/libusb-1.0.25/libusb +WORKDIR /opt/libusb-1.0.26/libusb RUN /bin/mkdir -p '/usr/local/lib' && \ /bin/bash ../libtool --mode=install /usr/bin/install -c libusb-1.0.la '/usr/local/lib' && \ /bin/mkdir -p '/usr/local/include/libusb-1.0' && \ /usr/bin/install -c -m 644 libusb.h '/usr/local/include/libusb-1.0' && \ /bin/mkdir -p '/usr/local/lib/pkgconfig' && \ - cd /opt/libusb-1.0.25/ && \ + cd /opt/libusb-1.0.26/ && \ /usr/bin/install -c -m 644 libusb-1.0.pc '/usr/local/lib/pkgconfig' && \ ldconfig diff --git a/docker/main/build_nginx.sh b/docker/main/build_nginx.sh index 56c9a146d..7dd7e14ef 100755 --- a/docker/main/build_nginx.sh +++ b/docker/main/build_nginx.sh @@ -2,7 +2,7 @@ set -euxo pipefail -NGINX_VERSION="1.25.1" +NGINX_VERSION="1.25.2" VOD_MODULE_VERSION="1.31" SECURE_TOKEN_MODULE_VERSION="1.5" RTMP_MODULE_VERSION="1.2.2" diff --git a/docker/main/install_s6_overlay.sh b/docker/main/install_s6_overlay.sh index 4e858ef07..75acba774 100755 --- a/docker/main/install_s6_overlay.sh +++ b/docker/main/install_s6_overlay.sh @@ -2,7 +2,7 @@ set -euxo pipefail -s6_version="3.1.4.1" +s6_version="3.1.5.0" if [[ "${TARGETARCH}" == "amd64" ]]; then s6_arch="x86_64" diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py index 790d90100..aefed5f83 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -39,6 +39,10 @@ if go2rtc_config.get("api") is None: elif go2rtc_config["api"].get("origin") is None: go2rtc_config["api"]["origin"] = "*" +# Need to set default location for HA config +if go2rtc_config.get("hass") is None: + go2rtc_config["hass"] = {"config": "/config"} + # we want to ensure that logs are easy to read if go2rtc_config.get("log") is None: go2rtc_config["log"] = {"format": "text"} diff --git a/docs/docs/configuration/audio_detectors.md b/docs/docs/configuration/audio_detectors.md index ef1d8227c..3e5fafdf8 100644 --- a/docs/docs/configuration/audio_detectors.md +++ b/docs/docs/configuration/audio_detectors.md @@ -50,7 +50,7 @@ cameras: ### Configuring Audio Events -The included audio model has over 500 different types of audio that can be detected, many of which are not practical. By default `bark`, `speech`, `yell`, and `scream` are enabled but these can be customized. +The included audio model has over [500 different types](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) of audio that can be detected, many of which are not practical. By default `bark`, `speech`, `yell`, and `scream` are enabled but these can be customized. ```yaml audio: diff --git a/docs/docs/configuration/autotracking.md b/docs/docs/configuration/autotracking.md index 6c08c3dfe..918cbcb43 100644 --- a/docs/docs/configuration/autotracking.md +++ b/docs/docs/configuration/autotracking.md @@ -11,7 +11,7 @@ Once Frigate determines that an object is not a false positive and has entered o Upon loss of tracking, Frigate will scan the region of the lost object for `timeout` seconds. If an object of the same type is found in that region, Frigate will autotrack that new object. -When tracking has ended, Frigate will return to the camera preset specified by the `return_preset` configuration entry. +When tracking has ended, Frigate will return to the camera firmware's PTZ preset specified by the `return_preset` configuration entry. ## Checking ONVIF camera support @@ -23,7 +23,7 @@ Alternatively, you can download and run [this simple Python script](https://gist ## Configuration -First, set up a PTZ preset in your camera's firmware and give it a name. +First, set up a PTZ preset in your camera's firmware and give it a name. If you're unsure how to do this, consult the documentation for your camera manufacturer's firmware. Some tutorials for common brands: [Amcrest](https://www.youtube.com/watch?v=lJlE9-krmrM), [Reolink](https://www.youtube.com/watch?v=VAnxHUY5i5w), [Dahua](https://www.youtube.com/watch?v=7sNbc5U-k54). Edit your Frigate configuration file and enter the ONVIF parameters for your camera. Specify the object types to track, a required zone the object must enter to begin autotracking, and the camera preset name you configured in your camera's firmware to return to when tracking has ended. Optionally, specify a delay in seconds before Frigate returns the camera to the preset. @@ -56,7 +56,7 @@ cameras: # Required: Begin automatically tracking an object when it enters any of the listed zones. required_zones: - zone_name - # Required: Name of ONVIF camera preset to return to when tracking is over. (default: shown below) + # Required: Name of ONVIF preset in camera's firmware to return to when tracking is over. (default: shown below) return_preset: home # Optional: Seconds to delay before returning to preset. (default: shown below) timeout: 10 diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 5bb7963a3..4442b3b57 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -579,7 +579,7 @@ cameras: # Required: Begin automatically tracking an object when it enters any of the listed zones. required_zones: - zone_name - # Required: Name of ONVIF camera preset to return to when tracking is over. + # Required: Name of ONVIF preset in camera's firmware to return to when tracking is over. (default: shown below) return_preset: preset_name # Optional: Seconds to delay before returning to preset. (default: shown below) timeout: 10 diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index 30b6f290b..c31a2dee5 100644 --- a/docs/docs/frigate/installation.md +++ b/docs/docs/frigate/installation.md @@ -25,8 +25,8 @@ Frigate uses the following locations for read/write operations in the container. - `/media/frigate/clips`: Used for snapshot storage. In the future, it will likely be renamed from `clips` to `snapshots`. The file structure here cannot be modified and isn't intended to be browsed or managed manually. - `/media/frigate/recordings`: Internal system storage for recording segments. The file structure here cannot be modified and isn't intended to be browsed or managed manually. - `/media/frigate/exports`: Storage for clips and timelapses that have been exported via the WebUI or API. -- `/tmp/cache`: Cache location for recording segments. Initial recordings are written here before being checked and converted to mp4 and moved to the recordings folder. -- `/dev/shm`: It is not recommended to modify this directory or map it with docker. This is the location for raw decoded frames in shared memory and it's size is impacted by the `shm-size` calculations below. +- `/tmp/cache`: Cache location for recording segments. Initial recordings are written here before being checked and converted to mp4 and moved to the recordings folder. Segments generated via the `clip.mp4` endpoints are also concatenated and processed here. It is recommended to use a [`tmpfs`](https://docs.docker.com/storage/tmpfs/) mount for this. +- `/dev/shm`: Internal cache for raw decoded frames in shared memory. It is not recommended to modify this directory or map it with docker. The minimum size is impacted by the `shm-size` calculations below. #### Common docker compose storage configurations @@ -51,7 +51,7 @@ services: Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is **64MB**. -The default shm size of **64MB** is fine for setups with **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size. +The default shm size of **64MB** is fine for setups with **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size, using [`--shm-size`](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) (or [`service.shm_size`](https://docs.docker.com/compose/compose-file/compose-file-v2/#shm_size) in docker-compose). The Frigate container also stores logs in shm, which can take up to **30MB**, so make sure to take this into account in your math as well. diff --git a/docs/docs/troubleshooting/recordings.md b/docs/docs/troubleshooting/recordings.md new file mode 100644 index 000000000..cfa3eb89f --- /dev/null +++ b/docs/docs/troubleshooting/recordings.md @@ -0,0 +1,38 @@ +--- +id: recordings +title: Troubleshooting Recordings +--- + +## `WARNING : Unable to keep up with recording segments in cache for {camera}. Keeping the 5 most recent segments out of 6 and discarding the rest...` + +This error can be caused by a number of different issues. The first step in troubleshooting is to enable debug logging for recording, this will enable logging showing how long it takes for recordings to be moved from RAM cache to the disk. + +```yaml +logger: + logs: + frigate.record.maintainer: debug +``` + +This will include logs like: + +``` +DEBUG : Copied /media/frigate/recordings/{segment_path} in 0.2 seconds. +``` + +It is important to let this run until the errors begin to happen, to confirm that there is not a slow down in the disk at the time of the error. + +### Copy Times > 1 second + +If the storage is too slow to keep up with the recordings then the maintainer will fall behind and purge the oldest recordings to ensure the cache does not fill up causing a crash. In this case it is important to diagnose why the copy times are slow. + +#### Check Storage Type + +Mounting a network share is a popular option for storing Recordings, but this can lead to reduced copy times and cause problems. Some users have found that using `NFS` instead of `SMB` considerably decreased the copy times and fixed the issue. It is also important to ensure that the network connection between the device running Frigate and the network share is stable and fast. + +#### Check mount options + +Some users found that mounting a drive via `fstab` with the `sync` option caused dramatically reduce performance and led to this issue. Using `async` instead greatly reduced copy times. + +### Copy Times < 1 second + +If the storage is working quickly then this error may be caused by CPU load on the machine being too high for Frigate to have the resources to keep up. Try temporarily shutting down other services to see if the issue improves. diff --git a/docs/sidebars.js b/docs/sidebars.js index 75dfeafce..085adfcb1 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -58,6 +58,7 @@ module.exports = { ], Troubleshooting: [ "troubleshooting/faqs", + "troubleshooting/recordings", ], Development: [ "development/contributing", diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 56a2c5c9b..e97095c8a 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -52,9 +52,6 @@ class Dispatcher: self.ptz_metrics = ptz_metrics self.comms = communicators - for comm in self.comms: - comm.subscribe(self._receive) - self._camera_settings_handlers: dict[str, Callable] = { "audio": self._on_audio_command, "detect": self._on_detect_command, @@ -67,6 +64,9 @@ class Dispatcher: "snapshots": self._on_snapshots_command, } + for comm in self.comms: + comm.subscribe(self._receive) + def _receive(self, topic: str, payload: str) -> None: """Handle receiving of payload from communicators.""" if topic.endswith("set"): diff --git a/frigate/config.py b/frigate/config.py index 7f1624ed4..f98da3855 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -444,6 +444,7 @@ class AudioConfig(FrigateBaseModel): enabled_in_config: Optional[bool] = Field( title="Keep track of original state of audio detection." ) + num_threads: int = Field(default=2, title="Number of detection threads", ge=1) class BirdseyeModeEnum(str, Enum): diff --git a/frigate/events/audio.py b/frigate/events/audio.py index 911013a38..76bfd5fa8 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -89,12 +89,13 @@ def listen_to_audio( class AudioTfl: - def __init__(self, stop_event: mp.Event): + def __init__(self, stop_event: mp.Event, num_threads=2): self.stop_event = stop_event - self.labels = load_labels("/audio-labelmap.txt") + self.num_threads = num_threads + self.labels = load_labels("/audio-labelmap.txt", prefill=521) self.interpreter = Interpreter( model_path="/cpu_audio_model.tflite", - num_threads=2, + num_threads=self.num_threads, ) self.interpreter.allocate_tensors() @@ -117,7 +118,7 @@ class AudioTfl: count = len(scores) for i in range(count): - if scores[i] < 0.4 or i == 20: + if scores[i] < AUDIO_MIN_CONFIDENCE or i == 20: break detections[i] = [ class_ids[i], @@ -164,7 +165,7 @@ class AudioEventMaintainer(threading.Thread): self.inter_process_communicator = inter_process_communicator self.detections: dict[dict[str, any]] = feature_metrics self.stop_event = stop_event - self.detector = AudioTfl(stop_event) + self.detector = AudioTfl(stop_event, self.config.audio.num_threads) self.shape = (int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE)),) self.chunk_size = int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE * 2)) self.logger = logging.getLogger(f"audio.{self.config.name}") diff --git a/frigate/events/cleanup.py b/frigate/events/cleanup.py index d70a290d7..793e01293 100644 --- a/frigate/events/cleanup.py +++ b/frigate/events/cleanup.py @@ -168,6 +168,7 @@ class EventCleanup(threading.Thread): camera, has_snapshot, has_clip, + end_time, row_number() over ( partition by label, camera, round(start_time/5,0)*5 order by end_time-start_time desc @@ -176,7 +177,7 @@ class EventCleanup(threading.Thread): ) select distinct id, camera, has_snapshot, has_clip from grouped_events - where copy_number > 1;""" + where copy_number > 1 and end_time not null;""" duplicate_events = Event.raw(duplicate_query) for event in duplicate_events: diff --git a/frigate/object_processing.py b/frigate/object_processing.py index 83684e1b6..a4bfb3fd9 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -29,6 +29,7 @@ from frigate.util.image import ( calculate_region, draw_box_with_label, draw_timestamp, + is_label_printable, ) logger = logging.getLogger(__name__) @@ -509,13 +510,21 @@ class CameraState: # draw the bounding boxes on the frame box = obj["box"] + text = ( + obj["label"] + if ( + not obj.get("sub_label") + or not is_label_printable(obj["sub_label"][0]) + ) + else obj["sub_label"][0] + ) draw_box_with_label( frame_copy, box[0], box[1], box[2], box[3], - obj["label"], + text, f"{obj['score']:.0%} {int(obj['area'])}", thickness=thickness, color=color, diff --git a/frigate/output.py b/frigate/output.py index e37920fda..5f0a3411e 100644 --- a/frigate/output.py +++ b/frigate/output.py @@ -210,7 +210,7 @@ class BroadcastThread(threading.Thread): ws.send(buf, binary=True) except ValueError: pass - except ConnectionResetError as e: + except (BrokenPipeError, ConnectionResetError) as e: logger.debug(f"Websocket unexpectedly closed {e}") elif self.converter.process.poll() is not None: break diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py index 705a87cc7..340063992 100644 --- a/frigate/ptz/onvif.py +++ b/frigate/ptz/onvif.py @@ -119,10 +119,16 @@ class OnvifController: move_request.Translation.PanTilt.space = ptz_config["Spaces"][ "RelativePanTiltTranslationSpace" ][fov_space_id]["URI"] + + try: if zoom_space_id is not None: move_request.Translation.Zoom.space = ptz_config["Spaces"][ "RelativeZoomTranslationSpace" ][0]["URI"] + except Exception: + # camera does not support relative zoom + pass + if move_request.Speed is None: move_request.Speed = ptz.GetStatus({"ProfileToken": profile.token}).Position self.cams[camera_name]["relative_move_request"] = move_request diff --git a/frigate/util/builtin.py b/frigate/util/builtin.py index 7eafc9d33..9c776f5df 100644 --- a/frigate/util/builtin.py +++ b/frigate/util/builtin.py @@ -134,7 +134,7 @@ def get_ffmpeg_arg_list(arg: Any) -> list: return arg if isinstance(arg, list) else shlex.split(arg) -def load_labels(path, encoding="utf-8"): +def load_labels(path, encoding="utf-8", prefill=91): """Loads labels from file (with or without index numbers). Args: path: path to label file. @@ -143,7 +143,7 @@ def load_labels(path, encoding="utf-8"): Dictionary mapping indices to labels. """ with open(path, "r", encoding=encoding) as f: - labels = {index: "unknown" for index in range(91)} + labels = {index: "unknown" for index in range(prefill)} lines = f.readlines() if not lines: return {} diff --git a/frigate/util/image.py b/frigate/util/image.py index 4af94500d..d5eaaf885 100644 --- a/frigate/util/image.py +++ b/frigate/util/image.py @@ -4,6 +4,7 @@ import datetime import logging from abc import ABC, abstractmethod from multiprocessing import shared_memory +from string import printable from typing import AnyStr, Optional import cv2 @@ -154,6 +155,11 @@ def draw_box_with_label( ) +def is_label_printable(label) -> bool: + """Check if label is printable.""" + return not bool(set(label) - set(printable)) + + def calculate_region(frame_shape, xmin, ymin, xmax, ymax, model_size, multiplier=2): # size is the longest edge and divisible by 4 size = int((max(xmax - xmin, ymax - ymin) * multiplier) // 4 * 4) diff --git a/web/src/components/MultiSelect.jsx b/web/src/components/MultiSelect.jsx index 1ff30a231..a04fc7778 100644 --- a/web/src/components/MultiSelect.jsx +++ b/web/src/components/MultiSelect.jsx @@ -5,6 +5,8 @@ import { ArrowDropdown } from '../icons/ArrowDropdown'; import Heading from './Heading'; import Button from './Button'; import CameraIcon from '../icons/Camera'; +import SpeakerIcon from '../icons/Speaker'; +import useSWR from 'swr'; export default function MultiSelect({ className, title, options, selection, onToggle, onShowAll, onSelectSingle }) { const popupRef = useRef(null); @@ -18,7 +20,7 @@ export default function MultiSelect({ className, title, options, selection, onTo }; const menuHeight = Math.round(window.innerHeight * 0.55); - + const { data: config } = useSWR('config'); return (
setState({ showMenu: true })}> @@ -59,7 +61,8 @@ export default function MultiSelect({ className, title, options, selection, onTo className="max-h-[35px] mx-2" onClick={() => onSelectSingle(item)} > - + { (title === "Labels" && config.audio.listen.includes(item)) ? ( ) : ( ) } +
diff --git a/web/src/icons/Audio.jsx b/web/src/icons/Audio.jsx index cec783854..4e178ccf0 100644 --- a/web/src/icons/Audio.jsx +++ b/web/src/icons/Audio.jsx @@ -1,7 +1,7 @@ import { h } from 'preact'; import { memo } from 'preact/compat'; -export function Snapshot({ className = 'h-6 w-6', stroke = 'currentColor', onClick = () => {} }) { +export function Audio({ className = 'h-6 w-6', stroke = 'currentColor', onClick = () => {} }) { return ( {}, +}) { + return ( + + + + ); +} +export default memo(Speaker);